@intrect/openswarm 0.9.2 → 0.10.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 (370) hide show
  1. package/README.md +76 -1
  2. package/config.example.yaml +6 -0
  3. package/dist/adapters/agenticLoop.d.ts +24 -0
  4. package/dist/adapters/agenticLoop.d.ts.map +1 -1
  5. package/dist/adapters/agenticLoop.js +130 -11
  6. package/dist/adapters/agenticLoop.js.map +1 -1
  7. package/dist/adapters/applyPatch.d.ts +21 -0
  8. package/dist/adapters/applyPatch.d.ts.map +1 -0
  9. package/dist/adapters/applyPatch.js +175 -0
  10. package/dist/adapters/applyPatch.js.map +1 -0
  11. package/dist/adapters/base.d.ts.map +1 -1
  12. package/dist/adapters/base.js +7 -0
  13. package/dist/adapters/base.js.map +1 -1
  14. package/dist/adapters/codex.js +10 -0
  15. package/dist/adapters/codex.js.map +1 -1
  16. package/dist/adapters/codexResponses.d.ts +8 -0
  17. package/dist/adapters/codexResponses.d.ts.map +1 -1
  18. package/dist/adapters/codexResponses.js +86 -8
  19. package/dist/adapters/codexResponses.js.map +1 -1
  20. package/dist/adapters/errorClassification.d.ts +8 -0
  21. package/dist/adapters/errorClassification.d.ts.map +1 -0
  22. package/dist/adapters/errorClassification.js +54 -0
  23. package/dist/adapters/errorClassification.js.map +1 -0
  24. package/dist/adapters/gpt.d.ts.map +1 -1
  25. package/dist/adapters/gpt.js +12 -1
  26. package/dist/adapters/gpt.js.map +1 -1
  27. package/dist/adapters/local.d.ts.map +1 -1
  28. package/dist/adapters/local.js +9 -1
  29. package/dist/adapters/local.js.map +1 -1
  30. package/dist/adapters/openrouter.d.ts.map +1 -1
  31. package/dist/adapters/openrouter.js +9 -1
  32. package/dist/adapters/openrouter.js.map +1 -1
  33. package/dist/adapters/rateLimitError.d.ts +29 -0
  34. package/dist/adapters/rateLimitError.d.ts.map +1 -0
  35. package/dist/adapters/rateLimitError.js +64 -0
  36. package/dist/adapters/rateLimitError.js.map +1 -0
  37. package/dist/adapters/resultParsing.d.ts.map +1 -1
  38. package/dist/adapters/resultParsing.js +18 -0
  39. package/dist/adapters/resultParsing.js.map +1 -1
  40. package/dist/adapters/tools.d.ts +3 -0
  41. package/dist/adapters/tools.d.ts.map +1 -1
  42. package/dist/adapters/tools.js +148 -11
  43. package/dist/adapters/tools.js.map +1 -1
  44. package/dist/adapters/types.d.ts +15 -0
  45. package/dist/adapters/types.d.ts.map +1 -1
  46. package/dist/adapters/webTools.d.ts.map +1 -1
  47. package/dist/adapters/webTools.js +44 -21
  48. package/dist/adapters/webTools.js.map +1 -1
  49. package/dist/agents/agentPair.d.ts +9 -0
  50. package/dist/agents/agentPair.d.ts.map +1 -1
  51. package/dist/agents/agentPair.js.map +1 -1
  52. package/dist/agents/auditor.d.ts.map +1 -1
  53. package/dist/agents/auditor.js +3 -0
  54. package/dist/agents/auditor.js.map +1 -1
  55. package/dist/agents/documenter.d.ts.map +1 -1
  56. package/dist/agents/documenter.js +3 -0
  57. package/dist/agents/documenter.js.map +1 -1
  58. package/dist/agents/draftAnalyzer.d.ts +30 -7
  59. package/dist/agents/draftAnalyzer.d.ts.map +1 -1
  60. package/dist/agents/draftAnalyzer.js +181 -30
  61. package/dist/agents/draftAnalyzer.js.map +1 -1
  62. package/dist/agents/pairPipeline.d.ts +7 -1
  63. package/dist/agents/pairPipeline.d.ts.map +1 -1
  64. package/dist/agents/pairPipeline.js +110 -66
  65. package/dist/agents/pairPipeline.js.map +1 -1
  66. package/dist/agents/pipelineFormat.d.ts.map +1 -1
  67. package/dist/agents/pipelineFormat.js +4 -0
  68. package/dist/agents/pipelineFormat.js.map +1 -1
  69. package/dist/agents/reviewer.d.ts +16 -0
  70. package/dist/agents/reviewer.d.ts.map +1 -1
  71. package/dist/agents/reviewer.js +30 -0
  72. package/dist/agents/reviewer.js.map +1 -1
  73. package/dist/agents/skillDocumenter.d.ts.map +1 -1
  74. package/dist/agents/skillDocumenter.js +3 -0
  75. package/dist/agents/skillDocumenter.js.map +1 -1
  76. package/dist/agents/tester.d.ts.map +1 -1
  77. package/dist/agents/tester.js +3 -0
  78. package/dist/agents/tester.js.map +1 -1
  79. package/dist/agents/worker.d.ts +14 -0
  80. package/dist/agents/worker.d.ts.map +1 -1
  81. package/dist/agents/worker.js +123 -22
  82. package/dist/agents/worker.js.map +1 -1
  83. package/dist/automation/autonomousRunner.d.ts +12 -0
  84. package/dist/automation/autonomousRunner.d.ts.map +1 -1
  85. package/dist/automation/autonomousRunner.js +218 -29
  86. package/dist/automation/autonomousRunner.js.map +1 -1
  87. package/dist/automation/runnerExecution.d.ts +15 -0
  88. package/dist/automation/runnerExecution.d.ts.map +1 -1
  89. package/dist/automation/runnerExecution.js +77 -1
  90. package/dist/automation/runnerExecution.js.map +1 -1
  91. package/dist/automation/runnerState.d.ts +7 -0
  92. package/dist/automation/runnerState.d.ts.map +1 -1
  93. package/dist/automation/runnerState.js +23 -1
  94. package/dist/automation/runnerState.js.map +1 -1
  95. package/dist/automation/runnerTypes.d.ts +2 -0
  96. package/dist/automation/runnerTypes.d.ts.map +1 -1
  97. package/dist/automation/scheduler.d.ts +12 -0
  98. package/dist/automation/scheduler.d.ts.map +1 -1
  99. package/dist/automation/scheduler.js +29 -2
  100. package/dist/automation/scheduler.js.map +1 -1
  101. package/dist/automation/taskSource.d.ts +9 -1
  102. package/dist/automation/taskSource.d.ts.map +1 -1
  103. package/dist/automation/taskSource.js +13 -2
  104. package/dist/automation/taskSource.js.map +1 -1
  105. package/dist/cli/auditPM.d.ts +40 -0
  106. package/dist/cli/auditPM.d.ts.map +1 -0
  107. package/dist/cli/auditPM.js +132 -0
  108. package/dist/cli/auditPM.js.map +1 -0
  109. package/dist/cli/designPipeline.d.ts +30 -0
  110. package/dist/cli/designPipeline.d.ts.map +1 -0
  111. package/dist/cli/designPipeline.js +113 -0
  112. package/dist/cli/designPipeline.js.map +1 -0
  113. package/dist/cli/mcpCommand.d.ts +22 -0
  114. package/dist/cli/mcpCommand.d.ts.map +1 -0
  115. package/dist/cli/mcpCommand.js +93 -0
  116. package/dist/cli/mcpCommand.js.map +1 -0
  117. package/dist/cli/projectHandler.d.ts.map +1 -1
  118. package/dist/cli/projectHandler.js +4 -2
  119. package/dist/cli/projectHandler.js.map +1 -1
  120. package/dist/cli/reviewAudit.d.ts +130 -0
  121. package/dist/cli/reviewAudit.d.ts.map +1 -0
  122. package/dist/cli/reviewAudit.js +283 -0
  123. package/dist/cli/reviewAudit.js.map +1 -0
  124. package/dist/cli/reviewCommand.d.ts +53 -0
  125. package/dist/cli/reviewCommand.d.ts.map +1 -0
  126. package/dist/cli/reviewCommand.js +207 -0
  127. package/dist/cli/reviewCommand.js.map +1 -0
  128. package/dist/cli/reviewMaxCommand.d.ts +35 -0
  129. package/dist/cli/reviewMaxCommand.d.ts.map +1 -0
  130. package/dist/cli/reviewMaxCommand.js +272 -0
  131. package/dist/cli/reviewMaxCommand.js.map +1 -0
  132. package/dist/cli/reviewProgress.d.ts +34 -0
  133. package/dist/cli/reviewProgress.d.ts.map +1 -0
  134. package/dist/cli/reviewProgress.js +109 -0
  135. package/dist/cli/reviewProgress.js.map +1 -0
  136. package/dist/cli/scheduleCommand.d.ts +15 -0
  137. package/dist/cli/scheduleCommand.d.ts.map +1 -0
  138. package/dist/cli/scheduleCommand.js +68 -0
  139. package/dist/cli/scheduleCommand.js.map +1 -0
  140. package/dist/cli.js +182 -26
  141. package/dist/cli.js.map +1 -1
  142. package/dist/core/config.d.ts +24 -0
  143. package/dist/core/config.d.ts.map +1 -1
  144. package/dist/core/config.js +42 -0
  145. package/dist/core/config.js.map +1 -1
  146. package/dist/core/providerOverride.d.ts.map +1 -1
  147. package/dist/core/providerOverride.js +16 -13
  148. package/dist/core/providerOverride.js.map +1 -1
  149. package/dist/core/service.d.ts.map +1 -1
  150. package/dist/core/service.js +11 -0
  151. package/dist/core/service.js.map +1 -1
  152. package/dist/core/types.d.ts +38 -0
  153. package/dist/core/types.d.ts.map +1 -1
  154. package/dist/discord/discordHandlers.d.ts.map +1 -1
  155. package/dist/discord/discordHandlers.js +1 -0
  156. package/dist/discord/discordHandlers.js.map +1 -1
  157. package/dist/index.js +4 -0
  158. package/dist/index.js.map +1 -1
  159. package/dist/linear/linear.d.ts +21 -0
  160. package/dist/linear/linear.d.ts.map +1 -1
  161. package/dist/linear/linear.js +159 -66
  162. package/dist/linear/linear.js.map +1 -1
  163. package/dist/linear/projectUpdater.js +3 -1
  164. package/dist/linear/projectUpdater.js.map +1 -1
  165. package/dist/locale/prompts/en.d.ts.map +1 -1
  166. package/dist/locale/prompts/en.js +104 -11
  167. package/dist/locale/prompts/en.js.map +1 -1
  168. package/dist/locale/prompts/ko.d.ts.map +1 -1
  169. package/dist/locale/prompts/ko.js +103 -11
  170. package/dist/locale/prompts/ko.js.map +1 -1
  171. package/dist/locale/types.d.ts +12 -0
  172. package/dist/locale/types.d.ts.map +1 -1
  173. package/dist/mcp/mcpClient.d.ts +28 -1
  174. package/dist/mcp/mcpClient.d.ts.map +1 -1
  175. package/dist/mcp/mcpClient.js +74 -3
  176. package/dist/mcp/mcpClient.js.map +1 -1
  177. package/dist/orchestration/decisionEngine.d.ts +20 -0
  178. package/dist/orchestration/decisionEngine.d.ts.map +1 -1
  179. package/dist/orchestration/decisionEngine.js +23 -0
  180. package/dist/orchestration/decisionEngine.js.map +1 -1
  181. package/dist/orchestration/taskScheduler.d.ts.map +1 -1
  182. package/dist/orchestration/taskScheduler.js +12 -1
  183. package/dist/orchestration/taskScheduler.js.map +1 -1
  184. package/dist/support/chatBackend.d.ts +18 -0
  185. package/dist/support/chatBackend.d.ts.map +1 -1
  186. package/dist/support/chatBackend.js +92 -8
  187. package/dist/support/chatBackend.js.map +1 -1
  188. package/dist/support/chatSession.d.ts +51 -0
  189. package/dist/support/chatSession.d.ts.map +1 -0
  190. package/dist/support/chatSession.js +134 -0
  191. package/dist/support/chatSession.js.map +1 -0
  192. package/dist/support/chatTui.d.ts.map +1 -1
  193. package/dist/support/chatTui.js +6 -75
  194. package/dist/support/chatTui.js.map +1 -1
  195. package/dist/support/concurrencyPool.d.ts +18 -0
  196. package/dist/support/concurrencyPool.d.ts.map +1 -0
  197. package/dist/support/concurrencyPool.js +46 -0
  198. package/dist/support/concurrencyPool.js.map +1 -0
  199. package/dist/support/dashboardHtml.d.ts +1 -1
  200. package/dist/support/dashboardHtml.d.ts.map +1 -1
  201. package/dist/support/dashboardHtml.js +0 -28
  202. package/dist/support/dashboardHtml.js.map +1 -1
  203. package/dist/support/editParser.d.ts +26 -0
  204. package/dist/support/editParser.d.ts.map +1 -1
  205. package/dist/support/editParser.js +43 -0
  206. package/dist/support/editParser.js.map +1 -1
  207. package/dist/support/goalCommand.d.ts +35 -0
  208. package/dist/support/goalCommand.d.ts.map +1 -0
  209. package/dist/support/goalCommand.js +112 -0
  210. package/dist/support/goalCommand.js.map +1 -0
  211. package/dist/support/index.d.ts +0 -1
  212. package/dist/support/index.d.ts.map +1 -1
  213. package/dist/support/index.js +0 -1
  214. package/dist/support/index.js.map +1 -1
  215. package/dist/support/web.d.ts.map +1 -1
  216. package/dist/support/web.js +0 -7
  217. package/dist/support/web.js.map +1 -1
  218. package/dist/taskState/store.d.ts +5 -0
  219. package/dist/taskState/store.d.ts.map +1 -1
  220. package/dist/taskState/store.js +16 -0
  221. package/dist/taskState/store.js.map +1 -1
  222. package/dist/telemetry/telemetry.d.ts +42 -0
  223. package/dist/telemetry/telemetry.d.ts.map +1 -0
  224. package/dist/telemetry/telemetry.js +138 -0
  225. package/dist/telemetry/telemetry.js.map +1 -0
  226. package/dist/tui/App.d.ts +20 -0
  227. package/dist/tui/App.d.ts.map +1 -0
  228. package/dist/tui/App.js +52 -0
  229. package/dist/tui/App.js.map +1 -0
  230. package/dist/tui/chatModel.d.ts +81 -0
  231. package/dist/tui/chatModel.d.ts.map +1 -0
  232. package/dist/tui/chatModel.js +129 -0
  233. package/dist/tui/chatModel.js.map +1 -0
  234. package/dist/tui/components/AuditBoard.d.ts +10 -0
  235. package/dist/tui/components/AuditBoard.d.ts.map +1 -0
  236. package/dist/tui/components/AuditBoard.js +50 -0
  237. package/dist/tui/components/AuditBoard.js.map +1 -0
  238. package/dist/tui/components/ChatInput.d.ts +14 -0
  239. package/dist/tui/components/ChatInput.d.ts.map +1 -0
  240. package/dist/tui/components/ChatInput.js +46 -0
  241. package/dist/tui/components/ChatInput.js.map +1 -0
  242. package/dist/tui/components/ChatLog.d.ts +12 -0
  243. package/dist/tui/components/ChatLog.d.ts.map +1 -0
  244. package/dist/tui/components/ChatLog.js +47 -0
  245. package/dist/tui/components/ChatLog.js.map +1 -0
  246. package/dist/tui/components/CommandPalette.d.ts +6 -0
  247. package/dist/tui/components/CommandPalette.d.ts.map +1 -0
  248. package/dist/tui/components/CommandPalette.js +14 -0
  249. package/dist/tui/components/CommandPalette.js.map +1 -0
  250. package/dist/tui/components/ContextBar.d.ts +9 -0
  251. package/dist/tui/components/ContextBar.d.ts.map +1 -0
  252. package/dist/tui/components/ContextBar.js +18 -0
  253. package/dist/tui/components/ContextBar.js.map +1 -0
  254. package/dist/tui/components/DataTable.d.ts +6 -0
  255. package/dist/tui/components/DataTable.d.ts.map +1 -0
  256. package/dist/tui/components/DataTable.js +13 -0
  257. package/dist/tui/components/DataTable.js.map +1 -0
  258. package/dist/tui/components/HelpBar.d.ts +2 -0
  259. package/dist/tui/components/HelpBar.d.ts.map +1 -0
  260. package/dist/tui/components/HelpBar.js +8 -0
  261. package/dist/tui/components/HelpBar.js.map +1 -0
  262. package/dist/tui/components/LiveLog.d.ts +6 -0
  263. package/dist/tui/components/LiveLog.d.ts.map +1 -0
  264. package/dist/tui/components/LiveLog.js +10 -0
  265. package/dist/tui/components/LiveLog.js.map +1 -0
  266. package/dist/tui/components/LogLine.d.ts +4 -0
  267. package/dist/tui/components/LogLine.d.ts.map +1 -0
  268. package/dist/tui/components/LogLine.js +8 -0
  269. package/dist/tui/components/LogLine.js.map +1 -0
  270. package/dist/tui/components/SelectList.d.ts +6 -0
  271. package/dist/tui/components/SelectList.d.ts.map +1 -0
  272. package/dist/tui/components/SelectList.js +15 -0
  273. package/dist/tui/components/SelectList.js.map +1 -0
  274. package/dist/tui/components/StageTimeline.d.ts +7 -0
  275. package/dist/tui/components/StageTimeline.d.ts.map +1 -0
  276. package/dist/tui/components/StageTimeline.js +18 -0
  277. package/dist/tui/components/StageTimeline.js.map +1 -0
  278. package/dist/tui/components/StatusBar.d.ts +7 -0
  279. package/dist/tui/components/StatusBar.d.ts.map +1 -0
  280. package/dist/tui/components/StatusBar.js +8 -0
  281. package/dist/tui/components/StatusBar.js.map +1 -0
  282. package/dist/tui/components/SubagentTree.d.ts +10 -0
  283. package/dist/tui/components/SubagentTree.d.ts.map +1 -0
  284. package/dist/tui/components/SubagentTree.js +12 -0
  285. package/dist/tui/components/SubagentTree.js.map +1 -0
  286. package/dist/tui/components/TabBar.d.ts +5 -0
  287. package/dist/tui/components/TabBar.d.ts.map +1 -0
  288. package/dist/tui/components/TabBar.js +9 -0
  289. package/dist/tui/components/TabBar.js.map +1 -0
  290. package/dist/tui/components/WorkingIndicator.d.ts +6 -0
  291. package/dist/tui/components/WorkingIndicator.d.ts.map +1 -0
  292. package/dist/tui/components/WorkingIndicator.js +20 -0
  293. package/dist/tui/components/WorkingIndicator.js.map +1 -0
  294. package/dist/tui/hooks/useMonitor.d.ts +8 -0
  295. package/dist/tui/hooks/useMonitor.d.ts.map +1 -0
  296. package/dist/tui/hooks/useMonitor.js +42 -0
  297. package/dist/tui/hooks/useMonitor.js.map +1 -0
  298. package/dist/tui/hooks/usePipelineEvents.d.ts +6 -0
  299. package/dist/tui/hooks/usePipelineEvents.d.ts.map +1 -0
  300. package/dist/tui/hooks/usePipelineEvents.js +21 -0
  301. package/dist/tui/hooks/usePipelineEvents.js.map +1 -0
  302. package/dist/tui/hooks/useTerminalSize.d.ts +6 -0
  303. package/dist/tui/hooks/useTerminalSize.d.ts.map +1 -0
  304. package/dist/tui/hooks/useTerminalSize.js +26 -0
  305. package/dist/tui/hooks/useTerminalSize.js.map +1 -0
  306. package/dist/tui/index.d.ts +22 -0
  307. package/dist/tui/index.d.ts.map +1 -0
  308. package/dist/tui/index.js +18 -0
  309. package/dist/tui/index.js.map +1 -0
  310. package/dist/tui/inputDebug.d.ts +20 -0
  311. package/dist/tui/inputDebug.d.ts.map +1 -0
  312. package/dist/tui/inputDebug.js +42 -0
  313. package/dist/tui/inputDebug.js.map +1 -0
  314. package/dist/tui/loadingMessages.d.ts +11 -0
  315. package/dist/tui/loadingMessages.d.ts.map +1 -0
  316. package/dist/tui/loadingMessages.js +39 -0
  317. package/dist/tui/loadingMessages.js.map +1 -0
  318. package/dist/tui/logFormat.d.ts +10 -0
  319. package/dist/tui/logFormat.d.ts.map +1 -0
  320. package/dist/tui/logFormat.js +86 -0
  321. package/dist/tui/logFormat.js.map +1 -0
  322. package/dist/tui/markdown.d.ts +3 -0
  323. package/dist/tui/markdown.d.ts.map +1 -0
  324. package/dist/tui/markdown.js +33 -0
  325. package/dist/tui/markdown.js.map +1 -0
  326. package/dist/tui/monitorApi.d.ts +6 -0
  327. package/dist/tui/monitorApi.d.ts.map +1 -0
  328. package/dist/tui/monitorApi.js +35 -0
  329. package/dist/tui/monitorApi.js.map +1 -0
  330. package/dist/tui/monitorRows.d.ts +43 -0
  331. package/dist/tui/monitorRows.d.ts.map +1 -0
  332. package/dist/tui/monitorRows.js +65 -0
  333. package/dist/tui/monitorRows.js.map +1 -0
  334. package/dist/tui/panels/ChatPanel.d.ts +16 -0
  335. package/dist/tui/panels/ChatPanel.d.ts.map +1 -0
  336. package/dist/tui/panels/ChatPanel.js +305 -0
  337. package/dist/tui/panels/ChatPanel.js.map +1 -0
  338. package/dist/tui/panels/LogsPanel.d.ts +8 -0
  339. package/dist/tui/panels/LogsPanel.d.ts.map +1 -0
  340. package/dist/tui/panels/LogsPanel.js +19 -0
  341. package/dist/tui/panels/LogsPanel.js.map +1 -0
  342. package/dist/tui/panels/MonitorPanel.d.ts +8 -0
  343. package/dist/tui/panels/MonitorPanel.d.ts.map +1 -0
  344. package/dist/tui/panels/MonitorPanel.js +19 -0
  345. package/dist/tui/panels/MonitorPanel.js.map +1 -0
  346. package/dist/tui/panels/PipelinePanel.d.ts +6 -0
  347. package/dist/tui/panels/PipelinePanel.d.ts.map +1 -0
  348. package/dist/tui/panels/PipelinePanel.js +22 -0
  349. package/dist/tui/panels/PipelinePanel.js.map +1 -0
  350. package/dist/tui/pipelineEvents.d.ts +21 -0
  351. package/dist/tui/pipelineEvents.d.ts.map +1 -0
  352. package/dist/tui/pipelineEvents.js +30 -0
  353. package/dist/tui/pipelineEvents.js.map +1 -0
  354. package/dist/tui/sse.d.ts +24 -0
  355. package/dist/tui/sse.d.ts.map +1 -0
  356. package/dist/tui/sse.js +78 -0
  357. package/dist/tui/sse.js.map +1 -0
  358. package/dist/tui/subagentTree.d.ts +10 -0
  359. package/dist/tui/subagentTree.d.ts.map +1 -0
  360. package/dist/tui/subagentTree.js +30 -0
  361. package/dist/tui/subagentTree.js.map +1 -0
  362. package/dist/tui/tabs.d.ts +10 -0
  363. package/dist/tui/tabs.d.ts.map +1 -0
  364. package/dist/tui/tabs.js +26 -0
  365. package/dist/tui/tabs.js.map +1 -0
  366. package/dist/tui/theme.d.ts +29 -0
  367. package/dist/tui/theme.d.ts.map +1 -0
  368. package/dist/tui/theme.js +34 -0
  369. package/dist/tui/theme.js.map +1 -0
  370. package/package.json +13 -3
@@ -1,3 +1,3 @@
1
- declare const DASHBOARD_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>OpenSwarm :: Supervisor</title>\n <style>\n /*\n * Design tokens \u2014 adapted from VEGA (GitHub Dark inspired).\n * Legacy variable names are preserved so the rest of the stylesheet keeps\n * working unchanged; only the values shifted to a calmer, more readable\n * palette while keeping the semantic intent (--green = primary action,\n * --amber = warning, --red = destructive, --dim = secondary text).\n */\n :root {\n --bg: #0d1117; /* page background */\n --bg2: #161b22; /* surface (cards, header) */\n --bg3: #1c2128; /* surface raised (hover) */\n --green: #58a6ff; /* primary accent (was matrix green) */\n --green-dim: rgba(88, 166, 255, 0.12);\n --green-mid: #58a6ff;\n --green-lo: #30363d;\n --cyan: #79c0ff;\n --cyan-dim: rgba(121, 192, 255, 0.14);\n --amber: #d29922;\n --red: #f85149;\n --white: #c9d1d9; /* primary text */\n --dim: #8b949e; /* muted text */\n --border: #30363d;\n --border2: rgba(48, 54, 61, 0.55);\n --radius-sm: 6px;\n --radius-md: 8px;\n --radius-lg: 12px;\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n html, body { height: 100%; overflow: hidden; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif;\n background: var(--bg);\n color: var(--white);\n font-size: 14px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n }\n /* Monospace contexts (logs, paths, IDs) still use a mono stack */\n code, pre, .mono, .log-line, .repo-item-path, .scan-path-row .path, .issue-id {\n font-family: \"SF Mono\", \"JetBrains Mono\", \"Fira Code\", Consolas, monospace;\n }\n\n /* ===== SCROLLBAR ===== */\n ::-webkit-scrollbar { width: 8px; height: 8px; }\n ::-webkit-scrollbar-track { background: transparent; }\n ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }\n ::-webkit-scrollbar-thumb:hover { background: var(--dim); }\n\n /* ===== HEADER ===== */\n header {\n height: 38px;\n background: var(--bg2);\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n padding: 0 1rem;\n gap: 0.75rem;\n flex-shrink: 0;\n }\n .hdr-logo {\n color: var(--green);\n font-weight: bold;\n font-size: 14px;\n letter-spacing: 0.15em;\n }\n .hdr-fullname { color: var(--dim); font-size: 11px; letter-spacing: 0.05em; margin-left: 0.25rem; }\n .hdr-sep { color: var(--dim); margin-left: 0.5rem; }\n .hdr-sub { color: var(--dim); font-size: 11px; letter-spacing: 0.1em; }\n .hdr-right { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }\n #sse-status {\n font-size: 10px;\n padding: 1px 6px;\n border: 1px solid var(--dim);\n color: var(--dim);\n letter-spacing: 0.1em;\n }\n #sse-status.connected { border-color: var(--green); color: var(--green); }\n #sse-status.disconnected { border-color: var(--red); color: var(--red); }\n .btn {\n font-family: inherit;\n font-size: 11px;\n font-weight: 500;\n padding: 4px 12px;\n background: var(--bg2);\n border: 1px solid var(--border);\n border-radius: var(--radius-sm);\n color: var(--white);\n cursor: pointer;\n letter-spacing: 0.04em;\n transition: border-color 0.15s, background 0.15s, color 0.15s;\n }\n .btn:hover:not(:disabled) { border-color: var(--green); color: var(--green); background: var(--green-dim); }\n .btn:disabled { opacity: 0.4; cursor: default; }\n .btn.primary { background: var(--green); border-color: var(--green); color: #0d1117; font-weight: 600; }\n .btn.primary:hover:not(:disabled) { opacity: 0.88; background: var(--green); color: #0d1117; }\n .btn-active { border-color: var(--amber); color: var(--amber); }\n .btn-active:hover:not(:disabled) { background: #332200; border-color: var(--amber); }\n .btn-danger { border-color: #551111; color: var(--red); }\n .btn-danger:hover:not(:disabled) { background: #220000; border-color: var(--red); }\n #turbo-btn { border-color: #553300; color: #ff8800; transition: all 0.3s; }\n #turbo-btn:hover:not(:disabled) { background: #221100; border-color: #ff8800; }\n #turbo-btn.turbo-active { background: #331800; border-color: #ff8800; color: #ffaa00; box-shadow: 0 0 8px rgba(255,136,0,0.3); animation: turbo-pulse 2s infinite; }\n @keyframes turbo-pulse { 0%,100% { box-shadow: 0 0 4px rgba(255,136,0,0.2); } 50% { box-shadow: 0 0 12px rgba(255,136,0,0.5); } }\n .move-to-todo-btn {\n font-family: inherit;\n font-size: 9px;\n padding: 1px 6px;\n background: transparent;\n border: 1px solid var(--cyan-dim);\n color: var(--cyan);\n cursor: pointer;\n margin-left: auto;\n flex-shrink: 0;\n transition: all 0.15s;\n }\n .move-to-todo-btn:hover:not(:disabled) { border-color: var(--cyan); background: var(--cyan-dim); }\n .move-to-todo-btn:disabled { opacity: 0.4; cursor: default; }\n .svc-group { display: flex; align-items: center; gap: 4px; margin-right: 8px; }\n .svc-status {\n font-size: 9px; padding: 1px 6px;\n border: 1px solid var(--dim); color: var(--dim);\n letter-spacing: 0.1em; text-transform: uppercase;\n }\n .svc-status.active { border-color: var(--green); color: var(--green); }\n .svc-status.inactive { border-color: var(--red); color: var(--red); }\n .svc-sep { color: var(--border); margin: 0 2px; }\n\n /* ===== STATS BAR ===== */\n .stats-bar {\n height: 36px;\n background: var(--bg2);\n border-bottom: 1px solid var(--border2);\n display: flex;\n align-items: center;\n padding: 0 1rem;\n gap: 1.5rem;\n flex-shrink: 0;\n }\n .stat {\n display: flex;\n align-items: baseline;\n gap: 0.4rem;\n }\n .stat-label { font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.1em; }\n .stat-val { font-size: 13px; font-weight: 500; color: var(--green); }\n .stat-val.amber { color: var(--amber); }\n .stat-val.cyan { color: var(--cyan); }\n .stat-val.red { color: #ff5555; }\n #stat-adapter, #stat-pair-adapters {\n font-size: 10px;\n font-weight: 400;\n letter-spacing: 0.02em;\n }\n .provider-toggle {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px;\n border: 1px solid var(--border);\n background: var(--bg3);\n }\n .provider-btn {\n font-family: inherit;\n font-size: 9px;\n line-height: 1;\n padding: 4px 8px;\n background: transparent;\n border: 1px solid transparent;\n color: var(--dim);\n cursor: pointer;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n }\n .provider-btn:hover:not(:disabled) {\n color: var(--white);\n border-color: var(--border);\n }\n .provider-btn.active {\n color: var(--green);\n border-color: var(--green-lo);\n background: var(--green-dim);\n }\n .stat-divider { color: var(--border); }\n\n /* ===== MAIN GRID ===== */\n .main-grid {\n display: grid;\n grid-template-columns: 290px 1fr 340px;\n height: calc(100vh - 74px);\n overflow: hidden;\n }\n .col {\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--border);\n overflow: hidden;\n }\n .col:last-child { border-right: none; }\n\n /* ===== PANEL ===== */\n .panel { display: flex; flex-direction: column; overflow: hidden; flex: 1; }\n .panel + .panel { border-top: 1px solid var(--border); }\n .panel-hdr {\n height: 28px;\n padding: 0 0.75rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n background: var(--bg3);\n border-bottom: 1px solid var(--border2);\n flex-shrink: 0;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.12em;\n color: var(--dim);\n }\n .panel-hdr-title { color: var(--green-mid); }\n .panel-hdr-badge {\n margin-left: auto;\n font-size: 9px;\n color: var(--dim);\n }\n .panel-body {\n flex: 1;\n overflow-y: auto;\n padding: 0.5rem;\n }\n .empty { color: var(--dim); font-size: 11px; text-align: center; padding: 1.5rem 0.5rem; }\n\n /* ===== PROJECTS ===== */\n .proj-card {\n border: 1px solid var(--border);\n margin-bottom: 4px;\n background: var(--bg2);\n }\n .proj-card.disabled { opacity: 0.45; }\n .proj-hdr {\n display: flex;\n align-items: center;\n padding: 5px 7px;\n gap: 6px;\n cursor: pointer;\n user-select: none;\n }\n .proj-hdr:hover { background: var(--green-dim); }\n .proj-arrow { color: var(--dim); font-size: 9px; width: 10px; flex-shrink: 0; }\n .proj-card.expanded .proj-arrow::before { content: \"\u25BC\"; }\n .proj-card:not(.expanded) .proj-arrow::before { content: \"\u25B6\"; }\n .proj-info { flex: 1; min-width: 0; }\n .proj-name { color: var(--green); font-size: 12px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .proj-path { color: var(--dim); font-size: 9px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .proj-counts { display: flex; gap: 3px; }\n .cnt { font-size: 9px; padding: 1px 4px; font-weight: bold; }\n .cnt-run { color: var(--green); border: 1px solid var(--green-lo); }\n .cnt-que { color: var(--amber); border: 1px solid #332200; }\n .cnt-pnd { color: var(--cyan); border: 1px solid var(--cyan-dim); }\n .proj-toggle { flex-shrink: 0; }\n .toggle { position: relative; display: inline-block; width: 30px; height: 16px; }\n .toggle input { opacity: 0; width: 0; height: 0; }\n .slider {\n position: absolute; cursor: pointer;\n top: 0; left: 0; right: 0; bottom: 0;\n background: #111; border: 1px solid var(--dim);\n border-radius: 16px; transition: 0.2s;\n }\n .slider:before {\n position: absolute; content: \"\";\n height: 10px; width: 10px;\n left: 2px; bottom: 2px;\n background: var(--dim); border-radius: 50%; transition: 0.2s;\n }\n input:checked + .slider { background: var(--green-dim); border-color: var(--green-lo); }\n input:checked + .slider:before { background: var(--green); transform: translateX(14px); }\n .proj-issues { border-top: 1px solid var(--border2); padding: 4px 7px; }\n .issue-sec-label {\n font-size: 9px; color: var(--dim); text-transform: uppercase;\n letter-spacing: 0.1em; margin: 4px 0 2px;\n }\n .issue-row {\n display: flex; align-items: center; gap: 4px;\n padding: 2px 0; font-size: 11px;\n border-bottom: 1px solid var(--border2);\n }\n .issue-row:last-child { border-bottom: none; }\n .git-info { color: var(--dim); font-size: 9px; display: flex; gap: 6px; align-items: center; }\n .git-branch-name { color: var(--cyan); }\n .git-dirty { color: var(--amber); }\n .git-sync { color: var(--dim); }\n .pr-row { display: flex; align-items: center; gap: 4px; padding: 2px 0; font-size: 11px; border-bottom: 1px solid var(--border2); }\n .pr-row:last-child { border-bottom: none; }\n .pr-num { color: var(--cyan); font-size: 9px; min-width: 32px; }\n .pr-branch { color: var(--green-lo); font-size: 9px; max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .pr-title { flex: 1; color: var(--white); font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .pr-age { color: var(--dim); font-size: 9px; flex-shrink: 0; }\n .idot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }\n .idot-run { background: var(--green); }\n .idot-que { background: var(--amber); }\n .idot-pnd { background: var(--dim); }\n .prio { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }\n .prio-1 { background: var(--red); }\n .prio-2 { background: var(--amber); }\n .prio-3 { background: var(--green-mid); }\n .prio-4 { background: var(--dim); }\n .issue-id { color: var(--cyan); font-size: 9px; min-width: 50px; }\n .issue-title { flex: 1; color: var(--white); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .issue-row.issue-backlog { opacity: 0.45; }\n\n /* ===== PROCESS ROW ===== */\n .proc-row {\n display: flex; align-items: center; gap: 6px;\n padding: 4px 6px; border-bottom: 1px solid var(--border2);\n font-size: 11px;\n }\n .proc-pid { color: var(--cyan); font-size: 10px; min-width: 42px; font-variant-numeric: tabular-nums; }\n .proc-stage { color: var(--green); min-width: 56px; font-weight: bold; text-transform: uppercase; font-size: 10px; }\n .proc-model { color: var(--dim); font-size: 9px; min-width: 56px; }\n .proc-dur { color: var(--amber); font-size: 9px; min-width: 42px; text-align: right; font-variant-numeric: tabular-nums; }\n .proc-activity { font-size: 10px; min-width: 16px; text-align: center; }\n .proc-kill {\n font-family: inherit; font-size: 9px; padding: 1px 5px;\n background: transparent; border: 1px solid #551111; color: var(--red);\n cursor: pointer; margin-left: auto;\n }\n .proc-kill:hover { background: #220000; border-color: var(--red); }\n\n /* ===== PIPELINE ===== */\n .stage-block {\n border-bottom: 1px solid var(--border2);\n }\n .stage-row {\n display: flex; align-items: center; gap: 6px;\n padding: 4px 8px;\n font-size: 11px;\n cursor: pointer;\n transition: background 0.12s;\n }\n .stage-row:hover { background: var(--bg3); }\n .stage-row.has-details::after {\n content: \"\u203A\"; color: var(--dim); margin-left: 4px;\n transition: transform 0.15s;\n }\n .stage-block.expanded .stage-row.has-details::after {\n transform: rotate(90deg); display: inline-block;\n }\n .sdot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; background: var(--dim); }\n .sdot.start { background: var(--amber); }\n .sdot.complete { background: var(--green); }\n .sdot.fail { background: var(--red); }\n .sname { color: var(--white); min-width: 70px; }\n .srepo { color: var(--dim); font-size: 10px; min-width: 50px; max-width: 90px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .stask { color: var(--cyan); font-size: 10px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .ssummary {\n color: var(--white); font-size: 11px; flex: 2;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n opacity: 0.85;\n }\n .selapsed { color: var(--amber); font-size: 9px; flex-shrink: 0; min-width: 36px; text-align: right; }\n .smodel { color: var(--dim); font-size: 9px; flex-shrink: 0; min-width: 56px; text-align: right; }\n .stokens { color: var(--amber); font-size: 9px; flex-shrink: 0; min-width: 80px; text-align: right; white-space: nowrap; }\n .sstatus { font-size: 9px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.06em; flex-shrink: 0; }\n /* Expanded details */\n .stage-details {\n display: none;\n padding: 8px 14px 10px 22px;\n background: rgba(0,0,0,0.18);\n font-size: 11px;\n color: var(--white);\n border-top: 1px dashed var(--border2);\n }\n .stage-block.expanded .stage-details { display: block; }\n .stage-details .sd-line { display: flex; gap: 8px; padding: 1px 0; align-items: baseline; }\n .stage-details .sd-key { color: var(--dim); font-size: 10px; min-width: 70px; text-transform: uppercase; letter-spacing: 0.06em; }\n .stage-details .sd-val { color: var(--white); font-size: 11px; flex: 1; word-break: break-all; }\n .stage-details ul { margin: 2px 0 4px 14px; padding: 0; }\n .stage-details li { font-family: \"SF Mono\", \"JetBrains Mono\", monospace; font-size: 10px; color: var(--cyan); padding: 1px 0; }\n .sd-decision-approve { color: var(--green); font-weight: 600; }\n .sd-decision-revise { color: var(--amber); font-weight: 600; }\n .sd-decision-reject { color: var(--red); font-weight: 600; }\n\n /* ===== LOG TAB BAR ===== */\n .log-tab-bar {\n display: flex; gap: 0; border-bottom: 1px solid #1a2a1a;\n padding: 0 4px; overflow-x: auto; flex-shrink: 0;\n }\n .log-tab {\n background: transparent; border: none; border-bottom: 2px solid transparent;\n color: var(--dim); font-family: inherit; font-size: 10px;\n padding: 4px 8px; cursor: pointer; white-space: nowrap;\n text-transform: uppercase; letter-spacing: .05em;\n }\n .log-tab:hover { color: var(--green-mid); }\n .log-tab.active { color: var(--green); border-bottom-color: var(--green); }\n\n /* ===== LOG ===== */\n .log-area { font-size: 11px; line-height: 1.5; padding: 4px 0; }\n .log-line { padding: 3px 8px; display: flex; gap: 6px; align-items: flex-start; border-radius: 2px; margin: 1px 0; }\n .log-line:hover { background: rgba(255,255,255,0.03); }\n .log-line.log-success .ltext { color: var(--green); }\n .log-line.log-fail .ltext { color: var(--red); }\n .log-line.log-warn .ltext { color: var(--amber); }\n .log-line.log-system { opacity: 0.6; }\n .log-line.log-heading { border-top: 1px solid var(--border2); margin-top: 6px; padding-top: 8px; }\n .ltime { color: var(--dim); font-size: 9px; flex-shrink: 0; min-width: 36px; opacity: 0.7; padding-top: 2px; font-variant-numeric: tabular-nums; }\n .licon { flex-shrink: 0; min-width: 14px; text-align: center; font-size: 11px; padding-top: 1px; }\n .ltag { color: var(--green-lo); min-width: 52px; flex-shrink: 0; padding-top: 1px; font-size: 10px; font-weight: 500; }\n .lstage { color: var(--cyan); min-width: 60px; flex-shrink: 0; font-size: 10px; padding-top: 1px; text-transform: uppercase; letter-spacing: 0.03em; opacity: 0.8; }\n .ltext { color: #99aa99; word-break: break-word; white-space: pre-wrap; flex: 1; min-width: 0; }\n .ltext .lhighlight { color: var(--white); font-weight: 500; }\n .ltext .lcost { color: var(--amber); font-size: 10px; }\n .ltext .lfiles { color: var(--cyan); font-size: 10px; }\n .log-line.log-spacer { height: 6px; padding: 0; margin: 0; min-height: 6px; }\n .log-line.log-separator { opacity: 0.2; padding: 0 8px; margin: 4px 0; }\n .log-line.log-separator .ltext { color: var(--dim); }\n .log-line.log-code .ltext { font-family: 'JetBrains Mono', 'Fira Code', monospace; color: var(--cyan); opacity: 0.8; font-size: 10px; }\n .log-line.log-heading2 .ltext { color: var(--white); font-weight: 600; font-size: 12px; }\n .log-line.log-tool .ltext { color: var(--dim); font-style: italic; font-size: 10px; }\n\n /* ===== CHAT ===== */\n .chat-col { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; }\n .chat-messages {\n flex: 1;\n overflow-y: auto;\n padding: 0.5rem;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n .chat-line { display: flex; gap: 6px; font-size: 12px; line-height: 1.5; }\n .chat-prefix {\n flex-shrink: 0;\n font-weight: bold;\n }\n .chat-user .chat-prefix { color: var(--amber); }\n .chat-agent .chat-prefix { color: var(--cyan); }\n .chat-text { color: var(--white); white-space: pre-wrap; word-break: break-word; flex: 1; }\n .chat-agent .chat-text { color: var(--white); }\n .chat-user .chat-text { color: var(--amber); }\n .chat-ts { color: var(--dim); font-size: 9px; flex-shrink: 0; align-self: flex-start; padding-top: 2px; }\n .chat-thinking { animation: blink 1s infinite; color: var(--cyan); }\n @keyframes blink { 0%,100% { opacity:1; } 50% { opacity:0.3; } }\n\n .chat-input-area {\n border-top: 1px solid var(--border);\n padding: 6px 8px;\n display: flex;\n align-items: center;\n gap: 6px;\n background: var(--bg3);\n flex-shrink: 0;\n }\n .chat-prompt { color: var(--green); font-size: 12px; flex-shrink: 0; }\n .chat-input {\n flex: 1;\n background: transparent;\n border: none;\n outline: none;\n font-family: inherit;\n font-size: 12px;\n color: var(--green);\n caret-color: var(--green);\n }\n .chat-input::placeholder { color: var(--dim); }\n .chat-send {\n font-family: inherit;\n font-size: 10px;\n padding: 2px 8px;\n background: transparent;\n border: 1px solid var(--green-lo);\n color: var(--green-mid);\n cursor: pointer;\n }\n .chat-send:hover { border-color: var(--green); color: var(--green); }\n .chat-send:disabled { opacity: 0.3; cursor: default; }\n\n /* ===== REPO PICKER ===== */\n .repo-item {\n display: flex; align-items: center; gap: 8px;\n padding: 5px 12px; cursor: pointer; font-size: 11px;\n }\n .repo-item:hover { background: var(--green-dim); }\n .repo-item-name { color: var(--green); font-weight: bold; }\n .repo-item-path { color: var(--dim); font-size: 10px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .repo-item-badge { font-size: 9px; padding: 1px 5px; border: 1px solid var(--green-lo); color: var(--green-mid); flex-shrink: 0; }\n\n .scan-path-row { display: flex; align-items: center; gap: 6px; padding: 3px 0; font-size: 11px; }\n .scan-path-row .path { color: var(--dim); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .scan-path-badge { font-size: 9px; padding: 1px 5px; border: 1px solid #333; color: #556655; flex-shrink: 0; }\n .scan-path-remove { background: transparent; border: none; color: #553333; cursor: pointer; font-size: 12px; padding: 0 2px; flex-shrink: 0; }\n .scan-path-remove:hover { color: var(--red); }\n\n /* ===== TAB BAR (hidden on desktop) ===== */\n .tab-bar { display: none; }\n\n /* ===== MOBILE RESPONSIVE ===== */\n @media (max-width: 768px) {\n html, body { overflow: auto; }\n\n /* Header */\n header {\n height: auto;\n min-height: 38px;\n flex-wrap: wrap;\n padding: 6px 0.75rem;\n gap: 4px;\n }\n .hdr-fullname { display: none; }\n .hdr-right { width: 100%; justify-content: flex-end; }\n .svc-group .btn { font-size: 0; padding: 4px 8px; min-height: 32px; }\n .svc-group #svc-stop-btn::after { content: \"\\23F8\"; font-size: 12px; }\n .svc-group #svc-restart-btn::after { content: \"\\21BB\"; font-size: 12px; }\n #hb-btn { min-height: 32px; }\n\n /* Stats bar */\n .stats-bar {\n height: auto;\n min-height: 32px;\n flex-wrap: wrap;\n padding: 4px 0.75rem;\n gap: 0.5rem;\n font-size: 11px;\n }\n .stat-divider { display: none; }\n\n /* Tab bar */\n .tab-bar {\n display: flex;\n background: var(--bg2);\n border-bottom: 1px solid var(--border);\n }\n .tab {\n flex: 1;\n font-family: inherit;\n font-size: 11px;\n letter-spacing: 0.1em;\n padding: 10px 0;\n min-height: 44px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: var(--dim);\n cursor: pointer;\n text-align: center;\n transition: all 0.15s;\n }\n .tab.active {\n color: var(--green);\n border-bottom-color: var(--green);\n }\n\n /* Main grid \u2192 single column */\n .main-grid {\n display: flex;\n flex-direction: column;\n height: auto;\n min-height: calc(100vh - 160px);\n overflow: visible;\n }\n .col {\n display: none;\n border-right: none;\n overflow: visible;\n min-height: calc(100vh - 200px);\n }\n .col.mob-active {\n display: flex;\n flex: 1;\n }\n\n /* Repo picker \u2192 fullscreen */\n #repo-picker > div {\n width: 100% !important;\n max-height: 90vh !important;\n margin: 5vh 0 0;\n }\n .repo-item { min-height: 44px; }\n\n /* Chat input \u2192 sticky bottom */\n .chat-input-area {\n position: sticky;\n bottom: 0;\n z-index: 10;\n min-height: 44px;\n }\n .chat-input { min-height: 32px; font-size: 14px; }\n .chat-send { min-height: 36px; padding: 4px 12px; }\n\n /* Touch targets */\n .btn { min-height: 36px; padding: 4px 10px; }\n .proj-hdr { min-height: 44px; padding: 8px 7px; }\n .toggle { width: 40px; height: 22px; }\n .slider:before { height: 14px; width: 14px; left: 3px; bottom: 3px; }\n input:checked + .slider:before { transform: translateX(18px); }\n }\n </style>\n</head>\n<body>\n <!-- HEADER -->\n <header>\n <span class=\"hdr-logo\">OpenSwarm</span>\n <span class=\"hdr-fullname\">: Vector-Encoded General Agent</span>\n <span class=\"hdr-sep\">::</span>\n <span class=\"hdr-sub\">SUPERVISOR</span>\n <a href=\"/issues\" style=\"color:var(--cyan);font-size:11px;text-decoration:none;margin-left:1rem;letter-spacing:0.1em;border:1px solid var(--cyan-dim);padding:2px 8px;border-radius:3px\">ISSUES</a>\n <div class=\"hdr-right\">\n <div class=\"svc-group\">\n <span class=\"svc-status\" id=\"svc-status\">...</span>\n <span class=\"svc-sep\">\u2502</span>\n <div class=\"provider-toggle\">\n <button class=\"provider-btn\" id=\"provider-claude\" onclick=\"switchProvider('claude')\">Claude</button>\n <button class=\"provider-btn\" id=\"provider-codex\" onclick=\"switchProvider('codex')\">Codex</button>\n </div>\n <span class=\"svc-sep\">\u2502</span>\n <button class=\"btn\" id=\"turbo-btn\" onclick=\"toggleTurbo()\" title=\"Turbo: 5min heartbeat, 20 daily cap, 4h auto-expire\">TURBO</button>\n <span class=\"svc-sep\">\u2502</span>\n <button class=\"btn btn-danger\" id=\"svc-stop-btn\" onclick=\"svcAction('stop')\">\u23F8 STOP</button>\n <button class=\"btn\" id=\"svc-restart-btn\" onclick=\"svcAction('restart')\">\u21BB RESTART</button>\n </div>\n <span id=\"sse-status\">CONNECTING</span>\n <button class=\"btn btn-active\" id=\"hb-btn\" onclick=\"triggerHeartbeat()\">\u25B6 HEARTBEAT</button>\n <button class=\"btn\" id=\"pr-proc-btn\" onclick=\"triggerPRProcessor()\">\u21BB PR REVIEW</button>\n </div>\n </header>\n\n <!-- STATS BAR -->\n <div class=\"stats-bar\">\n <div class=\"stat\"><span class=\"stat-label\">RUN</span><span class=\"stat-val\" id=\"stat-running\">0</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">QUEUE</span><span class=\"stat-val amber\" id=\"stat-queued\">0</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">DONE</span><span class=\"stat-val\" id=\"stat-completed\">0</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">PACE</span><span class=\"stat-val\" id=\"stat-pace\">-</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">SSE</span><span class=\"stat-val cyan\" id=\"stat-sse\">-</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">CLI</span><span class=\"stat-val cyan\" id=\"stat-adapter\">-</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">PAIR</span><span class=\"stat-val cyan\" id=\"stat-pair-adapters\">-</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">UPTIME</span><span class=\"stat-val\" id=\"stat-uptime\">-</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">COST</span><span class=\"stat-val cyan\" id=\"stat-cost\">$0.00</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">QUOTA 5h</span><span class=\"stat-val\" id=\"stat-quota-5h\">-</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">QUOTA 7d</span><span class=\"stat-val\" id=\"stat-quota-7d\">-</span></div>\n </div>\n\n <!-- TAB BAR (mobile only) -->\n <div class=\"tab-bar\">\n <button class=\"tab active\" data-tab=\"0\">REPOS</button>\n <button class=\"tab\" data-tab=\"1\">PIPELINE</button>\n <button class=\"tab\" data-tab=\"2\">CHAT</button>\n </div>\n\n <!-- MAIN GRID -->\n <div class=\"main-grid\">\n\n <!-- LEFT: REPOSITORIES -->\n <div class=\"col\">\n <div class=\"panel\">\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">REPOSITORIES</span>\n <span class=\"panel-hdr-badge\" id=\"proj-summary\"></span>\n <button class=\"btn\" style=\"margin-left:auto;font-size:9px;padding:1px 6px\" onclick=\"openRepoPicker()\">+ ADD</button>\n </div>\n <div class=\"panel-body\" id=\"project-list\">\n <div class=\"empty\">loading...</div>\n </div>\n </div>\n <div class=\"panel\" id=\"monitor-panel\">\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">MONITORS & PROCESSES</span>\n <span class=\"panel-hdr-badge\" id=\"monitor-count\"></span>\n </div>\n <div class=\"panel-body\" id=\"monitor-list\">\n <div class=\"empty\">no monitors or processes</div>\n </div>\n </div>\n </div>\n\n <!-- REPO PICKER OVERLAY -->\n <div id=\"repo-picker\" style=\"display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);backdrop-filter:blur(4px);z-index:100;align-items:center;justify-content:center\">\n <div style=\"background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius-lg);width:560px;max-height:75vh;display:flex;flex-direction:column;box-shadow:0 10px 32px rgba(0,0,0,0.5)\">\n <div style=\"padding:14px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px\">\n <span style=\"color:var(--white);font-size:13px;font-weight:600;letter-spacing:.04em\">Add repository</span>\n <button onclick=\"closeRepoPicker()\" style=\"margin-left:auto;background:transparent;border:none;color:var(--dim);cursor:pointer;font-size:18px;line-height:1;padding:0 4px;border-radius:4px\" onmouseover=\"this.style.color='var(--white)'\" onmouseout=\"this.style.color='var(--dim)'\">\u2715</button>\n </div>\n <div style=\"padding:10px 18px;border-bottom:1px solid var(--border2)\">\n <input id=\"repo-search\" type=\"text\" placeholder=\"Filter repositories\u2026\"\n style=\"width:100%;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);outline:none;font-family:inherit;font-size:13px;color:var(--white);padding:7px 10px;caret-color:var(--green)\"\n oninput=\"filterRepos(this.value)\" onkeydown=\"if(event.key==='Escape')closeRepoPicker()\">\n </div>\n <div id=\"repo-picker-list\" style=\"overflow-y:auto;flex:1;padding:6px 0\"></div>\n <div id=\"scan-paths-section\" style=\"border-top:1px solid var(--border);padding:12px 18px;background:rgba(13,17,23,0.4)\">\n <div style=\"color:var(--dim);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px\">Scan paths</div>\n <div id=\"scan-paths-list\"></div>\n <div style=\"display:flex;gap:6px;margin-top:10px\">\n <button class=\"btn\" style=\"flex:1;justify-content:center\" onclick=\"openFolderBrowser()\">\uD83D\uDCC1 Browse for folder\u2026</button>\n <button class=\"btn\" style=\"font-size:11px\" onclick=\"toggleManualPathInput()\" title=\"Type a path manually\">\u2328</button>\n </div>\n <div id=\"manual-path-row\" style=\"display:none;gap:6px;margin-top:6px\">\n <input id=\"scan-path-input\" type=\"text\" placeholder=\"/absolute/path/to/scan\"\n style=\"flex:1;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);outline:none;font-family:inherit;font-size:12px;color:var(--white);padding:5px 8px;caret-color:var(--green)\"\n onkeydown=\"if(event.key==='Enter')addScanPath()\">\n <button class=\"btn primary\" style=\"font-size:11px\" onclick=\"addScanPath()\">Add</button>\n </div>\n </div>\n </div>\n </div>\n\n <!-- FOLDER BROWSER OVERLAY (native-style picker, server-side fs) -->\n <div id=\"folder-browser\" style=\"display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);backdrop-filter:blur(4px);z-index:110;align-items:center;justify-content:center\">\n <div style=\"background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius-lg);width:620px;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 10px 32px rgba(0,0,0,0.5)\">\n <div style=\"padding:14px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px\">\n <span style=\"color:var(--white);font-size:13px;font-weight:600;letter-spacing:.04em\">Choose folder to scan</span>\n <button onclick=\"closeFolderBrowser()\" style=\"margin-left:auto;background:transparent;border:none;color:var(--dim);cursor:pointer;font-size:18px;line-height:1;padding:0 4px;border-radius:4px\" onmouseover=\"this.style.color='var(--white)'\" onmouseout=\"this.style.color='var(--dim)'\">\u2715</button>\n </div>\n <div style=\"padding:10px 18px;border-bottom:1px solid var(--border2);display:flex;align-items:center;gap:6px\">\n <button class=\"btn\" id=\"fb-up\" style=\"font-size:11px;padding:3px 10px\" onclick=\"folderBrowserUp()\" title=\"Parent directory\">\u2191 Up</button>\n <input id=\"fb-path\" type=\"text\" readonly\n style=\"flex:1;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);outline:none;font-family:'SF Mono',monospace;font-size:12px;color:var(--white);padding:5px 10px\">\n </div>\n <div id=\"fb-list\" style=\"overflow-y:auto;flex:1;padding:4px 0\"></div>\n <div style=\"padding:12px 18px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end\">\n <button class=\"btn\" onclick=\"closeFolderBrowser()\">Cancel</button>\n <button class=\"btn primary\" id=\"fb-select\" onclick=\"folderBrowserSelect()\">Select this folder</button>\n </div>\n </div>\n </div>\n\n <!-- MIDDLE: PIPELINE + LOG -->\n <div class=\"col\">\n <div class=\"panel\" style=\"flex: 0 0 38%\">\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">PIPELINE</span>\n <span class=\"panel-hdr-badge\" id=\"stage-count\"></span>\n </div>\n <div class=\"panel-body\" id=\"stage-list\">\n <div class=\"empty\">no pipeline events</div>\n </div>\n </div>\n <div class=\"panel\">\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">LIVE LOG</span>\n <span class=\"panel-hdr-badge\" id=\"log-count\"></span>\n </div>\n <div class=\"log-tab-bar\" id=\"log-tab-bar\">\n <button class=\"log-tab active\" data-task=\"all\" onclick=\"selectLogTab(null)\">ALL</button>\n </div>\n <div class=\"panel-body log-area\" id=\"log-list\">\n <div class=\"empty\">no log output</div>\n </div>\n </div>\n </div>\n\n <!-- RIGHT: CHAT -->\n <div class=\"col\">\n <!-- PR PROCESSOR -->\n <div class=\"panel\" style=\"flex: 0 0 auto;\">\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">PR PROCESSOR</span>\n <span class=\"panel-hdr-badge\" id=\"pr-proc-badge\"></span>\n </div>\n <div class=\"panel-body\" style=\"font-size: 11px; line-height: 1.5;\">\n <div id=\"pr-proc-body\" style=\"color: var(--dim);\">Loading...</div>\n </div>\n </div>\n\n <!-- STUCK/FAILED ISSUES -->\n <div class=\"panel\" style=\"flex: 0 0 auto; max-height: 200px;\">\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">\u26A0 STUCK/FAILED</span>\n <span class=\"panel-hdr-badge\" id=\"stuck-badge\">0</span>\n <button class=\"btn\" style=\"margin-left: 0.5rem; font-size: 9px; padding: 1px 6px;\" onclick=\"restartStuckIssues()\" id=\"restart-stuck-btn\">\u21BB RESTART ALL</button>\n </div>\n <div class=\"panel-body\" style=\"font-size: 10px; line-height: 1.4; overflow-y: auto;\">\n <div id=\"stuck-list\" style=\"color: var(--dim);\">Loading...</div>\n </div>\n </div>\n\n <!-- AGENT CHAT -->\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">AGENT CHAT</span>\n <span class=\"panel-hdr-badge\" id=\"chat-status\"></span>\n </div>\n <div class=\"chat-col\">\n <div class=\"chat-messages\" id=\"chat-messages\"></div>\n <div class=\"chat-input-area\">\n <span class=\"chat-prompt\">&gt;</span>\n <input\n class=\"chat-input\" id=\"chat-input\"\n type=\"text\" placeholder=\"message OpenSwarm...\"\n onkeydown=\"if(event.key==='Enter')sendChat()\"\n >\n <button class=\"chat-send\" id=\"chat-send\" onclick=\"sendChat()\">SEND</button>\n </div>\n </div>\n </div>\n\n </div>\n\n <script>\n const MAX_LOG = 200;\n const MAX_STAGE = 100;\n\n let projects = [];\n let expandedProjects = new Set();\n let knowledgeCache = {};\n let logLines = [];\n let selectedLogTaskId = null; // null = ALL, string = specific taskId\n let stageRows = [];\n let chatBusy = false;\n let totalCostUsd = 0;\n const taskProjectMap = new Map();\n // taskId \u2192 { title, issueIdentifier } for pipeline display\n const taskTitleMap = new Map();\n // taskId \u2192 start timestamp for elapsed time\n const taskStartMap = new Map();\n\n // ---- SSE ----\n function connectSSE(skipReplay) {\n const url = skipReplay ? \"/api/events?skipReplay=1\" : \"/api/events\";\n const es = new EventSource(url);\n const statusEl = document.getElementById(\"sse-status\");\n es.onopen = () => { statusEl.textContent = \"LIVE\"; statusEl.className = \"connected\"; };\n es.onmessage = e => {\n let ev; try { ev = JSON.parse(e.data); } catch { return; }\n handleEvent(ev);\n };\n es.onerror = () => {\n statusEl.textContent = \"RECONNECTING\"; statusEl.className = \"disconnected\";\n es.close(); setTimeout(function() { connectSSE(false); }, 3000);\n };\n }\n\n function handleEvent(ev) {\n switch (ev.type) {\n case \"stats\": updateStats(ev.data); break;\n case \"task:queued\":\n taskProjectMap.set(ev.data.taskId, ev.data.projectPath);\n taskTitleMap.set(ev.data.taskId, { title: ev.data.title, issueIdentifier: ev.data.issueIdentifier });\n updateProjectTask(ev.data.projectPath, ev.data.taskId, ev.data.title, ev.data.priority, \"queued\");\n break;\n case \"task:started\": {\n const p = taskProjectMap.get(ev.data.taskId);\n if (ev.data.title) taskTitleMap.set(ev.data.taskId, { title: ev.data.title, issueIdentifier: ev.data.issueIdentifier });\n taskStartMap.set(ev.data.taskId, Date.now());\n if (p) updateProjectTask(p, ev.data.taskId, ev.data.title, null, \"running\");\n break;\n }\n case \"task:completed\": {\n const p = taskProjectMap.get(ev.data.taskId);\n if (p) removeProjectTask(p, ev.data.taskId);\n break;\n }\n case \"pipeline:stage\": addStageRow(ev.data); break;\n case \"pipeline:iteration\":\n addStageRow({ taskId: ev.data.taskId, stage: \"iter #\" + ev.data.iteration, status: \"start\" });\n break;\n case \"log\": addLogLine(ev.data); break;\n case \"project:toggled\": {\n const p = projects.find(x => x.path === ev.data.projectPath);\n if (p) { p.enabled = ev.data.enabled; renderProjects(); }\n break;\n }\n case \"task:cost\": {\n totalCostUsd += ev.data.cost?.costUsd ?? 0;\n document.getElementById(\"stat-cost\").textContent = \"$\" + totalCostUsd.toFixed(2);\n break;\n }\n case \"chat:agent\": appendChatMsg(\"agent\", ev.data.text, null, ev.data.ts); break;\n case \"monitor:checked\":\n case \"monitor:stateChange\":\n fetchMonitors();\n break;\n case \"process:spawn\":\n fetchProcesses();\n addLogLine({ taskId: ev.data.taskId || \"system\", stage: ev.data.stage || \"spawn\", line: \"Process spawned PID=\" + ev.data.pid + \" stage=\" + ev.data.stage + (ev.data.model ? \" model=\" + ev.data.model : \"\") });\n break;\n case \"process:exit\":\n fetchProcesses();\n addLogLine({ taskId: \"system\", stage: \"exit\", line: \"Process exited PID=\" + ev.data.pid + \" code=\" + ev.data.exitCode + \" duration=\" + (ev.data.durationMs / 1000).toFixed(1) + \"s\" });\n break;\n case \"heartbeat\": {\n const btn = document.getElementById(\"hb-btn\");\n btn.disabled = false; btn.textContent = \"\u25B6 HEARTBEAT\";\n break;\n }\n case \"pr_processor_start\":\n case \"pr_processor_end\":\n case \"pr_processor_pr\":\n fetchPRProcessorStatus();\n break;\n }\n }\n\n // ---- Stats ----\n function updateStats(data) {\n function shortModel(model) {\n if (!model) return \"-\";\n return model.length > 18 ? model.slice(0, 15) + \"...\" : model;\n }\n\n document.getElementById(\"stat-running\").textContent = data.runningTasks ?? 0;\n document.getElementById(\"stat-queued\").textContent = data.queuedTasks ?? 0;\n document.getElementById(\"stat-completed\").textContent = data.completedToday ?? 0;\n const defaultAdapter = data.adapters?.defaultAdapter ?? \"-\";\n const workerAdapter = data.adapters?.worker?.adapter ?? \"-\";\n const workerModel = shortModel(data.adapters?.worker?.model);\n const reviewerAdapter = data.adapters?.reviewer?.adapter ?? \"-\";\n const reviewerModel = shortModel(data.adapters?.reviewer?.model);\n const chatModel = workerModel || \"-\";\n document.getElementById(\"stat-adapter\").textContent = defaultAdapter;\n document.getElementById(\"stat-pair-adapters\").textContent =\n \"W \" + workerAdapter + \":\" + workerModel + \" / R \" + reviewerAdapter + \":\" + reviewerModel;\n document.getElementById(\"chat-status\").textContent = defaultAdapter + \":\" + chatModel;\n document.getElementById(\"provider-claude\").classList.toggle(\"active\", defaultAdapter === \"claude\");\n document.getElementById(\"provider-codex\").classList.toggle(\"active\", defaultAdapter === \"codex\");\n if (data.uptime != null) {\n const s = Math.floor(data.uptime / 1000);\n const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), ss = s % 60;\n document.getElementById(\"stat-uptime\").textContent =\n (h ? h + \"h \" : \"\") + (m ? m + \"m \" : \"\") + ss + \"s\";\n }\n // Turbo mode\n const turboBtn = document.getElementById(\"turbo-btn\");\n if (turboBtn) {\n turboBtn.classList.toggle(\"turbo-active\", !!data.turboMode);\n if (data.turboMode && data.turboExpiresAt) {\n const remainMin = Math.max(0, Math.round((data.turboExpiresAt - Date.now()) / 60000));\n turboBtn.textContent = \"TURBO \" + remainMin + \"m\";\n } else {\n turboBtn.textContent = \"TURBO\";\n }\n }\n // Daily pace\n const paceEl = document.getElementById(\"stat-pace\");\n if (paceEl && data.dailyPace) {\n const cap = data.turboMode ? 20 : 6;\n paceEl.textContent = data.dailyPace.completedToday + \"/\" + cap;\n paceEl.className = \"stat-val\" + (data.turboMode ? \" amber\" : \"\");\n }\n }\n\n // ---- Service control ----\n async function fetchSvcStatus() {\n try {\n const res = await fetch(\"/api/service/status\");\n const data = await res.json();\n const el = document.getElementById(\"svc-status\");\n const status = data.status || \"unknown\";\n el.textContent = status;\n el.className = \"svc-status \" + (status === \"active\" ? \"active\" : \"inactive\");\n } catch {\n const el = document.getElementById(\"svc-status\");\n el.textContent = \"unknown\";\n el.className = \"svc-status inactive\";\n }\n }\n\n // ---- Stuck/Failed Issues ----\n async function fetchStuckIssues() {\n try {\n const res = await fetch(\"/api/stuck-issues\");\n const data = await res.json();\n const list = document.getElementById(\"stuck-list\");\n const badge = document.getElementById(\"stuck-badge\");\n\n const totalStuck = data.stuckIssues?.length ?? 0;\n const totalFailed = data.failedIssues?.length ?? 0;\n const total = totalStuck + totalFailed;\n\n badge.textContent = total;\n badge.style.color = total > 0 ? \"var(--red)\" : \"var(--dim)\";\n\n if (total === 0) {\n list.innerHTML = '<div style=\"color: var(--green-mid); padding: 4px;\">\u2713 All issues healthy</div>';\n return;\n }\n\n let html = '';\n\n // Stuck issues (In Progress for >7 days)\n if (totalStuck > 0) {\n html += '<div style=\"color: var(--amber); font-weight: bold; margin-bottom: 4px; font-size: 9px; text-transform: uppercase;\">\u23F1 Stuck (' + totalStuck + ')</div>';\n data.stuckIssues.forEach(issue => {\n const priorityColor = issue.priority === 1 ? 'var(--red)' : issue.priority === 2 ? 'var(--amber)' : 'var(--dim)';\n html += '<div style=\"margin-bottom: 6px; padding: 4px; border-left: 2px solid ' + priorityColor + '; background: rgba(255, 170, 0, 0.05);\">';\n html += '<div style=\"color: var(--white); font-size: 10px; margin-bottom: 2px;\">' + issue.identifier + ': ' + issue.title.substring(0, 40) + (issue.title.length > 40 ? '...' : '') + '</div>';\n html += '<div style=\"color: var(--amber); font-size: 9px;\">' + issue.reason + '</div>';\n if (issue.project?.name) {\n html += '<div style=\"color: var(--dim); font-size: 9px; margin-top: 2px;\">\uD83D\uDCC1 ' + issue.project.name + '</div>';\n }\n html += '</div>';\n });\n }\n\n // Failed issues (retry, failed, blocked labels)\n if (totalFailed > 0) {\n if (totalStuck > 0) html += '<div style=\"height: 8px;\"></div>';\n html += '<div style=\"color: var(--red); font-weight: bold; margin-bottom: 4px; font-size: 9px; text-transform: uppercase;\">\u2716 Failed (' + totalFailed + ')</div>';\n data.failedIssues.forEach(issue => {\n const priorityColor = issue.priority === 1 ? 'var(--red)' : issue.priority === 2 ? 'var(--amber)' : 'var(--dim)';\n html += '<div style=\"margin-bottom: 6px; padding: 4px; border-left: 2px solid ' + priorityColor + '; background: rgba(255, 51, 51, 0.05);\">';\n html += '<div style=\"color: var(--white); font-size: 10px; margin-bottom: 2px;\">' + issue.identifier + ': ' + issue.title.substring(0, 40) + (issue.title.length > 40 ? '...' : '') + '</div>';\n html += '<div style=\"color: var(--red); font-size: 9px;\">' + issue.reason + '</div>';\n if (issue.project?.name) {\n html += '<div style=\"color: var(--dim); font-size: 9px; margin-top: 2px;\">\uD83D\uDCC1 ' + issue.project.name + '</div>';\n }\n html += '</div>';\n });\n }\n\n list.innerHTML = html;\n } catch (err) {\n console.error(\"Failed to fetch stuck issues:\", err);\n document.getElementById(\"stuck-list\").innerHTML = '<div style=\"color: var(--red);\">Error loading</div>';\n }\n }\n\n // ---- PR Processor Status ----\n async function fetchPRProcessorStatus() {\n try {\n const res = await fetch(\"/api/pr-processor-status\");\n const data = await res.json();\n const body = document.getElementById(\"pr-proc-body\");\n const badge = document.getElementById(\"pr-proc-badge\");\n\n if (!data) {\n body.innerHTML = '<div style=\"color: var(--dim);\">Not configured</div>';\n badge.textContent = \"OFF\";\n badge.style.color = \"var(--dim)\";\n return;\n }\n\n const status = data.processing ? \"RUNNING\" : \"IDLE\";\n badge.textContent = status;\n badge.style.color = data.processing ? \"var(--green)\" : \"var(--cyan)\";\n\n const formatTime = (ts) => {\n if (!ts) return \"N/A\";\n const d = new Date(ts);\n return d.toLocaleTimeString(\"en-US\", { hour: \"2-digit\", minute: \"2-digit\" });\n };\n\n let html = '<div style=\"display: flex; flex-direction: column; gap: 6px;\">';\n html += '<div><span style=\"color: var(--dim);\">Schedule:</span> <span style=\"color: var(--text);\">' + (data.schedule || \"N/A\") + '</span></div>';\n html += '<div><span style=\"color: var(--dim);\">Repos:</span> <span style=\"color: var(--text);\">' + (data.repos?.length || 0) + '</span></div>';\n\n if (data.currentPR) {\n html += '<div><span style=\"color: var(--amber);\">Processing:</span> <span style=\"color: var(--text); font-family: monospace; font-size: 10px;\">' + data.currentPR + '</span></div>';\n }\n\n html += '<div><span style=\"color: var(--dim);\">Last run:</span> <span style=\"color: var(--text);\">' + formatTime(data.lastRun) + '</span></div>';\n html += '<div><span style=\"color: var(--dim);\">Next run:</span> <span style=\"color: var(--text);\">' + formatTime(data.nextRun) + '</span></div>';\n\n if (data.conflictResolverEnabled) {\n html += '<div style=\"color: var(--green); font-size: 10px; margin-top: 4px;\">\u2713 Conflict Resolver: ON</div>';\n }\n\n html += '</div>';\n body.innerHTML = html;\n } catch (e) {\n const body = document.getElementById(\"pr-proc-body\");\n body.innerHTML = '<div style=\"color: var(--red);\">Error: ' + e.message + '</div>';\n }\n }\n\n async function svcAction(action) {\n const label = action === \"stop\" ? \"STOP\" : \"RESTART\";\n if (!confirm(\"Are you sure you want to \" + label + \" the service?\")) return;\n const btnId = action === \"stop\" ? \"svc-stop-btn\" : \"svc-restart-btn\";\n const btn = document.getElementById(btnId);\n btn.disabled = true;\n try {\n await fetch(\"/api/service/\" + action, { method: \"POST\" });\n addLogLine({ taskId: \"system\", stage: \"service\", line: \"Service \" + action + \" requested\" });\n } catch(e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"Service \" + action + \" failed: \" + e.message });\n }\n btn.disabled = false;\n setTimeout(fetchSvcStatus, 2000);\n }\n\n // ---- Heartbeat trigger ----\n async function triggerHeartbeat() {\n const btn = document.getElementById(\"hb-btn\");\n btn.disabled = true; btn.textContent = \"\u27F3 RUNNING\";\n addLogLine({ taskId: \"system\", stage: \"manual\", line: \"Heartbeat triggered by user\" });\n try {\n await fetch(\"/api/heartbeat\", { method: \"POST\" });\n } catch(e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"Heartbeat failed: \" + e.message });\n btn.disabled = false; btn.textContent = \"\u25B6 HEARTBEAT\";\n }\n }\n\n async function toggleTurbo() {\n const btn = document.getElementById(\"turbo-btn\");\n const isActive = btn.classList.contains(\"turbo-active\");\n const newState = !isActive;\n if (newState && !confirm(\"Enable TURBO mode? (5min heartbeat, 20 daily cap, auto-expires in 4h)\")) return;\n btn.disabled = true;\n try {\n const res = await fetch(\"/api/turbo\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ enabled: newState })\n });\n if (!res.ok) throw new Error(\"Failed\");\n addLogLine({ taskId: \"system\", stage: \"turbo\", line: newState ? \"TURBO MODE ON\" : \"TURBO MODE OFF\" });\n const stats = await fetch(\"/api/stats\").then(r => r.json());\n updateStats(stats);\n } catch (e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"Turbo toggle failed: \" + e.message });\n }\n btn.disabled = false;\n }\n\n async function switchProvider(provider) {\n try {\n const res = await fetch(\"/api/provider\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ provider })\n });\n if (!res.ok) {\n const data = await res.json().catch(() => ({}));\n throw new Error(data.error || \"Failed to switch provider\");\n }\n addLogLine({ taskId: \"system\", stage: \"provider\", line: \"Provider switched to \" + provider });\n const stats = await fetch(\"/api/stats\").then(r => r.json());\n updateStats(stats);\n } catch (e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"Provider switch failed: \" + e.message });\n }\n }\n\n // ---- PR Processor trigger ----\n async function triggerPRProcessor() {\n const btn = document.getElementById(\"pr-proc-btn\");\n btn.disabled = true; btn.textContent = \"\u27F3 PROCESSING\";\n addLogLine({ taskId: \"system\", stage: \"manual\", line: \"PR Processor triggered by user\" });\n try {\n const res = await fetch(\"/api/trigger-pr-processor\", { method: \"POST\" });\n if (!res.ok) {\n const data = await res.json();\n throw new Error(data.error || \"Failed to trigger PR processor\");\n }\n addLogLine({ taskId: \"system\", stage: \"manual\", line: \"PR Processor started successfully\" });\n setTimeout(() => {\n btn.disabled = false;\n btn.textContent = \"\u21BB PR REVIEW\";\n }, 3000);\n } catch(e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"PR Processor failed: \" + e.message });\n btn.disabled = false; btn.textContent = \"\u21BB PR REVIEW\";\n }\n }\n\n // ---- Restart stuck issues ----\n async function restartStuckIssues() {\n if (!confirm(\"Move all stuck/failed issues to Todo?\")) return;\n const btn = document.getElementById(\"restart-stuck-btn\");\n btn.disabled = true;\n btn.textContent = \"\u27F3 PROCESSING...\";\n\n try {\n const res = await fetch(\"/api/stuck-issues\");\n const data = await res.json();\n const allIssues = [...data.stuckIssues, ...data.failedIssues];\n\n let success = 0;\n let failed = 0;\n\n for (const issue of allIssues) {\n try {\n const moveRes = await fetch(\"/api/issue/move-to-todo\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ issueId: issue.id })\n });\n\n if (moveRes.ok) {\n success++;\n addLogLine({ taskId: \"system\", stage: \"stuck\", line: \"Moved \" + issue.identifier + \" to Todo\" });\n } else {\n failed++;\n }\n } catch (e) {\n failed++;\n }\n }\n\n addLogLine({ taskId: \"system\", stage: \"stuck\", line: \"Restart complete: \" + success + \" moved, \" + failed + \" failed\" });\n setTimeout(fetchStuckIssues, 1000);\n } catch (e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"Failed to restart stuck issues: \" + e.message });\n }\n\n btn.disabled = false;\n btn.textContent = \"\u21BB RESTART ALL\";\n }\n\n // ---- Project task updates ----\n function updateProjectTask(projectPath, taskId, title, priority, status) {\n const p = projects.find(x => x.path === projectPath);\n if (!p) return;\n\n // Get issueIdentifier from taskTitleMap\n const taskInfo = taskTitleMap.get(taskId);\n const issueIdentifier = taskInfo?.issueIdentifier;\n\n if (status === \"running\") {\n p.queued = p.queued.filter(t => t.id !== taskId);\n if (!p.running.find(t => t.id === taskId)) {\n p.running.push({ id: taskId, title, priority, issueIdentifier });\n }\n } else {\n if (!p.queued.find(t => t.id === taskId)) {\n p.queued.push({ id: taskId, title, priority, issueIdentifier });\n }\n }\n renderProjects();\n }\n function removeProjectTask(projectPath, taskId) {\n const p = projects.find(x => x.path === projectPath);\n if (!p) return;\n p.running = p.running.filter(t => t.id !== taskId);\n p.queued = p.queued.filter(t => t.id !== taskId);\n renderProjects();\n }\n\n // ---- Toggle project ----\n async function toggleProject(projectPath, enabled) {\n const p = projects.find(x => x.path === projectPath);\n if (p) p.enabled = enabled;\n renderProjects();\n try {\n await fetch(\"/api/projects/toggle\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ projectPath, enabled }),\n });\n } catch(e) {\n if (p) p.enabled = !enabled;\n renderProjects();\n }\n }\n\n // ---- Render Projects ----\n function renderProjects() {\n const el = document.getElementById(\"project-list\");\n const sumEl = document.getElementById(\"proj-summary\");\n if (!projects.length) { el.innerHTML = \"<div class=\\\"empty\\\">no repositories</div>\"; return; }\n const on = projects.filter(p => p.enabled).length;\n if (sumEl) sumEl.textContent = on + \"/\" + projects.length;\n\n el.innerHTML = projects.map(p => {\n // Use path as key, fall back to __n:name for unmapped projects\n const key = p.path || (\"__n:\" + p.name);\n const expanded = expandedProjects.has(key);\n const checked = p.enabled ? \"checked\" : \"\";\n const dCls = p.enabled ? \"\" : \" disabled\";\n const eCls = expanded ? \" expanded\" : \"\";\n // PRs always visible (not gated by expand)\n let prsHtml = \"\";\n if (p.prs && p.prs.length) {\n prsHtml = \"<div class=\\\"proj-issues\\\">\" +\n \"<div class=\\\"issue-sec-label\\\">open PRs (\" + p.prs.length + \")</div>\" +\n p.prs.map(function(pr) {\n return \"<div class=\\\"pr-row\\\">\" +\n \"<span class=\\\"pr-num\\\">#\" + pr.number + \"</span>\" +\n \"<span class=\\\"pr-branch\\\" title=\\\"\" + escapeAttr(pr.branch) + \"\\\">\" + escapeHtml(pr.branch) + \"</span>\" +\n \"<span class=\\\"pr-title\\\" title=\\\"\" + escapeAttr(pr.title) + \"\\\">\" + escapeHtml(pr.title) + \"</span>\" +\n \"<span class=\\\"pr-age\\\">\" + fmtAge(pr.updatedAt) + \"</span>\" +\n \"</div>\";\n }).join(\"\") +\n \"</div>\";\n }\n let issuesHtml = \"\";\n if (expanded) {\n const secs = [];\n if (p.running.length) secs.push(\n \"<div class=\\\"issue-sec-label\\\">running</div>\" +\n p.running.map(t => issueRow(t, \"idot-run\")).join(\"\")\n );\n if (p.queued.length) secs.push(\n \"<div class=\\\"issue-sec-label\\\">queued</div>\" +\n p.queued.map(t => issueRow(t, \"idot-que\")).join(\"\")\n );\n if (p.pending.length) {\n var stateOrder = [\"In Review\", \"In Progress\", \"Todo\", \"Backlog\"];\n var byState = {};\n for (var ti = 0; ti < p.pending.length; ti++) {\n var st = p.pending[ti].linearState || \"Todo\";\n if (!byState[st]) byState[st] = [];\n byState[st].push(p.pending[ti]);\n }\n for (var si = 0; si < stateOrder.length; si++) {\n var sn = stateOrder[si];\n if (!byState[sn] || !byState[sn].length) continue;\n secs.push(\n \"<div class=\\\"issue-sec-label\\\">\" + sn.toLowerCase() + \" (\" + byState[sn].length + \")</div>\" +\n byState[sn].map(t => issueRow(t, \"idot-pnd\")).join(\"\")\n );\n }\n var otherKeys = Object.keys(byState);\n for (var oi = 0; oi < otherKeys.length; oi++) {\n if (stateOrder.indexOf(otherKeys[oi]) === -1) {\n secs.push(\n \"<div class=\\\"issue-sec-label\\\">\" + otherKeys[oi].toLowerCase() + \" (\" + byState[otherKeys[oi]].length + \")</div>\" +\n byState[otherKeys[oi]].map(t => issueRow(t, \"idot-pnd\")).join(\"\")\n );\n }\n }\n }\n if (!secs.length) secs.push(\"<div class=\\\"empty\\\" style=\\\"padding:4px\\\">no issues</div>\");\n // Knowledge graph health info (if cached)\n var kgData = knowledgeCache[p.name] || knowledgeCache[p.path];\n if (kgData && kgData.summary) {\n var s = kgData.summary;\n secs.push(\n \"<div class=\\\"issue-sec-label\\\">code health</div>\" +\n \"<div style=\\\"padding:2px 8px;font-size:10px;color:#88aa88\\\">\" +\n \"modules:\" + s.totalModules + \" tests:\" + s.totalTestFiles +\n \" untested:\" + s.untestedModules.length +\n \" churn:\" + (s.avgChurnScore || 0).toFixed(2) +\n (s.hotModules.length ? \" hot:\" + s.hotModules.slice(0,3).map(function(m){return m.split(\"/\").pop()}).join(\",\") : \"\") +\n \"</div>\"\n );\n }\n issuesHtml = \"<div class=\\\"proj-issues\\\">\" + secs.join(\"\") + \"</div>\";\n }\n\n return (\n \"<div class=\\\"proj-card\" + dCls + eCls + \"\\\" data-key=\\\"\" + escapeAttr(key) + \"\\\">\" +\n \"<div class=\\\"proj-hdr\\\" data-key=\\\"\" + escapeAttr(key) + \"\\\" onclick=\\\"handleToggleExpand(this)\\\">\" +\n \"<span class=\\\"proj-arrow\\\"></span>\" +\n \"<div class=\\\"proj-info\\\">\" +\n \"<div class=\\\"proj-name\\\">\" + escapeHtml(p.name) + \"</div>\" +\n \"<div class=\\\"proj-path\\\">\" + escapeHtml(p.path) + \"</div>\" +\n (p.git ? \"<div class=\\\"git-info\\\">\" +\n \"\\u2387 <span class=\\\"git-branch-name\\\">\" + escapeHtml(p.git.branch) + \"</span>\" +\n (p.git.hasChanges ? \" <span class=\\\"git-dirty\\\">\\u25CF \" + p.git.uncommittedFiles + \"</span>\" : \"\") +\n ((p.git.ahead || p.git.behind) ? \" <span class=\\\"git-sync\\\">\" +\n (p.git.ahead ? \"\\u2191\" + p.git.ahead : \"\") +\n (p.git.behind ? \" \\u2193\" + p.git.behind : \"\") +\n \"</span>\" : \"\") +\n \"</div>\" : \"\") +\n \"</div>\" +\n \"<div class=\\\"proj-counts\\\">\" +\n (p.running.length ? \"<span class=\\\"cnt cnt-run\\\">\" + p.running.length + \"r</span>\" : \"\") +\n (p.queued.length ? \"<span class=\\\"cnt cnt-que\\\">\" + p.queued.length + \"q</span>\" : \"\") +\n (p.pending.length ? \"<span class=\\\"cnt cnt-pnd\\\">\" + p.pending.length + \"p</span>\" : \"\") +\n \"</div>\" +\n \"<div class=\\\"proj-toggle\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"display:flex;align-items:center;gap:4px\\\">\" +\n \"<button class=\\\"btn\\\" style=\\\"font-size:8px;padding:1px 4px;opacity:.5\\\" data-path=\\\"\" + escapeAttr(p.path) + \"\\\" onclick=\\\"handleUnpin(this)\\\">\u2715</button>\" +\n \"<label class=\\\"toggle\\\">\" +\n \"<input type=\\\"checkbox\\\" \" + checked + \" data-path=\\\"\" + escapeAttr(p.path) + \"\\\" onchange=\\\"handleToggleProject(this)\\\">\" +\n \"<span class=\\\"slider\\\"></span>\" +\n \"</label>\" +\n \"</div>\" +\n \"</div>\" +\n prsHtml +\n issuesHtml +\n \"</div>\"\n );\n }).join(\"\");\n }\n\n function issueRow(t, dotClass) {\n const prio = t.priority || 3;\n const extraCls = t.linearState === \"Backlog\" ? \" issue-backlog\" : \"\";\n const moveBtn = t.linearState === \"Backlog\" && t.linearId\n ? \"<button class=\\\"move-to-todo-btn\\\" data-issue-id=\\\"\" + escapeAttr(t.linearId) + \"\\\" onclick=\\\"handleMoveToTodo(this)\\\">\u2192 Todo</button>\"\n : \"\";\n return (\n \"<div class=\\\"issue-row\" + extraCls + \"\\\">\" +\n \"<span class=\\\"idot \" + dotClass + \"\\\"></span>\" +\n \"<span class=\\\"prio prio-\" + Math.min(4, prio) + \"\\\"></span>\" +\n (t.issueIdentifier ? \"<span class=\\\"issue-id\\\">\" + escapeHtml(t.issueIdentifier) + \"</span>\" : \"\") +\n \"<span class=\\\"issue-title\\\" title=\\\"\" + escapeAttr(t.title) + \"\\\">\" + escapeHtml(t.title) + \"</span>\" +\n moveBtn +\n \"</div>\"\n );\n }\n\n function toggleExpand(key) {\n if (expandedProjects.has(key)) expandedProjects.delete(key);\n else expandedProjects.add(key);\n renderProjects();\n }\n function handleToggleExpand(el) {\n const key = el.getAttribute(\"data-key\");\n if (key) toggleExpand(key);\n }\n function handleToggleProject(el) {\n const path = el.getAttribute(\"data-path\");\n if (path) toggleProject(path, el.checked);\n }\n async function handleUnpin(el) {\n const path = el.getAttribute(\"data-path\");\n if (!path) return;\n el.disabled = true;\n try {\n await fetch(\"/api/projects/unpin\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ projectPath: path }),\n });\n const res = await fetch(\"/api/projects\");\n projects = await res.json();\n renderProjects();\n // Update picker state so re-adding works correctly\n const item = allLocalProjects.find(function(p) { return p.path === path; });\n if (item) item.pinned = false;\n } catch(e) { el.disabled = false; }\n }\n async function handleMoveToTodo(el) {\n const issueId = el.getAttribute(\"data-issue-id\");\n if (!issueId) return;\n\n const originalText = el.textContent;\n el.disabled = true;\n el.textContent = \"Moving...\";\n\n try {\n const response = await fetch(\"/api/issue/move-to-todo\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ issueId }),\n });\n\n if (!response.ok) {\n throw new Error(\"Failed to move issue\");\n }\n\n // Refresh projects to show updated state\n const res = await fetch(\"/api/projects\");\n projects = await res.json();\n renderProjects();\n } catch(e) {\n el.disabled = false;\n el.textContent = originalText;\n alert(\"Failed to move issue to Todo: \" + e.message);\n }\n }\n\n // ---- Pipeline Stages ----\n function addStageRow(data) {\n stageRows.push(data);\n if (stageRows.length > MAX_STAGE) stageRows = stageRows.slice(-MAX_STAGE);\n renderStages();\n }\n function shortModel(name) {\n if (!name) return \"\";\n // Order matters: most specific suffix first so 'sonnet-4-6' isn't\n // captured by the generic 'sonnet-4' fallback below.\n if (name.includes(\"opus-4-7\")) return \"opus-4.7\";\n if (name.includes(\"opus-4-6\")) return \"opus-4.6\";\n if (name.includes(\"sonnet-4-6\")) return \"sonnet-4.6\";\n if (name.includes(\"sonnet-4-5\")) return \"sonnet-4.5\";\n if (name.includes(\"haiku-4-5\")) return \"haiku-4.5\";\n if (name.includes(\"opus-4\")) return \"opus-4\";\n if (name.includes(\"sonnet-4\")) return \"sonnet-4\";\n var parts = name.split(\"-\");\n return parts[parts.length - 1];\n }\n function fmtTokens(n) {\n if (n == null) return \"\";\n if (n >= 1000000) return (n / 1000000).toFixed(1) + \"M\";\n if (n >= 1000) return (n / 1000).toFixed(1) + \"k\";\n return String(n);\n }\n function buildStageDetails(r) {\n // Builds the expanded \"what did the worker actually do\" panel for\n // a pipeline:stage payload. Returns \"\" when there's nothing to show.\n const lines = [];\n function addLine(key, valHtml) {\n lines.push(\n \"<div class=\\\"sd-line\\\"><div class=\\\"sd-key\\\">\" + escapeHtml(key) + \"</div>\" +\n \"<div class=\\\"sd-val\\\">\" + valHtml + \"</div></div>\"\n );\n }\n function addList(key, items) {\n const ul = items.map(function(s) { return \"<li>\" + escapeHtml(String(s)) + \"</li>\"; }).join(\"\");\n lines.push(\n \"<div class=\\\"sd-line\\\"><div class=\\\"sd-key\\\">\" + escapeHtml(key) + \"</div>\" +\n \"<div class=\\\"sd-val\\\"><ul>\" + ul + \"</ul></div></div>\"\n );\n }\n\n if (r.summary) addLine(\"Summary\", escapeHtml(r.summary));\n if (r.decision) {\n const cls = \"sd-decision-\" + r.decision;\n addLine(\"Decision\", \"<span class=\\\"\" + cls + \"\\\">\" + escapeHtml(r.decision.toUpperCase()) + \"</span>\");\n }\n if (r.feedback) addLine(\"Feedback\", escapeHtml(r.feedback));\n if (Array.isArray(r.filesChanged) && r.filesChanged.length > 0) {\n const label = \"Files (\" + (r.filesChangedCount || r.filesChanged.length) + \")\";\n addList(label, r.filesChanged);\n }\n if (Array.isArray(r.commands) && r.commands.length > 0) {\n const label = \"Commands (\" + (r.commandsCount || r.commands.length) + \")\";\n addList(label, r.commands);\n }\n if (Array.isArray(r.issues) && r.issues.length > 0) {\n const label = \"Issues (\" + (r.issuesCount || r.issues.length) + \")\";\n addList(label, r.issues);\n }\n if (Array.isArray(r.failedTests) && r.failedTests.length > 0) {\n addList(\"Failed tests\", r.failedTests);\n }\n if (r.passed != null || r.failed != null) {\n addLine(\"Tests\", escapeHtml((r.passed || 0) + \" passed, \" + (r.failed || 0) + \" failed\" + (r.coverage != null ? \" \u2014 coverage \" + r.coverage + \"%\" : \"\")));\n }\n if (r.confidencePercent != null) addLine(\"Confidence\", escapeHtml(r.confidencePercent + \"%\"));\n if (r.haltReason) addLine(\"Halt\", escapeHtml(r.haltReason));\n if (r.bsScore != null) addLine(\"BS score\", escapeHtml(String(r.bsScore)));\n if (r.criticalCount || r.warningCount) {\n addLine(\"Audit\", escapeHtml((r.criticalCount || 0) + \" critical, \" + (r.warningCount || 0) + \" warnings\"));\n }\n if (r.changelogEntry) addLine(\"Changelog\", escapeHtml(r.changelogEntry));\n if (r.durationMs != null) addLine(\"Duration\", escapeHtml((r.durationMs / 1000).toFixed(1) + \"s\"));\n if (r.error) {\n addLine(\"Error\", \"<span style=\\\"color:var(--red)\\\">\" + escapeHtml(r.error) + \"</span>\");\n }\n return lines.join(\"\");\n }\n\n function toggleStageDetails(idx) {\n const block = document.querySelector(\"[data-stage-idx=\\\"\" + idx + \"\\\"]\");\n if (block) block.classList.toggle(\"expanded\");\n }\n\n function renderStages() {\n const el = document.getElementById(\"stage-list\");\n const cnt = document.getElementById(\"stage-count\");\n if (!stageRows.length) { el.innerHTML = \"<div class=\\\"empty\\\">no pipeline events</div>\"; return; }\n if (cnt) cnt.textContent = stageRows.length + \"/\" + MAX_STAGE;\n el.innerHTML = stageRows.slice().reverse().map((r, i) => {\n const info = r.taskId ? taskTitleMap.get(r.taskId) : null;\n let taskLabel = \"\";\n if (info) {\n taskLabel = info.issueIdentifier\n ? info.issueIdentifier + (info.title ? \" \" + info.title.slice(0, 22) : \"\")\n : (info.title ? info.title.slice(0, 30) : \"\");\n } else if (r.taskId) {\n taskLabel = r.taskId.slice(0, 8);\n }\n const projPath = r.taskId ? taskProjectMap.get(r.taskId) : null;\n const repoName = projPath ? projPath.split(\"/\").pop() : \"\";\n const startTs = r.taskId ? taskStartMap.get(r.taskId) : null;\n let elapsed = \"\";\n if (startTs) {\n const sec = Math.floor((Date.now() - startTs) / 1000);\n if (sec < 60) elapsed = sec + \"s\";\n else if (sec < 3600) elapsed = Math.floor(sec / 60) + \"m\" + (sec % 60) + \"s\";\n else elapsed = Math.floor(sec / 3600) + \"h\" + Math.floor((sec % 3600) / 60) + \"m\";\n }\n const modelStr = r.model ? shortModel(r.model) : \"\";\n let tokenStr = \"\";\n if (r.inputTokens || r.outputTokens) {\n tokenStr = fmtTokens(r.inputTokens) + \"/\" + fmtTokens(r.outputTokens);\n if (r.costUsd != null) tokenStr += \" $\" + r.costUsd.toFixed(2);\n }\n\n // Inline summary on the row itself, so the user sees *what* happened\n // without having to expand.\n let inlineSummary = \"\";\n if (r.decision) {\n const cls = \"sd-decision-\" + r.decision;\n inlineSummary = \"<span class=\\\"\" + cls + \"\\\">\" + escapeHtml(r.decision.toUpperCase()) + \"</span>\" +\n (r.feedback ? \" \u00B7 \" + escapeHtml(r.feedback.slice(0, 80)) : \"\");\n } else if (r.summary) {\n inlineSummary = escapeHtml(r.summary);\n if (r.filesChangedCount > 0) inlineSummary += \" \u00B7 \" + r.filesChangedCount + \" files\";\n } else if (r.passed != null || r.failed != null) {\n inlineSummary = \"\u2713 \" + (r.passed || 0) + \" \u2717 \" + (r.failed || 0);\n } else if (r.error) {\n inlineSummary = \"<span style=\\\"color:var(--red)\\\">\" + escapeHtml(r.error.slice(0, 120)) + \"</span>\";\n }\n\n const detailsHtml = buildStageDetails(r);\n const hasDetails = detailsHtml.length > 0;\n const rowClass = \"stage-row\" + (hasDetails ? \" has-details\" : \"\");\n const onclick = hasDetails ? \" onclick=\\\"toggleStageDetails(\" + i + \")\\\"\" : \"\";\n\n return (\n \"<div class=\\\"stage-block\\\" data-stage-idx=\\\"\" + i + \"\\\">\" +\n \"<div class=\\\"\" + rowClass + \"\\\"\" + onclick + \">\" +\n \"<div class=\\\"sdot \" + (r.status || \"\") + \"\\\"></div>\" +\n \"<div class=\\\"srepo\\\">\" + escapeHtml(repoName) + \"</div>\" +\n \"<div class=\\\"sname\\\">\" + escapeHtml(r.stage) + \"</div>\" +\n \"<div class=\\\"stask\\\" title=\\\"\" + escapeAttr(r.taskId || \"\") + \"\\\">\" + escapeHtml(taskLabel) + \"</div>\" +\n \"<div class=\\\"ssummary\\\">\" + inlineSummary + \"</div>\" +\n \"<div class=\\\"smodel\\\">\" + escapeHtml(modelStr) + \"</div>\" +\n \"<div class=\\\"stokens\\\">\" + escapeHtml(tokenStr) + \"</div>\" +\n \"<div class=\\\"selapsed\\\">\" + elapsed + \"</div>\" +\n \"<div class=\\\"sstatus\\\">\" + (r.status || \"\") + \"</div>\" +\n \"</div>\" +\n (hasDetails ? \"<div class=\\\"stage-details\\\">\" + detailsHtml + \"</div>\" : \"\") +\n \"</div>\"\n );\n }).join(\"\");\n el.scrollTop = 0;\n }\n\n // ---- Log Tab ----\n function selectLogTab(taskId) {\n selectedLogTaskId = taskId;\n document.querySelectorAll('.log-tab').forEach(t =>\n t.classList.toggle('active', t.dataset.task === (taskId ?? 'all'))\n );\n renderLog();\n }\n\n function updateLogTabs() {\n const bar = document.getElementById('log-tab-bar');\n const taskIds = [...new Set(logLines.map(l => l.taskId).filter(id => id && id !== 'system'))];\n // Sort by most recent start time\n taskIds.sort((a, b) => (taskStartMap.get(b) || 0) - (taskStartMap.get(a) || 0));\n\n let html = '<button class=\"log-tab' + (selectedLogTaskId === null ? ' active' : '')\n + '\" data-task=\"all\" onclick=\"selectLogTab(null)\">ALL</button>';\n\n for (const tid of taskIds) {\n const info = taskTitleMap.get(tid);\n const label = info?.issueIdentifier || tid.slice(0, 8);\n const isActive = selectedLogTaskId === tid;\n html += '<button class=\"log-tab' + (isActive ? ' active' : '')\n + '\" data-task=\"' + tid + '\" onclick=\"selectLogTab(\\'' + tid + '\\')\">'\n + escapeHtml(label) + '</button>';\n }\n bar.innerHTML = html;\n }\n\n // ---- Log ----\n function addLogLine(data) {\n data._ts = Date.now();\n logLines.push(data);\n if (logLines.length > MAX_LOG) logLines = logLines.slice(-MAX_LOG);\n updateLogTabs();\n renderLog();\n }\n\n function classifyLog(line) {\n if (!line) return { cls: \"log-spacer\", icon: \"\" };\n if (line === \"\u2500\u2500\u2500\") return { cls: \"log-separator\", icon: \"\" };\n if (/^\u25A0 /.test(line)) return { cls: \"log-heading2\", icon: \"\u25A0\" };\n if (/^[\u250C\u2514\u2502]/.test(line)) return { cls: \"log-code\", icon: \"\" };\n if (/^\u25B8 /.test(line)) return { cls: \"log-tool\", icon: \"\u25B8\" };\n if (/^\u25B6|Heartbeat started|Stage started|Iteration [0-9]/.test(line)) return { cls: \"log-heading\", icon: \"\u25B6\" };\n if (/^\u2713|success=true|approved|completed|Done|Created sub-issue/.test(line)) return { cls: \"log-success\", icon: \"\u2713\" };\n if (/^\u2717|success=false|failed|error|Error|rejected|exceeded/.test(line)) return { cls: \"log-fail\", icon: \"\u2717\" };\n if (/^\u27F3|Fetching|Decomposing|Running|Scheduling|Spawning/.test(line)) return { cls: \"\", icon: \"\u27F3\" };\n if (/^\u26D4|Blocked|Time window|blocked/.test(line)) return { cls: \"log-warn\", icon: \"\u26D4\" };\n if (/^\u2014|No task|already completed|no log/.test(line)) return { cls: \"log-system\", icon: \"\u2014\" };\n if (/Cost:|\\$[\\.0-9]/.test(line)) return { cls: \"\", icon: \"\uD83D\uDCB2\" };\n if (/Git detected|files changed|filesChanged/.test(line)) return { cls: \"\", icon: \"\uD83D\uDCC1\" };\n if (/Selected [0-9]+ tasks/.test(line)) return { cls: \"\", icon: \"\uD83C\uDFAF\" };\n if (/Enqueued|executePipeline/.test(line)) return { cls: \"\", icon: \"\uD83D\uDCCB\" };\n if (/Direct path|Project|path found/.test(line)) return { cls: \"log-system\", icon: \"\uD83D\uDCC2\" };\n return { cls: \"\", icon: \"\u00B7\" };\n }\n\n function formatLogText(raw) {\n if (!raw) return \"\";\n // Detect and humanize raw JSON that slipped through\n const trimmed = raw.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n try {\n const obj = JSON.parse(trimmed);\n if (obj.needsDecomposition === false) {\n const r = obj.reason ? obj.reason.slice(0, 120) : \"\";\n raw = \"\\u2713 No decomposition needed (\" + (obj.totalEstimatedMinutes || \"?\") + \"min) \" + r;\n } else if (obj.needsDecomposition === true && obj.subTasks) {\n raw = \"\\uD83D\\uDD00 Decomposed into \" + obj.subTasks.length + \" sub-tasks (total \" + (obj.totalEstimatedMinutes || \"?\") + \"min)\";\n } else if (obj.success !== undefined) {\n raw = (obj.success ? \"\\u2713 \" : \"\\u2717 \") + (obj.summary || obj.error || JSON.stringify(obj).slice(0, 120));\n }\n } catch { /* not valid JSON */ }\n }\n let t = escapeHtml(raw);\n // inline bold: **text** \u2192 highlighted\n t = t.replace(/\\*\\*([^*]+)\\*\\*/g, '<span class=\"lhighlight\">$1</span>');\n // highlight cost figures\n t = t.replace(/(\\$[\\d.]+)/g, '<span class=\"lcost\">$1</span>');\n // highlight file counts\n t = t.replace(/(\\d+ files? changed)/g, '<span class=\"lfiles\">$1</span>');\n // highlight durations\n t = t.replace(/(\\d+\\.\\d+s|\\d+ms)/g, '<span class=\"lcost\">$1</span>');\n // highlight task titles in quotes\n t = t.replace(/(&quot;[^&]+&quot;)/g, '<span class=\"lhighlight\">$1</span>');\n // highlight issue identifiers\n t = t.replace(/(INT-\\d+)/g, '<span class=\"lhighlight\">$1</span>');\n return t;\n }\n\n function fmtLogTime(ts) {\n if (!ts) return \"\";\n const d = new Date(ts);\n return d.getHours().toString().padStart(2,\"0\") + \":\" +\n d.getMinutes().toString().padStart(2,\"0\") + \":\" +\n d.getSeconds().toString().padStart(2,\"0\");\n }\n\n function renderLog() {\n const el = document.getElementById(\"log-list\");\n const cnt = document.getElementById(\"log-count\");\n const filtered = selectedLogTaskId === null\n ? logLines\n : logLines.filter(l => l.taskId === selectedLogTaskId);\n if (!filtered.length) {\n el.innerHTML = \"<div class=\\\"empty\\\">\" + (selectedLogTaskId ? \"no logs for this task\" : \"no log output\") + \"</div>\";\n if (cnt) cnt.textContent = selectedLogTaskId ? filtered.length + \"/\" + logLines.length : \"\";\n return;\n }\n if (cnt) cnt.textContent = (selectedLogTaskId ? filtered.length + \"/\" : \"\") + logLines.length + \"/\" + MAX_LOG;\n const atBottom = el.scrollHeight - el.scrollTop <= el.clientHeight + 50;\n el.innerHTML = filtered.map(l => {\n const info = l.taskId ? taskTitleMap.get(l.taskId) : null;\n const tag = info?.issueIdentifier\n ? info.issueIdentifier\n : (l.taskId === \"system\" ? \"SYS\" : (l.taskId || \"\").slice(0, 8));\n const { cls, icon } = classifyLog(l.line);\n // spacer/separator use minimal rendering\n if (cls === \"log-spacer\") return \"<div class=\\\"log-line log-spacer\\\"></div>\";\n if (cls === \"log-separator\") return \"<div class=\\\"log-line log-separator\\\"><span class=\\\"ltext\\\">\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500</span></div>\";\n const time = fmtLogTime(l._ts);\n const stage = l.stage && l.stage !== \"heartbeat\" ? l.stage : \"\";\n // heading2: strip \u25A0 prefix (icon handles it)\n const displayLine = cls === \"log-heading2\" ? (l.line || \"\").replace(/^\u25A0 /, \"\") : l.line;\n // tool: strip \u25B8 prefix\n const displayLine2 = cls === \"log-tool\" ? (displayLine || \"\").replace(/^\u25B8 /, \"\") : displayLine;\n return (\n \"<div class=\\\"log-line \" + cls + \"\\\">\" +\n \"<span class=\\\"ltime\\\">\" + time + \"</span>\" +\n \"<span class=\\\"licon\\\">\" + icon + \"</span>\" +\n \"<span class=\\\"ltag\\\" title=\\\"\" + escapeAttr(l.taskId || \"\") + \"\\\">\" + escapeHtml(tag) + \"</span>\" +\n (stage ? \"<span class=\\\"lstage\\\">\" + escapeHtml(stage) + \"</span>\" : \"\") +\n \"<span class=\\\"ltext\\\">\" + formatLogText(displayLine2) + \"</span>\" +\n \"</div>\"\n );\n }).join(\"\");\n if (atBottom) el.scrollTop = el.scrollHeight;\n }\n\n // ---- Chat ----\n function fmtTime(ts) {\n const d = new Date(ts);\n return d.getHours().toString().padStart(2,\"0\") + \":\" +\n d.getMinutes().toString().padStart(2,\"0\");\n }\n\n function appendChatMsg(role, text, id, ts) {\n const container = document.getElementById(\"chat-messages\");\n const line = document.createElement(\"div\");\n line.className = \"chat-line chat-\" + role;\n if (id) line.id = id;\n const prefix = role === \"user\"\n ? \"<span class=\\\"chat-prefix\\\">YOU &gt;</span>\"\n : \"<span class=\\\"chat-prefix\\\">OpenSwarm&gt;</span>\";\n const tsStr = ts ? \"<span class=\\\"chat-ts\\\">\" + fmtTime(ts) + \"</span>\" : \"\";\n line.innerHTML = prefix + \" <span class=\\\"chat-text\\\">\" + escapeHtml(text) + \"</span>\" + tsStr;\n container.appendChild(line);\n container.scrollTop = container.scrollHeight;\n }\n\n async function sendChat() {\n if (chatBusy) return;\n const input = document.getElementById(\"chat-input\");\n const sendBtn = document.getElementById(\"chat-send\");\n const msg = input.value.trim();\n if (!msg) return;\n input.value = \"\";\n chatBusy = true;\n sendBtn.disabled = true;\n\n appendChatMsg(\"user\", msg, null, Date.now());\n\n const thinkId = \"think-\" + Date.now();\n const thinkEl = document.createElement(\"div\");\n thinkEl.id = thinkId;\n thinkEl.className = \"chat-line chat-agent\";\n thinkEl.innerHTML = \"<span class=\\\"chat-prefix\\\">OpenSwarm&gt;</span> <span class=\\\"chat-text chat-thinking\\\">thinking...</span>\";\n document.getElementById(\"chat-messages\").appendChild(thinkEl);\n document.getElementById(\"chat-messages\").scrollTop = 99999;\n\n try {\n const res = await fetch(\"/api/chat\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ message: msg }),\n });\n const data = await res.json();\n document.getElementById(thinkId)?.remove();\n appendChatMsg(\"agent\", data.response || data.error || \"(no response)\", null, Date.now());\n } catch(e) {\n document.getElementById(thinkId)?.remove();\n appendChatMsg(\"agent\", \"[ERROR] \" + e.message, null, Date.now());\n }\n\n chatBusy = false;\n sendBtn.disabled = false;\n input.focus();\n }\n\n // ---- Utils ----\n function fmtAge(isoDate) {\n if (!isoDate) return \"\";\n var diff = Math.max(0, Date.now() - new Date(isoDate).getTime());\n var sec = Math.floor(diff / 1000);\n if (sec < 60) return sec + \"s\";\n var min = Math.floor(sec / 60);\n if (min < 60) return min + \"m\";\n var hr = Math.floor(min / 60);\n if (hr < 24) return hr + \"h\";\n var day = Math.floor(hr / 24);\n if (day < 7) return day + \"d\";\n var wk = Math.floor(day / 7);\n return wk + \"w\";\n }\n function escapeHtml(text) {\n const d = document.createElement(\"div\"); d.textContent = String(text || \"\"); return d.innerHTML;\n }\n function escapeAttr(text) {\n return String(text || \"\").replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\");\n }\n\n // ---- Repo Picker ----\n let allLocalProjects = [];\n let pickerOpen = false;\n\n async function openRepoPicker() {\n if (pickerOpen) return;\n pickerOpen = true;\n const overlay = document.getElementById(\"repo-picker\");\n overlay.style.display = \"flex\";\n document.getElementById(\"repo-search\").value = \"\";\n document.getElementById(\"repo-picker-list\").innerHTML =\n \"<div class=\\\"empty\\\">loading...</div>\";\n document.getElementById(\"repo-search\").focus();\n\n try {\n fetchScanPaths();\n const res = await fetch(\"/api/local-projects\");\n allLocalProjects = await res.json();\n filterRepos(\"\");\n } catch(e) {\n document.getElementById(\"repo-picker-list\").innerHTML =\n \"<div class=\\\"empty\\\">failed to load: \" + escapeHtml(e.message) + \"</div>\";\n }\n }\n\n function closeRepoPicker() {\n pickerOpen = false;\n document.getElementById(\"repo-picker\").style.display = \"none\";\n }\n\n function filterRepos(q) {\n const list = document.getElementById(\"repo-picker-list\");\n const filtered = q\n ? allLocalProjects.filter(p =>\n p.name.toLowerCase().includes(q.toLowerCase()) ||\n p.path.toLowerCase().includes(q.toLowerCase()))\n : allLocalProjects;\n\n if (!filtered.length) {\n list.innerHTML = \"<div class=\\\"empty\\\">no results</div>\";\n return;\n }\n list.innerHTML = filtered.slice(0, 80).map(p => {\n const badge = p.pinned ? \"<span class=\\\"repo-item-badge\\\">pinned</span>\" : \"\";\n return (\n \"<div class=\\\"repo-item\\\" data-path=\\\"\" + escapeAttr(p.path) + \"\\\" onclick=\\\"pickRepo(this)\\\">\" +\n \"<div>\" +\n \"<div class=\\\"repo-item-name\\\">\" + escapeHtml(p.name) + \"</div>\" +\n \"<div class=\\\"repo-item-path\\\">\" + escapeHtml(p.path) + \"</div>\" +\n \"</div>\" +\n badge +\n \"</div>\"\n );\n }).join(\"\");\n }\n\n async function pickRepo(el) {\n const path = el.getAttribute(\"data-path\");\n if (!path) return;\n el.style.opacity = \"0.4\";\n try {\n await fetch(\"/api/projects/pin\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ projectPath: path }),\n });\n // Refresh project list\n const res = await fetch(\"/api/projects\");\n projects = await res.json();\n renderProjects();\n // Mark as pinned in local picker list\n const item = allLocalProjects.find(p => p.path === path);\n if (item) item.pinned = true;\n filterRepos(document.getElementById(\"repo-search\").value);\n } catch(e) {\n console.error(\"Pin failed:\", e);\n }\n el.style.opacity = \"1\";\n }\n\n // ---- Scan Paths ----\n async function fetchScanPaths() {\n try {\n const res = await fetch(\"/api/scan-paths\");\n if (res.ok) {\n const data = await res.json();\n renderScanPaths(data);\n }\n } catch(e) {\n console.error(\"fetchScanPaths error:\", e);\n }\n }\n\n function renderScanPaths(data) {\n const list = document.getElementById(\"scan-paths-list\");\n if (!list) return;\n const rows = [];\n for (const p of (data.configPaths || [])) {\n rows.push(\n \"<div class=\\\"scan-path-row\\\">\" +\n \"<span class=\\\"path\\\">\" + escapeHtml(p) + \"</span>\" +\n \"<button class=\\\"scan-path-remove\\\" title=\\\"remove\\\" onclick=\\\"removeScanPath('\" + escapeAttr(p) + \"')\\\">\u2715</button>\" +\n \"</div>\"\n );\n }\n for (const p of (data.customPaths || [])) {\n rows.push(\n \"<div class=\\\"scan-path-row\\\">\" +\n \"<span class=\\\"path\\\">\" + escapeHtml(p) + \"</span>\" +\n \"<button class=\\\"scan-path-remove\\\" onclick=\\\"removeScanPath('\" + escapeAttr(p) + \"')\\\">\u2715</button>\" +\n \"</div>\"\n );\n }\n list.innerHTML = rows.length > 0 ? rows.join(\"\") : \"<div style=\\\"color:#334433;font-size:10px\\\">no scan paths configured</div>\";\n }\n\n async function addScanPath(explicitPath) {\n const input = document.getElementById(\"scan-path-input\");\n const path = (explicitPath ?? input.value).trim();\n if (!path) return;\n if (!explicitPath) input.value = \"\";\n try {\n await fetch(\"/api/scan-paths\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ path }),\n });\n await fetchScanPaths();\n // Refresh project list in picker\n const res = await fetch(\"/api/local-projects\");\n allLocalProjects = await res.json();\n filterRepos(document.getElementById(\"repo-search\").value);\n } catch(e) {\n console.error(\"addScanPath error:\", e);\n }\n }\n\n function toggleManualPathInput() {\n const row = document.getElementById(\"manual-path-row\");\n if (!row) return;\n if (row.style.display === \"none\" || row.style.display === \"\") {\n row.style.display = \"flex\";\n const input = document.getElementById(\"scan-path-input\");\n if (input) input.focus();\n } else {\n row.style.display = \"none\";\n }\n }\n\n // ---- Folder Browser (native-style picker via /api/fs/list) ----\n var folderBrowserCurrent = null;\n var folderBrowserParent = null;\n\n async function openFolderBrowser(startPath) {\n const modal = document.getElementById(\"folder-browser\");\n if (!modal) return;\n modal.style.display = \"flex\";\n await loadFolderBrowser(startPath || \"~/dev\");\n }\n\n function closeFolderBrowser() {\n const modal = document.getElementById(\"folder-browser\");\n if (modal) modal.style.display = \"none\";\n }\n\n async function loadFolderBrowser(path) {\n const list = document.getElementById(\"fb-list\");\n const pathEl = document.getElementById(\"fb-path\");\n const upBtn = document.getElementById(\"fb-up\");\n const selectBtn = document.getElementById(\"fb-select\");\n if (!list || !pathEl) return;\n list.innerHTML = \"<div style=\\\"padding:18px;color:var(--dim);font-size:12px;text-align:center\\\">Loading\u2026</div>\";\n try {\n const res = await fetch(\"/api/fs/list?path=\" + encodeURIComponent(path));\n if (!res.ok) {\n const err = await res.json().catch(() => ({}));\n list.innerHTML = \"<div style=\\\"padding:18px;color:var(--red);font-size:12px;text-align:center\\\">\" + escapeHtml(err.error || (\"HTTP \" + res.status)) + \"</div>\";\n return;\n }\n const data = await res.json();\n folderBrowserCurrent = data.path;\n folderBrowserParent = data.parent;\n pathEl.value = data.path;\n if (upBtn) upBtn.disabled = !data.parent;\n if (selectBtn) selectBtn.textContent = \"Select \\\"\" + (data.name || data.path) + \"\\\"\";\n\n const dirs = (data.entries || []).filter(function(e) { return e.isDir; });\n if (dirs.length === 0) {\n list.innerHTML = \"<div style=\\\"padding:18px;color:var(--dim);font-size:12px;text-align:center\\\">No subfolders</div>\";\n return;\n }\n list.innerHTML = dirs.map(function(e) {\n return \"<div class=\\\"fb-row\\\" data-name=\\\"\" + escapeAttr(e.name) + \"\\\" onclick=\\\"folderBrowserEnter(this.getAttribute('data-name'))\\\"\" +\n \" style=\\\"padding:7px 18px;cursor:pointer;font-size:13px;color:var(--white);display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--border2);transition:background .12s\\\"\" +\n \" onmouseover=\\\"this.style.background='var(--bg3)'\\\" onmouseout=\\\"this.style.background=''\\\">\" +\n \"<span style=\\\"color:var(--green)\\\">\uD83D\uDCC1</span>\" +\n \"<span>\" + escapeHtml(e.name) + \"</span>\" +\n \"</div>\";\n }).join(\"\");\n } catch (e) {\n list.innerHTML = \"<div style=\\\"padding:18px;color:var(--red);font-size:12px;text-align:center\\\">\" + escapeHtml(String(e)) + \"</div>\";\n }\n }\n\n function folderBrowserEnter(name) {\n if (!folderBrowserCurrent || !name) return;\n // Join via the server side by sending the absolute path of the child\n const sep = folderBrowserCurrent.endsWith(\"/\") ? \"\" : \"/\";\n loadFolderBrowser(folderBrowserCurrent + sep + name);\n }\n\n function folderBrowserUp() {\n if (folderBrowserParent) loadFolderBrowser(folderBrowserParent);\n }\n\n async function folderBrowserSelect() {\n if (!folderBrowserCurrent) return;\n const picked = folderBrowserCurrent;\n closeFolderBrowser();\n await addScanPath(picked);\n }\n\n async function removeScanPath(path) {\n try {\n await fetch(\"/api/scan-paths/\" + encodeURIComponent(path), {\n method: \"DELETE\",\n });\n await fetchScanPaths();\n // Refresh project list in picker\n const res = await fetch(\"/api/local-projects\");\n allLocalProjects = await res.json();\n filterRepos(document.getElementById(\"repo-search\").value);\n } catch(e) {\n console.error(\"removeScanPath error:\", e);\n }\n }\n\n // ---- Monitors ----\n var monitorsData = [];\n async function fetchMonitors() {\n try {\n const res = await fetch(\"/api/monitors\");\n if (res.ok) { monitorsData = await res.json(); renderMonitors(); }\n } catch {}\n }\n function renderMonitors() {\n renderMonitorsAndProcesses();\n }\n function fmtDur(ms) {\n var s = Math.floor(ms / 1000);\n var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);\n if (h >= 24) return Math.floor(h / 24) + \"d \" + (h % 24) + \"h\";\n if (h > 0) return h + \"h \" + m + \"m\";\n return m + \"m\";\n }\n\n // ---- Processes ----\n var processesData = [];\n async function fetchProcesses() {\n try {\n const res = await fetch(\"/api/processes\");\n if (res.ok) { processesData = await res.json(); renderMonitorsAndProcesses(); }\n } catch {}\n }\n async function stopProcess(id, isPipeline) {\n var verb = isPipeline ? \"Cancel task\" : \"Kill process\";\n if (!confirm(verb + \" \" + id + \"?\")) return;\n try {\n await fetch(\"/api/processes/\" + encodeURIComponent(id), { method: \"DELETE\" });\n processesData = processesData.filter(p => String(p.id) !== String(id));\n renderMonitorsAndProcesses();\n } catch(e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"Stop failed: \" + e.message });\n }\n }\n function procActivityIcon(lastActivityAt) {\n var ago = (Date.now() - lastActivityAt) / 1000;\n if (ago < 10) return \"\\u26A1\";\n if (ago < 60) return \"\\u23F8\";\n return \"\\u2757\";\n }\n function renderMonitorsAndProcesses() {\n var panel = document.getElementById(\"monitor-panel\");\n var el = document.getElementById(\"monitor-list\");\n var countEl = document.getElementById(\"monitor-count\");\n var hasMonitors = monitorsData.length > 0;\n var hasProcesses = processesData.length > 0;\n if (!hasMonitors && !hasProcesses) {\n el.innerHTML = \"<div class=\\\"empty\\\">no monitors or processes</div>\";\n var counts = [];\n if (countEl) countEl.textContent = \"\";\n return;\n }\n var parts = [];\n if (processesData.length) parts.push(processesData.length + \"p\");\n if (monitorsData.length) parts.push(monitorsData.length + \"m\");\n if (countEl) countEl.textContent = parts.join(\" \");\n var html = \"\";\n // Processes section\n if (hasProcesses) {\n html += \"<div class=\\\"issue-sec-label\\\">processes</div>\";\n html += processesData.map(function(p) {\n var dur = fmtDur(Date.now() - p.spawnedAt);\n var isPipeline = p.kind === \"pipeline\";\n var act = isPipeline ? \"\\u2699\" : procActivityIcon(p.lastActivityAt);\n var modelStr = p.model ? shortModel(p.model) : \"\";\n var projName = p.project || (p.projectPath ? p.projectPath.split(\"/\").pop() : \"\");\n // In-process pipeline tasks have no OS PID \u2014 show the issue id instead, and\n // a CANCEL button (aborts the pipeline + its in-flight adapter call).\n var lead = isPipeline ? escapeHtml(p.taskId || \"task\") : p.pid;\n var btn = isPipeline\n ? '<button class=\"proc-kill\" onclick=\"stopProcess(\\'' + escapeAttr(String(p.id)) + '\\', true)\">CANCEL</button>'\n : '<button class=\"proc-kill\" onclick=\"stopProcess(\\'' + escapeAttr(String(p.id)) + '\\', false)\">KILL</button>';\n return '<div class=\"proc-row\">' +\n '<span class=\"proc-pid\">' + lead + '</span>' +\n '<span class=\"proc-stage\">' + escapeHtml(p.stage) + '</span>' +\n '<span class=\"proc-model\">' + escapeHtml(modelStr) + '</span>' +\n '<span style=\"color:var(--dim);font-size:9px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\" title=\"' + escapeAttr(p.projectPath || \"\") + '\">' + escapeHtml(projName) + '</span>' +\n '<span class=\"proc-activity\">' + act + '</span>' +\n '<span class=\"proc-dur\">' + dur + '</span>' +\n btn +\n '</div>';\n }).join(\"\");\n }\n // Monitors section\n if (hasMonitors) {\n html += \"<div class=\\\"issue-sec-label\\\">monitors</div>\";\n html += monitorsData.map(function(m) {\n var stateColor = m.state === \"running\" ? \"var(--green)\" : m.state === \"completed\" ? \"var(--cyan)\" : m.state === \"failed\" || m.state === \"timeout\" ? \"var(--red)\" : \"var(--dim)\";\n var elapsed = m.registeredAt ? fmtDur(Date.now() - m.registeredAt) : \"-\";\n var lastOut = m.lastOutput ? escapeHtml(m.lastOutput.slice(0, 80)) : \"-\";\n return '<div style=\"padding:4px 6px;border-bottom:1px solid var(--border);font-size:11px\">' +\n '<div style=\"display:flex;align-items:center;gap:6px\">' +\n '<span style=\"color:' + stateColor + ';font-weight:bold\">[' + m.state.toUpperCase() + ']</span>' +\n '<span style=\"color:var(--green)\">' + escapeHtml(m.name) + '</span>' +\n '<span style=\"margin-left:auto;color:var(--dim)\">' + elapsed + '</span>' +\n '</div>' +\n '<div style=\"color:var(--dim);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\" title=\"' + escapeAttr(m.lastOutput || \"\") + '\">' + lastOut + '</div>' +\n (m.issueId ? '<div style=\"color:var(--cyan-dim);font-size:10px;margin-top:1px\">' + escapeHtml(m.issueId) + ' | checks: ' + m.checkCount + '</div>' : '') +\n '</div>';\n }).join(\"\");\n }\n el.innerHTML = html;\n }\n\n // ---- Init ----\n async function loadInitial() {\n try {\n // 1\uB2E8\uACC4: \uD544\uC218 \uB370\uC774\uD130 \uBA3C\uC800 \uB85C\uB4DC (\uC131\uB2A5 \uAC1C\uC120)\n const [statsRes, projectsRes] = await Promise.all([\n fetch(\"/api/stats\"),\n fetch(\"/api/projects\"),\n ]);\n const stats = await statsRes.json();\n updateStats(stats);\n document.getElementById(\"stat-sse\").textContent = stats.sseClients ?? \"-\";\n\n projects = await projectsRes.json();\n renderProjects();\n\n // 2\uB2E8\uACC4: \uCD94\uAC00 \uB370\uC774\uD130\uB294 \uBE44\uB3D9\uAE30\uB85C \uC9C0\uC5F0 \uB85C\uB4DC (\uBE0C\uB77C\uC6B0\uC800 \uB80C\uB354\uB9C1 \uBE14\uB85C\uD0B9 \uC81C\uAC70)\n loadSupplementalData();\n } catch(e) {\n console.error(\"Init failed:\", e);\n }\n }\n\n // \uCD94\uAC00 \uB370\uC774\uD130 \uC9C0\uC5F0 \uB85C\uB4DC (\uCD08\uAE30 \uB80C\uB354\uB9C1\uC744 \uBC29\uD574\uD558\uC9C0 \uC54A\uC74C)\n async function loadSupplementalData() {\n try {\n const [chatRes, logsRes, stagesRes] = await Promise.all([\n fetch(\"/api/chat/history\"),\n fetch(\"/api/logs\"),\n fetch(\"/api/stages\"),\n ]);\n\n const history = await chatRes.json();\n for (const msg of history) appendChatMsg(msg.role, msg.text, null, msg.ts);\n\n // Restore logs\n const logs = await logsRes.json();\n for (const ev of logs) addLogLine(ev.data);\n\n // Restore pipeline/task events\n const stages = await stagesRes.json();\n for (const ev of stages) handleEvent(ev);\n } catch(e) {\n console.error(\"Supplemental data load failed:\", e);\n }\n }\n\n // \uC131\uB2A5 \uCD5C\uC801\uD654: stats + projects \uD3F4\uB9C1\uC744 60\uCD08\uB85C \uC99D\uAC00 (\uBCC0\uD654 \uBE48\uB3C4 \uB0AE\uC74C)\n setInterval(async () => {\n try {\n const [sRes, pRes] = await Promise.all([fetch(\"/api/stats\"), fetch(\"/api/projects\")]);\n const stats = await sRes.json();\n document.getElementById(\"stat-sse\").textContent = stats.sseClients ?? \"-\";\n updateStats(stats);\n const fresh = await pRes.json();\n fresh.forEach(p => {\n const local = projects.find(l => l.path === p.path);\n if (local) p.enabled = local.enabled;\n });\n projects = fresh;\n renderProjects();\n } catch {}\n }, 60000);\n\n // ---- Mobile Tab Navigation ----\n function switchTab(idx) {\n const cols = document.querySelectorAll(\".main-grid > .col\");\n const tabs = document.querySelectorAll(\".tab-bar .tab\");\n cols.forEach((c, i) => c.classList.toggle(\"mob-active\", i === idx));\n tabs.forEach((t, i) => t.classList.toggle(\"active\", i === idx));\n }\n document.querySelector(\".tab-bar\").addEventListener(\"click\", e => {\n const tab = e.target.closest(\".tab\");\n if (!tab) return;\n switchTab(parseInt(tab.dataset.tab, 10));\n });\n // Activate first tab on load\n switchTab(0);\n\n // Knowledge graph data fetcher\n async function fetchKnowledgeData() {\n try {\n const res = await fetch(\"/api/knowledge\");\n if (res.ok) {\n const data = await res.json();\n for (const item of data) {\n knowledgeCache[item.slug] = item;\n // Also cache by last segment for name-based lookup\n const parts = item.slug.split(\"-\");\n knowledgeCache[parts[parts.length - 1]] = item;\n }\n renderProjects();\n }\n } catch {}\n }\n\n // ---- Quota tracker ----\n async function fetchQuota() {\n try {\n var res = await fetch(\"/api/quota\");\n if (!res.ok) return;\n var q = await res.json();\n if (q.error) return;\n var el5h = document.getElementById(\"stat-quota-5h\");\n var el7d = document.getElementById(\"stat-quota-7d\");\n if (q.five_hour && el5h) {\n var u5 = Math.round(q.five_hour.utilization);\n el5h.textContent = u5 + \"%\";\n el5h.className = \"stat-val \" + (u5 >= 80 ? \"red\" : u5 >= 50 ? \"amber\" : \"cyan\");\n }\n if (q.seven_day && el7d) {\n var u7 = Math.round(q.seven_day.utilization);\n el7d.textContent = u7 + \"%\";\n el7d.className = \"stat-val \" + (u7 >= 80 ? \"red\" : u7 >= 50 ? \"amber\" : \"cyan\");\n }\n } catch {}\n }\n\n // \uC131\uB2A5 \uCD5C\uC801\uD654: \uCD08\uAE30 \uB85C\uB4DC \uD6C4 2\uB2E8\uACC4 \uD398\uCE6D (\uB80C\uB354\uB9C1 \uBE14\uB85C\uD0B9 \uBC29\uC9C0)\n loadInitial().then(function() { connectSSE(true); });\n\n // 1\uB2E8\uACC4: \uCD08\uAE30\uD654 \uD6C4 \uC989\uC2DC \uD544\uC218 \uD3F4\uB9C1\uB9CC \uC2DC\uC791\n setInterval(fetchSvcStatus, 15000);\n setInterval(fetchPRProcessorStatus, 60000); // \uC131\uB2A5 \uCD5C\uC801\uD654: 30\uCD08 \u2192 60\uCD08 (\uBCC0\uD654 \uBE48\uB3C4 \uB0AE\uC74C)\n setInterval(fetchStuckIssues, 60000); // \uC131\uB2A5 \uCD5C\uC801\uD654: 30\uCD08 \u2192 60\uCD08 (Linear API \uBD80\uD558 \uAC10\uC18C)\n setInterval(fetchKnowledgeData, 60000);\n setInterval(fetchMonitors, 60000);\n setInterval(fetchProcesses, 30000);\n setInterval(fetchQuota, 300000);\n\n // 2\uB2E8\uACC4: \uB80C\uB354\uB9C1 \uC548\uC815\uD654 \uD6C4 \uBE44\uD544\uC218 \uB370\uC774\uD130 \uB85C\uB4DC (3\uCD08 \uC9C0\uC5F0)\n setTimeout(function() {\n fetchSvcStatus();\n fetchPRProcessorStatus();\n fetchStuckIssues();\n fetchKnowledgeData();\n fetchMonitors();\n fetchProcesses();\n fetchQuota();\n }, 3000);\n\n // \uB80C\uB354\uB9C1 \uC131\uB2A5: \uC2A4\uD14C\uC774\uC9C0 \uC5C5\uB370\uC774\uD2B8 \uD3F4\uB9C1 \uC81C\uAC70 (SSE \uC774\uBCA4\uD2B8 \uD65C\uC6A9)\n // setInterval(() => { if (stageRows.length) renderStages(); }, 10000);\n </script>\n</body>\n</html>";
1
+ declare const DASHBOARD_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>OpenSwarm :: Supervisor</title>\n <style>\n /*\n * Design tokens \u2014 adapted from VEGA (GitHub Dark inspired).\n * Legacy variable names are preserved so the rest of the stylesheet keeps\n * working unchanged; only the values shifted to a calmer, more readable\n * palette while keeping the semantic intent (--green = primary action,\n * --amber = warning, --red = destructive, --dim = secondary text).\n */\n :root {\n --bg: #0d1117; /* page background */\n --bg2: #161b22; /* surface (cards, header) */\n --bg3: #1c2128; /* surface raised (hover) */\n --green: #58a6ff; /* primary accent (was matrix green) */\n --green-dim: rgba(88, 166, 255, 0.12);\n --green-mid: #58a6ff;\n --green-lo: #30363d;\n --cyan: #79c0ff;\n --cyan-dim: rgba(121, 192, 255, 0.14);\n --amber: #d29922;\n --red: #f85149;\n --white: #c9d1d9; /* primary text */\n --dim: #8b949e; /* muted text */\n --border: #30363d;\n --border2: rgba(48, 54, 61, 0.55);\n --radius-sm: 6px;\n --radius-md: 8px;\n --radius-lg: 12px;\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n html, body { height: 100%; overflow: hidden; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif;\n background: var(--bg);\n color: var(--white);\n font-size: 14px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n }\n /* Monospace contexts (logs, paths, IDs) still use a mono stack */\n code, pre, .mono, .log-line, .repo-item-path, .scan-path-row .path, .issue-id {\n font-family: \"SF Mono\", \"JetBrains Mono\", \"Fira Code\", Consolas, monospace;\n }\n\n /* ===== SCROLLBAR ===== */\n ::-webkit-scrollbar { width: 8px; height: 8px; }\n ::-webkit-scrollbar-track { background: transparent; }\n ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }\n ::-webkit-scrollbar-thumb:hover { background: var(--dim); }\n\n /* ===== HEADER ===== */\n header {\n height: 38px;\n background: var(--bg2);\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n padding: 0 1rem;\n gap: 0.75rem;\n flex-shrink: 0;\n }\n .hdr-logo {\n color: var(--green);\n font-weight: bold;\n font-size: 14px;\n letter-spacing: 0.15em;\n }\n .hdr-fullname { color: var(--dim); font-size: 11px; letter-spacing: 0.05em; margin-left: 0.25rem; }\n .hdr-sep { color: var(--dim); margin-left: 0.5rem; }\n .hdr-sub { color: var(--dim); font-size: 11px; letter-spacing: 0.1em; }\n .hdr-right { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }\n #sse-status {\n font-size: 10px;\n padding: 1px 6px;\n border: 1px solid var(--dim);\n color: var(--dim);\n letter-spacing: 0.1em;\n }\n #sse-status.connected { border-color: var(--green); color: var(--green); }\n #sse-status.disconnected { border-color: var(--red); color: var(--red); }\n .btn {\n font-family: inherit;\n font-size: 11px;\n font-weight: 500;\n padding: 4px 12px;\n background: var(--bg2);\n border: 1px solid var(--border);\n border-radius: var(--radius-sm);\n color: var(--white);\n cursor: pointer;\n letter-spacing: 0.04em;\n transition: border-color 0.15s, background 0.15s, color 0.15s;\n }\n .btn:hover:not(:disabled) { border-color: var(--green); color: var(--green); background: var(--green-dim); }\n .btn:disabled { opacity: 0.4; cursor: default; }\n .btn.primary { background: var(--green); border-color: var(--green); color: #0d1117; font-weight: 600; }\n .btn.primary:hover:not(:disabled) { opacity: 0.88; background: var(--green); color: #0d1117; }\n .btn-active { border-color: var(--amber); color: var(--amber); }\n .btn-active:hover:not(:disabled) { background: #332200; border-color: var(--amber); }\n .btn-danger { border-color: #551111; color: var(--red); }\n .btn-danger:hover:not(:disabled) { background: #220000; border-color: var(--red); }\n #turbo-btn { border-color: #553300; color: #ff8800; transition: all 0.3s; }\n #turbo-btn:hover:not(:disabled) { background: #221100; border-color: #ff8800; }\n #turbo-btn.turbo-active { background: #331800; border-color: #ff8800; color: #ffaa00; box-shadow: 0 0 8px rgba(255,136,0,0.3); animation: turbo-pulse 2s infinite; }\n @keyframes turbo-pulse { 0%,100% { box-shadow: 0 0 4px rgba(255,136,0,0.2); } 50% { box-shadow: 0 0 12px rgba(255,136,0,0.5); } }\n .move-to-todo-btn {\n font-family: inherit;\n font-size: 9px;\n padding: 1px 6px;\n background: transparent;\n border: 1px solid var(--cyan-dim);\n color: var(--cyan);\n cursor: pointer;\n margin-left: auto;\n flex-shrink: 0;\n transition: all 0.15s;\n }\n .move-to-todo-btn:hover:not(:disabled) { border-color: var(--cyan); background: var(--cyan-dim); }\n .move-to-todo-btn:disabled { opacity: 0.4; cursor: default; }\n .svc-group { display: flex; align-items: center; gap: 4px; margin-right: 8px; }\n .svc-status {\n font-size: 9px; padding: 1px 6px;\n border: 1px solid var(--dim); color: var(--dim);\n letter-spacing: 0.1em; text-transform: uppercase;\n }\n .svc-status.active { border-color: var(--green); color: var(--green); }\n .svc-status.inactive { border-color: var(--red); color: var(--red); }\n .svc-sep { color: var(--border); margin: 0 2px; }\n\n /* ===== STATS BAR ===== */\n .stats-bar {\n height: 36px;\n background: var(--bg2);\n border-bottom: 1px solid var(--border2);\n display: flex;\n align-items: center;\n padding: 0 1rem;\n gap: 1.5rem;\n flex-shrink: 0;\n }\n .stat {\n display: flex;\n align-items: baseline;\n gap: 0.4rem;\n }\n .stat-label { font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.1em; }\n .stat-val { font-size: 13px; font-weight: 500; color: var(--green); }\n .stat-val.amber { color: var(--amber); }\n .stat-val.cyan { color: var(--cyan); }\n .stat-val.red { color: #ff5555; }\n #stat-adapter, #stat-pair-adapters {\n font-size: 10px;\n font-weight: 400;\n letter-spacing: 0.02em;\n }\n .provider-toggle {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px;\n border: 1px solid var(--border);\n background: var(--bg3);\n }\n .provider-btn {\n font-family: inherit;\n font-size: 9px;\n line-height: 1;\n padding: 4px 8px;\n background: transparent;\n border: 1px solid transparent;\n color: var(--dim);\n cursor: pointer;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n }\n .provider-btn:hover:not(:disabled) {\n color: var(--white);\n border-color: var(--border);\n }\n .provider-btn.active {\n color: var(--green);\n border-color: var(--green-lo);\n background: var(--green-dim);\n }\n .stat-divider { color: var(--border); }\n\n /* ===== MAIN GRID ===== */\n .main-grid {\n display: grid;\n grid-template-columns: 290px 1fr 340px;\n height: calc(100vh - 74px);\n overflow: hidden;\n }\n .col {\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--border);\n overflow: hidden;\n }\n .col:last-child { border-right: none; }\n\n /* ===== PANEL ===== */\n .panel { display: flex; flex-direction: column; overflow: hidden; flex: 1; }\n .panel + .panel { border-top: 1px solid var(--border); }\n .panel-hdr {\n height: 28px;\n padding: 0 0.75rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n background: var(--bg3);\n border-bottom: 1px solid var(--border2);\n flex-shrink: 0;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.12em;\n color: var(--dim);\n }\n .panel-hdr-title { color: var(--green-mid); }\n .panel-hdr-badge {\n margin-left: auto;\n font-size: 9px;\n color: var(--dim);\n }\n .panel-body {\n flex: 1;\n overflow-y: auto;\n padding: 0.5rem;\n }\n .empty { color: var(--dim); font-size: 11px; text-align: center; padding: 1.5rem 0.5rem; }\n\n /* ===== PROJECTS ===== */\n .proj-card {\n border: 1px solid var(--border);\n margin-bottom: 4px;\n background: var(--bg2);\n }\n .proj-card.disabled { opacity: 0.45; }\n .proj-hdr {\n display: flex;\n align-items: center;\n padding: 5px 7px;\n gap: 6px;\n cursor: pointer;\n user-select: none;\n }\n .proj-hdr:hover { background: var(--green-dim); }\n .proj-arrow { color: var(--dim); font-size: 9px; width: 10px; flex-shrink: 0; }\n .proj-card.expanded .proj-arrow::before { content: \"\u25BC\"; }\n .proj-card:not(.expanded) .proj-arrow::before { content: \"\u25B6\"; }\n .proj-info { flex: 1; min-width: 0; }\n .proj-name { color: var(--green); font-size: 12px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .proj-path { color: var(--dim); font-size: 9px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .proj-counts { display: flex; gap: 3px; }\n .cnt { font-size: 9px; padding: 1px 4px; font-weight: bold; }\n .cnt-run { color: var(--green); border: 1px solid var(--green-lo); }\n .cnt-que { color: var(--amber); border: 1px solid #332200; }\n .cnt-pnd { color: var(--cyan); border: 1px solid var(--cyan-dim); }\n .proj-toggle { flex-shrink: 0; }\n .toggle { position: relative; display: inline-block; width: 30px; height: 16px; }\n .toggle input { opacity: 0; width: 0; height: 0; }\n .slider {\n position: absolute; cursor: pointer;\n top: 0; left: 0; right: 0; bottom: 0;\n background: #111; border: 1px solid var(--dim);\n border-radius: 16px; transition: 0.2s;\n }\n .slider:before {\n position: absolute; content: \"\";\n height: 10px; width: 10px;\n left: 2px; bottom: 2px;\n background: var(--dim); border-radius: 50%; transition: 0.2s;\n }\n input:checked + .slider { background: var(--green-dim); border-color: var(--green-lo); }\n input:checked + .slider:before { background: var(--green); transform: translateX(14px); }\n .proj-issues { border-top: 1px solid var(--border2); padding: 4px 7px; }\n .issue-sec-label {\n font-size: 9px; color: var(--dim); text-transform: uppercase;\n letter-spacing: 0.1em; margin: 4px 0 2px;\n }\n .issue-row {\n display: flex; align-items: center; gap: 4px;\n padding: 2px 0; font-size: 11px;\n border-bottom: 1px solid var(--border2);\n }\n .issue-row:last-child { border-bottom: none; }\n .git-info { color: var(--dim); font-size: 9px; display: flex; gap: 6px; align-items: center; }\n .git-branch-name { color: var(--cyan); }\n .git-dirty { color: var(--amber); }\n .git-sync { color: var(--dim); }\n .pr-row { display: flex; align-items: center; gap: 4px; padding: 2px 0; font-size: 11px; border-bottom: 1px solid var(--border2); }\n .pr-row:last-child { border-bottom: none; }\n .pr-num { color: var(--cyan); font-size: 9px; min-width: 32px; }\n .pr-branch { color: var(--green-lo); font-size: 9px; max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .pr-title { flex: 1; color: var(--white); font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .pr-age { color: var(--dim); font-size: 9px; flex-shrink: 0; }\n .idot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }\n .idot-run { background: var(--green); }\n .idot-que { background: var(--amber); }\n .idot-pnd { background: var(--dim); }\n .prio { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }\n .prio-1 { background: var(--red); }\n .prio-2 { background: var(--amber); }\n .prio-3 { background: var(--green-mid); }\n .prio-4 { background: var(--dim); }\n .issue-id { color: var(--cyan); font-size: 9px; min-width: 50px; }\n .issue-title { flex: 1; color: var(--white); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .issue-row.issue-backlog { opacity: 0.45; }\n\n /* ===== PROCESS ROW ===== */\n .proc-row {\n display: flex; align-items: center; gap: 6px;\n padding: 4px 6px; border-bottom: 1px solid var(--border2);\n font-size: 11px;\n }\n .proc-pid { color: var(--cyan); font-size: 10px; min-width: 42px; font-variant-numeric: tabular-nums; }\n .proc-stage { color: var(--green); min-width: 56px; font-weight: bold; text-transform: uppercase; font-size: 10px; }\n .proc-model { color: var(--dim); font-size: 9px; min-width: 56px; }\n .proc-dur { color: var(--amber); font-size: 9px; min-width: 42px; text-align: right; font-variant-numeric: tabular-nums; }\n .proc-activity { font-size: 10px; min-width: 16px; text-align: center; }\n .proc-kill {\n font-family: inherit; font-size: 9px; padding: 1px 5px;\n background: transparent; border: 1px solid #551111; color: var(--red);\n cursor: pointer; margin-left: auto;\n }\n .proc-kill:hover { background: #220000; border-color: var(--red); }\n\n /* ===== PIPELINE ===== */\n .stage-block {\n border-bottom: 1px solid var(--border2);\n }\n .stage-row {\n display: flex; align-items: center; gap: 6px;\n padding: 4px 8px;\n font-size: 11px;\n cursor: pointer;\n transition: background 0.12s;\n }\n .stage-row:hover { background: var(--bg3); }\n .stage-row.has-details::after {\n content: \"\u203A\"; color: var(--dim); margin-left: 4px;\n transition: transform 0.15s;\n }\n .stage-block.expanded .stage-row.has-details::after {\n transform: rotate(90deg); display: inline-block;\n }\n .sdot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; background: var(--dim); }\n .sdot.start { background: var(--amber); }\n .sdot.complete { background: var(--green); }\n .sdot.fail { background: var(--red); }\n .sname { color: var(--white); min-width: 70px; }\n .srepo { color: var(--dim); font-size: 10px; min-width: 50px; max-width: 90px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .stask { color: var(--cyan); font-size: 10px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .ssummary {\n color: var(--white); font-size: 11px; flex: 2;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n opacity: 0.85;\n }\n .selapsed { color: var(--amber); font-size: 9px; flex-shrink: 0; min-width: 36px; text-align: right; }\n .smodel { color: var(--dim); font-size: 9px; flex-shrink: 0; min-width: 56px; text-align: right; }\n .stokens { color: var(--amber); font-size: 9px; flex-shrink: 0; min-width: 80px; text-align: right; white-space: nowrap; }\n .sstatus { font-size: 9px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.06em; flex-shrink: 0; }\n /* Expanded details */\n .stage-details {\n display: none;\n padding: 8px 14px 10px 22px;\n background: rgba(0,0,0,0.18);\n font-size: 11px;\n color: var(--white);\n border-top: 1px dashed var(--border2);\n }\n .stage-block.expanded .stage-details { display: block; }\n .stage-details .sd-line { display: flex; gap: 8px; padding: 1px 0; align-items: baseline; }\n .stage-details .sd-key { color: var(--dim); font-size: 10px; min-width: 70px; text-transform: uppercase; letter-spacing: 0.06em; }\n .stage-details .sd-val { color: var(--white); font-size: 11px; flex: 1; word-break: break-all; }\n .stage-details ul { margin: 2px 0 4px 14px; padding: 0; }\n .stage-details li { font-family: \"SF Mono\", \"JetBrains Mono\", monospace; font-size: 10px; color: var(--cyan); padding: 1px 0; }\n .sd-decision-approve { color: var(--green); font-weight: 600; }\n .sd-decision-revise { color: var(--amber); font-weight: 600; }\n .sd-decision-reject { color: var(--red); font-weight: 600; }\n\n /* ===== LOG TAB BAR ===== */\n .log-tab-bar {\n display: flex; gap: 0; border-bottom: 1px solid #1a2a1a;\n padding: 0 4px; overflow-x: auto; flex-shrink: 0;\n }\n .log-tab {\n background: transparent; border: none; border-bottom: 2px solid transparent;\n color: var(--dim); font-family: inherit; font-size: 10px;\n padding: 4px 8px; cursor: pointer; white-space: nowrap;\n text-transform: uppercase; letter-spacing: .05em;\n }\n .log-tab:hover { color: var(--green-mid); }\n .log-tab.active { color: var(--green); border-bottom-color: var(--green); }\n\n /* ===== LOG ===== */\n .log-area { font-size: 11px; line-height: 1.5; padding: 4px 0; }\n .log-line { padding: 3px 8px; display: flex; gap: 6px; align-items: flex-start; border-radius: 2px; margin: 1px 0; }\n .log-line:hover { background: rgba(255,255,255,0.03); }\n .log-line.log-success .ltext { color: var(--green); }\n .log-line.log-fail .ltext { color: var(--red); }\n .log-line.log-warn .ltext { color: var(--amber); }\n .log-line.log-system { opacity: 0.6; }\n .log-line.log-heading { border-top: 1px solid var(--border2); margin-top: 6px; padding-top: 8px; }\n .ltime { color: var(--dim); font-size: 9px; flex-shrink: 0; min-width: 36px; opacity: 0.7; padding-top: 2px; font-variant-numeric: tabular-nums; }\n .licon { flex-shrink: 0; min-width: 14px; text-align: center; font-size: 11px; padding-top: 1px; }\n .ltag { color: var(--green-lo); min-width: 52px; flex-shrink: 0; padding-top: 1px; font-size: 10px; font-weight: 500; }\n .lstage { color: var(--cyan); min-width: 60px; flex-shrink: 0; font-size: 10px; padding-top: 1px; text-transform: uppercase; letter-spacing: 0.03em; opacity: 0.8; }\n .ltext { color: #99aa99; word-break: break-word; white-space: pre-wrap; flex: 1; min-width: 0; }\n .ltext .lhighlight { color: var(--white); font-weight: 500; }\n .ltext .lcost { color: var(--amber); font-size: 10px; }\n .ltext .lfiles { color: var(--cyan); font-size: 10px; }\n .log-line.log-spacer { height: 6px; padding: 0; margin: 0; min-height: 6px; }\n .log-line.log-separator { opacity: 0.2; padding: 0 8px; margin: 4px 0; }\n .log-line.log-separator .ltext { color: var(--dim); }\n .log-line.log-code .ltext { font-family: 'JetBrains Mono', 'Fira Code', monospace; color: var(--cyan); opacity: 0.8; font-size: 10px; }\n .log-line.log-heading2 .ltext { color: var(--white); font-weight: 600; font-size: 12px; }\n .log-line.log-tool .ltext { color: var(--dim); font-style: italic; font-size: 10px; }\n\n /* ===== CHAT ===== */\n .chat-col { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; }\n .chat-messages {\n flex: 1;\n overflow-y: auto;\n padding: 0.5rem;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n .chat-line { display: flex; gap: 6px; font-size: 12px; line-height: 1.5; }\n .chat-prefix {\n flex-shrink: 0;\n font-weight: bold;\n }\n .chat-user .chat-prefix { color: var(--amber); }\n .chat-agent .chat-prefix { color: var(--cyan); }\n .chat-text { color: var(--white); white-space: pre-wrap; word-break: break-word; flex: 1; }\n .chat-agent .chat-text { color: var(--white); }\n .chat-user .chat-text { color: var(--amber); }\n .chat-ts { color: var(--dim); font-size: 9px; flex-shrink: 0; align-self: flex-start; padding-top: 2px; }\n .chat-thinking { animation: blink 1s infinite; color: var(--cyan); }\n @keyframes blink { 0%,100% { opacity:1; } 50% { opacity:0.3; } }\n\n .chat-input-area {\n border-top: 1px solid var(--border);\n padding: 6px 8px;\n display: flex;\n align-items: center;\n gap: 6px;\n background: var(--bg3);\n flex-shrink: 0;\n }\n .chat-prompt { color: var(--green); font-size: 12px; flex-shrink: 0; }\n .chat-input {\n flex: 1;\n background: transparent;\n border: none;\n outline: none;\n font-family: inherit;\n font-size: 12px;\n color: var(--green);\n caret-color: var(--green);\n }\n .chat-input::placeholder { color: var(--dim); }\n .chat-send {\n font-family: inherit;\n font-size: 10px;\n padding: 2px 8px;\n background: transparent;\n border: 1px solid var(--green-lo);\n color: var(--green-mid);\n cursor: pointer;\n }\n .chat-send:hover { border-color: var(--green); color: var(--green); }\n .chat-send:disabled { opacity: 0.3; cursor: default; }\n\n /* ===== REPO PICKER ===== */\n .repo-item {\n display: flex; align-items: center; gap: 8px;\n padding: 5px 12px; cursor: pointer; font-size: 11px;\n }\n .repo-item:hover { background: var(--green-dim); }\n .repo-item-name { color: var(--green); font-weight: bold; }\n .repo-item-path { color: var(--dim); font-size: 10px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .repo-item-badge { font-size: 9px; padding: 1px 5px; border: 1px solid var(--green-lo); color: var(--green-mid); flex-shrink: 0; }\n\n .scan-path-row { display: flex; align-items: center; gap: 6px; padding: 3px 0; font-size: 11px; }\n .scan-path-row .path { color: var(--dim); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .scan-path-badge { font-size: 9px; padding: 1px 5px; border: 1px solid #333; color: #556655; flex-shrink: 0; }\n .scan-path-remove { background: transparent; border: none; color: #553333; cursor: pointer; font-size: 12px; padding: 0 2px; flex-shrink: 0; }\n .scan-path-remove:hover { color: var(--red); }\n\n /* ===== TAB BAR (hidden on desktop) ===== */\n .tab-bar { display: none; }\n\n /* ===== MOBILE RESPONSIVE ===== */\n @media (max-width: 768px) {\n html, body { overflow: auto; }\n\n /* Header */\n header {\n height: auto;\n min-height: 38px;\n flex-wrap: wrap;\n padding: 6px 0.75rem;\n gap: 4px;\n }\n .hdr-fullname { display: none; }\n .hdr-right { width: 100%; justify-content: flex-end; }\n .svc-group .btn { font-size: 0; padding: 4px 8px; min-height: 32px; }\n .svc-group #svc-stop-btn::after { content: \"\\23F8\"; font-size: 12px; }\n .svc-group #svc-restart-btn::after { content: \"\\21BB\"; font-size: 12px; }\n #hb-btn { min-height: 32px; }\n\n /* Stats bar */\n .stats-bar {\n height: auto;\n min-height: 32px;\n flex-wrap: wrap;\n padding: 4px 0.75rem;\n gap: 0.5rem;\n font-size: 11px;\n }\n .stat-divider { display: none; }\n\n /* Tab bar */\n .tab-bar {\n display: flex;\n background: var(--bg2);\n border-bottom: 1px solid var(--border);\n }\n .tab {\n flex: 1;\n font-family: inherit;\n font-size: 11px;\n letter-spacing: 0.1em;\n padding: 10px 0;\n min-height: 44px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: var(--dim);\n cursor: pointer;\n text-align: center;\n transition: all 0.15s;\n }\n .tab.active {\n color: var(--green);\n border-bottom-color: var(--green);\n }\n\n /* Main grid \u2192 single column */\n .main-grid {\n display: flex;\n flex-direction: column;\n height: auto;\n min-height: calc(100vh - 160px);\n overflow: visible;\n }\n .col {\n display: none;\n border-right: none;\n overflow: visible;\n min-height: calc(100vh - 200px);\n }\n .col.mob-active {\n display: flex;\n flex: 1;\n }\n\n /* Repo picker \u2192 fullscreen */\n #repo-picker > div {\n width: 100% !important;\n max-height: 90vh !important;\n margin: 5vh 0 0;\n }\n .repo-item { min-height: 44px; }\n\n /* Chat input \u2192 sticky bottom */\n .chat-input-area {\n position: sticky;\n bottom: 0;\n z-index: 10;\n min-height: 44px;\n }\n .chat-input { min-height: 32px; font-size: 14px; }\n .chat-send { min-height: 36px; padding: 4px 12px; }\n\n /* Touch targets */\n .btn { min-height: 36px; padding: 4px 10px; }\n .proj-hdr { min-height: 44px; padding: 8px 7px; }\n .toggle { width: 40px; height: 22px; }\n .slider:before { height: 14px; width: 14px; left: 3px; bottom: 3px; }\n input:checked + .slider:before { transform: translateX(18px); }\n }\n </style>\n</head>\n<body>\n <!-- HEADER -->\n <header>\n <span class=\"hdr-logo\">OpenSwarm</span>\n <span class=\"hdr-fullname\">: Vector-Encoded General Agent</span>\n <span class=\"hdr-sep\">::</span>\n <span class=\"hdr-sub\">SUPERVISOR</span>\n <a href=\"/issues\" style=\"color:var(--cyan);font-size:11px;text-decoration:none;margin-left:1rem;letter-spacing:0.1em;border:1px solid var(--cyan-dim);padding:2px 8px;border-radius:3px\">ISSUES</a>\n <div class=\"hdr-right\">\n <div class=\"svc-group\">\n <span class=\"svc-status\" id=\"svc-status\">...</span>\n <span class=\"svc-sep\">\u2502</span>\n <div class=\"provider-toggle\">\n <button class=\"provider-btn\" id=\"provider-claude\" onclick=\"switchProvider('claude')\">Claude</button>\n <button class=\"provider-btn\" id=\"provider-codex\" onclick=\"switchProvider('codex')\">Codex</button>\n </div>\n <span class=\"svc-sep\">\u2502</span>\n <button class=\"btn\" id=\"turbo-btn\" onclick=\"toggleTurbo()\" title=\"Turbo: 5min heartbeat, 20 daily cap, 4h auto-expire\">TURBO</button>\n <span class=\"svc-sep\">\u2502</span>\n <button class=\"btn btn-danger\" id=\"svc-stop-btn\" onclick=\"svcAction('stop')\">\u23F8 STOP</button>\n <button class=\"btn\" id=\"svc-restart-btn\" onclick=\"svcAction('restart')\">\u21BB RESTART</button>\n </div>\n <span id=\"sse-status\">CONNECTING</span>\n <button class=\"btn btn-active\" id=\"hb-btn\" onclick=\"triggerHeartbeat()\">\u25B6 HEARTBEAT</button>\n <button class=\"btn\" id=\"pr-proc-btn\" onclick=\"triggerPRProcessor()\">\u21BB PR REVIEW</button>\n </div>\n </header>\n\n <!-- STATS BAR -->\n <div class=\"stats-bar\">\n <div class=\"stat\"><span class=\"stat-label\">RUN</span><span class=\"stat-val\" id=\"stat-running\">0</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">QUEUE</span><span class=\"stat-val amber\" id=\"stat-queued\">0</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">DONE</span><span class=\"stat-val\" id=\"stat-completed\">0</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">PACE</span><span class=\"stat-val\" id=\"stat-pace\">-</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">SSE</span><span class=\"stat-val cyan\" id=\"stat-sse\">-</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">CLI</span><span class=\"stat-val cyan\" id=\"stat-adapter\">-</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">PAIR</span><span class=\"stat-val cyan\" id=\"stat-pair-adapters\">-</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">UPTIME</span><span class=\"stat-val\" id=\"stat-uptime\">-</span></div>\n <span class=\"stat-divider\">\u2502</span>\n <div class=\"stat\"><span class=\"stat-label\">COST</span><span class=\"stat-val cyan\" id=\"stat-cost\">$0.00</span></div>\n </div>\n\n <!-- TAB BAR (mobile only) -->\n <div class=\"tab-bar\">\n <button class=\"tab active\" data-tab=\"0\">REPOS</button>\n <button class=\"tab\" data-tab=\"1\">PIPELINE</button>\n <button class=\"tab\" data-tab=\"2\">CHAT</button>\n </div>\n\n <!-- MAIN GRID -->\n <div class=\"main-grid\">\n\n <!-- LEFT: REPOSITORIES -->\n <div class=\"col\">\n <div class=\"panel\">\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">REPOSITORIES</span>\n <span class=\"panel-hdr-badge\" id=\"proj-summary\"></span>\n <button class=\"btn\" style=\"margin-left:auto;font-size:9px;padding:1px 6px\" onclick=\"openRepoPicker()\">+ ADD</button>\n </div>\n <div class=\"panel-body\" id=\"project-list\">\n <div class=\"empty\">loading...</div>\n </div>\n </div>\n <div class=\"panel\" id=\"monitor-panel\">\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">MONITORS & PROCESSES</span>\n <span class=\"panel-hdr-badge\" id=\"monitor-count\"></span>\n </div>\n <div class=\"panel-body\" id=\"monitor-list\">\n <div class=\"empty\">no monitors or processes</div>\n </div>\n </div>\n </div>\n\n <!-- REPO PICKER OVERLAY -->\n <div id=\"repo-picker\" style=\"display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);backdrop-filter:blur(4px);z-index:100;align-items:center;justify-content:center\">\n <div style=\"background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius-lg);width:560px;max-height:75vh;display:flex;flex-direction:column;box-shadow:0 10px 32px rgba(0,0,0,0.5)\">\n <div style=\"padding:14px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px\">\n <span style=\"color:var(--white);font-size:13px;font-weight:600;letter-spacing:.04em\">Add repository</span>\n <button onclick=\"closeRepoPicker()\" style=\"margin-left:auto;background:transparent;border:none;color:var(--dim);cursor:pointer;font-size:18px;line-height:1;padding:0 4px;border-radius:4px\" onmouseover=\"this.style.color='var(--white)'\" onmouseout=\"this.style.color='var(--dim)'\">\u2715</button>\n </div>\n <div style=\"padding:10px 18px;border-bottom:1px solid var(--border2)\">\n <input id=\"repo-search\" type=\"text\" placeholder=\"Filter repositories\u2026\"\n style=\"width:100%;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);outline:none;font-family:inherit;font-size:13px;color:var(--white);padding:7px 10px;caret-color:var(--green)\"\n oninput=\"filterRepos(this.value)\" onkeydown=\"if(event.key==='Escape')closeRepoPicker()\">\n </div>\n <div id=\"repo-picker-list\" style=\"overflow-y:auto;flex:1;padding:6px 0\"></div>\n <div id=\"scan-paths-section\" style=\"border-top:1px solid var(--border);padding:12px 18px;background:rgba(13,17,23,0.4)\">\n <div style=\"color:var(--dim);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px\">Scan paths</div>\n <div id=\"scan-paths-list\"></div>\n <div style=\"display:flex;gap:6px;margin-top:10px\">\n <button class=\"btn\" style=\"flex:1;justify-content:center\" onclick=\"openFolderBrowser()\">\uD83D\uDCC1 Browse for folder\u2026</button>\n <button class=\"btn\" style=\"font-size:11px\" onclick=\"toggleManualPathInput()\" title=\"Type a path manually\">\u2328</button>\n </div>\n <div id=\"manual-path-row\" style=\"display:none;gap:6px;margin-top:6px\">\n <input id=\"scan-path-input\" type=\"text\" placeholder=\"/absolute/path/to/scan\"\n style=\"flex:1;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);outline:none;font-family:inherit;font-size:12px;color:var(--white);padding:5px 8px;caret-color:var(--green)\"\n onkeydown=\"if(event.key==='Enter')addScanPath()\">\n <button class=\"btn primary\" style=\"font-size:11px\" onclick=\"addScanPath()\">Add</button>\n </div>\n </div>\n </div>\n </div>\n\n <!-- FOLDER BROWSER OVERLAY (native-style picker, server-side fs) -->\n <div id=\"folder-browser\" style=\"display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);backdrop-filter:blur(4px);z-index:110;align-items:center;justify-content:center\">\n <div style=\"background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius-lg);width:620px;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 10px 32px rgba(0,0,0,0.5)\">\n <div style=\"padding:14px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px\">\n <span style=\"color:var(--white);font-size:13px;font-weight:600;letter-spacing:.04em\">Choose folder to scan</span>\n <button onclick=\"closeFolderBrowser()\" style=\"margin-left:auto;background:transparent;border:none;color:var(--dim);cursor:pointer;font-size:18px;line-height:1;padding:0 4px;border-radius:4px\" onmouseover=\"this.style.color='var(--white)'\" onmouseout=\"this.style.color='var(--dim)'\">\u2715</button>\n </div>\n <div style=\"padding:10px 18px;border-bottom:1px solid var(--border2);display:flex;align-items:center;gap:6px\">\n <button class=\"btn\" id=\"fb-up\" style=\"font-size:11px;padding:3px 10px\" onclick=\"folderBrowserUp()\" title=\"Parent directory\">\u2191 Up</button>\n <input id=\"fb-path\" type=\"text\" readonly\n style=\"flex:1;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);outline:none;font-family:'SF Mono',monospace;font-size:12px;color:var(--white);padding:5px 10px\">\n </div>\n <div id=\"fb-list\" style=\"overflow-y:auto;flex:1;padding:4px 0\"></div>\n <div style=\"padding:12px 18px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end\">\n <button class=\"btn\" onclick=\"closeFolderBrowser()\">Cancel</button>\n <button class=\"btn primary\" id=\"fb-select\" onclick=\"folderBrowserSelect()\">Select this folder</button>\n </div>\n </div>\n </div>\n\n <!-- MIDDLE: PIPELINE + LOG -->\n <div class=\"col\">\n <div class=\"panel\" style=\"flex: 0 0 38%\">\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">PIPELINE</span>\n <span class=\"panel-hdr-badge\" id=\"stage-count\"></span>\n </div>\n <div class=\"panel-body\" id=\"stage-list\">\n <div class=\"empty\">no pipeline events</div>\n </div>\n </div>\n <div class=\"panel\">\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">LIVE LOG</span>\n <span class=\"panel-hdr-badge\" id=\"log-count\"></span>\n </div>\n <div class=\"log-tab-bar\" id=\"log-tab-bar\">\n <button class=\"log-tab active\" data-task=\"all\" onclick=\"selectLogTab(null)\">ALL</button>\n </div>\n <div class=\"panel-body log-area\" id=\"log-list\">\n <div class=\"empty\">no log output</div>\n </div>\n </div>\n </div>\n\n <!-- RIGHT: CHAT -->\n <div class=\"col\">\n <!-- PR PROCESSOR -->\n <div class=\"panel\" style=\"flex: 0 0 auto;\">\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">PR PROCESSOR</span>\n <span class=\"panel-hdr-badge\" id=\"pr-proc-badge\"></span>\n </div>\n <div class=\"panel-body\" style=\"font-size: 11px; line-height: 1.5;\">\n <div id=\"pr-proc-body\" style=\"color: var(--dim);\">Loading...</div>\n </div>\n </div>\n\n <!-- STUCK/FAILED ISSUES -->\n <div class=\"panel\" style=\"flex: 0 0 auto; max-height: 200px;\">\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">\u26A0 STUCK/FAILED</span>\n <span class=\"panel-hdr-badge\" id=\"stuck-badge\">0</span>\n <button class=\"btn\" style=\"margin-left: 0.5rem; font-size: 9px; padding: 1px 6px;\" onclick=\"restartStuckIssues()\" id=\"restart-stuck-btn\">\u21BB RESTART ALL</button>\n </div>\n <div class=\"panel-body\" style=\"font-size: 10px; line-height: 1.4; overflow-y: auto;\">\n <div id=\"stuck-list\" style=\"color: var(--dim);\">Loading...</div>\n </div>\n </div>\n\n <!-- AGENT CHAT -->\n <div class=\"panel-hdr\">\n <span class=\"panel-hdr-title\">AGENT CHAT</span>\n <span class=\"panel-hdr-badge\" id=\"chat-status\"></span>\n </div>\n <div class=\"chat-col\">\n <div class=\"chat-messages\" id=\"chat-messages\"></div>\n <div class=\"chat-input-area\">\n <span class=\"chat-prompt\">&gt;</span>\n <input\n class=\"chat-input\" id=\"chat-input\"\n type=\"text\" placeholder=\"message OpenSwarm...\"\n onkeydown=\"if(event.key==='Enter')sendChat()\"\n >\n <button class=\"chat-send\" id=\"chat-send\" onclick=\"sendChat()\">SEND</button>\n </div>\n </div>\n </div>\n\n </div>\n\n <script>\n const MAX_LOG = 200;\n const MAX_STAGE = 100;\n\n let projects = [];\n let expandedProjects = new Set();\n let knowledgeCache = {};\n let logLines = [];\n let selectedLogTaskId = null; // null = ALL, string = specific taskId\n let stageRows = [];\n let chatBusy = false;\n let totalCostUsd = 0;\n const taskProjectMap = new Map();\n // taskId \u2192 { title, issueIdentifier } for pipeline display\n const taskTitleMap = new Map();\n // taskId \u2192 start timestamp for elapsed time\n const taskStartMap = new Map();\n\n // ---- SSE ----\n function connectSSE(skipReplay) {\n const url = skipReplay ? \"/api/events?skipReplay=1\" : \"/api/events\";\n const es = new EventSource(url);\n const statusEl = document.getElementById(\"sse-status\");\n es.onopen = () => { statusEl.textContent = \"LIVE\"; statusEl.className = \"connected\"; };\n es.onmessage = e => {\n let ev; try { ev = JSON.parse(e.data); } catch { return; }\n handleEvent(ev);\n };\n es.onerror = () => {\n statusEl.textContent = \"RECONNECTING\"; statusEl.className = \"disconnected\";\n es.close(); setTimeout(function() { connectSSE(false); }, 3000);\n };\n }\n\n function handleEvent(ev) {\n switch (ev.type) {\n case \"stats\": updateStats(ev.data); break;\n case \"task:queued\":\n taskProjectMap.set(ev.data.taskId, ev.data.projectPath);\n taskTitleMap.set(ev.data.taskId, { title: ev.data.title, issueIdentifier: ev.data.issueIdentifier });\n updateProjectTask(ev.data.projectPath, ev.data.taskId, ev.data.title, ev.data.priority, \"queued\");\n break;\n case \"task:started\": {\n const p = taskProjectMap.get(ev.data.taskId);\n if (ev.data.title) taskTitleMap.set(ev.data.taskId, { title: ev.data.title, issueIdentifier: ev.data.issueIdentifier });\n taskStartMap.set(ev.data.taskId, Date.now());\n if (p) updateProjectTask(p, ev.data.taskId, ev.data.title, null, \"running\");\n break;\n }\n case \"task:completed\": {\n const p = taskProjectMap.get(ev.data.taskId);\n if (p) removeProjectTask(p, ev.data.taskId);\n break;\n }\n case \"pipeline:stage\": addStageRow(ev.data); break;\n case \"pipeline:iteration\":\n addStageRow({ taskId: ev.data.taskId, stage: \"iter #\" + ev.data.iteration, status: \"start\" });\n break;\n case \"log\": addLogLine(ev.data); break;\n case \"project:toggled\": {\n const p = projects.find(x => x.path === ev.data.projectPath);\n if (p) { p.enabled = ev.data.enabled; renderProjects(); }\n break;\n }\n case \"task:cost\": {\n totalCostUsd += ev.data.cost?.costUsd ?? 0;\n document.getElementById(\"stat-cost\").textContent = \"$\" + totalCostUsd.toFixed(2);\n break;\n }\n case \"chat:agent\": appendChatMsg(\"agent\", ev.data.text, null, ev.data.ts); break;\n case \"monitor:checked\":\n case \"monitor:stateChange\":\n fetchMonitors();\n break;\n case \"process:spawn\":\n fetchProcesses();\n addLogLine({ taskId: ev.data.taskId || \"system\", stage: ev.data.stage || \"spawn\", line: \"Process spawned PID=\" + ev.data.pid + \" stage=\" + ev.data.stage + (ev.data.model ? \" model=\" + ev.data.model : \"\") });\n break;\n case \"process:exit\":\n fetchProcesses();\n addLogLine({ taskId: \"system\", stage: \"exit\", line: \"Process exited PID=\" + ev.data.pid + \" code=\" + ev.data.exitCode + \" duration=\" + (ev.data.durationMs / 1000).toFixed(1) + \"s\" });\n break;\n case \"heartbeat\": {\n const btn = document.getElementById(\"hb-btn\");\n btn.disabled = false; btn.textContent = \"\u25B6 HEARTBEAT\";\n break;\n }\n case \"pr_processor_start\":\n case \"pr_processor_end\":\n case \"pr_processor_pr\":\n fetchPRProcessorStatus();\n break;\n }\n }\n\n // ---- Stats ----\n function updateStats(data) {\n function shortModel(model) {\n if (!model) return \"-\";\n return model.length > 18 ? model.slice(0, 15) + \"...\" : model;\n }\n\n document.getElementById(\"stat-running\").textContent = data.runningTasks ?? 0;\n document.getElementById(\"stat-queued\").textContent = data.queuedTasks ?? 0;\n document.getElementById(\"stat-completed\").textContent = data.completedToday ?? 0;\n const defaultAdapter = data.adapters?.defaultAdapter ?? \"-\";\n const workerAdapter = data.adapters?.worker?.adapter ?? \"-\";\n const workerModel = shortModel(data.adapters?.worker?.model);\n const reviewerAdapter = data.adapters?.reviewer?.adapter ?? \"-\";\n const reviewerModel = shortModel(data.adapters?.reviewer?.model);\n const chatModel = workerModel || \"-\";\n document.getElementById(\"stat-adapter\").textContent = defaultAdapter;\n document.getElementById(\"stat-pair-adapters\").textContent =\n \"W \" + workerAdapter + \":\" + workerModel + \" / R \" + reviewerAdapter + \":\" + reviewerModel;\n document.getElementById(\"chat-status\").textContent = defaultAdapter + \":\" + chatModel;\n document.getElementById(\"provider-claude\").classList.toggle(\"active\", defaultAdapter === \"claude\");\n document.getElementById(\"provider-codex\").classList.toggle(\"active\", defaultAdapter === \"codex\");\n if (data.uptime != null) {\n const s = Math.floor(data.uptime / 1000);\n const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), ss = s % 60;\n document.getElementById(\"stat-uptime\").textContent =\n (h ? h + \"h \" : \"\") + (m ? m + \"m \" : \"\") + ss + \"s\";\n }\n // Turbo mode\n const turboBtn = document.getElementById(\"turbo-btn\");\n if (turboBtn) {\n turboBtn.classList.toggle(\"turbo-active\", !!data.turboMode);\n if (data.turboMode && data.turboExpiresAt) {\n const remainMin = Math.max(0, Math.round((data.turboExpiresAt - Date.now()) / 60000));\n turboBtn.textContent = \"TURBO \" + remainMin + \"m\";\n } else {\n turboBtn.textContent = \"TURBO\";\n }\n }\n // Daily pace\n const paceEl = document.getElementById(\"stat-pace\");\n if (paceEl && data.dailyPace) {\n const cap = data.turboMode ? 20 : 6;\n paceEl.textContent = data.dailyPace.completedToday + \"/\" + cap;\n paceEl.className = \"stat-val\" + (data.turboMode ? \" amber\" : \"\");\n }\n }\n\n // ---- Service control ----\n async function fetchSvcStatus() {\n try {\n const res = await fetch(\"/api/service/status\");\n const data = await res.json();\n const el = document.getElementById(\"svc-status\");\n const status = data.status || \"unknown\";\n el.textContent = status;\n el.className = \"svc-status \" + (status === \"active\" ? \"active\" : \"inactive\");\n } catch {\n const el = document.getElementById(\"svc-status\");\n el.textContent = \"unknown\";\n el.className = \"svc-status inactive\";\n }\n }\n\n // ---- Stuck/Failed Issues ----\n async function fetchStuckIssues() {\n try {\n const res = await fetch(\"/api/stuck-issues\");\n const data = await res.json();\n const list = document.getElementById(\"stuck-list\");\n const badge = document.getElementById(\"stuck-badge\");\n\n const totalStuck = data.stuckIssues?.length ?? 0;\n const totalFailed = data.failedIssues?.length ?? 0;\n const total = totalStuck + totalFailed;\n\n badge.textContent = total;\n badge.style.color = total > 0 ? \"var(--red)\" : \"var(--dim)\";\n\n if (total === 0) {\n list.innerHTML = '<div style=\"color: var(--green-mid); padding: 4px;\">\u2713 All issues healthy</div>';\n return;\n }\n\n let html = '';\n\n // Stuck issues (In Progress for >7 days)\n if (totalStuck > 0) {\n html += '<div style=\"color: var(--amber); font-weight: bold; margin-bottom: 4px; font-size: 9px; text-transform: uppercase;\">\u23F1 Stuck (' + totalStuck + ')</div>';\n data.stuckIssues.forEach(issue => {\n const priorityColor = issue.priority === 1 ? 'var(--red)' : issue.priority === 2 ? 'var(--amber)' : 'var(--dim)';\n html += '<div style=\"margin-bottom: 6px; padding: 4px; border-left: 2px solid ' + priorityColor + '; background: rgba(255, 170, 0, 0.05);\">';\n html += '<div style=\"color: var(--white); font-size: 10px; margin-bottom: 2px;\">' + issue.identifier + ': ' + issue.title.substring(0, 40) + (issue.title.length > 40 ? '...' : '') + '</div>';\n html += '<div style=\"color: var(--amber); font-size: 9px;\">' + issue.reason + '</div>';\n if (issue.project?.name) {\n html += '<div style=\"color: var(--dim); font-size: 9px; margin-top: 2px;\">\uD83D\uDCC1 ' + issue.project.name + '</div>';\n }\n html += '</div>';\n });\n }\n\n // Failed issues (retry, failed, blocked labels)\n if (totalFailed > 0) {\n if (totalStuck > 0) html += '<div style=\"height: 8px;\"></div>';\n html += '<div style=\"color: var(--red); font-weight: bold; margin-bottom: 4px; font-size: 9px; text-transform: uppercase;\">\u2716 Failed (' + totalFailed + ')</div>';\n data.failedIssues.forEach(issue => {\n const priorityColor = issue.priority === 1 ? 'var(--red)' : issue.priority === 2 ? 'var(--amber)' : 'var(--dim)';\n html += '<div style=\"margin-bottom: 6px; padding: 4px; border-left: 2px solid ' + priorityColor + '; background: rgba(255, 51, 51, 0.05);\">';\n html += '<div style=\"color: var(--white); font-size: 10px; margin-bottom: 2px;\">' + issue.identifier + ': ' + issue.title.substring(0, 40) + (issue.title.length > 40 ? '...' : '') + '</div>';\n html += '<div style=\"color: var(--red); font-size: 9px;\">' + issue.reason + '</div>';\n if (issue.project?.name) {\n html += '<div style=\"color: var(--dim); font-size: 9px; margin-top: 2px;\">\uD83D\uDCC1 ' + issue.project.name + '</div>';\n }\n html += '</div>';\n });\n }\n\n list.innerHTML = html;\n } catch (err) {\n console.error(\"Failed to fetch stuck issues:\", err);\n document.getElementById(\"stuck-list\").innerHTML = '<div style=\"color: var(--red);\">Error loading</div>';\n }\n }\n\n // ---- PR Processor Status ----\n async function fetchPRProcessorStatus() {\n try {\n const res = await fetch(\"/api/pr-processor-status\");\n const data = await res.json();\n const body = document.getElementById(\"pr-proc-body\");\n const badge = document.getElementById(\"pr-proc-badge\");\n\n if (!data) {\n body.innerHTML = '<div style=\"color: var(--dim);\">Not configured</div>';\n badge.textContent = \"OFF\";\n badge.style.color = \"var(--dim)\";\n return;\n }\n\n const status = data.processing ? \"RUNNING\" : \"IDLE\";\n badge.textContent = status;\n badge.style.color = data.processing ? \"var(--green)\" : \"var(--cyan)\";\n\n const formatTime = (ts) => {\n if (!ts) return \"N/A\";\n const d = new Date(ts);\n return d.toLocaleTimeString(\"en-US\", { hour: \"2-digit\", minute: \"2-digit\" });\n };\n\n let html = '<div style=\"display: flex; flex-direction: column; gap: 6px;\">';\n html += '<div><span style=\"color: var(--dim);\">Schedule:</span> <span style=\"color: var(--text);\">' + (data.schedule || \"N/A\") + '</span></div>';\n html += '<div><span style=\"color: var(--dim);\">Repos:</span> <span style=\"color: var(--text);\">' + (data.repos?.length || 0) + '</span></div>';\n\n if (data.currentPR) {\n html += '<div><span style=\"color: var(--amber);\">Processing:</span> <span style=\"color: var(--text); font-family: monospace; font-size: 10px;\">' + data.currentPR + '</span></div>';\n }\n\n html += '<div><span style=\"color: var(--dim);\">Last run:</span> <span style=\"color: var(--text);\">' + formatTime(data.lastRun) + '</span></div>';\n html += '<div><span style=\"color: var(--dim);\">Next run:</span> <span style=\"color: var(--text);\">' + formatTime(data.nextRun) + '</span></div>';\n\n if (data.conflictResolverEnabled) {\n html += '<div style=\"color: var(--green); font-size: 10px; margin-top: 4px;\">\u2713 Conflict Resolver: ON</div>';\n }\n\n html += '</div>';\n body.innerHTML = html;\n } catch (e) {\n const body = document.getElementById(\"pr-proc-body\");\n body.innerHTML = '<div style=\"color: var(--red);\">Error: ' + e.message + '</div>';\n }\n }\n\n async function svcAction(action) {\n const label = action === \"stop\" ? \"STOP\" : \"RESTART\";\n if (!confirm(\"Are you sure you want to \" + label + \" the service?\")) return;\n const btnId = action === \"stop\" ? \"svc-stop-btn\" : \"svc-restart-btn\";\n const btn = document.getElementById(btnId);\n btn.disabled = true;\n try {\n await fetch(\"/api/service/\" + action, { method: \"POST\" });\n addLogLine({ taskId: \"system\", stage: \"service\", line: \"Service \" + action + \" requested\" });\n } catch(e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"Service \" + action + \" failed: \" + e.message });\n }\n btn.disabled = false;\n setTimeout(fetchSvcStatus, 2000);\n }\n\n // ---- Heartbeat trigger ----\n async function triggerHeartbeat() {\n const btn = document.getElementById(\"hb-btn\");\n btn.disabled = true; btn.textContent = \"\u27F3 RUNNING\";\n addLogLine({ taskId: \"system\", stage: \"manual\", line: \"Heartbeat triggered by user\" });\n try {\n await fetch(\"/api/heartbeat\", { method: \"POST\" });\n } catch(e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"Heartbeat failed: \" + e.message });\n btn.disabled = false; btn.textContent = \"\u25B6 HEARTBEAT\";\n }\n }\n\n async function toggleTurbo() {\n const btn = document.getElementById(\"turbo-btn\");\n const isActive = btn.classList.contains(\"turbo-active\");\n const newState = !isActive;\n if (newState && !confirm(\"Enable TURBO mode? (5min heartbeat, 20 daily cap, auto-expires in 4h)\")) return;\n btn.disabled = true;\n try {\n const res = await fetch(\"/api/turbo\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ enabled: newState })\n });\n if (!res.ok) throw new Error(\"Failed\");\n addLogLine({ taskId: \"system\", stage: \"turbo\", line: newState ? \"TURBO MODE ON\" : \"TURBO MODE OFF\" });\n const stats = await fetch(\"/api/stats\").then(r => r.json());\n updateStats(stats);\n } catch (e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"Turbo toggle failed: \" + e.message });\n }\n btn.disabled = false;\n }\n\n async function switchProvider(provider) {\n try {\n const res = await fetch(\"/api/provider\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ provider })\n });\n if (!res.ok) {\n const data = await res.json().catch(() => ({}));\n throw new Error(data.error || \"Failed to switch provider\");\n }\n addLogLine({ taskId: \"system\", stage: \"provider\", line: \"Provider switched to \" + provider });\n const stats = await fetch(\"/api/stats\").then(r => r.json());\n updateStats(stats);\n } catch (e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"Provider switch failed: \" + e.message });\n }\n }\n\n // ---- PR Processor trigger ----\n async function triggerPRProcessor() {\n const btn = document.getElementById(\"pr-proc-btn\");\n btn.disabled = true; btn.textContent = \"\u27F3 PROCESSING\";\n addLogLine({ taskId: \"system\", stage: \"manual\", line: \"PR Processor triggered by user\" });\n try {\n const res = await fetch(\"/api/trigger-pr-processor\", { method: \"POST\" });\n if (!res.ok) {\n const data = await res.json();\n throw new Error(data.error || \"Failed to trigger PR processor\");\n }\n addLogLine({ taskId: \"system\", stage: \"manual\", line: \"PR Processor started successfully\" });\n setTimeout(() => {\n btn.disabled = false;\n btn.textContent = \"\u21BB PR REVIEW\";\n }, 3000);\n } catch(e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"PR Processor failed: \" + e.message });\n btn.disabled = false; btn.textContent = \"\u21BB PR REVIEW\";\n }\n }\n\n // ---- Restart stuck issues ----\n async function restartStuckIssues() {\n if (!confirm(\"Move all stuck/failed issues to Todo?\")) return;\n const btn = document.getElementById(\"restart-stuck-btn\");\n btn.disabled = true;\n btn.textContent = \"\u27F3 PROCESSING...\";\n\n try {\n const res = await fetch(\"/api/stuck-issues\");\n const data = await res.json();\n const allIssues = [...data.stuckIssues, ...data.failedIssues];\n\n let success = 0;\n let failed = 0;\n\n for (const issue of allIssues) {\n try {\n const moveRes = await fetch(\"/api/issue/move-to-todo\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ issueId: issue.id })\n });\n\n if (moveRes.ok) {\n success++;\n addLogLine({ taskId: \"system\", stage: \"stuck\", line: \"Moved \" + issue.identifier + \" to Todo\" });\n } else {\n failed++;\n }\n } catch (e) {\n failed++;\n }\n }\n\n addLogLine({ taskId: \"system\", stage: \"stuck\", line: \"Restart complete: \" + success + \" moved, \" + failed + \" failed\" });\n setTimeout(fetchStuckIssues, 1000);\n } catch (e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"Failed to restart stuck issues: \" + e.message });\n }\n\n btn.disabled = false;\n btn.textContent = \"\u21BB RESTART ALL\";\n }\n\n // ---- Project task updates ----\n function updateProjectTask(projectPath, taskId, title, priority, status) {\n const p = projects.find(x => x.path === projectPath);\n if (!p) return;\n\n // Get issueIdentifier from taskTitleMap\n const taskInfo = taskTitleMap.get(taskId);\n const issueIdentifier = taskInfo?.issueIdentifier;\n\n if (status === \"running\") {\n p.queued = p.queued.filter(t => t.id !== taskId);\n if (!p.running.find(t => t.id === taskId)) {\n p.running.push({ id: taskId, title, priority, issueIdentifier });\n }\n } else {\n if (!p.queued.find(t => t.id === taskId)) {\n p.queued.push({ id: taskId, title, priority, issueIdentifier });\n }\n }\n renderProjects();\n }\n function removeProjectTask(projectPath, taskId) {\n const p = projects.find(x => x.path === projectPath);\n if (!p) return;\n p.running = p.running.filter(t => t.id !== taskId);\n p.queued = p.queued.filter(t => t.id !== taskId);\n renderProjects();\n }\n\n // ---- Toggle project ----\n async function toggleProject(projectPath, enabled) {\n const p = projects.find(x => x.path === projectPath);\n if (p) p.enabled = enabled;\n renderProjects();\n try {\n await fetch(\"/api/projects/toggle\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ projectPath, enabled }),\n });\n } catch(e) {\n if (p) p.enabled = !enabled;\n renderProjects();\n }\n }\n\n // ---- Render Projects ----\n function renderProjects() {\n const el = document.getElementById(\"project-list\");\n const sumEl = document.getElementById(\"proj-summary\");\n if (!projects.length) { el.innerHTML = \"<div class=\\\"empty\\\">no repositories</div>\"; return; }\n const on = projects.filter(p => p.enabled).length;\n if (sumEl) sumEl.textContent = on + \"/\" + projects.length;\n\n el.innerHTML = projects.map(p => {\n // Use path as key, fall back to __n:name for unmapped projects\n const key = p.path || (\"__n:\" + p.name);\n const expanded = expandedProjects.has(key);\n const checked = p.enabled ? \"checked\" : \"\";\n const dCls = p.enabled ? \"\" : \" disabled\";\n const eCls = expanded ? \" expanded\" : \"\";\n // PRs always visible (not gated by expand)\n let prsHtml = \"\";\n if (p.prs && p.prs.length) {\n prsHtml = \"<div class=\\\"proj-issues\\\">\" +\n \"<div class=\\\"issue-sec-label\\\">open PRs (\" + p.prs.length + \")</div>\" +\n p.prs.map(function(pr) {\n return \"<div class=\\\"pr-row\\\">\" +\n \"<span class=\\\"pr-num\\\">#\" + pr.number + \"</span>\" +\n \"<span class=\\\"pr-branch\\\" title=\\\"\" + escapeAttr(pr.branch) + \"\\\">\" + escapeHtml(pr.branch) + \"</span>\" +\n \"<span class=\\\"pr-title\\\" title=\\\"\" + escapeAttr(pr.title) + \"\\\">\" + escapeHtml(pr.title) + \"</span>\" +\n \"<span class=\\\"pr-age\\\">\" + fmtAge(pr.updatedAt) + \"</span>\" +\n \"</div>\";\n }).join(\"\") +\n \"</div>\";\n }\n let issuesHtml = \"\";\n if (expanded) {\n const secs = [];\n if (p.running.length) secs.push(\n \"<div class=\\\"issue-sec-label\\\">running</div>\" +\n p.running.map(t => issueRow(t, \"idot-run\")).join(\"\")\n );\n if (p.queued.length) secs.push(\n \"<div class=\\\"issue-sec-label\\\">queued</div>\" +\n p.queued.map(t => issueRow(t, \"idot-que\")).join(\"\")\n );\n if (p.pending.length) {\n var stateOrder = [\"In Review\", \"In Progress\", \"Todo\", \"Backlog\"];\n var byState = {};\n for (var ti = 0; ti < p.pending.length; ti++) {\n var st = p.pending[ti].linearState || \"Todo\";\n if (!byState[st]) byState[st] = [];\n byState[st].push(p.pending[ti]);\n }\n for (var si = 0; si < stateOrder.length; si++) {\n var sn = stateOrder[si];\n if (!byState[sn] || !byState[sn].length) continue;\n secs.push(\n \"<div class=\\\"issue-sec-label\\\">\" + sn.toLowerCase() + \" (\" + byState[sn].length + \")</div>\" +\n byState[sn].map(t => issueRow(t, \"idot-pnd\")).join(\"\")\n );\n }\n var otherKeys = Object.keys(byState);\n for (var oi = 0; oi < otherKeys.length; oi++) {\n if (stateOrder.indexOf(otherKeys[oi]) === -1) {\n secs.push(\n \"<div class=\\\"issue-sec-label\\\">\" + otherKeys[oi].toLowerCase() + \" (\" + byState[otherKeys[oi]].length + \")</div>\" +\n byState[otherKeys[oi]].map(t => issueRow(t, \"idot-pnd\")).join(\"\")\n );\n }\n }\n }\n if (!secs.length) secs.push(\"<div class=\\\"empty\\\" style=\\\"padding:4px\\\">no issues</div>\");\n // Knowledge graph health info (if cached)\n var kgData = knowledgeCache[p.name] || knowledgeCache[p.path];\n if (kgData && kgData.summary) {\n var s = kgData.summary;\n secs.push(\n \"<div class=\\\"issue-sec-label\\\">code health</div>\" +\n \"<div style=\\\"padding:2px 8px;font-size:10px;color:#88aa88\\\">\" +\n \"modules:\" + s.totalModules + \" tests:\" + s.totalTestFiles +\n \" untested:\" + s.untestedModules.length +\n \" churn:\" + (s.avgChurnScore || 0).toFixed(2) +\n (s.hotModules.length ? \" hot:\" + s.hotModules.slice(0,3).map(function(m){return m.split(\"/\").pop()}).join(\",\") : \"\") +\n \"</div>\"\n );\n }\n issuesHtml = \"<div class=\\\"proj-issues\\\">\" + secs.join(\"\") + \"</div>\";\n }\n\n return (\n \"<div class=\\\"proj-card\" + dCls + eCls + \"\\\" data-key=\\\"\" + escapeAttr(key) + \"\\\">\" +\n \"<div class=\\\"proj-hdr\\\" data-key=\\\"\" + escapeAttr(key) + \"\\\" onclick=\\\"handleToggleExpand(this)\\\">\" +\n \"<span class=\\\"proj-arrow\\\"></span>\" +\n \"<div class=\\\"proj-info\\\">\" +\n \"<div class=\\\"proj-name\\\">\" + escapeHtml(p.name) + \"</div>\" +\n \"<div class=\\\"proj-path\\\">\" + escapeHtml(p.path) + \"</div>\" +\n (p.git ? \"<div class=\\\"git-info\\\">\" +\n \"\\u2387 <span class=\\\"git-branch-name\\\">\" + escapeHtml(p.git.branch) + \"</span>\" +\n (p.git.hasChanges ? \" <span class=\\\"git-dirty\\\">\\u25CF \" + p.git.uncommittedFiles + \"</span>\" : \"\") +\n ((p.git.ahead || p.git.behind) ? \" <span class=\\\"git-sync\\\">\" +\n (p.git.ahead ? \"\\u2191\" + p.git.ahead : \"\") +\n (p.git.behind ? \" \\u2193\" + p.git.behind : \"\") +\n \"</span>\" : \"\") +\n \"</div>\" : \"\") +\n \"</div>\" +\n \"<div class=\\\"proj-counts\\\">\" +\n (p.running.length ? \"<span class=\\\"cnt cnt-run\\\">\" + p.running.length + \"r</span>\" : \"\") +\n (p.queued.length ? \"<span class=\\\"cnt cnt-que\\\">\" + p.queued.length + \"q</span>\" : \"\") +\n (p.pending.length ? \"<span class=\\\"cnt cnt-pnd\\\">\" + p.pending.length + \"p</span>\" : \"\") +\n \"</div>\" +\n \"<div class=\\\"proj-toggle\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"display:flex;align-items:center;gap:4px\\\">\" +\n \"<button class=\\\"btn\\\" style=\\\"font-size:8px;padding:1px 4px;opacity:.5\\\" data-path=\\\"\" + escapeAttr(p.path) + \"\\\" onclick=\\\"handleUnpin(this)\\\">\u2715</button>\" +\n \"<label class=\\\"toggle\\\">\" +\n \"<input type=\\\"checkbox\\\" \" + checked + \" data-path=\\\"\" + escapeAttr(p.path) + \"\\\" onchange=\\\"handleToggleProject(this)\\\">\" +\n \"<span class=\\\"slider\\\"></span>\" +\n \"</label>\" +\n \"</div>\" +\n \"</div>\" +\n prsHtml +\n issuesHtml +\n \"</div>\"\n );\n }).join(\"\");\n }\n\n function issueRow(t, dotClass) {\n const prio = t.priority || 3;\n const extraCls = t.linearState === \"Backlog\" ? \" issue-backlog\" : \"\";\n const moveBtn = t.linearState === \"Backlog\" && t.linearId\n ? \"<button class=\\\"move-to-todo-btn\\\" data-issue-id=\\\"\" + escapeAttr(t.linearId) + \"\\\" onclick=\\\"handleMoveToTodo(this)\\\">\u2192 Todo</button>\"\n : \"\";\n return (\n \"<div class=\\\"issue-row\" + extraCls + \"\\\">\" +\n \"<span class=\\\"idot \" + dotClass + \"\\\"></span>\" +\n \"<span class=\\\"prio prio-\" + Math.min(4, prio) + \"\\\"></span>\" +\n (t.issueIdentifier ? \"<span class=\\\"issue-id\\\">\" + escapeHtml(t.issueIdentifier) + \"</span>\" : \"\") +\n \"<span class=\\\"issue-title\\\" title=\\\"\" + escapeAttr(t.title) + \"\\\">\" + escapeHtml(t.title) + \"</span>\" +\n moveBtn +\n \"</div>\"\n );\n }\n\n function toggleExpand(key) {\n if (expandedProjects.has(key)) expandedProjects.delete(key);\n else expandedProjects.add(key);\n renderProjects();\n }\n function handleToggleExpand(el) {\n const key = el.getAttribute(\"data-key\");\n if (key) toggleExpand(key);\n }\n function handleToggleProject(el) {\n const path = el.getAttribute(\"data-path\");\n if (path) toggleProject(path, el.checked);\n }\n async function handleUnpin(el) {\n const path = el.getAttribute(\"data-path\");\n if (!path) return;\n el.disabled = true;\n try {\n await fetch(\"/api/projects/unpin\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ projectPath: path }),\n });\n const res = await fetch(\"/api/projects\");\n projects = await res.json();\n renderProjects();\n // Update picker state so re-adding works correctly\n const item = allLocalProjects.find(function(p) { return p.path === path; });\n if (item) item.pinned = false;\n } catch(e) { el.disabled = false; }\n }\n async function handleMoveToTodo(el) {\n const issueId = el.getAttribute(\"data-issue-id\");\n if (!issueId) return;\n\n const originalText = el.textContent;\n el.disabled = true;\n el.textContent = \"Moving...\";\n\n try {\n const response = await fetch(\"/api/issue/move-to-todo\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ issueId }),\n });\n\n if (!response.ok) {\n throw new Error(\"Failed to move issue\");\n }\n\n // Refresh projects to show updated state\n const res = await fetch(\"/api/projects\");\n projects = await res.json();\n renderProjects();\n } catch(e) {\n el.disabled = false;\n el.textContent = originalText;\n alert(\"Failed to move issue to Todo: \" + e.message);\n }\n }\n\n // ---- Pipeline Stages ----\n function addStageRow(data) {\n stageRows.push(data);\n if (stageRows.length > MAX_STAGE) stageRows = stageRows.slice(-MAX_STAGE);\n renderStages();\n }\n function shortModel(name) {\n if (!name) return \"\";\n // Order matters: most specific suffix first so 'sonnet-4-6' isn't\n // captured by the generic 'sonnet-4' fallback below.\n if (name.includes(\"opus-4-7\")) return \"opus-4.7\";\n if (name.includes(\"opus-4-6\")) return \"opus-4.6\";\n if (name.includes(\"sonnet-4-6\")) return \"sonnet-4.6\";\n if (name.includes(\"sonnet-4-5\")) return \"sonnet-4.5\";\n if (name.includes(\"haiku-4-5\")) return \"haiku-4.5\";\n if (name.includes(\"opus-4\")) return \"opus-4\";\n if (name.includes(\"sonnet-4\")) return \"sonnet-4\";\n var parts = name.split(\"-\");\n return parts[parts.length - 1];\n }\n function fmtTokens(n) {\n if (n == null) return \"\";\n if (n >= 1000000) return (n / 1000000).toFixed(1) + \"M\";\n if (n >= 1000) return (n / 1000).toFixed(1) + \"k\";\n return String(n);\n }\n function buildStageDetails(r) {\n // Builds the expanded \"what did the worker actually do\" panel for\n // a pipeline:stage payload. Returns \"\" when there's nothing to show.\n const lines = [];\n function addLine(key, valHtml) {\n lines.push(\n \"<div class=\\\"sd-line\\\"><div class=\\\"sd-key\\\">\" + escapeHtml(key) + \"</div>\" +\n \"<div class=\\\"sd-val\\\">\" + valHtml + \"</div></div>\"\n );\n }\n function addList(key, items) {\n const ul = items.map(function(s) { return \"<li>\" + escapeHtml(String(s)) + \"</li>\"; }).join(\"\");\n lines.push(\n \"<div class=\\\"sd-line\\\"><div class=\\\"sd-key\\\">\" + escapeHtml(key) + \"</div>\" +\n \"<div class=\\\"sd-val\\\"><ul>\" + ul + \"</ul></div></div>\"\n );\n }\n\n if (r.summary) addLine(\"Summary\", escapeHtml(r.summary));\n if (r.decision) {\n const cls = \"sd-decision-\" + r.decision;\n addLine(\"Decision\", \"<span class=\\\"\" + cls + \"\\\">\" + escapeHtml(r.decision.toUpperCase()) + \"</span>\");\n }\n if (r.feedback) addLine(\"Feedback\", escapeHtml(r.feedback));\n if (Array.isArray(r.filesChanged) && r.filesChanged.length > 0) {\n const label = \"Files (\" + (r.filesChangedCount || r.filesChanged.length) + \")\";\n addList(label, r.filesChanged);\n }\n if (Array.isArray(r.commands) && r.commands.length > 0) {\n const label = \"Commands (\" + (r.commandsCount || r.commands.length) + \")\";\n addList(label, r.commands);\n }\n if (Array.isArray(r.issues) && r.issues.length > 0) {\n const label = \"Issues (\" + (r.issuesCount || r.issues.length) + \")\";\n addList(label, r.issues);\n }\n if (Array.isArray(r.failedTests) && r.failedTests.length > 0) {\n addList(\"Failed tests\", r.failedTests);\n }\n if (r.passed != null || r.failed != null) {\n addLine(\"Tests\", escapeHtml((r.passed || 0) + \" passed, \" + (r.failed || 0) + \" failed\" + (r.coverage != null ? \" \u2014 coverage \" + r.coverage + \"%\" : \"\")));\n }\n if (r.confidencePercent != null) addLine(\"Confidence\", escapeHtml(r.confidencePercent + \"%\"));\n if (r.haltReason) addLine(\"Halt\", escapeHtml(r.haltReason));\n if (r.bsScore != null) addLine(\"BS score\", escapeHtml(String(r.bsScore)));\n if (r.criticalCount || r.warningCount) {\n addLine(\"Audit\", escapeHtml((r.criticalCount || 0) + \" critical, \" + (r.warningCount || 0) + \" warnings\"));\n }\n if (r.changelogEntry) addLine(\"Changelog\", escapeHtml(r.changelogEntry));\n if (r.durationMs != null) addLine(\"Duration\", escapeHtml((r.durationMs / 1000).toFixed(1) + \"s\"));\n if (r.error) {\n addLine(\"Error\", \"<span style=\\\"color:var(--red)\\\">\" + escapeHtml(r.error) + \"</span>\");\n }\n return lines.join(\"\");\n }\n\n function toggleStageDetails(idx) {\n const block = document.querySelector(\"[data-stage-idx=\\\"\" + idx + \"\\\"]\");\n if (block) block.classList.toggle(\"expanded\");\n }\n\n function renderStages() {\n const el = document.getElementById(\"stage-list\");\n const cnt = document.getElementById(\"stage-count\");\n if (!stageRows.length) { el.innerHTML = \"<div class=\\\"empty\\\">no pipeline events</div>\"; return; }\n if (cnt) cnt.textContent = stageRows.length + \"/\" + MAX_STAGE;\n el.innerHTML = stageRows.slice().reverse().map((r, i) => {\n const info = r.taskId ? taskTitleMap.get(r.taskId) : null;\n let taskLabel = \"\";\n if (info) {\n taskLabel = info.issueIdentifier\n ? info.issueIdentifier + (info.title ? \" \" + info.title.slice(0, 22) : \"\")\n : (info.title ? info.title.slice(0, 30) : \"\");\n } else if (r.taskId) {\n taskLabel = r.taskId.slice(0, 8);\n }\n const projPath = r.taskId ? taskProjectMap.get(r.taskId) : null;\n const repoName = projPath ? projPath.split(\"/\").pop() : \"\";\n const startTs = r.taskId ? taskStartMap.get(r.taskId) : null;\n let elapsed = \"\";\n if (startTs) {\n const sec = Math.floor((Date.now() - startTs) / 1000);\n if (sec < 60) elapsed = sec + \"s\";\n else if (sec < 3600) elapsed = Math.floor(sec / 60) + \"m\" + (sec % 60) + \"s\";\n else elapsed = Math.floor(sec / 3600) + \"h\" + Math.floor((sec % 3600) / 60) + \"m\";\n }\n const modelStr = r.model ? shortModel(r.model) : \"\";\n let tokenStr = \"\";\n if (r.inputTokens || r.outputTokens) {\n tokenStr = fmtTokens(r.inputTokens) + \"/\" + fmtTokens(r.outputTokens);\n if (r.costUsd != null) tokenStr += \" $\" + r.costUsd.toFixed(2);\n }\n\n // Inline summary on the row itself, so the user sees *what* happened\n // without having to expand.\n let inlineSummary = \"\";\n if (r.decision) {\n const cls = \"sd-decision-\" + r.decision;\n inlineSummary = \"<span class=\\\"\" + cls + \"\\\">\" + escapeHtml(r.decision.toUpperCase()) + \"</span>\" +\n (r.feedback ? \" \u00B7 \" + escapeHtml(r.feedback.slice(0, 80)) : \"\");\n } else if (r.summary) {\n inlineSummary = escapeHtml(r.summary);\n if (r.filesChangedCount > 0) inlineSummary += \" \u00B7 \" + r.filesChangedCount + \" files\";\n } else if (r.passed != null || r.failed != null) {\n inlineSummary = \"\u2713 \" + (r.passed || 0) + \" \u2717 \" + (r.failed || 0);\n } else if (r.error) {\n inlineSummary = \"<span style=\\\"color:var(--red)\\\">\" + escapeHtml(r.error.slice(0, 120)) + \"</span>\";\n }\n\n const detailsHtml = buildStageDetails(r);\n const hasDetails = detailsHtml.length > 0;\n const rowClass = \"stage-row\" + (hasDetails ? \" has-details\" : \"\");\n const onclick = hasDetails ? \" onclick=\\\"toggleStageDetails(\" + i + \")\\\"\" : \"\";\n\n return (\n \"<div class=\\\"stage-block\\\" data-stage-idx=\\\"\" + i + \"\\\">\" +\n \"<div class=\\\"\" + rowClass + \"\\\"\" + onclick + \">\" +\n \"<div class=\\\"sdot \" + (r.status || \"\") + \"\\\"></div>\" +\n \"<div class=\\\"srepo\\\">\" + escapeHtml(repoName) + \"</div>\" +\n \"<div class=\\\"sname\\\">\" + escapeHtml(r.stage) + \"</div>\" +\n \"<div class=\\\"stask\\\" title=\\\"\" + escapeAttr(r.taskId || \"\") + \"\\\">\" + escapeHtml(taskLabel) + \"</div>\" +\n \"<div class=\\\"ssummary\\\">\" + inlineSummary + \"</div>\" +\n \"<div class=\\\"smodel\\\">\" + escapeHtml(modelStr) + \"</div>\" +\n \"<div class=\\\"stokens\\\">\" + escapeHtml(tokenStr) + \"</div>\" +\n \"<div class=\\\"selapsed\\\">\" + elapsed + \"</div>\" +\n \"<div class=\\\"sstatus\\\">\" + (r.status || \"\") + \"</div>\" +\n \"</div>\" +\n (hasDetails ? \"<div class=\\\"stage-details\\\">\" + detailsHtml + \"</div>\" : \"\") +\n \"</div>\"\n );\n }).join(\"\");\n el.scrollTop = 0;\n }\n\n // ---- Log Tab ----\n function selectLogTab(taskId) {\n selectedLogTaskId = taskId;\n document.querySelectorAll('.log-tab').forEach(t =>\n t.classList.toggle('active', t.dataset.task === (taskId ?? 'all'))\n );\n renderLog();\n }\n\n function updateLogTabs() {\n const bar = document.getElementById('log-tab-bar');\n const taskIds = [...new Set(logLines.map(l => l.taskId).filter(id => id && id !== 'system'))];\n // Sort by most recent start time\n taskIds.sort((a, b) => (taskStartMap.get(b) || 0) - (taskStartMap.get(a) || 0));\n\n let html = '<button class=\"log-tab' + (selectedLogTaskId === null ? ' active' : '')\n + '\" data-task=\"all\" onclick=\"selectLogTab(null)\">ALL</button>';\n\n for (const tid of taskIds) {\n const info = taskTitleMap.get(tid);\n const label = info?.issueIdentifier || tid.slice(0, 8);\n const isActive = selectedLogTaskId === tid;\n html += '<button class=\"log-tab' + (isActive ? ' active' : '')\n + '\" data-task=\"' + tid + '\" onclick=\"selectLogTab(\\'' + tid + '\\')\">'\n + escapeHtml(label) + '</button>';\n }\n bar.innerHTML = html;\n }\n\n // ---- Log ----\n function addLogLine(data) {\n data._ts = Date.now();\n logLines.push(data);\n if (logLines.length > MAX_LOG) logLines = logLines.slice(-MAX_LOG);\n updateLogTabs();\n renderLog();\n }\n\n function classifyLog(line) {\n if (!line) return { cls: \"log-spacer\", icon: \"\" };\n if (line === \"\u2500\u2500\u2500\") return { cls: \"log-separator\", icon: \"\" };\n if (/^\u25A0 /.test(line)) return { cls: \"log-heading2\", icon: \"\u25A0\" };\n if (/^[\u250C\u2514\u2502]/.test(line)) return { cls: \"log-code\", icon: \"\" };\n if (/^\u25B8 /.test(line)) return { cls: \"log-tool\", icon: \"\u25B8\" };\n if (/^\u25B6|Heartbeat started|Stage started|Iteration [0-9]/.test(line)) return { cls: \"log-heading\", icon: \"\u25B6\" };\n if (/^\u2713|success=true|approved|completed|Done|Created sub-issue/.test(line)) return { cls: \"log-success\", icon: \"\u2713\" };\n if (/^\u2717|success=false|failed|error|Error|rejected|exceeded/.test(line)) return { cls: \"log-fail\", icon: \"\u2717\" };\n if (/^\u27F3|Fetching|Decomposing|Running|Scheduling|Spawning/.test(line)) return { cls: \"\", icon: \"\u27F3\" };\n if (/^\u26D4|Blocked|Time window|blocked/.test(line)) return { cls: \"log-warn\", icon: \"\u26D4\" };\n if (/^\u2014|No task|already completed|no log/.test(line)) return { cls: \"log-system\", icon: \"\u2014\" };\n if (/Cost:|\\$[\\.0-9]/.test(line)) return { cls: \"\", icon: \"\uD83D\uDCB2\" };\n if (/Git detected|files changed|filesChanged/.test(line)) return { cls: \"\", icon: \"\uD83D\uDCC1\" };\n if (/Selected [0-9]+ tasks/.test(line)) return { cls: \"\", icon: \"\uD83C\uDFAF\" };\n if (/Enqueued|executePipeline/.test(line)) return { cls: \"\", icon: \"\uD83D\uDCCB\" };\n if (/Direct path|Project|path found/.test(line)) return { cls: \"log-system\", icon: \"\uD83D\uDCC2\" };\n return { cls: \"\", icon: \"\u00B7\" };\n }\n\n function formatLogText(raw) {\n if (!raw) return \"\";\n // Detect and humanize raw JSON that slipped through\n const trimmed = raw.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n try {\n const obj = JSON.parse(trimmed);\n if (obj.needsDecomposition === false) {\n const r = obj.reason ? obj.reason.slice(0, 120) : \"\";\n raw = \"\\u2713 No decomposition needed (\" + (obj.totalEstimatedMinutes || \"?\") + \"min) \" + r;\n } else if (obj.needsDecomposition === true && obj.subTasks) {\n raw = \"\\uD83D\\uDD00 Decomposed into \" + obj.subTasks.length + \" sub-tasks (total \" + (obj.totalEstimatedMinutes || \"?\") + \"min)\";\n } else if (obj.success !== undefined) {\n raw = (obj.success ? \"\\u2713 \" : \"\\u2717 \") + (obj.summary || obj.error || JSON.stringify(obj).slice(0, 120));\n }\n } catch { /* not valid JSON */ }\n }\n let t = escapeHtml(raw);\n // inline bold: **text** \u2192 highlighted\n t = t.replace(/\\*\\*([^*]+)\\*\\*/g, '<span class=\"lhighlight\">$1</span>');\n // highlight cost figures\n t = t.replace(/(\\$[\\d.]+)/g, '<span class=\"lcost\">$1</span>');\n // highlight file counts\n t = t.replace(/(\\d+ files? changed)/g, '<span class=\"lfiles\">$1</span>');\n // highlight durations\n t = t.replace(/(\\d+\\.\\d+s|\\d+ms)/g, '<span class=\"lcost\">$1</span>');\n // highlight task titles in quotes\n t = t.replace(/(&quot;[^&]+&quot;)/g, '<span class=\"lhighlight\">$1</span>');\n // highlight issue identifiers\n t = t.replace(/(INT-\\d+)/g, '<span class=\"lhighlight\">$1</span>');\n return t;\n }\n\n function fmtLogTime(ts) {\n if (!ts) return \"\";\n const d = new Date(ts);\n return d.getHours().toString().padStart(2,\"0\") + \":\" +\n d.getMinutes().toString().padStart(2,\"0\") + \":\" +\n d.getSeconds().toString().padStart(2,\"0\");\n }\n\n function renderLog() {\n const el = document.getElementById(\"log-list\");\n const cnt = document.getElementById(\"log-count\");\n const filtered = selectedLogTaskId === null\n ? logLines\n : logLines.filter(l => l.taskId === selectedLogTaskId);\n if (!filtered.length) {\n el.innerHTML = \"<div class=\\\"empty\\\">\" + (selectedLogTaskId ? \"no logs for this task\" : \"no log output\") + \"</div>\";\n if (cnt) cnt.textContent = selectedLogTaskId ? filtered.length + \"/\" + logLines.length : \"\";\n return;\n }\n if (cnt) cnt.textContent = (selectedLogTaskId ? filtered.length + \"/\" : \"\") + logLines.length + \"/\" + MAX_LOG;\n const atBottom = el.scrollHeight - el.scrollTop <= el.clientHeight + 50;\n el.innerHTML = filtered.map(l => {\n const info = l.taskId ? taskTitleMap.get(l.taskId) : null;\n const tag = info?.issueIdentifier\n ? info.issueIdentifier\n : (l.taskId === \"system\" ? \"SYS\" : (l.taskId || \"\").slice(0, 8));\n const { cls, icon } = classifyLog(l.line);\n // spacer/separator use minimal rendering\n if (cls === \"log-spacer\") return \"<div class=\\\"log-line log-spacer\\\"></div>\";\n if (cls === \"log-separator\") return \"<div class=\\\"log-line log-separator\\\"><span class=\\\"ltext\\\">\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500</span></div>\";\n const time = fmtLogTime(l._ts);\n const stage = l.stage && l.stage !== \"heartbeat\" ? l.stage : \"\";\n // heading2: strip \u25A0 prefix (icon handles it)\n const displayLine = cls === \"log-heading2\" ? (l.line || \"\").replace(/^\u25A0 /, \"\") : l.line;\n // tool: strip \u25B8 prefix\n const displayLine2 = cls === \"log-tool\" ? (displayLine || \"\").replace(/^\u25B8 /, \"\") : displayLine;\n return (\n \"<div class=\\\"log-line \" + cls + \"\\\">\" +\n \"<span class=\\\"ltime\\\">\" + time + \"</span>\" +\n \"<span class=\\\"licon\\\">\" + icon + \"</span>\" +\n \"<span class=\\\"ltag\\\" title=\\\"\" + escapeAttr(l.taskId || \"\") + \"\\\">\" + escapeHtml(tag) + \"</span>\" +\n (stage ? \"<span class=\\\"lstage\\\">\" + escapeHtml(stage) + \"</span>\" : \"\") +\n \"<span class=\\\"ltext\\\">\" + formatLogText(displayLine2) + \"</span>\" +\n \"</div>\"\n );\n }).join(\"\");\n if (atBottom) el.scrollTop = el.scrollHeight;\n }\n\n // ---- Chat ----\n function fmtTime(ts) {\n const d = new Date(ts);\n return d.getHours().toString().padStart(2,\"0\") + \":\" +\n d.getMinutes().toString().padStart(2,\"0\");\n }\n\n function appendChatMsg(role, text, id, ts) {\n const container = document.getElementById(\"chat-messages\");\n const line = document.createElement(\"div\");\n line.className = \"chat-line chat-\" + role;\n if (id) line.id = id;\n const prefix = role === \"user\"\n ? \"<span class=\\\"chat-prefix\\\">YOU &gt;</span>\"\n : \"<span class=\\\"chat-prefix\\\">OpenSwarm&gt;</span>\";\n const tsStr = ts ? \"<span class=\\\"chat-ts\\\">\" + fmtTime(ts) + \"</span>\" : \"\";\n line.innerHTML = prefix + \" <span class=\\\"chat-text\\\">\" + escapeHtml(text) + \"</span>\" + tsStr;\n container.appendChild(line);\n container.scrollTop = container.scrollHeight;\n }\n\n async function sendChat() {\n if (chatBusy) return;\n const input = document.getElementById(\"chat-input\");\n const sendBtn = document.getElementById(\"chat-send\");\n const msg = input.value.trim();\n if (!msg) return;\n input.value = \"\";\n chatBusy = true;\n sendBtn.disabled = true;\n\n appendChatMsg(\"user\", msg, null, Date.now());\n\n const thinkId = \"think-\" + Date.now();\n const thinkEl = document.createElement(\"div\");\n thinkEl.id = thinkId;\n thinkEl.className = \"chat-line chat-agent\";\n thinkEl.innerHTML = \"<span class=\\\"chat-prefix\\\">OpenSwarm&gt;</span> <span class=\\\"chat-text chat-thinking\\\">thinking...</span>\";\n document.getElementById(\"chat-messages\").appendChild(thinkEl);\n document.getElementById(\"chat-messages\").scrollTop = 99999;\n\n try {\n const res = await fetch(\"/api/chat\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ message: msg }),\n });\n const data = await res.json();\n document.getElementById(thinkId)?.remove();\n appendChatMsg(\"agent\", data.response || data.error || \"(no response)\", null, Date.now());\n } catch(e) {\n document.getElementById(thinkId)?.remove();\n appendChatMsg(\"agent\", \"[ERROR] \" + e.message, null, Date.now());\n }\n\n chatBusy = false;\n sendBtn.disabled = false;\n input.focus();\n }\n\n // ---- Utils ----\n function fmtAge(isoDate) {\n if (!isoDate) return \"\";\n var diff = Math.max(0, Date.now() - new Date(isoDate).getTime());\n var sec = Math.floor(diff / 1000);\n if (sec < 60) return sec + \"s\";\n var min = Math.floor(sec / 60);\n if (min < 60) return min + \"m\";\n var hr = Math.floor(min / 60);\n if (hr < 24) return hr + \"h\";\n var day = Math.floor(hr / 24);\n if (day < 7) return day + \"d\";\n var wk = Math.floor(day / 7);\n return wk + \"w\";\n }\n function escapeHtml(text) {\n const d = document.createElement(\"div\"); d.textContent = String(text || \"\"); return d.innerHTML;\n }\n function escapeAttr(text) {\n return String(text || \"\").replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\");\n }\n\n // ---- Repo Picker ----\n let allLocalProjects = [];\n let pickerOpen = false;\n\n async function openRepoPicker() {\n if (pickerOpen) return;\n pickerOpen = true;\n const overlay = document.getElementById(\"repo-picker\");\n overlay.style.display = \"flex\";\n document.getElementById(\"repo-search\").value = \"\";\n document.getElementById(\"repo-picker-list\").innerHTML =\n \"<div class=\\\"empty\\\">loading...</div>\";\n document.getElementById(\"repo-search\").focus();\n\n try {\n fetchScanPaths();\n const res = await fetch(\"/api/local-projects\");\n allLocalProjects = await res.json();\n filterRepos(\"\");\n } catch(e) {\n document.getElementById(\"repo-picker-list\").innerHTML =\n \"<div class=\\\"empty\\\">failed to load: \" + escapeHtml(e.message) + \"</div>\";\n }\n }\n\n function closeRepoPicker() {\n pickerOpen = false;\n document.getElementById(\"repo-picker\").style.display = \"none\";\n }\n\n function filterRepos(q) {\n const list = document.getElementById(\"repo-picker-list\");\n const filtered = q\n ? allLocalProjects.filter(p =>\n p.name.toLowerCase().includes(q.toLowerCase()) ||\n p.path.toLowerCase().includes(q.toLowerCase()))\n : allLocalProjects;\n\n if (!filtered.length) {\n list.innerHTML = \"<div class=\\\"empty\\\">no results</div>\";\n return;\n }\n list.innerHTML = filtered.slice(0, 80).map(p => {\n const badge = p.pinned ? \"<span class=\\\"repo-item-badge\\\">pinned</span>\" : \"\";\n return (\n \"<div class=\\\"repo-item\\\" data-path=\\\"\" + escapeAttr(p.path) + \"\\\" onclick=\\\"pickRepo(this)\\\">\" +\n \"<div>\" +\n \"<div class=\\\"repo-item-name\\\">\" + escapeHtml(p.name) + \"</div>\" +\n \"<div class=\\\"repo-item-path\\\">\" + escapeHtml(p.path) + \"</div>\" +\n \"</div>\" +\n badge +\n \"</div>\"\n );\n }).join(\"\");\n }\n\n async function pickRepo(el) {\n const path = el.getAttribute(\"data-path\");\n if (!path) return;\n el.style.opacity = \"0.4\";\n try {\n await fetch(\"/api/projects/pin\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ projectPath: path }),\n });\n // Refresh project list\n const res = await fetch(\"/api/projects\");\n projects = await res.json();\n renderProjects();\n // Mark as pinned in local picker list\n const item = allLocalProjects.find(p => p.path === path);\n if (item) item.pinned = true;\n filterRepos(document.getElementById(\"repo-search\").value);\n } catch(e) {\n console.error(\"Pin failed:\", e);\n }\n el.style.opacity = \"1\";\n }\n\n // ---- Scan Paths ----\n async function fetchScanPaths() {\n try {\n const res = await fetch(\"/api/scan-paths\");\n if (res.ok) {\n const data = await res.json();\n renderScanPaths(data);\n }\n } catch(e) {\n console.error(\"fetchScanPaths error:\", e);\n }\n }\n\n function renderScanPaths(data) {\n const list = document.getElementById(\"scan-paths-list\");\n if (!list) return;\n const rows = [];\n for (const p of (data.configPaths || [])) {\n rows.push(\n \"<div class=\\\"scan-path-row\\\">\" +\n \"<span class=\\\"path\\\">\" + escapeHtml(p) + \"</span>\" +\n \"<button class=\\\"scan-path-remove\\\" title=\\\"remove\\\" onclick=\\\"removeScanPath('\" + escapeAttr(p) + \"')\\\">\u2715</button>\" +\n \"</div>\"\n );\n }\n for (const p of (data.customPaths || [])) {\n rows.push(\n \"<div class=\\\"scan-path-row\\\">\" +\n \"<span class=\\\"path\\\">\" + escapeHtml(p) + \"</span>\" +\n \"<button class=\\\"scan-path-remove\\\" onclick=\\\"removeScanPath('\" + escapeAttr(p) + \"')\\\">\u2715</button>\" +\n \"</div>\"\n );\n }\n list.innerHTML = rows.length > 0 ? rows.join(\"\") : \"<div style=\\\"color:#334433;font-size:10px\\\">no scan paths configured</div>\";\n }\n\n async function addScanPath(explicitPath) {\n const input = document.getElementById(\"scan-path-input\");\n const path = (explicitPath ?? input.value).trim();\n if (!path) return;\n if (!explicitPath) input.value = \"\";\n try {\n await fetch(\"/api/scan-paths\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ path }),\n });\n await fetchScanPaths();\n // Refresh project list in picker\n const res = await fetch(\"/api/local-projects\");\n allLocalProjects = await res.json();\n filterRepos(document.getElementById(\"repo-search\").value);\n } catch(e) {\n console.error(\"addScanPath error:\", e);\n }\n }\n\n function toggleManualPathInput() {\n const row = document.getElementById(\"manual-path-row\");\n if (!row) return;\n if (row.style.display === \"none\" || row.style.display === \"\") {\n row.style.display = \"flex\";\n const input = document.getElementById(\"scan-path-input\");\n if (input) input.focus();\n } else {\n row.style.display = \"none\";\n }\n }\n\n // ---- Folder Browser (native-style picker via /api/fs/list) ----\n var folderBrowserCurrent = null;\n var folderBrowserParent = null;\n\n async function openFolderBrowser(startPath) {\n const modal = document.getElementById(\"folder-browser\");\n if (!modal) return;\n modal.style.display = \"flex\";\n await loadFolderBrowser(startPath || \"~/dev\");\n }\n\n function closeFolderBrowser() {\n const modal = document.getElementById(\"folder-browser\");\n if (modal) modal.style.display = \"none\";\n }\n\n async function loadFolderBrowser(path) {\n const list = document.getElementById(\"fb-list\");\n const pathEl = document.getElementById(\"fb-path\");\n const upBtn = document.getElementById(\"fb-up\");\n const selectBtn = document.getElementById(\"fb-select\");\n if (!list || !pathEl) return;\n list.innerHTML = \"<div style=\\\"padding:18px;color:var(--dim);font-size:12px;text-align:center\\\">Loading\u2026</div>\";\n try {\n const res = await fetch(\"/api/fs/list?path=\" + encodeURIComponent(path));\n if (!res.ok) {\n const err = await res.json().catch(() => ({}));\n list.innerHTML = \"<div style=\\\"padding:18px;color:var(--red);font-size:12px;text-align:center\\\">\" + escapeHtml(err.error || (\"HTTP \" + res.status)) + \"</div>\";\n return;\n }\n const data = await res.json();\n folderBrowserCurrent = data.path;\n folderBrowserParent = data.parent;\n pathEl.value = data.path;\n if (upBtn) upBtn.disabled = !data.parent;\n if (selectBtn) selectBtn.textContent = \"Select \\\"\" + (data.name || data.path) + \"\\\"\";\n\n const dirs = (data.entries || []).filter(function(e) { return e.isDir; });\n if (dirs.length === 0) {\n list.innerHTML = \"<div style=\\\"padding:18px;color:var(--dim);font-size:12px;text-align:center\\\">No subfolders</div>\";\n return;\n }\n list.innerHTML = dirs.map(function(e) {\n return \"<div class=\\\"fb-row\\\" data-name=\\\"\" + escapeAttr(e.name) + \"\\\" onclick=\\\"folderBrowserEnter(this.getAttribute('data-name'))\\\"\" +\n \" style=\\\"padding:7px 18px;cursor:pointer;font-size:13px;color:var(--white);display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--border2);transition:background .12s\\\"\" +\n \" onmouseover=\\\"this.style.background='var(--bg3)'\\\" onmouseout=\\\"this.style.background=''\\\">\" +\n \"<span style=\\\"color:var(--green)\\\">\uD83D\uDCC1</span>\" +\n \"<span>\" + escapeHtml(e.name) + \"</span>\" +\n \"</div>\";\n }).join(\"\");\n } catch (e) {\n list.innerHTML = \"<div style=\\\"padding:18px;color:var(--red);font-size:12px;text-align:center\\\">\" + escapeHtml(String(e)) + \"</div>\";\n }\n }\n\n function folderBrowserEnter(name) {\n if (!folderBrowserCurrent || !name) return;\n // Join via the server side by sending the absolute path of the child\n const sep = folderBrowserCurrent.endsWith(\"/\") ? \"\" : \"/\";\n loadFolderBrowser(folderBrowserCurrent + sep + name);\n }\n\n function folderBrowserUp() {\n if (folderBrowserParent) loadFolderBrowser(folderBrowserParent);\n }\n\n async function folderBrowserSelect() {\n if (!folderBrowserCurrent) return;\n const picked = folderBrowserCurrent;\n closeFolderBrowser();\n await addScanPath(picked);\n }\n\n async function removeScanPath(path) {\n try {\n await fetch(\"/api/scan-paths/\" + encodeURIComponent(path), {\n method: \"DELETE\",\n });\n await fetchScanPaths();\n // Refresh project list in picker\n const res = await fetch(\"/api/local-projects\");\n allLocalProjects = await res.json();\n filterRepos(document.getElementById(\"repo-search\").value);\n } catch(e) {\n console.error(\"removeScanPath error:\", e);\n }\n }\n\n // ---- Monitors ----\n var monitorsData = [];\n async function fetchMonitors() {\n try {\n const res = await fetch(\"/api/monitors\");\n if (res.ok) { monitorsData = await res.json(); renderMonitors(); }\n } catch {}\n }\n function renderMonitors() {\n renderMonitorsAndProcesses();\n }\n function fmtDur(ms) {\n var s = Math.floor(ms / 1000);\n var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);\n if (h >= 24) return Math.floor(h / 24) + \"d \" + (h % 24) + \"h\";\n if (h > 0) return h + \"h \" + m + \"m\";\n return m + \"m\";\n }\n\n // ---- Processes ----\n var processesData = [];\n async function fetchProcesses() {\n try {\n const res = await fetch(\"/api/processes\");\n if (res.ok) { processesData = await res.json(); renderMonitorsAndProcesses(); }\n } catch {}\n }\n async function stopProcess(id, isPipeline) {\n var verb = isPipeline ? \"Cancel task\" : \"Kill process\";\n if (!confirm(verb + \" \" + id + \"?\")) return;\n try {\n await fetch(\"/api/processes/\" + encodeURIComponent(id), { method: \"DELETE\" });\n processesData = processesData.filter(p => String(p.id) !== String(id));\n renderMonitorsAndProcesses();\n } catch(e) {\n addLogLine({ taskId: \"system\", stage: \"error\", line: \"Stop failed: \" + e.message });\n }\n }\n function procActivityIcon(lastActivityAt) {\n var ago = (Date.now() - lastActivityAt) / 1000;\n if (ago < 10) return \"\\u26A1\";\n if (ago < 60) return \"\\u23F8\";\n return \"\\u2757\";\n }\n function renderMonitorsAndProcesses() {\n var panel = document.getElementById(\"monitor-panel\");\n var el = document.getElementById(\"monitor-list\");\n var countEl = document.getElementById(\"monitor-count\");\n var hasMonitors = monitorsData.length > 0;\n var hasProcesses = processesData.length > 0;\n if (!hasMonitors && !hasProcesses) {\n el.innerHTML = \"<div class=\\\"empty\\\">no monitors or processes</div>\";\n var counts = [];\n if (countEl) countEl.textContent = \"\";\n return;\n }\n var parts = [];\n if (processesData.length) parts.push(processesData.length + \"p\");\n if (monitorsData.length) parts.push(monitorsData.length + \"m\");\n if (countEl) countEl.textContent = parts.join(\" \");\n var html = \"\";\n // Processes section\n if (hasProcesses) {\n html += \"<div class=\\\"issue-sec-label\\\">processes</div>\";\n html += processesData.map(function(p) {\n var dur = fmtDur(Date.now() - p.spawnedAt);\n var isPipeline = p.kind === \"pipeline\";\n var act = isPipeline ? \"\\u2699\" : procActivityIcon(p.lastActivityAt);\n var modelStr = p.model ? shortModel(p.model) : \"\";\n var projName = p.project || (p.projectPath ? p.projectPath.split(\"/\").pop() : \"\");\n // In-process pipeline tasks have no OS PID \u2014 show the issue id instead, and\n // a CANCEL button (aborts the pipeline + its in-flight adapter call).\n var lead = isPipeline ? escapeHtml(p.taskId || \"task\") : p.pid;\n var btn = isPipeline\n ? '<button class=\"proc-kill\" onclick=\"stopProcess(\\'' + escapeAttr(String(p.id)) + '\\', true)\">CANCEL</button>'\n : '<button class=\"proc-kill\" onclick=\"stopProcess(\\'' + escapeAttr(String(p.id)) + '\\', false)\">KILL</button>';\n return '<div class=\"proc-row\">' +\n '<span class=\"proc-pid\">' + lead + '</span>' +\n '<span class=\"proc-stage\">' + escapeHtml(p.stage) + '</span>' +\n '<span class=\"proc-model\">' + escapeHtml(modelStr) + '</span>' +\n '<span style=\"color:var(--dim);font-size:9px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\" title=\"' + escapeAttr(p.projectPath || \"\") + '\">' + escapeHtml(projName) + '</span>' +\n '<span class=\"proc-activity\">' + act + '</span>' +\n '<span class=\"proc-dur\">' + dur + '</span>' +\n btn +\n '</div>';\n }).join(\"\");\n }\n // Monitors section\n if (hasMonitors) {\n html += \"<div class=\\\"issue-sec-label\\\">monitors</div>\";\n html += monitorsData.map(function(m) {\n var stateColor = m.state === \"running\" ? \"var(--green)\" : m.state === \"completed\" ? \"var(--cyan)\" : m.state === \"failed\" || m.state === \"timeout\" ? \"var(--red)\" : \"var(--dim)\";\n var elapsed = m.registeredAt ? fmtDur(Date.now() - m.registeredAt) : \"-\";\n var lastOut = m.lastOutput ? escapeHtml(m.lastOutput.slice(0, 80)) : \"-\";\n return '<div style=\"padding:4px 6px;border-bottom:1px solid var(--border);font-size:11px\">' +\n '<div style=\"display:flex;align-items:center;gap:6px\">' +\n '<span style=\"color:' + stateColor + ';font-weight:bold\">[' + m.state.toUpperCase() + ']</span>' +\n '<span style=\"color:var(--green)\">' + escapeHtml(m.name) + '</span>' +\n '<span style=\"margin-left:auto;color:var(--dim)\">' + elapsed + '</span>' +\n '</div>' +\n '<div style=\"color:var(--dim);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\" title=\"' + escapeAttr(m.lastOutput || \"\") + '\">' + lastOut + '</div>' +\n (m.issueId ? '<div style=\"color:var(--cyan-dim);font-size:10px;margin-top:1px\">' + escapeHtml(m.issueId) + ' | checks: ' + m.checkCount + '</div>' : '') +\n '</div>';\n }).join(\"\");\n }\n el.innerHTML = html;\n }\n\n // ---- Init ----\n async function loadInitial() {\n try {\n // 1\uB2E8\uACC4: \uD544\uC218 \uB370\uC774\uD130 \uBA3C\uC800 \uB85C\uB4DC (\uC131\uB2A5 \uAC1C\uC120)\n const [statsRes, projectsRes] = await Promise.all([\n fetch(\"/api/stats\"),\n fetch(\"/api/projects\"),\n ]);\n const stats = await statsRes.json();\n updateStats(stats);\n document.getElementById(\"stat-sse\").textContent = stats.sseClients ?? \"-\";\n\n projects = await projectsRes.json();\n renderProjects();\n\n // 2\uB2E8\uACC4: \uCD94\uAC00 \uB370\uC774\uD130\uB294 \uBE44\uB3D9\uAE30\uB85C \uC9C0\uC5F0 \uB85C\uB4DC (\uBE0C\uB77C\uC6B0\uC800 \uB80C\uB354\uB9C1 \uBE14\uB85C\uD0B9 \uC81C\uAC70)\n loadSupplementalData();\n } catch(e) {\n console.error(\"Init failed:\", e);\n }\n }\n\n // \uCD94\uAC00 \uB370\uC774\uD130 \uC9C0\uC5F0 \uB85C\uB4DC (\uCD08\uAE30 \uB80C\uB354\uB9C1\uC744 \uBC29\uD574\uD558\uC9C0 \uC54A\uC74C)\n async function loadSupplementalData() {\n try {\n const [chatRes, logsRes, stagesRes] = await Promise.all([\n fetch(\"/api/chat/history\"),\n fetch(\"/api/logs\"),\n fetch(\"/api/stages\"),\n ]);\n\n const history = await chatRes.json();\n for (const msg of history) appendChatMsg(msg.role, msg.text, null, msg.ts);\n\n // Restore logs\n const logs = await logsRes.json();\n for (const ev of logs) addLogLine(ev.data);\n\n // Restore pipeline/task events\n const stages = await stagesRes.json();\n for (const ev of stages) handleEvent(ev);\n } catch(e) {\n console.error(\"Supplemental data load failed:\", e);\n }\n }\n\n // \uC131\uB2A5 \uCD5C\uC801\uD654: stats + projects \uD3F4\uB9C1\uC744 60\uCD08\uB85C \uC99D\uAC00 (\uBCC0\uD654 \uBE48\uB3C4 \uB0AE\uC74C)\n setInterval(async () => {\n try {\n const [sRes, pRes] = await Promise.all([fetch(\"/api/stats\"), fetch(\"/api/projects\")]);\n const stats = await sRes.json();\n document.getElementById(\"stat-sse\").textContent = stats.sseClients ?? \"-\";\n updateStats(stats);\n const fresh = await pRes.json();\n fresh.forEach(p => {\n const local = projects.find(l => l.path === p.path);\n if (local) p.enabled = local.enabled;\n });\n projects = fresh;\n renderProjects();\n } catch {}\n }, 60000);\n\n // ---- Mobile Tab Navigation ----\n function switchTab(idx) {\n const cols = document.querySelectorAll(\".main-grid > .col\");\n const tabs = document.querySelectorAll(\".tab-bar .tab\");\n cols.forEach((c, i) => c.classList.toggle(\"mob-active\", i === idx));\n tabs.forEach((t, i) => t.classList.toggle(\"active\", i === idx));\n }\n document.querySelector(\".tab-bar\").addEventListener(\"click\", e => {\n const tab = e.target.closest(\".tab\");\n if (!tab) return;\n switchTab(parseInt(tab.dataset.tab, 10));\n });\n // Activate first tab on load\n switchTab(0);\n\n // Knowledge graph data fetcher\n async function fetchKnowledgeData() {\n try {\n const res = await fetch(\"/api/knowledge\");\n if (res.ok) {\n const data = await res.json();\n for (const item of data) {\n knowledgeCache[item.slug] = item;\n // Also cache by last segment for name-based lookup\n const parts = item.slug.split(\"-\");\n knowledgeCache[parts[parts.length - 1]] = item;\n }\n renderProjects();\n }\n } catch {}\n }\n\n // \uC131\uB2A5 \uCD5C\uC801\uD654: \uCD08\uAE30 \uB85C\uB4DC \uD6C4 2\uB2E8\uACC4 \uD398\uCE6D (\uB80C\uB354\uB9C1 \uBE14\uB85C\uD0B9 \uBC29\uC9C0)\n loadInitial().then(function() { connectSSE(true); });\n\n // 1\uB2E8\uACC4: \uCD08\uAE30\uD654 \uD6C4 \uC989\uC2DC \uD544\uC218 \uD3F4\uB9C1\uB9CC \uC2DC\uC791\n setInterval(fetchSvcStatus, 15000);\n setInterval(fetchPRProcessorStatus, 60000); // \uC131\uB2A5 \uCD5C\uC801\uD654: 30\uCD08 \u2192 60\uCD08 (\uBCC0\uD654 \uBE48\uB3C4 \uB0AE\uC74C)\n setInterval(fetchStuckIssues, 60000); // \uC131\uB2A5 \uCD5C\uC801\uD654: 30\uCD08 \u2192 60\uCD08 (Linear API \uBD80\uD558 \uAC10\uC18C)\n setInterval(fetchKnowledgeData, 60000);\n setInterval(fetchMonitors, 60000);\n setInterval(fetchProcesses, 30000);\n\n // 2\uB2E8\uACC4: \uB80C\uB354\uB9C1 \uC548\uC815\uD654 \uD6C4 \uBE44\uD544\uC218 \uB370\uC774\uD130 \uB85C\uB4DC (3\uCD08 \uC9C0\uC5F0)\n setTimeout(function() {\n fetchSvcStatus();\n fetchPRProcessorStatus();\n fetchStuckIssues();\n fetchKnowledgeData();\n fetchMonitors();\n fetchProcesses();\n }, 3000);\n\n // \uB80C\uB354\uB9C1 \uC131\uB2A5: \uC2A4\uD14C\uC774\uC9C0 \uC5C5\uB370\uC774\uD2B8 \uD3F4\uB9C1 \uC81C\uAC70 (SSE \uC774\uBCA4\uD2B8 \uD65C\uC6A9)\n // setInterval(() => { if (stageRows.length) renderStages(); }, 10000);\n </script>\n</body>\n</html>";
2
2
  export { DASHBOARD_HTML };
3
3
  //# sourceMappingURL=dashboardHtml.d.ts.map