@amsterdamdatalabs/enact-factory 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (644) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +566 -0
  3. package/dist/adapters/agenticLoop.d.ts +90 -0
  4. package/dist/adapters/agenticLoop.d.ts.map +1 -0
  5. package/dist/adapters/agenticLoop.js +219 -0
  6. package/dist/adapters/agenticLoop.js.map +1 -0
  7. package/dist/adapters/base.d.ts +16 -0
  8. package/dist/adapters/base.d.ts.map +1 -0
  9. package/dist/adapters/base.js +135 -0
  10. package/dist/adapters/base.js.map +1 -0
  11. package/dist/adapters/claude.d.ts +13 -0
  12. package/dist/adapters/claude.d.ts.map +1 -0
  13. package/dist/adapters/claude.js +318 -0
  14. package/dist/adapters/claude.js.map +1 -0
  15. package/dist/adapters/codex.d.ts +14 -0
  16. package/dist/adapters/codex.d.ts.map +1 -0
  17. package/dist/adapters/codex.js +366 -0
  18. package/dist/adapters/codex.js.map +1 -0
  19. package/dist/adapters/cryptoQuantAdapter.d.ts +85 -0
  20. package/dist/adapters/cryptoQuantAdapter.d.ts.map +1 -0
  21. package/dist/adapters/cryptoQuantAdapter.js +238 -0
  22. package/dist/adapters/cryptoQuantAdapter.js.map +1 -0
  23. package/dist/adapters/cursor.d.ts +13 -0
  24. package/dist/adapters/cursor.d.ts.map +1 -0
  25. package/dist/adapters/cursor.js +300 -0
  26. package/dist/adapters/cursor.js.map +1 -0
  27. package/dist/adapters/envPath.d.ts +20 -0
  28. package/dist/adapters/envPath.d.ts.map +1 -0
  29. package/dist/adapters/envPath.js +49 -0
  30. package/dist/adapters/envPath.js.map +1 -0
  31. package/dist/adapters/hermes.d.ts +13 -0
  32. package/dist/adapters/hermes.d.ts.map +1 -0
  33. package/dist/adapters/hermes.js +283 -0
  34. package/dist/adapters/hermes.js.map +1 -0
  35. package/dist/adapters/index.d.ts +18 -0
  36. package/dist/adapters/index.d.ts.map +1 -0
  37. package/dist/adapters/index.js +56 -0
  38. package/dist/adapters/index.js.map +1 -0
  39. package/dist/adapters/opencode.d.ts +13 -0
  40. package/dist/adapters/opencode.d.ts.map +1 -0
  41. package/dist/adapters/opencode.js +282 -0
  42. package/dist/adapters/opencode.js.map +1 -0
  43. package/dist/adapters/processRegistry.d.ts +38 -0
  44. package/dist/adapters/processRegistry.d.ts.map +1 -0
  45. package/dist/adapters/processRegistry.js +147 -0
  46. package/dist/adapters/processRegistry.js.map +1 -0
  47. package/dist/adapters/responses.d.ts +16 -0
  48. package/dist/adapters/responses.d.ts.map +1 -0
  49. package/dist/adapters/responses.js +244 -0
  50. package/dist/adapters/responses.js.map +1 -0
  51. package/dist/adapters/streamBuffer.d.ts +59 -0
  52. package/dist/adapters/streamBuffer.d.ts.map +1 -0
  53. package/dist/adapters/streamBuffer.js +123 -0
  54. package/dist/adapters/streamBuffer.js.map +1 -0
  55. package/dist/adapters/tools.d.ts +30 -0
  56. package/dist/adapters/tools.d.ts.map +1 -0
  57. package/dist/adapters/tools.js +219 -0
  58. package/dist/adapters/tools.js.map +1 -0
  59. package/dist/adapters/types.d.ts +82 -0
  60. package/dist/adapters/types.d.ts.map +1 -0
  61. package/dist/adapters/types.js +6 -0
  62. package/dist/adapters/types.js.map +1 -0
  63. package/dist/agents/agentBus.d.ts +160 -0
  64. package/dist/agents/agentBus.d.ts.map +1 -0
  65. package/dist/agents/agentBus.js +350 -0
  66. package/dist/agents/agentBus.js.map +1 -0
  67. package/dist/agents/agentPair.d.ts +215 -0
  68. package/dist/agents/agentPair.d.ts.map +1 -0
  69. package/dist/agents/agentPair.js +456 -0
  70. package/dist/agents/agentPair.js.map +1 -0
  71. package/dist/agents/auditor.d.ts +27 -0
  72. package/dist/agents/auditor.d.ts.map +1 -0
  73. package/dist/agents/auditor.js +238 -0
  74. package/dist/agents/auditor.js.map +1 -0
  75. package/dist/agents/cliStreamParser.d.ts +18 -0
  76. package/dist/agents/cliStreamParser.d.ts.map +1 -0
  77. package/dist/agents/cliStreamParser.js +156 -0
  78. package/dist/agents/cliStreamParser.js.map +1 -0
  79. package/dist/agents/documenter.d.ts +31 -0
  80. package/dist/agents/documenter.d.ts.map +1 -0
  81. package/dist/agents/documenter.js +286 -0
  82. package/dist/agents/documenter.js.map +1 -0
  83. package/dist/agents/draftAnalyzer.d.ts +50 -0
  84. package/dist/agents/draftAnalyzer.d.ts.map +1 -0
  85. package/dist/agents/draftAnalyzer.js +289 -0
  86. package/dist/agents/draftAnalyzer.js.map +1 -0
  87. package/dist/agents/evaluator.d.ts +61 -0
  88. package/dist/agents/evaluator.d.ts.map +1 -0
  89. package/dist/agents/evaluator.js +338 -0
  90. package/dist/agents/evaluator.js.map +1 -0
  91. package/dist/agents/executor.d.ts +33 -0
  92. package/dist/agents/executor.d.ts.map +1 -0
  93. package/dist/agents/executor.js +130 -0
  94. package/dist/agents/executor.js.map +1 -0
  95. package/dist/agents/index.d.ts +10 -0
  96. package/dist/agents/index.d.ts.map +1 -0
  97. package/dist/agents/index.js +10 -0
  98. package/dist/agents/index.js.map +1 -0
  99. package/dist/agents/pairMetrics.d.ts +63 -0
  100. package/dist/agents/pairMetrics.d.ts.map +1 -0
  101. package/dist/agents/pairMetrics.js +232 -0
  102. package/dist/agents/pairMetrics.js.map +1 -0
  103. package/dist/agents/pairPipeline.d.ts +184 -0
  104. package/dist/agents/pairPipeline.d.ts.map +1 -0
  105. package/dist/agents/pairPipeline.js +934 -0
  106. package/dist/agents/pairPipeline.js.map +1 -0
  107. package/dist/agents/pairWebhook.d.ts +59 -0
  108. package/dist/agents/pairWebhook.d.ts.map +1 -0
  109. package/dist/agents/pairWebhook.js +242 -0
  110. package/dist/agents/pairWebhook.js.map +1 -0
  111. package/dist/agents/pipelineFormat.d.ts +8 -0
  112. package/dist/agents/pipelineFormat.d.ts.map +1 -0
  113. package/dist/agents/pipelineFormat.js +65 -0
  114. package/dist/agents/pipelineFormat.js.map +1 -0
  115. package/dist/agents/pipelineGuards.d.ts +23 -0
  116. package/dist/agents/pipelineGuards.d.ts.map +1 -0
  117. package/dist/agents/pipelineGuards.js +257 -0
  118. package/dist/agents/pipelineGuards.js.map +1 -0
  119. package/dist/agents/reviewer.d.ts +37 -0
  120. package/dist/agents/reviewer.d.ts.map +1 -0
  121. package/dist/agents/reviewer.js +214 -0
  122. package/dist/agents/reviewer.js.map +1 -0
  123. package/dist/agents/skillDocumenter.d.ts +23 -0
  124. package/dist/agents/skillDocumenter.d.ts.map +1 -0
  125. package/dist/agents/skillDocumenter.js +219 -0
  126. package/dist/agents/skillDocumenter.js.map +1 -0
  127. package/dist/agents/tester.d.ts +37 -0
  128. package/dist/agents/tester.d.ts.map +1 -0
  129. package/dist/agents/tester.js +309 -0
  130. package/dist/agents/tester.js.map +1 -0
  131. package/dist/automation/autonomousRunner.d.ts +145 -0
  132. package/dist/automation/autonomousRunner.d.ts.map +1 -0
  133. package/dist/automation/autonomousRunner.js +1272 -0
  134. package/dist/automation/autonomousRunner.js.map +1 -0
  135. package/dist/automation/dailyReporter.d.ts +26 -0
  136. package/dist/automation/dailyReporter.d.ts.map +1 -0
  137. package/dist/automation/dailyReporter.js +130 -0
  138. package/dist/automation/dailyReporter.js.map +1 -0
  139. package/dist/automation/index.d.ts +5 -0
  140. package/dist/automation/index.d.ts.map +1 -0
  141. package/dist/automation/index.js +5 -0
  142. package/dist/automation/index.js.map +1 -0
  143. package/dist/automation/longRunningMonitor.d.ts +26 -0
  144. package/dist/automation/longRunningMonitor.d.ts.map +1 -0
  145. package/dist/automation/longRunningMonitor.js +356 -0
  146. package/dist/automation/longRunningMonitor.js.map +1 -0
  147. package/dist/automation/prOwnership.d.ts +18 -0
  148. package/dist/automation/prOwnership.d.ts.map +1 -0
  149. package/dist/automation/prOwnership.js +61 -0
  150. package/dist/automation/prOwnership.js.map +1 -0
  151. package/dist/automation/runnerExecution.d.ts +57 -0
  152. package/dist/automation/runnerExecution.d.ts.map +1 -0
  153. package/dist/automation/runnerExecution.js +701 -0
  154. package/dist/automation/runnerExecution.js.map +1 -0
  155. package/dist/automation/runnerState.d.ts +170 -0
  156. package/dist/automation/runnerState.d.ts.map +1 -0
  157. package/dist/automation/runnerState.js +496 -0
  158. package/dist/automation/runnerState.js.map +1 -0
  159. package/dist/automation/runnerTypes.d.ts +57 -0
  160. package/dist/automation/runnerTypes.d.ts.map +1 -0
  161. package/dist/automation/runnerTypes.js +5 -0
  162. package/dist/automation/runnerTypes.js.map +1 -0
  163. package/dist/automation/scheduler.d.ts +75 -0
  164. package/dist/automation/scheduler.d.ts.map +1 -0
  165. package/dist/automation/scheduler.js +402 -0
  166. package/dist/automation/scheduler.js.map +1 -0
  167. package/dist/azdo/azdo.d.ts +70 -0
  168. package/dist/azdo/azdo.d.ts.map +1 -0
  169. package/dist/azdo/azdo.js +328 -0
  170. package/dist/azdo/azdo.js.map +1 -0
  171. package/dist/azdo/index.d.ts +3 -0
  172. package/dist/azdo/index.d.ts.map +1 -0
  173. package/dist/azdo/index.js +3 -0
  174. package/dist/azdo/index.js.map +1 -0
  175. package/dist/azdo/projectUpdater.d.ts +13 -0
  176. package/dist/azdo/projectUpdater.d.ts.map +1 -0
  177. package/dist/azdo/projectUpdater.js +155 -0
  178. package/dist/azdo/projectUpdater.js.map +1 -0
  179. package/dist/azureDevOps/client.d.ts +75 -0
  180. package/dist/azureDevOps/client.d.ts.map +1 -0
  181. package/dist/azureDevOps/client.js +150 -0
  182. package/dist/azureDevOps/client.js.map +1 -0
  183. package/dist/azureDevOps/hierarchy.d.ts +119 -0
  184. package/dist/azureDevOps/hierarchy.d.ts.map +1 -0
  185. package/dist/azureDevOps/hierarchy.js +470 -0
  186. package/dist/azureDevOps/hierarchy.js.map +1 -0
  187. package/dist/azureDevOps/mapper.d.ts +101 -0
  188. package/dist/azureDevOps/mapper.d.ts.map +1 -0
  189. package/dist/azureDevOps/mapper.js +438 -0
  190. package/dist/azureDevOps/mapper.js.map +1 -0
  191. package/dist/azureDevOps/stateMapping.d.ts +15 -0
  192. package/dist/azureDevOps/stateMapping.d.ts.map +1 -0
  193. package/dist/azureDevOps/stateMapping.js +141 -0
  194. package/dist/azureDevOps/stateMapping.js.map +1 -0
  195. package/dist/cli/authHandler.d.ts +13 -0
  196. package/dist/cli/authHandler.d.ts.map +1 -0
  197. package/dist/cli/authHandler.js +70 -0
  198. package/dist/cli/authHandler.js.map +1 -0
  199. package/dist/cli/checkHandler.d.ts +27 -0
  200. package/dist/cli/checkHandler.d.ts.map +1 -0
  201. package/dist/cli/checkHandler.js +560 -0
  202. package/dist/cli/checkHandler.js.map +1 -0
  203. package/dist/cli/daemon.d.ts +30 -0
  204. package/dist/cli/daemon.d.ts.map +1 -0
  205. package/dist/cli/daemon.js +141 -0
  206. package/dist/cli/daemon.js.map +1 -0
  207. package/dist/cli/factoryCommands.d.ts +3 -0
  208. package/dist/cli/factoryCommands.d.ts.map +1 -0
  209. package/dist/cli/factoryCommands.js +165 -0
  210. package/dist/cli/factoryCommands.js.map +1 -0
  211. package/dist/cli/promptHandler.d.ts +13 -0
  212. package/dist/cli/promptHandler.d.ts.map +1 -0
  213. package/dist/cli/promptHandler.js +193 -0
  214. package/dist/cli/promptHandler.js.map +1 -0
  215. package/dist/cli.d.ts +3 -0
  216. package/dist/cli.d.ts.map +1 -0
  217. package/dist/cli.js +320 -0
  218. package/dist/cli.js.map +1 -0
  219. package/dist/core/agentLifecycle.d.ts +322 -0
  220. package/dist/core/agentLifecycle.d.ts.map +1 -0
  221. package/dist/core/agentLifecycle.js +230 -0
  222. package/dist/core/agentLifecycle.js.map +1 -0
  223. package/dist/core/areaMapping.d.ts +9 -0
  224. package/dist/core/areaMapping.d.ts.map +1 -0
  225. package/dist/core/areaMapping.js +37 -0
  226. package/dist/core/areaMapping.js.map +1 -0
  227. package/dist/core/config.d.ts +469 -0
  228. package/dist/core/config.d.ts.map +1 -0
  229. package/dist/core/config.js +780 -0
  230. package/dist/core/config.js.map +1 -0
  231. package/dist/core/dashboardContract.d.ts +204 -0
  232. package/dist/core/dashboardContract.d.ts.map +1 -0
  233. package/dist/core/dashboardContract.js +205 -0
  234. package/dist/core/dashboardContract.js.map +1 -0
  235. package/dist/core/devopsModel.d.ts +138 -0
  236. package/dist/core/devopsModel.d.ts.map +1 -0
  237. package/dist/core/devopsModel.js +137 -0
  238. package/dist/core/devopsModel.js.map +1 -0
  239. package/dist/core/envFile.d.ts +11 -0
  240. package/dist/core/envFile.d.ts.map +1 -0
  241. package/dist/core/envFile.js +104 -0
  242. package/dist/core/envFile.js.map +1 -0
  243. package/dist/core/eventHub.d.ts +220 -0
  244. package/dist/core/eventHub.d.ts.map +1 -0
  245. package/dist/core/eventHub.js +136 -0
  246. package/dist/core/eventHub.js.map +1 -0
  247. package/dist/core/index.d.ts +8 -0
  248. package/dist/core/index.d.ts.map +1 -0
  249. package/dist/core/index.js +7 -0
  250. package/dist/core/index.js.map +1 -0
  251. package/dist/core/laneExecutionState.d.ts +29 -0
  252. package/dist/core/laneExecutionState.d.ts.map +1 -0
  253. package/dist/core/laneExecutionState.js +18 -0
  254. package/dist/core/laneExecutionState.js.map +1 -0
  255. package/dist/core/laneStatus.d.ts +49 -0
  256. package/dist/core/laneStatus.d.ts.map +1 -0
  257. package/dist/core/laneStatus.js +153 -0
  258. package/dist/core/laneStatus.js.map +1 -0
  259. package/dist/core/prSidecar.d.ts +96 -0
  260. package/dist/core/prSidecar.d.ts.map +1 -0
  261. package/dist/core/prSidecar.js +33 -0
  262. package/dist/core/prSidecar.js.map +1 -0
  263. package/dist/core/runtimeConfig.d.ts +6 -0
  264. package/dist/core/runtimeConfig.d.ts.map +1 -0
  265. package/dist/core/runtimeConfig.js +24 -0
  266. package/dist/core/runtimeConfig.js.map +1 -0
  267. package/dist/core/scmProvider.d.ts +19 -0
  268. package/dist/core/scmProvider.d.ts.map +1 -0
  269. package/dist/core/scmProvider.js +38 -0
  270. package/dist/core/scmProvider.js.map +1 -0
  271. package/dist/core/service.d.ts +10 -0
  272. package/dist/core/service.d.ts.map +1 -0
  273. package/dist/core/service.js +297 -0
  274. package/dist/core/service.js.map +1 -0
  275. package/dist/core/traceCollector.d.ts +105 -0
  276. package/dist/core/traceCollector.d.ts.map +1 -0
  277. package/dist/core/traceCollector.js +141 -0
  278. package/dist/core/traceCollector.js.map +1 -0
  279. package/dist/core/types.d.ts +432 -0
  280. package/dist/core/types.d.ts.map +1 -0
  281. package/dist/core/types.js +2 -0
  282. package/dist/core/types.js.map +1 -0
  283. package/dist/core/workItemMapper.d.ts +39 -0
  284. package/dist/core/workItemMapper.d.ts.map +1 -0
  285. package/dist/core/workItemMapper.js +427 -0
  286. package/dist/core/workItemMapper.js.map +1 -0
  287. package/dist/core/workItemModel.d.ts +120 -0
  288. package/dist/core/workItemModel.d.ts.map +1 -0
  289. package/dist/core/workItemModel.js +104 -0
  290. package/dist/core/workItemModel.js.map +1 -0
  291. package/dist/core/workItemPayload.d.ts +195 -0
  292. package/dist/core/workItemPayload.d.ts.map +1 -0
  293. package/dist/core/workItemPayload.js +24 -0
  294. package/dist/core/workItemPayload.js.map +1 -0
  295. package/dist/core/workspaceConfig.d.ts +57 -0
  296. package/dist/core/workspaceConfig.d.ts.map +1 -0
  297. package/dist/core/workspaceConfig.js +184 -0
  298. package/dist/core/workspaceConfig.js.map +1 -0
  299. package/dist/doctor.d.ts +18 -0
  300. package/dist/doctor.d.ts.map +1 -0
  301. package/dist/doctor.js +34 -0
  302. package/dist/doctor.js.map +1 -0
  303. package/dist/factory/activeSkill.d.ts +11 -0
  304. package/dist/factory/activeSkill.d.ts.map +1 -0
  305. package/dist/factory/activeSkill.js +44 -0
  306. package/dist/factory/activeSkill.js.map +1 -0
  307. package/dist/factory/assignment.d.ts +54 -0
  308. package/dist/factory/assignment.d.ts.map +1 -0
  309. package/dist/factory/assignment.js +94 -0
  310. package/dist/factory/assignment.js.map +1 -0
  311. package/dist/factory/auditLog.d.ts +10 -0
  312. package/dist/factory/auditLog.d.ts.map +1 -0
  313. package/dist/factory/auditLog.js +38 -0
  314. package/dist/factory/auditLog.js.map +1 -0
  315. package/dist/factory/closureRequirements.d.ts +12 -0
  316. package/dist/factory/closureRequirements.d.ts.map +1 -0
  317. package/dist/factory/closureRequirements.js +30 -0
  318. package/dist/factory/closureRequirements.js.map +1 -0
  319. package/dist/factory/delegationPrompt.d.ts +3 -0
  320. package/dist/factory/delegationPrompt.d.ts.map +1 -0
  321. package/dist/factory/delegationPrompt.js +16 -0
  322. package/dist/factory/delegationPrompt.js.map +1 -0
  323. package/dist/factory/http.d.ts +3 -0
  324. package/dist/factory/http.d.ts.map +1 -0
  325. package/dist/factory/http.js +555 -0
  326. package/dist/factory/http.js.map +1 -0
  327. package/dist/factory/lifecyclePushMap.d.ts +4 -0
  328. package/dist/factory/lifecyclePushMap.d.ts.map +1 -0
  329. package/dist/factory/lifecyclePushMap.js +7 -0
  330. package/dist/factory/lifecyclePushMap.js.map +1 -0
  331. package/dist/factory/missions.d.ts +125 -0
  332. package/dist/factory/missions.d.ts.map +1 -0
  333. package/dist/factory/missions.js +304 -0
  334. package/dist/factory/missions.js.map +1 -0
  335. package/dist/factory/mode.d.ts +9 -0
  336. package/dist/factory/mode.d.ts.map +1 -0
  337. package/dist/factory/mode.js +30 -0
  338. package/dist/factory/mode.js.map +1 -0
  339. package/dist/factory/operatorActiveSkill.d.ts +15 -0
  340. package/dist/factory/operatorActiveSkill.d.ts.map +1 -0
  341. package/dist/factory/operatorActiveSkill.js +95 -0
  342. package/dist/factory/operatorActiveSkill.js.map +1 -0
  343. package/dist/factory/paseoDispatcher.d.ts +52 -0
  344. package/dist/factory/paseoDispatcher.d.ts.map +1 -0
  345. package/dist/factory/paseoDispatcher.js +122 -0
  346. package/dist/factory/paseoDispatcher.js.map +1 -0
  347. package/dist/factory/paseoLifecycle.d.ts +32 -0
  348. package/dist/factory/paseoLifecycle.d.ts.map +1 -0
  349. package/dist/factory/paseoLifecycle.js +260 -0
  350. package/dist/factory/paseoLifecycle.js.map +1 -0
  351. package/dist/factory/paths.d.ts +31 -0
  352. package/dist/factory/paths.d.ts.map +1 -0
  353. package/dist/factory/paths.js +139 -0
  354. package/dist/factory/paths.js.map +1 -0
  355. package/dist/factory/progressWatchdog.d.ts +58 -0
  356. package/dist/factory/progressWatchdog.d.ts.map +1 -0
  357. package/dist/factory/progressWatchdog.js +160 -0
  358. package/dist/factory/progressWatchdog.js.map +1 -0
  359. package/dist/factory/roster.d.ts +59 -0
  360. package/dist/factory/roster.d.ts.map +1 -0
  361. package/dist/factory/roster.js +116 -0
  362. package/dist/factory/roster.js.map +1 -0
  363. package/dist/factory/runtime.d.ts +44 -0
  364. package/dist/factory/runtime.d.ts.map +1 -0
  365. package/dist/factory/runtime.js +238 -0
  366. package/dist/factory/runtime.js.map +1 -0
  367. package/dist/factory/sync.d.ts +29 -0
  368. package/dist/factory/sync.d.ts.map +1 -0
  369. package/dist/factory/sync.js +77 -0
  370. package/dist/factory/sync.js.map +1 -0
  371. package/dist/factory/workitemQueues.d.ts +37 -0
  372. package/dist/factory/workitemQueues.d.ts.map +1 -0
  373. package/dist/factory/workitemQueues.js +99 -0
  374. package/dist/factory/workitemQueues.js.map +1 -0
  375. package/dist/factory/workitemTriage.d.ts +9 -0
  376. package/dist/factory/workitemTriage.d.ts.map +1 -0
  377. package/dist/factory/workitemTriage.js +81 -0
  378. package/dist/factory/workitemTriage.js.map +1 -0
  379. package/dist/hooks.d.ts +18 -0
  380. package/dist/hooks.d.ts.map +1 -0
  381. package/dist/hooks.js +96 -0
  382. package/dist/hooks.js.map +1 -0
  383. package/dist/index.d.ts +3 -0
  384. package/dist/index.d.ts.map +1 -0
  385. package/dist/index.js +90 -0
  386. package/dist/index.js.map +1 -0
  387. package/dist/install/agentCatalog.d.ts +7 -0
  388. package/dist/install/agentCatalog.d.ts.map +1 -0
  389. package/dist/install/agentCatalog.js +28 -0
  390. package/dist/install/agentCatalog.js.map +1 -0
  391. package/dist/install/bundlePaths.d.ts +10 -0
  392. package/dist/install/bundlePaths.d.ts.map +1 -0
  393. package/dist/install/bundlePaths.js +30 -0
  394. package/dist/install/bundlePaths.js.map +1 -0
  395. package/dist/install/codex.d.ts +43 -0
  396. package/dist/install/codex.d.ts.map +1 -0
  397. package/dist/install/codex.js +207 -0
  398. package/dist/install/codex.js.map +1 -0
  399. package/dist/install/enactHome.d.ts +37 -0
  400. package/dist/install/enactHome.d.ts.map +1 -0
  401. package/dist/install/enactHome.js +152 -0
  402. package/dist/install/enactHome.js.map +1 -0
  403. package/dist/install/plugins.d.ts +115 -0
  404. package/dist/install/plugins.d.ts.map +1 -0
  405. package/dist/install/plugins.js +259 -0
  406. package/dist/install/plugins.js.map +1 -0
  407. package/dist/install/setup.d.ts +33 -0
  408. package/dist/install/setup.d.ts.map +1 -0
  409. package/dist/install/setup.js +167 -0
  410. package/dist/install/setup.js.map +1 -0
  411. package/dist/locale/en.d.ts +3 -0
  412. package/dist/locale/en.d.ts.map +1 -0
  413. package/dist/locale/en.js +435 -0
  414. package/dist/locale/en.js.map +1 -0
  415. package/dist/locale/index.d.ts +28 -0
  416. package/dist/locale/index.d.ts.map +1 -0
  417. package/dist/locale/index.js +84 -0
  418. package/dist/locale/index.js.map +1 -0
  419. package/dist/locale/prompts/en.d.ts +3 -0
  420. package/dist/locale/prompts/en.d.ts.map +1 -0
  421. package/dist/locale/prompts/en.js +254 -0
  422. package/dist/locale/prompts/en.js.map +1 -0
  423. package/dist/locale/types.d.ts +433 -0
  424. package/dist/locale/types.d.ts.map +1 -0
  425. package/dist/locale/types.js +5 -0
  426. package/dist/locale/types.js.map +1 -0
  427. package/dist/mcp/server.d.ts +489 -0
  428. package/dist/mcp/server.d.ts.map +1 -0
  429. package/dist/mcp/server.js +597 -0
  430. package/dist/mcp/server.js.map +1 -0
  431. package/dist/orchestration/decisionEngine.d.ts +175 -0
  432. package/dist/orchestration/decisionEngine.d.ts.map +1 -0
  433. package/dist/orchestration/decisionEngine.js +471 -0
  434. package/dist/orchestration/decisionEngine.js.map +1 -0
  435. package/dist/orchestration/index.d.ts +5 -0
  436. package/dist/orchestration/index.d.ts.map +1 -0
  437. package/dist/orchestration/index.js +5 -0
  438. package/dist/orchestration/index.js.map +1 -0
  439. package/dist/orchestration/workItemParser.d.ts +67 -0
  440. package/dist/orchestration/workItemParser.d.ts.map +1 -0
  441. package/dist/orchestration/workItemParser.js +560 -0
  442. package/dist/orchestration/workItemParser.js.map +1 -0
  443. package/dist/orchestration/workItemScheduler.d.ts +141 -0
  444. package/dist/orchestration/workItemScheduler.d.ts.map +1 -0
  445. package/dist/orchestration/workItemScheduler.js +317 -0
  446. package/dist/orchestration/workItemScheduler.js.map +1 -0
  447. package/dist/orchestration/workflow.d.ts +145 -0
  448. package/dist/orchestration/workflow.d.ts.map +1 -0
  449. package/dist/orchestration/workflow.js +301 -0
  450. package/dist/orchestration/workflow.js.map +1 -0
  451. package/dist/providers/codexSessions.d.ts +93 -0
  452. package/dist/providers/codexSessions.d.ts.map +1 -0
  453. package/dist/providers/codexSessions.js +366 -0
  454. package/dist/providers/codexSessions.js.map +1 -0
  455. package/dist/registry/bsDetector.d.ts +24 -0
  456. package/dist/registry/bsDetector.d.ts.map +1 -0
  457. package/dist/registry/bsDetector.js +276 -0
  458. package/dist/registry/bsDetector.js.map +1 -0
  459. package/dist/registry/entityScanner.d.ts +36 -0
  460. package/dist/registry/entityScanner.d.ts.map +1 -0
  461. package/dist/registry/entityScanner.js +693 -0
  462. package/dist/registry/entityScanner.js.map +1 -0
  463. package/dist/registry/index.d.ts +9 -0
  464. package/dist/registry/index.d.ts.map +1 -0
  465. package/dist/registry/index.js +13 -0
  466. package/dist/registry/index.js.map +1 -0
  467. package/dist/registry/schema.d.ts +307 -0
  468. package/dist/registry/schema.d.ts.map +1 -0
  469. package/dist/registry/schema.js +139 -0
  470. package/dist/registry/schema.js.map +1 -0
  471. package/dist/registry/sqliteStore.d.ts +101 -0
  472. package/dist/registry/sqliteStore.d.ts.map +1 -0
  473. package/dist/registry/sqliteStore.js +688 -0
  474. package/dist/registry/sqliteStore.js.map +1 -0
  475. package/dist/registry/workItemBridge.d.ts +8 -0
  476. package/dist/registry/workItemBridge.d.ts.map +1 -0
  477. package/dist/registry/workItemBridge.js +30 -0
  478. package/dist/registry/workItemBridge.js.map +1 -0
  479. package/dist/runners/cliRunner.d.ts +11 -0
  480. package/dist/runners/cliRunner.d.ts.map +1 -0
  481. package/dist/runners/cliRunner.js +193 -0
  482. package/dist/runners/cliRunner.js.map +1 -0
  483. package/dist/support/apiCache.d.ts +85 -0
  484. package/dist/support/apiCache.d.ts.map +1 -0
  485. package/dist/support/apiCache.js +163 -0
  486. package/dist/support/apiCache.js.map +1 -0
  487. package/dist/support/chat.d.ts +3 -0
  488. package/dist/support/chat.d.ts.map +1 -0
  489. package/dist/support/chat.js +305 -0
  490. package/dist/support/chat.js.map +1 -0
  491. package/dist/support/chatBackend.d.ts +25 -0
  492. package/dist/support/chatBackend.d.ts.map +1 -0
  493. package/dist/support/chatBackend.js +289 -0
  494. package/dist/support/chatBackend.js.map +1 -0
  495. package/dist/support/chatTui.d.ts +3 -0
  496. package/dist/support/chatTui.d.ts.map +1 -0
  497. package/dist/support/chatTui.js +1082 -0
  498. package/dist/support/chatTui.js.map +1 -0
  499. package/dist/support/costTracker.d.ts +29 -0
  500. package/dist/support/costTracker.d.ts.map +1 -0
  501. package/dist/support/costTracker.js +113 -0
  502. package/dist/support/costTracker.js.map +1 -0
  503. package/dist/support/dashboardHtml.d.ts +5 -0
  504. package/dist/support/dashboardHtml.d.ts.map +1 -0
  505. package/dist/support/dashboardHtml.js +2629 -0
  506. package/dist/support/dashboardHtml.js.map +1 -0
  507. package/dist/support/dev.d.ts +55 -0
  508. package/dist/support/dev.d.ts.map +1 -0
  509. package/dist/support/dev.js +298 -0
  510. package/dist/support/dev.js.map +1 -0
  511. package/dist/support/editParser.d.ts +37 -0
  512. package/dist/support/editParser.d.ts.map +1 -0
  513. package/dist/support/editParser.js +365 -0
  514. package/dist/support/editParser.js.map +1 -0
  515. package/dist/support/ghosttyThemeCatalog.generated.d.ts +2 -0
  516. package/dist/support/ghosttyThemeCatalog.generated.d.ts.map +1 -0
  517. package/dist/support/ghosttyThemeCatalog.generated.js +11116 -0
  518. package/dist/support/ghosttyThemeCatalog.generated.js.map +1 -0
  519. package/dist/support/gitStatus.d.ts +21 -0
  520. package/dist/support/gitStatus.d.ts.map +1 -0
  521. package/dist/support/gitStatus.js +108 -0
  522. package/dist/support/gitStatus.js.map +1 -0
  523. package/dist/support/gitTracker.d.ts +30 -0
  524. package/dist/support/gitTracker.d.ts.map +1 -0
  525. package/dist/support/gitTracker.js +143 -0
  526. package/dist/support/gitTracker.js.map +1 -0
  527. package/dist/support/index.d.ts +12 -0
  528. package/dist/support/index.d.ts.map +1 -0
  529. package/dist/support/index.js +12 -0
  530. package/dist/support/index.js.map +1 -0
  531. package/dist/support/planner.d.ts +64 -0
  532. package/dist/support/planner.d.ts.map +1 -0
  533. package/dist/support/planner.js +396 -0
  534. package/dist/support/planner.js.map +1 -0
  535. package/dist/support/projectMapper.d.ts +46 -0
  536. package/dist/support/projectMapper.d.ts.map +1 -0
  537. package/dist/support/projectMapper.js +273 -0
  538. package/dist/support/projectMapper.js.map +1 -0
  539. package/dist/support/pty-helper.py +117 -0
  540. package/dist/support/quotaTracker.d.ts +29 -0
  541. package/dist/support/quotaTracker.d.ts.map +1 -0
  542. package/dist/support/quotaTracker.js +89 -0
  543. package/dist/support/quotaTracker.js.map +1 -0
  544. package/dist/support/rateLimiter.d.ts +101 -0
  545. package/dist/support/rateLimiter.d.ts.map +1 -0
  546. package/dist/support/rateLimiter.js +219 -0
  547. package/dist/support/rateLimiter.js.map +1 -0
  548. package/dist/support/rollback.d.ts +61 -0
  549. package/dist/support/rollback.d.ts.map +1 -0
  550. package/dist/support/rollback.js +329 -0
  551. package/dist/support/rollback.js.map +1 -0
  552. package/dist/support/sharedShell.d.ts +17 -0
  553. package/dist/support/sharedShell.d.ts.map +1 -0
  554. package/dist/support/sharedShell.js +439 -0
  555. package/dist/support/sharedShell.js.map +1 -0
  556. package/dist/support/stuckDetector.d.ts +68 -0
  557. package/dist/support/stuckDetector.d.ts.map +1 -0
  558. package/dist/support/stuckDetector.js +174 -0
  559. package/dist/support/stuckDetector.js.map +1 -0
  560. package/dist/support/terminalBridge.d.ts +18 -0
  561. package/dist/support/terminalBridge.d.ts.map +1 -0
  562. package/dist/support/terminalBridge.js +553 -0
  563. package/dist/support/terminalBridge.js.map +1 -0
  564. package/dist/support/timeWindow.d.ts +60 -0
  565. package/dist/support/timeWindow.d.ts.map +1 -0
  566. package/dist/support/timeWindow.js +236 -0
  567. package/dist/support/timeWindow.js.map +1 -0
  568. package/dist/support/uiThemes.d.ts +44 -0
  569. package/dist/support/uiThemes.d.ts.map +1 -0
  570. package/dist/support/uiThemes.js +290 -0
  571. package/dist/support/uiThemes.js.map +1 -0
  572. package/dist/support/web.d.ts +29 -0
  573. package/dist/support/web.d.ts.map +1 -0
  574. package/dist/support/web.js +1097 -0
  575. package/dist/support/web.js.map +1 -0
  576. package/dist/support/worktreeManager.d.ts +20 -0
  577. package/dist/support/worktreeManager.d.ts.map +1 -0
  578. package/dist/support/worktreeManager.js +140 -0
  579. package/dist/support/worktreeManager.js.map +1 -0
  580. package/dist/task_state_model.py +55 -0
  581. package/dist/workItemState/store.d.ts +122 -0
  582. package/dist/workItemState/store.d.ts.map +1 -0
  583. package/dist/workItemState/store.js +438 -0
  584. package/dist/workItemState/store.js.map +1 -0
  585. package/dist/workItems/azdoBridge.d.ts +42 -0
  586. package/dist/workItems/azdoBridge.d.ts.map +1 -0
  587. package/dist/workItems/azdoBridge.js +143 -0
  588. package/dist/workItems/azdoBridge.js.map +1 -0
  589. package/dist/workItems/azdoSyncRuntime.d.ts +28 -0
  590. package/dist/workItems/azdoSyncRuntime.d.ts.map +1 -0
  591. package/dist/workItems/azdoSyncRuntime.js +158 -0
  592. package/dist/workItems/azdoSyncRuntime.js.map +1 -0
  593. package/dist/workItems/azureDevOpsSync.d.ts +128 -0
  594. package/dist/workItems/azureDevOpsSync.d.ts.map +1 -0
  595. package/dist/workItems/azureDevOpsSync.js +748 -0
  596. package/dist/workItems/azureDevOpsSync.js.map +1 -0
  597. package/dist/workItems/helpers.d.ts +11 -0
  598. package/dist/workItems/helpers.d.ts.map +1 -0
  599. package/dist/workItems/helpers.js +17 -0
  600. package/dist/workItems/helpers.js.map +1 -0
  601. package/dist/workItems/index.d.ts +21 -0
  602. package/dist/workItems/index.d.ts.map +1 -0
  603. package/dist/workItems/index.js +89 -0
  604. package/dist/workItems/index.js.map +1 -0
  605. package/dist/workItems/localWorkItemFetcher.d.ts +55 -0
  606. package/dist/workItems/localWorkItemFetcher.d.ts.map +1 -0
  607. package/dist/workItems/localWorkItemFetcher.js +209 -0
  608. package/dist/workItems/localWorkItemFetcher.js.map +1 -0
  609. package/dist/workItems/migrations/001_rename_workItem_to_work_item.sql +10 -0
  610. package/dist/workItems/postgresStore.d.ts +78 -0
  611. package/dist/workItems/postgresStore.d.ts.map +1 -0
  612. package/dist/workItems/postgresStore.js +937 -0
  613. package/dist/workItems/postgresStore.js.map +1 -0
  614. package/dist/workItems/schema.d.ts +257 -0
  615. package/dist/workItems/schema.d.ts.map +1 -0
  616. package/dist/workItems/schema.js +176 -0
  617. package/dist/workItems/schema.js.map +1 -0
  618. package/dist/workItems/sqliteStore.d.ts +124 -0
  619. package/dist/workItems/sqliteStore.d.ts.map +1 -0
  620. package/dist/workItems/sqliteStore.js +713 -0
  621. package/dist/workItems/sqliteStore.js.map +1 -0
  622. package/dist/workItems/workItemBoardHtml.d.ts +5 -0
  623. package/dist/workItems/workItemBoardHtml.d.ts.map +1 -0
  624. package/dist/workItems/workItemBoardHtml.js +2192 -0
  625. package/dist/workItems/workItemBoardHtml.js.map +1 -0
  626. package/package.json +99 -0
  627. package/templates/AGENTS.md +432 -0
  628. package/templates/BOOT.md +25 -0
  629. package/templates/BOOTSTRAP.md +50 -0
  630. package/templates/CHANGELOG_AUDIT.md +74 -0
  631. package/templates/HEARTBEAT.md +86 -0
  632. package/templates/IDENTITY.md +27 -0
  633. package/templates/PR_LAND.md +75 -0
  634. package/templates/PR_REVIEW.md +97 -0
  635. package/templates/SOUL.dev.md +52 -0
  636. package/templates/SOUL.md +81 -0
  637. package/templates/TOOLS.md +52 -0
  638. package/templates/USER.md +22 -0
  639. package/templates/WORKITEM_ANALYSIS.md +31 -0
  640. package/templates/agents/executor.md +26 -0
  641. package/templates/agents/plan.md +22 -0
  642. package/templates/agents/ralph.md +37 -0
  643. package/templates/agents/review.md +22 -0
  644. package/templates/agents/team.md +39 -0
@@ -0,0 +1,2629 @@
1
+ import { DEFAULT_DASHBOARD_SURFACE_CONFIG, } from '../core/dashboardContract.js';
2
+ import { DEFAULT_THEME_ID, THEME_RECENTS_LIMIT, THEME_RECENTS_STORAGE_KEY, THEME_STORAGE_KEY, serializedThemes, serializedThemeFavorites, themeCssVars, themeOptionsHtml, } from '../support/uiThemes.js';
3
+ import { renderSharedRightRail, renderSharedShellCss, renderSharedShellHeader, renderSharedShellStatsBar, } from '../support/sharedShell.js';
4
+ // Wave 5: Dashboard Realignment — three-column IA (25% / 45% / 30%)
5
+ // Layout contract: left=work-item tree+filters, center=operational canvas, right=terminal+sidecars
6
+ // Top selector: repository scope only (All + config-driven repos)
7
+ // PR sidecar: collapsible, default collapsed, lives in right column below terminal host
8
+ // State: layered badges per work item via GET /api/factory/workitems
9
+ function escAttr(value) {
10
+ return value
11
+ .replace(/&/g, '&')
12
+ .replace(/"/g, '"')
13
+ .replace(/</g, '&lt;')
14
+ .replace(/>/g, '&gt;');
15
+ }
16
+ export function createDashboardHtml(config = DEFAULT_DASHBOARD_SURFACE_CONFIG) {
17
+ const repoOptionsHtml = renderOptions(config.repoOptions, config.selectedRepo === 'All' ? '' : config.selectedRepo);
18
+ const appThemeOptionsHtml = themeOptionsHtml(DEFAULT_THEME_ID);
19
+ const appThemeVars = themeCssVars(DEFAULT_THEME_ID);
20
+ const appThemesJson = serializedThemes();
21
+ const appThemeFavoritesJson = serializedThemeFavorites();
22
+ const sharedShellCss = renderSharedShellCss();
23
+ const sharedHeaderHtml = renderSharedShellHeader({
24
+ repoOptionsHtml,
25
+ themeOptionsHtml: appThemeOptionsHtml,
26
+ themeOnChange: 'applyAppTheme(this.value)',
27
+ themeFilterOnInput: 'filterAppThemes(this.value)',
28
+ });
29
+ const sharedStatsBarHtml = renderSharedShellStatsBar({
30
+ switchHref: '/workitems',
31
+ switchLabel: 'WORK ITEMS',
32
+ });
33
+ const sharedRightRailHtml = renderSharedRightRail();
34
+ function renderOptions(options, selected) {
35
+ return options
36
+ .map((option) => `<option value="${escAttr(option.value)}"${option.value === selected ? ' selected' : ''}>${escAttr(option.label)}</option>`)
37
+ .join('');
38
+ }
39
+ return `<!DOCTYPE html>
40
+ <html lang="en">
41
+ <head>
42
+ <meta charset="UTF-8">
43
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
44
+ <title>Enact Factory</title>
45
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
46
+ <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
47
+ <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
48
+ <script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links/lib/xterm-addon-web-links.js"></script>
49
+ <style>
50
+ :root {
51
+ ${appThemeVars}
52
+ --panel-soft: var(--panelSoft);
53
+ --green-dim: var(--greenDim);
54
+ --green-mid: var(--greenMid);
55
+ --green-lo: var(--greenLo);
56
+ --cyan-dim: var(--cyanDim);
57
+ }
58
+ * { box-sizing: border-box; margin: 0; padding: 0; }
59
+ html, body { height: 100%; overflow: hidden; }
60
+ body {
61
+ font-family: 'Cascadia Code', 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
62
+ background:
63
+ radial-gradient(circle at 12% 0%, var(--glow), transparent 32rem),
64
+ linear-gradient(180deg, var(--bodyTop) 0%, var(--bg) 48%, var(--bodyBottom) 100%);
65
+ color: var(--white);
66
+ font-size: 15px;
67
+ line-height: 1.45;
68
+ height: 100vh;
69
+ display: flex;
70
+ flex-direction: column;
71
+ overflow: hidden;
72
+ }
73
+
74
+ /* ===== SCROLLBAR ===== */
75
+ ::-webkit-scrollbar { width: 4px; }
76
+ ::-webkit-scrollbar-track { background: var(--bg); }
77
+ ::-webkit-scrollbar-thumb { background: var(--green-lo); border-radius: 2px; }
78
+ ${sharedShellCss}
79
+ .move-to-todo-btn {
80
+ font-family: inherit;
81
+ font-size: 9px;
82
+ padding: 1px 6px;
83
+ background: transparent;
84
+ border: 1px solid var(--cyan-dim);
85
+ color: var(--cyan);
86
+ cursor: pointer;
87
+ margin-left: auto;
88
+ flex-shrink: 0;
89
+ transition: all 0.15s;
90
+ }
91
+ .move-to-todo-btn:hover:not(:disabled) { border-color: var(--cyan); background: var(--cyan-dim); }
92
+ .move-to-todo-btn:disabled { opacity: 0.4; cursor: default; }
93
+ #stat-adapter {
94
+ font-size: 10px;
95
+ font-weight: 400;
96
+ letter-spacing: 0.02em;
97
+ }
98
+ .provider-toggle {
99
+ display: inline-flex;
100
+ align-items: center;
101
+ gap: 4px;
102
+ padding: 2px;
103
+ border: 1px solid var(--border);
104
+ background: var(--bg3);
105
+ }
106
+ .provider-btn {
107
+ font-family: inherit;
108
+ font-size: 9px;
109
+ line-height: 1;
110
+ padding: 4px 8px;
111
+ background: transparent;
112
+ border: 1px solid transparent;
113
+ color: var(--dim);
114
+ cursor: pointer;
115
+ letter-spacing: 0.08em;
116
+ text-transform: uppercase;
117
+ }
118
+ .provider-btn:hover:not(:disabled) {
119
+ color: var(--white);
120
+ border-color: var(--border);
121
+ }
122
+ .provider-btn.active {
123
+ color: var(--green);
124
+ border-color: var(--green-lo);
125
+ background: var(--green-dim);
126
+ }
127
+ /* ===== HEADER STATUS BAR (left section) ===== */
128
+ .status-bar {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 0.75rem;
132
+ }
133
+
134
+ /* ===== MAIN GRID — 25% / 45% / 30% ===== */
135
+ .main-grid {
136
+ display: grid;
137
+ grid-template-columns: 25% 45% 30%;
138
+ flex: 1;
139
+ min-height: 0;
140
+ overflow: hidden;
141
+ }
142
+ .col {
143
+ display: flex;
144
+ flex-direction: column;
145
+ border-right: 1px solid var(--border);
146
+ overflow: hidden;
147
+ background: linear-gradient(
148
+ 180deg,
149
+ color-mix(in srgb, var(--panelSoft) 78%, transparent),
150
+ color-mix(in srgb, var(--panel) 94%, transparent)
151
+ );
152
+ }
153
+ .col:last-child { border-right: none; }
154
+
155
+ /* ===== LAYERED STATE BADGES ===== */
156
+ .badge-row {
157
+ display: flex;
158
+ flex-wrap: wrap;
159
+ gap: 2px;
160
+ margin-top: 2px;
161
+ }
162
+ .badge {
163
+ font-size: 8px;
164
+ padding: 1px 4px;
165
+ border-radius: 2px;
166
+ border: 1px solid;
167
+ letter-spacing: 0.05em;
168
+ text-transform: uppercase;
169
+ flex-shrink: 0;
170
+ }
171
+ .badge-neutral { border-color: var(--dim); color: var(--dim); }
172
+ .badge-active { border-color: var(--green-lo); color: var(--green-mid); background: var(--green-dim); }
173
+ .badge-success { border-color: var(--green-mid); color: var(--green); }
174
+ .badge-warn { border-color: var(--warnBorder); color: var(--amber); }
175
+ .badge-danger { border-color: var(--dangerBorder); color: var(--red); }
176
+ .badge-dim { border-color: var(--border); color: var(--border2); opacity: 0.7; }
177
+
178
+ /* ===== PANEL ===== */
179
+ .panel { display: flex; flex-direction: column; overflow: hidden; flex: 1; }
180
+ .panel + .panel { border-top: 1px solid var(--border); }
181
+ .panel-hdr {
182
+ height: 28px;
183
+ padding: 0 0.75rem;
184
+ display: flex;
185
+ align-items: center;
186
+ gap: 0.5rem;
187
+ background: var(--bg3);
188
+ border-bottom: 1px solid var(--border2);
189
+ flex-shrink: 0;
190
+ font-size: 10px;
191
+ text-transform: uppercase;
192
+ letter-spacing: 0.08em;
193
+ color: var(--dim);
194
+ }
195
+ .panel-hdr-title { color: var(--green-mid); }
196
+ .panel-hdr-badge {
197
+ margin-left: auto;
198
+ font-size: 9px;
199
+ color: var(--dim);
200
+ }
201
+ .panel-body {
202
+ flex: 1;
203
+ overflow-y: auto;
204
+ padding: 0.5rem;
205
+ background: var(--panel);
206
+ }
207
+ .empty { color: var(--dim); font-size: 12px; text-align: center; padding: 1.75rem 0.5rem; }
208
+
209
+ /* ===== PROJECTS ===== */
210
+ .proj-card {
211
+ border: 1px solid var(--border);
212
+ margin-bottom: 4px;
213
+ background: var(--bg2);
214
+ }
215
+ .proj-card.disabled { opacity: 0.45; }
216
+ .proj-hdr {
217
+ display: flex;
218
+ align-items: center;
219
+ padding: 5px 7px;
220
+ gap: 6px;
221
+ cursor: pointer;
222
+ user-select: none;
223
+ }
224
+ .proj-hdr:hover { background: var(--green-dim); }
225
+ .proj-arrow { color: var(--dim); font-size: 9px; width: 10px; flex-shrink: 0; }
226
+ .proj-card.expanded .proj-arrow::before { content: "▼"; }
227
+ .proj-card:not(.expanded) .proj-arrow::before { content: "▶"; }
228
+ .proj-info { flex: 1; min-width: 0; }
229
+ .proj-name { color: var(--green); font-size: 12px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
230
+ .proj-path { color: var(--dim); font-size: 9px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
231
+ .proj-counts { display: flex; gap: 3px; }
232
+ .cnt { font-size: 9px; padding: 1px 4px; font-weight: bold; }
233
+ .cnt-run { color: var(--green); border: 1px solid var(--green-lo); }
234
+ .cnt-que { color: var(--amber); border: 1px solid var(--warnBorder); }
235
+ .cnt-pnd { color: var(--cyan); border: 1px solid var(--cyan-dim); }
236
+ .proj-toggle { flex-shrink: 0; }
237
+ .toggle { position: relative; display: inline-block; width: 30px; height: 16px; }
238
+ .toggle input { opacity: 0; width: 0; height: 0; }
239
+ .slider {
240
+ position: absolute; cursor: pointer;
241
+ top: 0; left: 0; right: 0; bottom: 0;
242
+ background: var(--neutralBg); border: 1px solid var(--dim);
243
+ border-radius: 16px; transition: 0.2s;
244
+ }
245
+ .slider:before {
246
+ position: absolute; content: "";
247
+ height: 10px; width: 10px;
248
+ left: 2px; bottom: 2px;
249
+ background: var(--dim); border-radius: 50%; transition: 0.2s;
250
+ }
251
+ input:checked + .slider { background: var(--green-dim); border-color: var(--green-lo); }
252
+ input:checked + .slider:before { background: var(--green); transform: translateX(14px); }
253
+ .proj-workItems { border-top: 1px solid var(--border2); padding: 4px 7px; }
254
+ .workItem-sec-label {
255
+ font-size: 9px; color: var(--dim); text-transform: uppercase;
256
+ letter-spacing: 0.1em; margin: 4px 0 2px;
257
+ }
258
+ .workItem-row {
259
+ display: flex; align-items: center; gap: 4px;
260
+ padding: 2px 0; font-size: 11px;
261
+ border-bottom: 1px solid var(--border2);
262
+ }
263
+ .workItem-row:last-child { border-bottom: none; }
264
+ .git-info { color: var(--dim); font-size: 9px; display: flex; gap: 6px; align-items: center; }
265
+ .git-branch-name { color: var(--cyan); }
266
+ .git-dirty { color: var(--amber); }
267
+ .git-sync { color: var(--dim); }
268
+ .pr-row { display: flex; align-items: center; gap: 4px; padding: 2px 0; font-size: 11px; border-bottom: 1px solid var(--border2); }
269
+ .pr-row:last-child { border-bottom: none; }
270
+ .pr-num { color: var(--cyan); font-size: 9px; min-width: 32px; }
271
+ .pr-branch { color: var(--green-lo); font-size: 9px; max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
272
+ .pr-title { flex: 1; color: var(--white); font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
273
+ .pr-age { color: var(--dim); font-size: 9px; flex-shrink: 0; }
274
+ .idot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
275
+ .idot-run { background: var(--green); }
276
+ .idot-que { background: var(--amber); }
277
+ .idot-pnd { background: var(--dim); }
278
+ .prio { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
279
+ .prio-1 { background: var(--red); }
280
+ .prio-2 { background: var(--amber); }
281
+ .prio-3 { background: var(--green-mid); }
282
+ .prio-4 { background: var(--dim); }
283
+ .workItem-id { color: var(--cyan); font-size: 9px; min-width: 50px; }
284
+ .workItem-title { flex: 1; color: var(--white); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
285
+ .workItem-retry {
286
+ color: var(--amber);
287
+ font-size: 9px;
288
+ border: 1px solid var(--warnBorder);
289
+ background: var(--warnBg);
290
+ border-radius: 3px;
291
+ padding: 0 4px;
292
+ flex-shrink: 0;
293
+ }
294
+ .workItem-blocked {
295
+ color: var(--red);
296
+ font-size: 9px;
297
+ border: 1px solid var(--dangerBorder);
298
+ background: var(--dangerBg);
299
+ border-radius: 3px;
300
+ padding: 0 4px;
301
+ flex-shrink: 0;
302
+ }
303
+ .workItem-row.workItem-backlog { opacity: 0.45; }
304
+
305
+ /* ===== PROCESS ROW ===== */
306
+ .proc-row {
307
+ display: flex; align-items: center; gap: 6px;
308
+ padding: 4px 6px; border-bottom: 1px solid var(--border2);
309
+ font-size: 11px;
310
+ }
311
+ .proc-pid { color: var(--cyan); font-size: 10px; min-width: 42px; font-variant-numeric: tabular-nums; }
312
+ .proc-stage { color: var(--green); min-width: 56px; font-weight: bold; text-transform: uppercase; font-size: 10px; }
313
+ .proc-model { color: var(--dim); font-size: 9px; min-width: 56px; }
314
+ .proc-dur { color: var(--amber); font-size: 9px; min-width: 42px; text-align: right; font-variant-numeric: tabular-nums; }
315
+ .proc-activity { font-size: 10px; min-width: 16px; text-align: center; }
316
+ .proc-kill {
317
+ font-family: inherit; font-size: 9px; padding: 1px 5px;
318
+ background: transparent; border: 1px solid var(--dangerBorder); color: var(--red);
319
+ cursor: pointer; margin-left: auto;
320
+ }
321
+ .proc-kill:hover { background: var(--dangerBg); border-color: var(--red); }
322
+
323
+ /* ===== PIPELINE ===== */
324
+ .stage-row {
325
+ display: flex; align-items: center; gap: 6px;
326
+ padding: 3px 0; border-bottom: 1px solid var(--border2);
327
+ font-size: 11px;
328
+ }
329
+ .sdot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; background: var(--dim); }
330
+ .sdot.start { background: var(--amber); }
331
+ .sdot.complete { background: var(--green); }
332
+ .sdot.fail { background: var(--red); }
333
+ .sname { color: var(--white); min-width: 70px; }
334
+ .srepo { color: var(--green-lo); font-size: 9px; min-width: 50px; max-width: 90px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
335
+ .sworkItem { color: var(--cyan); font-size: 10px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
336
+ .selapsed { color: var(--amber); font-size: 9px; flex-shrink: 0; min-width: 36px; text-align: right; }
337
+ .smodel { color: var(--dim); font-size: 9px; flex-shrink: 0; min-width: 56px; text-align: right; }
338
+ .stokens { color: var(--amber); font-size: 9px; flex-shrink: 0; min-width: 80px; text-align: right; white-space: nowrap; }
339
+ .sstatus { margin-left: auto; font-size: 9px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.06em; flex-shrink: 0; }
340
+
341
+ /* ===== LOG TAB BAR ===== */
342
+ .log-tab-bar {
343
+ display: flex; gap: 0; border-bottom: 1px solid var(--border2);
344
+ background: var(--panelSoft);
345
+ padding: 0 4px; overflow-x: auto; flex-shrink: 0;
346
+ }
347
+ .log-tab {
348
+ -webkit-appearance: none;
349
+ appearance: none;
350
+ background: transparent; border: none; border-bottom: 2px solid transparent;
351
+ color: var(--dim); font-family: inherit; font-size: 10px;
352
+ padding: 4px 8px; cursor: pointer; white-space: nowrap;
353
+ text-transform: uppercase; letter-spacing: .05em;
354
+ }
355
+ .log-tab:hover { color: var(--green-mid); }
356
+ .log-tab.active { color: var(--green); border-bottom-color: var(--green); }
357
+
358
+ /* ===== LOG ===== */
359
+ .log-area { font-size: 11px; line-height: 1.5; padding: 4px 0; }
360
+ .log-line { padding: 3px 8px; display: flex; gap: 6px; align-items: flex-start; border-radius: 2px; margin: 1px 0; }
361
+ .log-line:hover { background: color-mix(in srgb, var(--white) 3%, transparent); }
362
+ .log-line.log-success .ltext { color: var(--green); }
363
+ .log-line.log-fail .ltext { color: var(--red); }
364
+ .log-line.log-warn .ltext { color: var(--amber); }
365
+ .log-line.log-system { opacity: 0.6; }
366
+ .log-line.log-heading { border-top: 1px solid var(--border2); margin-top: 6px; padding-top: 8px; }
367
+ .ltime { color: var(--dim); font-size: 9px; flex-shrink: 0; min-width: 36px; opacity: 0.7; padding-top: 2px; font-variant-numeric: tabular-nums; }
368
+ .licon { flex-shrink: 0; min-width: 14px; text-align: center; font-size: 11px; padding-top: 1px; }
369
+ .ltag { color: var(--green-lo); min-width: 52px; flex-shrink: 0; padding-top: 1px; font-size: 10px; font-weight: 500; }
370
+ .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; }
371
+ .ltext { color: var(--mutedStrong); word-break: break-word; white-space: pre-wrap; flex: 1; min-width: 0; }
372
+ .ltext .lhighlight { color: var(--white); font-weight: 500; }
373
+ .ltext .lcost { color: var(--amber); font-size: 10px; }
374
+ .ltext .lfiles { color: var(--cyan); font-size: 10px; }
375
+ .log-line.log-spacer { height: 6px; padding: 0; margin: 0; min-height: 6px; }
376
+ .log-line.log-separator { opacity: 0.2; padding: 0 8px; margin: 4px 0; }
377
+ .log-line.log-separator .ltext { color: var(--dim); }
378
+ .log-line.log-code .ltext { font-family: 'JetBrains Mono', 'Fira Code', monospace; color: var(--cyan); opacity: 0.8; font-size: 10px; }
379
+ .log-line.log-heading2 .ltext { color: var(--white); font-weight: 600; font-size: 12px; }
380
+ .log-line.log-tool .ltext { color: var(--dim); font-style: italic; font-size: 10px; }
381
+
382
+ /* ===== REPO PICKER ===== */
383
+ .repo-item {
384
+ display: flex; align-items: center; gap: 8px;
385
+ padding: 5px 12px; cursor: pointer; font-size: 11px;
386
+ }
387
+ .repo-item:hover { background: var(--green-dim); }
388
+ .repo-item-name { color: var(--green); font-weight: bold; }
389
+ .repo-item-path { color: var(--dim); font-size: 10px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
390
+ .repo-item-badge { font-size: 9px; padding: 1px 5px; border: 1px solid var(--green-lo); color: var(--green-mid); flex-shrink: 0; }
391
+
392
+ .scan-path-row { display: flex; align-items: center; gap: 6px; padding: 3px 0; font-size: 11px; }
393
+ .scan-path-row .path { color: var(--dim); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
394
+ .scan-path-badge { font-size: 9px; padding: 1px 5px; border: 1px solid var(--neutralBorder); color: var(--mutedStrong); flex-shrink: 0; }
395
+ .scan-path-remove { background: transparent; border: none; color: var(--dangerBorder); cursor: pointer; font-size: 12px; padding: 0 2px; flex-shrink: 0; }
396
+ .scan-path-remove:hover { color: var(--red); }
397
+
398
+ /* ===== TAB BAR (hidden on desktop) ===== */
399
+ .tab-bar { display: none; }
400
+
401
+ /* ===== MOBILE RESPONSIVE ===== */
402
+ @media (max-width: 768px) {
403
+ html, body { overflow: auto; }
404
+
405
+ /* Header */
406
+ header {
407
+ height: auto;
408
+ min-height: 38px;
409
+ flex-wrap: wrap;
410
+ padding: 6px 0.75rem;
411
+ gap: 4px;
412
+ }
413
+ .hdr-right { width: 100%; justify-content: flex-end; }
414
+ .svc-group .btn { font-size: 0; padding: 4px 8px; min-height: 32px; }
415
+
416
+ /* Stats bar */
417
+ .stats-bar {
418
+ height: auto;
419
+ min-height: 32px;
420
+ flex-wrap: wrap;
421
+ padding: 4px 0.75rem;
422
+ gap: 0.5rem;
423
+ font-size: 11px;
424
+ }
425
+ .stat-divider { display: none; }
426
+
427
+ /* Tab bar */
428
+ .tab-bar {
429
+ display: flex;
430
+ background: var(--bg2);
431
+ border-bottom: 1px solid var(--border);
432
+ }
433
+ .tab {
434
+ flex: 1;
435
+ font-family: inherit;
436
+ font-size: 11px;
437
+ letter-spacing: 0.1em;
438
+ padding: 10px 0;
439
+ min-height: 44px;
440
+ background: transparent;
441
+ border: none;
442
+ border-bottom: 2px solid transparent;
443
+ color: var(--dim);
444
+ cursor: pointer;
445
+ text-align: center;
446
+ transition: all 0.15s;
447
+ }
448
+ .tab.active {
449
+ color: var(--green);
450
+ border-bottom-color: var(--green);
451
+ }
452
+
453
+ /* Main grid → single column */
454
+ .main-grid {
455
+ display: flex;
456
+ flex-direction: column;
457
+ height: auto;
458
+ min-height: calc(100vh - 180px);
459
+ overflow: visible;
460
+ }
461
+ .col {
462
+ display: none;
463
+ border-right: none;
464
+ overflow: visible;
465
+ min-height: calc(100vh - 220px);
466
+ }
467
+ .col.mob-active {
468
+ display: flex;
469
+ flex: 1;
470
+ }
471
+
472
+ /* Repo picker → fullscreen */
473
+ #repo-picker > div {
474
+ width: 100% !important;
475
+ max-height: 90vh !important;
476
+ margin: 5vh 0 0;
477
+ }
478
+ .repo-item { min-height: 44px; }
479
+
480
+ /* Touch targets */
481
+ .btn { min-height: 36px; padding: 4px 10px; }
482
+ .proj-hdr { min-height: 44px; padding: 8px 7px; }
483
+ .toggle { width: 40px; height: 22px; }
484
+ .slider:before { height: 14px; width: 14px; left: 3px; bottom: 3px; }
485
+ input:checked + .slider:before { transform: translateX(18px); }
486
+ }
487
+ </style>
488
+ </head>
489
+ <body>
490
+ <!-- HEADER: status bar (left) + runtime controls (right) -->
491
+ ${sharedHeaderHtml}
492
+
493
+ <!-- STATS BAR — throughput + cost + quota -->
494
+ ${sharedStatsBarHtml}
495
+
496
+ <!-- TAB BAR (mobile only) -->
497
+ <div class="tab-bar">
498
+ <button class="tab active" data-tab="0">WORK ITEMS</button>
499
+ <button class="tab" data-tab="1">PIPELINE</button>
500
+ <button class="tab" data-tab="2">TERMINAL</button>
501
+ </div>
502
+
503
+ <!-- MAIN GRID: 25% left / 50% center / 25% right -->
504
+ <div class="main-grid">
505
+
506
+ <!-- LEFT COLUMN (25%): work-item tree + area filters + monitors -->
507
+ <div class="col" data-column="left">
508
+ <!-- WORK ITEM TREE (canonical endpoint, layered-state badges) -->
509
+ <div class="panel" style="flex: 1 1 60%">
510
+ <div class="panel-hdr">
511
+ <span class="panel-hdr-title">WORK ITEMS</span>
512
+ <span class="panel-hdr-badge" id="work-item-count"></span>
513
+ </div>
514
+ <div class="panel-body" id="work-item-list">
515
+ <div class="empty">loading...</div>
516
+ </div>
517
+ </div>
518
+
519
+ <!-- REPOSITORIES (pinned projects + toggles) -->
520
+ <div class="panel" style="flex: 0 0 auto;">
521
+ <div class="panel-hdr">
522
+ <span class="panel-hdr-title">REPOSITORIES</span>
523
+ <span class="panel-hdr-badge" id="proj-summary"></span>
524
+ <button class="btn" style="margin-left:auto;font-size:9px;padding:1px 6px" onclick="openRepoPicker()">+ ADD</button>
525
+ </div>
526
+ <div class="panel-body" id="project-list" style="max-height: 260px;">
527
+ <div class="empty">loading...</div>
528
+ </div>
529
+ </div>
530
+
531
+ <!-- MONITORS & PROCESSES -->
532
+ <div class="panel" id="monitor-panel" style="flex: 0 0 auto; max-height: 180px;">
533
+ <div class="panel-hdr">
534
+ <span class="panel-hdr-title">MONITORS & PROCESSES</span>
535
+ <span class="panel-hdr-badge" id="monitor-count"></span>
536
+ </div>
537
+ <div class="panel-body" id="monitor-list">
538
+ <div class="empty">no monitors or processes</div>
539
+ </div>
540
+ </div>
541
+ </div>
542
+
543
+ <!-- REPO PICKER OVERLAY -->
544
+ <div id="repo-picker" style="display:none;position:fixed;inset:0;background:var(--overlay);z-index:100;align-items:center;justify-content:center">
545
+ <div style="background:var(--panel);border:1px solid var(--border);width:500px;max-height:70vh;display:flex;flex-direction:column">
546
+ <div style="padding:8px 12px;border-bottom:1px solid var(--border2);display:flex;align-items:center;gap:8px">
547
+ <span style="color:var(--green-mid);font-size:11px;text-transform:uppercase;letter-spacing:.1em">ADD REPOSITORY</span>
548
+ <button onclick="closeRepoPicker()" style="margin-left:auto;background:transparent;border:none;color:var(--mutedStrong);cursor:pointer;font-size:14px">✕</button>
549
+ </div>
550
+ <div style="padding:6px 12px;border-bottom:1px solid var(--border2)">
551
+ <input id="repo-search" type="text" placeholder="filter repositories..."
552
+ style="width:100%;background:transparent;border:none;outline:none;font-family:inherit;font-size:12px;color:var(--green);caret-color:var(--green)"
553
+ oninput="filterRepos(this.value)" onkeydown="if(event.key==='Escape')closeRepoPicker()">
554
+ </div>
555
+ <div id="repo-picker-list" style="overflow-y:auto;flex:1;padding:4px 0"></div>
556
+ <div id="scan-paths-section" style="border-top:1px solid var(--border2);padding:8px 12px">
557
+ <div style="color:var(--green-mid);font-size:10px;text-transform:uppercase;letter-spacing:.1em;margin-bottom:6px">SCAN PATHS</div>
558
+ <div id="scan-paths-list"></div>
559
+ <div style="display:flex;gap:4px;margin-top:6px">
560
+ <input id="scan-path-input" type="text" placeholder="/path/to/scan"
561
+ style="flex:1;background:transparent;border:1px solid var(--border2);outline:none;font-family:inherit;font-size:11px;color:var(--green);padding:3px 6px;caret-color:var(--green)"
562
+ onkeydown="if(event.key==='Enter')addScanPath()">
563
+ <button class="btn" style="font-size:9px;padding:1px 6px" onclick="addScanPath()">+ ADD</button>
564
+ </div>
565
+ </div>
566
+ </div>
567
+ </div>
568
+
569
+ <!-- CENTER COLUMN (50%): primary operational canvas — pipeline + live log -->
570
+ <div class="col" data-column="center">
571
+ <div class="panel" style="flex: 0 0 38%">
572
+ <div class="panel-hdr">
573
+ <span class="panel-hdr-title">PIPELINE</span>
574
+ <span class="panel-hdr-badge" id="stage-count"></span>
575
+ </div>
576
+ <div class="panel-body" id="stage-list">
577
+ <div class="empty">no pipeline events</div>
578
+ </div>
579
+ </div>
580
+ <div class="panel">
581
+ <div class="panel-hdr">
582
+ <span class="panel-hdr-title">LIVE LOG</span>
583
+ <span class="panel-hdr-badge" id="log-count"></span>
584
+ </div>
585
+ <div class="log-tab-bar" id="log-tab-bar">
586
+ <button class="log-tab active" data-workItem="all" onclick="selectLogTab(null)">ALL</button>
587
+ </div>
588
+ <div class="panel-body log-area" id="log-list">
589
+ <div class="empty">no log output</div>
590
+ </div>
591
+ </div>
592
+ </div>
593
+
594
+ <!-- RIGHT COLUMN (25%): terminal host + sidecars (collapsible) -->
595
+ <div class="col" data-column="right">
596
+ ${sharedRightRailHtml}
597
+ </div>
598
+
599
+ </div>
600
+
601
+ <script>
602
+ const MAX_LOG = 200;
603
+ const MAX_STAGE = 100;
604
+
605
+ let projects = [];
606
+ let expandedProjects = new Set();
607
+ let knowledgeCache = {};
608
+ let logLines = [];
609
+ let selectedLogWorkItemId = null; // null = ALL, string = specific workItemId
610
+ let stageRows = [];
611
+ let totalCostUsd = 0;
612
+ let lastSyncAt = null;
613
+ let terminalSocket = null;
614
+ let terminalInstance = null;
615
+ let terminalFitAddon = null;
616
+ let terminalReconnectTimer = null;
617
+ let terminalSessionId = null;
618
+ let terminalShuttingDown = false;
619
+ let laneRows = [];
620
+ let appThemeQuery = "";
621
+ const TERMINAL_SESSION_STORAGE_KEY = "enact-factory:dashboard:terminal-session";
622
+ const APP_THEMES = ${appThemesJson};
623
+ const THEME_FAVORITES = ${appThemeFavoritesJson};
624
+ const THEME_STORAGE = '${THEME_STORAGE_KEY}';
625
+ const THEME_RECENTS = '${THEME_RECENTS_STORAGE_KEY}';
626
+ const THEME_RECENT_LIMIT = ${THEME_RECENTS_LIMIT};
627
+ const FALLBACK_THEME = '${DEFAULT_THEME_ID}';
628
+ const workItemProjectMap = new Map();
629
+ // workItemId → { title, workItemIdentifier } for pipeline display
630
+ const workItemTitleMap = new Map();
631
+ // workItemId → start timestamp for elapsed time
632
+ const workItemStartMap = new Map();
633
+
634
+ function isAppThemeId(value) {
635
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(APP_THEMES, value);
636
+ }
637
+
638
+ function themeCssName(rawKey) {
639
+ return '--' + rawKey.replace(/[A-Z]/g, function(match) { return '-' + match.toLowerCase(); });
640
+ }
641
+
642
+ function getStoredThemeId() {
643
+ try {
644
+ return window.localStorage.getItem(THEME_STORAGE) || FALLBACK_THEME;
645
+ } catch {
646
+ return FALLBACK_THEME;
647
+ }
648
+ }
649
+
650
+ function getRecentThemeIds() {
651
+ try {
652
+ const raw = window.localStorage.getItem(THEME_RECENTS);
653
+ if (!raw) return [];
654
+ const parsed = JSON.parse(raw);
655
+ if (!Array.isArray(parsed)) return [];
656
+ return parsed.filter(isAppThemeId);
657
+ } catch {
658
+ return [];
659
+ }
660
+ }
661
+
662
+ function saveRecentThemeId(themeId) {
663
+ try {
664
+ const next = [themeId, ...getRecentThemeIds().filter((id) => id !== themeId)]
665
+ .slice(0, THEME_RECENT_LIMIT);
666
+ window.localStorage.setItem(THEME_RECENTS, JSON.stringify(next));
667
+ } catch {
668
+ // ignore storage failures
669
+ }
670
+ }
671
+
672
+ function themeMatchesQuery(theme, query) {
673
+ if (!query) return true;
674
+ const haystack = (theme.id + " " + theme.label).toLowerCase();
675
+ return haystack.includes(query);
676
+ }
677
+
678
+ function buildThemeOptionsHtml(selectedThemeId, query) {
679
+ const normalizedQuery = (query || "").trim().toLowerCase();
680
+ const allThemes = Object.values(APP_THEMES);
681
+ const enactTheme = allThemes.find((theme) => theme.id === 'enact');
682
+ const favoriteIds = new Set(THEME_FAVORITES.filter(isAppThemeId));
683
+ const recentIds = getRecentThemeIds()
684
+ .filter((id) => id !== 'enact');
685
+ const recentThemes = recentIds
686
+ .map((id) => APP_THEMES[id])
687
+ .filter(Boolean)
688
+ .sort((a, b) => recentIds.indexOf(a.id) - recentIds.indexOf(b.id));
689
+ const favorites = THEME_FAVORITES
690
+ .map((id) => APP_THEMES[id])
691
+ .filter(Boolean)
692
+ .filter((theme) => !recentThemes.some((recent) => recent.id === theme.id));
693
+ const excludedIds = new Set([
694
+ 'enact',
695
+ ...recentThemes.map((theme) => theme.id),
696
+ ...favorites.map((theme) => theme.id),
697
+ ]);
698
+ const remaining = allThemes
699
+ .filter((theme) => !excludedIds.has(theme.id))
700
+ .sort((a, b) => a.label.localeCompare(b.label));
701
+
702
+ const renderOption = (theme) =>
703
+ '<option value="' + theme.id + '"' + (theme.id === selectedThemeId ? ' selected' : '') + '>' + theme.label + '</option>';
704
+
705
+ const filterGroup = (themes) => themes.filter((theme) =>
706
+ theme.id === selectedThemeId ? true : themeMatchesQuery(theme, normalizedQuery),
707
+ );
708
+
709
+ const groups = [];
710
+ const enactThemes = enactTheme ? filterGroup([enactTheme]) : [];
711
+ const recentFiltered = filterGroup(recentThemes);
712
+ const favoritesFiltered = filterGroup(favorites);
713
+ const remainingFiltered = filterGroup(remaining);
714
+ if (enactThemes.length) groups.push('<optgroup label="Enact">' + enactThemes.map(renderOption).join('') + '</optgroup>');
715
+ if (recentFiltered.length) groups.push('<optgroup label="Recent">' + recentFiltered.map(renderOption).join('') + '</optgroup>');
716
+ if (favoritesFiltered.length) groups.push('<optgroup label="Ghostty Favorites">' + favoritesFiltered.map(renderOption).join('') + '</optgroup>');
717
+ if (remainingFiltered.length) groups.push('<optgroup label="Ghostty Themes">' + remainingFiltered.map(renderOption).join('') + '</optgroup>');
718
+ return groups.join('');
719
+ }
720
+
721
+ function filterAppThemes(query) {
722
+ appThemeQuery = query || "";
723
+ const select = document.getElementById("app-theme-select");
724
+ const currentTheme = select ? select.value : getStoredThemeId();
725
+ if (select) {
726
+ select.innerHTML = buildThemeOptionsHtml(currentTheme, appThemeQuery);
727
+ }
728
+ syncThemePickerUi(currentTheme);
729
+ }
730
+
731
+ function syncThemePickerUi(themeId) {
732
+ const select = document.getElementById("app-theme-select");
733
+ const current = document.getElementById("app-theme-current");
734
+ const launcher = document.getElementById("theme-launcher-btn");
735
+ const normalizedThemeId = isAppThemeId(themeId) ? themeId : getStoredThemeId();
736
+ const label = APP_THEMES[normalizedThemeId]?.label || APP_THEMES[FALLBACK_THEME]?.label || "Enact";
737
+ if (current) current.textContent = label;
738
+ if (select) select.value = normalizedThemeId;
739
+ if (launcher) launcher.setAttribute("aria-expanded", String(isThemePickerOpen()));
740
+ }
741
+
742
+ function isThemePickerOpen() {
743
+ return document.getElementById("theme-popover")?.classList.contains("open") ?? false;
744
+ }
745
+
746
+ function closeThemePicker() {
747
+ const popover = document.getElementById("theme-popover");
748
+ if (!popover) return;
749
+ popover.classList.remove("open");
750
+ syncThemePickerUi(getStoredThemeId());
751
+ }
752
+
753
+ function toggleThemePicker(event) {
754
+ if (event) event.stopPropagation();
755
+ const popover = document.getElementById("theme-popover");
756
+ if (!popover) return;
757
+ const nextOpen = !popover.classList.contains("open");
758
+ popover.classList.toggle("open", nextOpen);
759
+ syncThemePickerUi(getStoredThemeId());
760
+ if (nextOpen) {
761
+ const filter = document.getElementById("app-theme-filter");
762
+ if (filter) filter.focus();
763
+ }
764
+ }
765
+
766
+ function getActiveTheme() {
767
+ const themeId = getStoredThemeId();
768
+ const normalizedThemeId = isAppThemeId(themeId) ? themeId : FALLBACK_THEME;
769
+ return APP_THEMES[normalizedThemeId] || APP_THEMES[FALLBACK_THEME];
770
+ }
771
+
772
+ function applyAppTheme(themeId) {
773
+ const root = document.documentElement;
774
+ const normalizedThemeId = isAppThemeId(themeId) ? themeId : FALLBACK_THEME;
775
+ const theme = APP_THEMES[normalizedThemeId];
776
+ if (!theme) return;
777
+
778
+ const vars = theme.vars || {};
779
+ for (const [key, value] of Object.entries(vars)) {
780
+ const cssName = themeCssName(key);
781
+ root.style.setProperty(cssName, value);
782
+ root.style.setProperty('--' + key, value);
783
+ }
784
+
785
+ const legacyAlias = {
786
+ panelSoft: '--panel-soft',
787
+ greenDim: '--green-dim',
788
+ greenMid: '--green-mid',
789
+ greenLo: '--green-lo',
790
+ cyanDim: '--cyan-dim',
791
+ };
792
+ for (const [legacyKey, legacyVar] of Object.entries(legacyAlias)) {
793
+ root.style.setProperty(legacyVar, vars[legacyKey]);
794
+ }
795
+
796
+ const select = document.getElementById("app-theme-select");
797
+ if (select) {
798
+ select.innerHTML = buildThemeOptionsHtml(normalizedThemeId, appThemeQuery);
799
+ }
800
+ syncThemePickerUi(normalizedThemeId);
801
+
802
+ try {
803
+ window.localStorage.setItem(THEME_STORAGE, normalizedThemeId);
804
+ } catch {
805
+ // ignore storage failures
806
+ }
807
+ saveRecentThemeId(normalizedThemeId);
808
+ syncThemePickerUi(normalizedThemeId);
809
+ closeThemePicker();
810
+
811
+ if (terminalInstance) {
812
+ terminalInstance.options.theme = { ...theme.terminal };
813
+ if (typeof terminalInstance.refresh === "function") {
814
+ terminalInstance.refresh(0, Math.max(0, terminalInstance.rows - 1));
815
+ }
816
+ }
817
+ }
818
+
819
+ function bootAppTheme() {
820
+ applyAppTheme(getStoredThemeId());
821
+ syncThemePickerUi(getStoredThemeId());
822
+ }
823
+
824
+ function setTerminalStatus(text, tone) {
825
+ const el = document.getElementById("terminal-status");
826
+ if (!el) return;
827
+ el.textContent = text;
828
+ el.style.color =
829
+ tone === "ok" ? "var(--green)"
830
+ : tone === "warn" ? "var(--amber)"
831
+ : tone === "err" ? "var(--red)"
832
+ : "var(--dim)";
833
+ }
834
+
835
+ function terminalSocketUrl() {
836
+ const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
837
+ const url = new URL(proto + "//" + window.location.host + "/api/terminal");
838
+ const remembered = window.sessionStorage.getItem(TERMINAL_SESSION_STORAGE_KEY);
839
+ if (terminalSessionId) {
840
+ url.searchParams.set("sessionId", terminalSessionId);
841
+ } else if (remembered) {
842
+ url.searchParams.set("sessionId", remembered);
843
+ }
844
+ if (terminalInstance) {
845
+ url.searchParams.set("cols", String(terminalInstance.cols || 120));
846
+ url.searchParams.set("rows", String(terminalInstance.rows || 30));
847
+ }
848
+ return url.toString();
849
+ }
850
+
851
+ function scheduleTerminalReconnect() {
852
+ if (terminalShuttingDown || terminalReconnectTimer) return;
853
+ terminalReconnectTimer = window.setTimeout(function() {
854
+ terminalReconnectTimer = null;
855
+ connectTerminalSocket();
856
+ }, 1500);
857
+ }
858
+
859
+ function handleTerminalMessage(message) {
860
+ if (!terminalInstance || !message || typeof message.type !== "string") return;
861
+
862
+ if (message.type === "terminal:connected") {
863
+ terminalSessionId = message.sessionId || null;
864
+ if (terminalSessionId) {
865
+ window.sessionStorage.setItem(TERMINAL_SESSION_STORAGE_KEY, terminalSessionId);
866
+ }
867
+ setTerminalStatus("CONNECTED", "ok");
868
+ return;
869
+ }
870
+
871
+ if (message.type === "terminal:output") {
872
+ if (typeof message.data === "string") {
873
+ terminalInstance.write(message.data);
874
+ }
875
+ return;
876
+ }
877
+
878
+ if (message.type === "terminal:resized") {
879
+ setTerminalStatus((message.cols || terminalInstance.cols) + "x" + (message.rows || terminalInstance.rows), "dim");
880
+ return;
881
+ }
882
+
883
+ if (message.type === "terminal:exit") {
884
+ terminalSessionId = null;
885
+ window.sessionStorage.removeItem(TERMINAL_SESSION_STORAGE_KEY);
886
+ terminalInstance.writeln("\\r\\n\\x1b[33m[terminal exited]\\x1b[0m");
887
+ setTerminalStatus("EXITED", "warn");
888
+ scheduleTerminalReconnect();
889
+ return;
890
+ }
891
+
892
+ if (message.type === "terminal:error") {
893
+ terminalInstance.writeln("\\r\\n\\x1b[31m[terminal error] " + (message.error || "unknown") + "\\x1b[0m");
894
+ setTerminalStatus("ERROR", "err");
895
+ }
896
+ }
897
+
898
+ function connectTerminalSocket() {
899
+ if (!terminalInstance) return;
900
+ if (terminalSocket && (terminalSocket.readyState === WebSocket.OPEN || terminalSocket.readyState === WebSocket.CONNECTING)) {
901
+ return;
902
+ }
903
+
904
+ const attemptedSessionId = terminalSessionId || window.sessionStorage.getItem(TERMINAL_SESSION_STORAGE_KEY);
905
+ let opened = false;
906
+ setTerminalStatus("CONNECTING", "warn");
907
+ const socket = new WebSocket(terminalSocketUrl());
908
+ terminalSocket = socket;
909
+
910
+ socket.addEventListener("message", function(event) {
911
+ try {
912
+ const message = JSON.parse(typeof event.data === "string" ? event.data : "");
913
+ handleTerminalMessage(message);
914
+ } catch {
915
+ // ignore malformed bridge messages
916
+ }
917
+ });
918
+
919
+ socket.addEventListener("open", function() {
920
+ opened = true;
921
+ setTerminalStatus("HANDSHAKE", "dim");
922
+ if (terminalFitAddon) {
923
+ try { terminalFitAddon.fit(); } catch {}
924
+ }
925
+ });
926
+
927
+ socket.addEventListener("close", function() {
928
+ terminalSocket = null;
929
+ if (!terminalShuttingDown) {
930
+ if (!opened && attemptedSessionId) {
931
+ terminalSessionId = null;
932
+ window.sessionStorage.removeItem(TERMINAL_SESSION_STORAGE_KEY);
933
+ }
934
+ setTerminalStatus("RECONNECTING", "warn");
935
+ scheduleTerminalReconnect();
936
+ }
937
+ });
938
+
939
+ socket.addEventListener("error", function() {
940
+ setTerminalStatus("ERROR", "err");
941
+ });
942
+ }
943
+
944
+ function initializeTerminal() {
945
+ const host = document.getElementById("terminal-host");
946
+ if (!host) return;
947
+
948
+ if (!window.Terminal || !window.FitAddon) {
949
+ host.innerHTML = '<div class="empty">xterm failed to load</div>';
950
+ setTerminalStatus("UNAVAILABLE", "err");
951
+ return;
952
+ }
953
+
954
+ const activeTheme = getActiveTheme();
955
+ const terminal = new window.Terminal({
956
+ cursorBlink: true,
957
+ cursorStyle: "bar",
958
+ fontFamily: "JetBrains Mono, Menlo, monospace",
959
+ fontSize: 12,
960
+ lineHeight: 1.25,
961
+ scrollback: 5000,
962
+ theme: { ...activeTheme.terminal }
963
+ });
964
+ const fitAddon = new window.FitAddon.FitAddon();
965
+ terminal.loadAddon(fitAddon);
966
+ if (window.WebLinksAddon && window.WebLinksAddon.WebLinksAddon) {
967
+ terminal.loadAddon(new window.WebLinksAddon.WebLinksAddon());
968
+ }
969
+
970
+ terminal.open(host);
971
+ terminalInstance = terminal;
972
+ terminalFitAddon = fitAddon;
973
+
974
+ try { fitAddon.fit(); } catch {}
975
+ host.parentElement.classList.add("ready");
976
+ terminal.focus();
977
+ setTerminalStatus("BOOTING", "dim");
978
+
979
+ terminal.onData(function(data) {
980
+ if (!terminalSocket || terminalSocket.readyState !== WebSocket.OPEN) return;
981
+ terminalSocket.send(JSON.stringify({ type: "terminal:input", data: data }));
982
+ });
983
+
984
+ terminal.onResize(function(size) {
985
+ if (!terminalSocket || terminalSocket.readyState !== WebSocket.OPEN) return;
986
+ terminalSocket.send(JSON.stringify({ type: "terminal:resize", cols: size.cols, rows: size.rows }));
987
+ });
988
+
989
+ host.addEventListener("click", function() {
990
+ terminal.focus();
991
+ });
992
+
993
+ window.addEventListener("resize", function() {
994
+ if (!terminalFitAddon || !terminalInstance) return;
995
+ try { terminalFitAddon.fit(); } catch {}
996
+ if (terminalSocket && terminalSocket.readyState === WebSocket.OPEN) {
997
+ terminalSocket.send(JSON.stringify({
998
+ type: "terminal:resize",
999
+ cols: terminalInstance.cols,
1000
+ rows: terminalInstance.rows
1001
+ }));
1002
+ }
1003
+ });
1004
+
1005
+ window.addEventListener("beforeunload", function() {
1006
+ terminalShuttingDown = true;
1007
+ if (terminalSocket && terminalSocket.readyState === WebSocket.OPEN) {
1008
+ terminalSocket.close();
1009
+ }
1010
+ });
1011
+
1012
+ window.addEventListener("storage", function(event) {
1013
+ if (event.key === THEME_STORAGE) {
1014
+ bootAppTheme();
1015
+ }
1016
+ });
1017
+
1018
+ connectTerminalSocket();
1019
+ }
1020
+
1021
+ // ---- App status badge (single badge combining SSE + health) ----
1022
+ var _sseOpen = false;
1023
+ var _healthOk = false;
1024
+ var _healthChecked = false; // honest initial state: don't claim LIVE before first poll
1025
+
1026
+ function setAppStatus(state) {
1027
+ var el = document.getElementById("app-status");
1028
+ if (!el) return;
1029
+ if (state === "live") {
1030
+ el.textContent = "LIVE";
1031
+ el.className = "live";
1032
+ el.title = "LIVE — event stream open and backend healthy (DB reachable, sync ok)";
1033
+ } else if (state === "connecting") {
1034
+ el.textContent = "CONNECTING";
1035
+ el.className = "reconnecting";
1036
+ el.title = "CONNECTING — establishing event stream and checking backend health";
1037
+ } else if (state === "reconnecting") {
1038
+ el.textContent = "RECONNECTING";
1039
+ el.className = "reconnecting";
1040
+ el.title = "RECONNECTING — event stream lost; retrying automatically every 3s";
1041
+ } else {
1042
+ el.textContent = "DEGRADED";
1043
+ el.className = "degraded";
1044
+ el.title = "DEGRADED — app is running but the backend store (DB/sync) is unhealthy; retrying automatically";
1045
+ }
1046
+ }
1047
+
1048
+ function refreshAppStatus() {
1049
+ if (!_sseOpen) {
1050
+ setAppStatus("reconnecting");
1051
+ } else if (!_healthChecked) {
1052
+ // SSE open but first health poll hasn't completed — don't lie LIVE yet
1053
+ setAppStatus("connecting");
1054
+ } else if (!_healthOk) {
1055
+ setAppStatus("degraded");
1056
+ } else {
1057
+ setAppStatus("live");
1058
+ }
1059
+ }
1060
+
1061
+ async function pollHealth() {
1062
+ try {
1063
+ var res = await fetch("/api/health");
1064
+ var data = await res.json();
1065
+ // Backend is healthy only when DB is reachable AND sync is not degraded.
1066
+ _healthOk = !!(data && data.ok === true && data.sync !== "degraded");
1067
+ } catch {
1068
+ _healthOk = false;
1069
+ }
1070
+ _healthChecked = true;
1071
+ refreshAppStatus();
1072
+ }
1073
+
1074
+ // ---- SSE ----
1075
+ function connectSSE(skipReplay) {
1076
+ const url = skipReplay ? "/api/events?skipReplay=1" : "/api/events";
1077
+ const es = new EventSource(url);
1078
+ es.onopen = () => {
1079
+ _sseOpen = true;
1080
+ refreshAppStatus();
1081
+ // Kick an immediate health check so the badge can't momentarily lie LIVE
1082
+ // before the first poll — it shows CONNECTING until health is known.
1083
+ pollHealth();
1084
+ };
1085
+ es.onmessage = e => {
1086
+ let ev; try { ev = JSON.parse(e.data); } catch { return; }
1087
+ handleEvent(ev);
1088
+ };
1089
+ es.onerror = () => {
1090
+ _sseOpen = false;
1091
+ refreshAppStatus();
1092
+ es.close(); setTimeout(function() { connectSSE(false); }, 3000);
1093
+ };
1094
+ }
1095
+
1096
+ function handleEvent(ev) {
1097
+ switch (ev.type) {
1098
+ case "stats": updateStats(ev.data); break;
1099
+ case "workItem:queued":
1100
+ workItemProjectMap.set(ev.data.workItemId, ev.data.projectPath);
1101
+ workItemTitleMap.set(ev.data.workItemId, { title: ev.data.title, workItemIdentifier: ev.data.workItemIdentifier });
1102
+ updateProjectWorkItem(ev.data.projectPath, ev.data.workItemId, ev.data.title, ev.data.priority, "queued");
1103
+ break;
1104
+ case "workItem:started": {
1105
+ const p = workItemProjectMap.get(ev.data.workItemId);
1106
+ if (ev.data.title) workItemTitleMap.set(ev.data.workItemId, { title: ev.data.title, workItemIdentifier: ev.data.workItemIdentifier });
1107
+ workItemStartMap.set(ev.data.workItemId, Date.now());
1108
+ if (p) updateProjectWorkItem(p, ev.data.workItemId, ev.data.title, null, "running");
1109
+ break;
1110
+ }
1111
+ case "workItem:completed": {
1112
+ const p = workItemProjectMap.get(ev.data.workItemId);
1113
+ if (p) removeProjectWorkItem(p, ev.data.workItemId);
1114
+ break;
1115
+ }
1116
+ case "pipeline:stage": addStageRow(ev.data); break;
1117
+ case "pipeline:iteration":
1118
+ addStageRow({ workItemId: ev.data.workItemId, stage: "iter #" + ev.data.iteration, status: "start" });
1119
+ break;
1120
+ case "log": addLogLine(ev.data); break;
1121
+ case "project:toggled": {
1122
+ const p = projects.find(x => x.path === ev.data.projectPath);
1123
+ if (p) { p.enabled = ev.data.enabled; renderProjects(); }
1124
+ break;
1125
+ }
1126
+ case "workItem:cost": {
1127
+ totalCostUsd += ev.data.cost?.costUsd ?? 0;
1128
+ document.getElementById("stat-cost").textContent = "$" + totalCostUsd.toFixed(2);
1129
+ break;
1130
+ }
1131
+ case "monitor:checked":
1132
+ case "monitor:stateChange":
1133
+ fetchMonitors();
1134
+ break;
1135
+ case "process:spawn":
1136
+ fetchProcesses();
1137
+ addLogLine({ workItemId: ev.data.workItemId || "system", stage: ev.data.stage || "spawn", line: "Process spawned PID=" + ev.data.pid + " stage=" + ev.data.stage + (ev.data.model ? " model=" + ev.data.model : "") });
1138
+ break;
1139
+ case "process:exit":
1140
+ fetchProcesses();
1141
+ addLogLine({ workItemId: "system", stage: "exit", line: "Process exited PID=" + ev.data.pid + " code=" + ev.data.exitCode + " duration=" + (ev.data.durationMs / 1000).toFixed(1) + "s" });
1142
+ break;
1143
+ case "heartbeat": {
1144
+ // Heartbeat event received — re-check health proactively
1145
+ pollHealth();
1146
+ break;
1147
+ }
1148
+ case "sync:complete": {
1149
+ lastSyncAt = ev.data?.syncedAt || Date.now();
1150
+ renderSyncAgo();
1151
+ fetchProcesses();
1152
+ fetchWorkItems();
1153
+ break;
1154
+ }
1155
+ }
1156
+ }
1157
+
1158
+ // ---- Stats ----
1159
+ function updateStats(data) {
1160
+ document.getElementById("stat-running").textContent = data.runningWorkItems ?? 0;
1161
+ document.getElementById("stat-queued").textContent = data.queuedWorkItems ?? 0;
1162
+ document.getElementById("stat-completed").textContent = data.completedToday ?? 0;
1163
+ const defaultAdapter = data.adapters?.defaultAdapter ?? "-";
1164
+ document.getElementById("stat-adapter").textContent = defaultAdapter;
1165
+ if (data.uptime != null) {
1166
+ const s = Math.floor(data.uptime / 1000);
1167
+ const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), ss = s % 60;
1168
+ document.getElementById("stat-uptime").textContent =
1169
+ (h ? h + "h " : "") + (m ? m + "m " : "") + ss + "s";
1170
+ }
1171
+ // Turbo mode
1172
+ const turboBtn = document.getElementById("turbo-btn");
1173
+ if (turboBtn) {
1174
+ turboBtn.classList.toggle("turbo-active", !!data.turboMode);
1175
+ if (data.turboMode && data.turboExpiresAt) {
1176
+ const remainMin = Math.max(0, Math.round((data.turboExpiresAt - Date.now()) / 60000));
1177
+ turboBtn.textContent = "TURBO " + remainMin + "m";
1178
+ } else {
1179
+ turboBtn.textContent = "TURBO";
1180
+ }
1181
+ }
1182
+ // Daily pace
1183
+ const paceEl = document.getElementById("stat-pace");
1184
+ if (paceEl && data.dailyPace) {
1185
+ const cap = data.turboMode ? 20 : 6;
1186
+ paceEl.textContent = data.dailyPace.completedToday + "/" + cap;
1187
+ paceEl.className = "stat-val" + (data.turboMode ? " amber" : "");
1188
+ }
1189
+ // Execution pause state
1190
+ if (data.executionPaused != null) {
1191
+ setExecutionState(!!data.executionPaused, data.runningWorkItems ?? 0);
1192
+ }
1193
+ }
1194
+
1195
+ // ---- Execution pause control ----
1196
+ function setExecutionState(paused, runningCount) {
1197
+ const btn = document.getElementById("exec-toggle");
1198
+ const lbl = document.getElementById("exec-state");
1199
+ if (!btn || !lbl) return;
1200
+ if (paused) {
1201
+ btn.textContent = "▶ Resume";
1202
+ lbl.textContent = runningCount > 0 ? "⏸ Paused · " + runningCount + " draining" : "⏸ Paused";
1203
+ } else {
1204
+ btn.textContent = "⏸ Pause";
1205
+ lbl.textContent = "▶ Running";
1206
+ }
1207
+ }
1208
+
1209
+ async function toggleExecution() {
1210
+ const btn = document.getElementById("exec-toggle");
1211
+ const lbl = document.getElementById("exec-state");
1212
+ const isPaused = btn && btn.textContent.trim().startsWith("▶");
1213
+ if (btn) btn.disabled = true;
1214
+ try {
1215
+ const endpoint = isPaused ? "/api/execution/resume" : "/api/execution/pause";
1216
+ const res = await fetch(endpoint, { method: "POST" });
1217
+ if (!res.ok) throw new Error("Execution toggle failed: " + res.status);
1218
+ const status = await fetch("/api/execution/status").then(r => r.json());
1219
+ setExecutionState(!!status.paused, status.inFlightCount ?? 0);
1220
+ } catch(e) {
1221
+ addLogLine({ workItemId: "system", stage: "error", line: "Execution toggle failed: " + e.message });
1222
+ } finally {
1223
+ if (btn) btn.disabled = false;
1224
+ }
1225
+ }
1226
+
1227
+ // fetchSvcStatus removed — replaced by pollHealth() + unified #app-status badge
1228
+
1229
+ // ---- Stuck/Failed WorkItems ----
1230
+ async function fetchStuckWorkItems() {
1231
+ try {
1232
+ const res = await fetch("/api/workitems/stuck");
1233
+ const data = await res.json();
1234
+ const list = document.getElementById("stuck-list");
1235
+ const badge = document.getElementById("stuck-badge");
1236
+
1237
+ const totalStuck = data.stuckWorkItems?.length ?? 0;
1238
+ const totalFailed = data.failedWorkItems?.length ?? 0;
1239
+ const total = totalStuck + totalFailed;
1240
+
1241
+ badge.textContent = total;
1242
+ badge.style.color = total > 0 ? "var(--red)" : "var(--dim)";
1243
+
1244
+ if (total === 0) {
1245
+ list.innerHTML = '<div style="color: var(--green-mid); padding: 4px;">✓ All work items healthy</div>';
1246
+ return;
1247
+ }
1248
+
1249
+ let html = '';
1250
+
1251
+ // Stuck work items (In Progress for >7 days)
1252
+ if (totalStuck > 0) {
1253
+ html += '<div style="color: var(--amber); font-weight: bold; margin-bottom: 4px; font-size: 9px; text-transform: uppercase;">⏱ Stuck (' + totalStuck + ')</div>';
1254
+ data.stuckWorkItems.forEach(workItem => {
1255
+ const priorityColor = workItem.priority === 1 ? 'var(--red)' : workItem.priority === 2 ? 'var(--amber)' : 'var(--dim)';
1256
+ html += '<div style="margin-bottom: 6px; padding: 4px; border-left: 2px solid ' + priorityColor + '; background: var(--warnBg);">';
1257
+ html += '<div style="color: var(--white); font-size: 10px; margin-bottom: 2px;">' + workItem.identifier + ': ' + workItem.title.substring(0, 40) + (workItem.title.length > 40 ? '...' : '') + '</div>';
1258
+ html += '<div style="color: var(--amber); font-size: 9px;">' + workItem.reason + '</div>';
1259
+ if (workItem.project?.name) {
1260
+ html += '<div style="color: var(--dim); font-size: 9px; margin-top: 2px;">📁 ' + workItem.project.name + '</div>';
1261
+ }
1262
+ html += '</div>';
1263
+ });
1264
+ }
1265
+
1266
+ // Failed work items (retry, failed, blocked labels)
1267
+ if (totalFailed > 0) {
1268
+ if (totalStuck > 0) html += '<div style="height: 8px;"></div>';
1269
+ html += '<div style="color: var(--red); font-weight: bold; margin-bottom: 4px; font-size: 9px; text-transform: uppercase;">✖ Failed (' + totalFailed + ')</div>';
1270
+ data.failedWorkItems.forEach(workItem => {
1271
+ const priorityColor = workItem.priority === 1 ? 'var(--red)' : workItem.priority === 2 ? 'var(--amber)' : 'var(--dim)';
1272
+ html += '<div style="margin-bottom: 6px; padding: 4px; border-left: 2px solid ' + priorityColor + '; background: var(--dangerBg);">';
1273
+ html += '<div style="color: var(--white); font-size: 10px; margin-bottom: 2px;">' + workItem.identifier + ': ' + workItem.title.substring(0, 40) + (workItem.title.length > 40 ? '...' : '') + '</div>';
1274
+ html += '<div style="color: var(--red); font-size: 9px;">' + workItem.reason + '</div>';
1275
+ if (workItem.project?.name) {
1276
+ html += '<div style="color: var(--dim); font-size: 9px; margin-top: 2px;">📁 ' + workItem.project.name + '</div>';
1277
+ }
1278
+ html += '</div>';
1279
+ });
1280
+ }
1281
+
1282
+ list.innerHTML = html;
1283
+ } catch (err) {
1284
+ console.error("Failed to fetch stuck work items:", err);
1285
+ document.getElementById("stuck-list").innerHTML = '<div style="color: var(--red);">Error loading</div>';
1286
+ }
1287
+ }
1288
+
1289
+ // svcAction and triggerHeartbeat removed — STOP/RESTART buttons and HEARTBEAT
1290
+ // button removed from unified-process model; use triggerSync() for a manual refresh.
1291
+
1292
+ // ---- Sync trigger ----
1293
+ async function triggerSync() {
1294
+ const btn = document.getElementById("sync-btn");
1295
+ const origLabel = "⟳ SYNC";
1296
+ if (btn) { btn.disabled = true; btn.textContent = "⟳ SYNCING"; }
1297
+ try {
1298
+ const res = await fetch("/api/workitems/sync", { method: "POST" });
1299
+ const data = await res.json().catch(() => ({}));
1300
+ if (data && data.ok) {
1301
+ lastSyncAt = data.syncedAt || Date.now();
1302
+ renderSyncAgo();
1303
+ fetchProcesses();
1304
+ fetchWorkItems();
1305
+ }
1306
+ } catch(e) {
1307
+ addLogLine({ workItemId: "system", stage: "error", line: "Sync failed: " + e.message });
1308
+ } finally {
1309
+ if (btn) { btn.disabled = false; btn.textContent = origLabel; }
1310
+ }
1311
+ }
1312
+
1313
+ function renderSyncAgo() {
1314
+ const el = document.getElementById("sync-ago");
1315
+ if (!el) return;
1316
+ if (lastSyncAt === null) { el.textContent = ""; return; }
1317
+ const secs = Math.round((Date.now() - lastSyncAt) / 1000);
1318
+ if (secs > 90) {
1319
+ el.textContent = "synced " + Math.round(secs / 60) + "m ago";
1320
+ } else {
1321
+ el.textContent = "synced " + secs + "s ago";
1322
+ }
1323
+ }
1324
+
1325
+ async function toggleTurbo() {
1326
+ const btn = document.getElementById("turbo-btn");
1327
+ const isActive = btn.classList.contains("turbo-active");
1328
+ const newState = !isActive;
1329
+ if (newState && !confirm("Enable TURBO mode? (5min heartbeat, 20 daily cap, auto-expires in 4h)")) return;
1330
+ btn.disabled = true;
1331
+ try {
1332
+ const res = await fetch("/api/turbo", {
1333
+ method: "POST",
1334
+ headers: { "Content-Type": "application/json" },
1335
+ body: JSON.stringify({ enabled: newState })
1336
+ });
1337
+ if (!res.ok) throw new Error("Failed");
1338
+ addLogLine({ workItemId: "system", stage: "turbo", line: newState ? "TURBO MODE ON" : "TURBO MODE OFF" });
1339
+ const stats = await fetch("/api/stats").then(r => r.json());
1340
+ updateStats(stats);
1341
+ } catch (e) {
1342
+ addLogLine({ workItemId: "system", stage: "error", line: "Turbo toggle failed: " + e.message });
1343
+ }
1344
+ btn.disabled = false;
1345
+ }
1346
+
1347
+ // ---- Repository scope selector ----
1348
+ let currentRepoScope = "All";
1349
+ function onRepoScopeChange(repo) {
1350
+ currentRepoScope = repo;
1351
+ renderWorkItems();
1352
+ }
1353
+
1354
+ // ---- Stuck/Failed Sidecar toggle ----
1355
+ let stuckSidecarOpen = false;
1356
+ function toggleStuckSidecar() {
1357
+ stuckSidecarOpen = !stuckSidecarOpen;
1358
+ const body = document.getElementById("stuck-sidecar-body");
1359
+ const arrow = document.getElementById("stuck-sidecar-arrow");
1360
+ if (body) body.className = "sidecar-body " + (stuckSidecarOpen ? "expanded" : "collapsed");
1361
+ if (arrow) arrow.textContent = stuckSidecarOpen ? "▼" : "▶";
1362
+ if (stuckSidecarOpen) fetchStuckWorkItems();
1363
+ }
1364
+
1365
+ // ---- Work-item list (canonical endpoint with layered-state badges) ----
1366
+ // Consumes GET /api/factory/workitems and renders per-item layered-state badges.
1367
+ let workItemsCache = [];
1368
+
1369
+ const BADGE_TONE_STYLE = {
1370
+ neutral: "border-color:var(--dim);color:var(--dim)",
1371
+ active: "border-color:var(--green-lo);color:var(--green-mid);background:var(--green-dim)",
1372
+ success: "border-color:var(--green-mid);color:var(--green)",
1373
+ warn: "border-color:var(--warnBorder);color:var(--amber)",
1374
+ danger: "border-color:var(--dangerBorder);color:var(--red)",
1375
+ dim: "border-color:var(--border2);color:var(--border2);opacity:0.7",
1376
+ };
1377
+
1378
+ function badgeHtml(label, tone) {
1379
+ const style = BADGE_TONE_STYLE[tone] || BADGE_TONE_STYLE.neutral;
1380
+ return '<span class="badge" style="' + style + '">' + escapeHtml(label) + '</span>';
1381
+ }
1382
+
1383
+ function presentationFor(payload) {
1384
+ // Inline JS version of src/core/dashboardContract.ts presentationFor()
1385
+ const ls = payload.layeredState;
1386
+
1387
+ // business
1388
+ const bizState = ls.business && typeof ls.business === "object" ? (ls.business.state || String(ls.business)) : String(ls.business || "");
1389
+ const bizTone = bizState === "In Progress" ? "active" : bizState === "Done" ? "success" : bizState === "Removed" ? "dim" : "neutral";
1390
+
1391
+ // execution
1392
+ const phase = ls.execution ? ls.execution.currentPhase : "idle";
1393
+ const execTone = ["implementing","planning","reviewing","revising"].includes(phase) ? "active"
1394
+ : phase === "done" ? "success"
1395
+ : phase === "failed" || phase === "halted" ? "danger"
1396
+ : "dim";
1397
+
1398
+ // review
1399
+ const review = ls.review || "none";
1400
+ const reviewTone = review === "approved" ? "success" : review === "pending" ? "warn" : (review === "rejected" || review === "revision") ? "danger" : "dim";
1401
+
1402
+ // prSidecar
1403
+ const prStatus = ls.prSidecar ? (ls.prSidecar.status || "none") : "none";
1404
+ const prTone = prStatus === "merged" ? "success" : prStatus === "opened" ? "active" : prStatus === "pending" ? "warn" : (prStatus === "failed" || prStatus === "closed") ? "danger" : "dim";
1405
+
1406
+ // resolution
1407
+ const res = ls.resolution || "unresolved";
1408
+ const resTone = res === "succeeded" ? "success" : res === "failed" ? "danger" : res === "halted" ? "warn" : (res === "cancelled" || res === "duplicate") ? "dim" : "neutral";
1409
+
1410
+ return {
1411
+ bizBadge: badgeHtml(bizState, bizTone),
1412
+ execBadge: badgeHtml(phase, execTone),
1413
+ reviewBadge: badgeHtml(review, reviewTone),
1414
+ prBadge: badgeHtml(prStatus, prTone),
1415
+ resBadge: badgeHtml(res, resTone),
1416
+ };
1417
+ }
1418
+
1419
+ async function fetchWorkItems() {
1420
+ try {
1421
+ const res = await fetch("/api/factory/workitems");
1422
+ if (res.ok) {
1423
+ const payload = await res.json();
1424
+ workItemsCache = Array.isArray(payload?.data) ? payload.data : [];
1425
+ renderWorkItems();
1426
+ const sumEl = document.getElementById("work-item-summary");
1427
+ if (sumEl) sumEl.textContent = workItemsCache.length + " items";
1428
+ }
1429
+ } catch {
1430
+ // silently ignore network errors
1431
+ }
1432
+ }
1433
+
1434
+ function normalizeLaneRecord(raw) {
1435
+ if (!raw || typeof raw !== "object") return null;
1436
+ const wi = raw.workItem || {};
1437
+ const location = raw.repo || raw.repository || raw.repoPath || raw.worktree || raw.projectPath || wi.repo || wi.repository || wi.repositoryPath || "";
1438
+ const branch = raw.branch || raw.gitBranch || raw.worktreeBranch || wi.branch || wi.ref || "";
1439
+ const sessionId = raw.sessionId || (raw.session && raw.session.id) || raw.terminalSessionId || raw.ptySessionId;
1440
+ const pid = raw.pid || (raw.process && raw.process.pid) || raw.processId || "";
1441
+ const executionStatus = raw.executionStatus || raw.execution?.status || raw.status || raw.state || "idle";
1442
+ const assignmentStatus = raw.assignmentStatus || raw.assignment?.status || raw.assignmentState || raw.state || "unknown";
1443
+ const operatorLane = raw.operatorLane || raw.lane || raw.assignmentLane || "unassigned";
1444
+ const workItemId = raw.workItemId || wi.id || wi.identifier || wi.azdoId || "";
1445
+ const workItemTitle = raw.workItemTitle || raw.title || wi.title || "";
1446
+ const dispatcher = raw.dispatcher || raw.assignment?.dispatcher || "";
1447
+ const paseoAgentId = raw.paseoAgentId || raw.assignment?.paseoAgentId || "";
1448
+ const paseoStatus = raw.paseoStatus || raw.assignment?.paseoStatus || "";
1449
+ return {
1450
+ operatorLane,
1451
+ workItemId,
1452
+ workItemTitle,
1453
+ host: raw.host || raw.hostname || "",
1454
+ location: raw.paseoCwd || location,
1455
+ branch: raw.paseoWorktree || branch,
1456
+ sessionId,
1457
+ pid,
1458
+ executionStatus,
1459
+ assignmentStatus,
1460
+ dispatcher,
1461
+ paseoAgentId,
1462
+ paseoProvider: raw.paseoProvider || raw.assignment?.paseoProvider || "",
1463
+ paseoMode: raw.paseoMode || raw.assignment?.paseoMode || "",
1464
+ paseoStatus,
1465
+ paseoLastCheckedAt: raw.paseoLastCheckedAt || raw.assignment?.paseoLastCheckedAt || "",
1466
+ paseoSummary: raw.paseoSummary || raw.assignment?.paseoSummary || "",
1467
+ isTerminalSession:
1468
+ String(raw.sessionType || raw.kind || raw.type || raw.assignmentType || raw.connectorType || raw.connectionType || "").toLowerCase() === "terminal" ||
1469
+ /terminal/.test(String(raw.workItemTitle || raw.title || "").toLowerCase()) ||
1470
+ String(raw.isTerminal || "").toLowerCase() === "true",
1471
+ };
1472
+ }
1473
+
1474
+ function normalizeTerminalSessionRecord(raw) {
1475
+ if (!raw || typeof raw !== "object") return null;
1476
+ return {
1477
+ operatorLane: "terminal",
1478
+ workItemId: "",
1479
+ workItemTitle: raw.title || "embedded operator terminal",
1480
+ host: "",
1481
+ location: raw.cwd || "",
1482
+ branch: "",
1483
+ sessionId: raw.sessionId || "",
1484
+ pid: raw.pid || "",
1485
+ executionStatus: raw.exited ? "exited" : "connected",
1486
+ assignmentStatus: raw.exited ? "exited" : "connected",
1487
+ isTerminalSession: true,
1488
+ };
1489
+ }
1490
+
1491
+ function renderLaneSummary(payload) {
1492
+ const list = document.getElementById("lane-list");
1493
+ const summary = document.getElementById("lane-summary");
1494
+ const lanes = Array.isArray(payload?.lanes) ? payload.lanes : [];
1495
+ const terminalSessions = Array.isArray(payload?.terminalSessions) ? payload.terminalSessions : [];
1496
+ const totalRows = lanes.length + terminalSessions.length;
1497
+
1498
+ if (summary) {
1499
+ if (payload?.summary && typeof payload.summary === "object") {
1500
+ const laneCount = payload.summary.totalLanes ?? lanes.length;
1501
+ const terminalCount = payload.summary.activeTerminalSessions ?? terminalSessions.length;
1502
+ summary.textContent = String(laneCount) + "L/" + String(terminalCount) + "T";
1503
+ } else {
1504
+ summary.textContent = String(totalRows) + " active";
1505
+ }
1506
+ }
1507
+
1508
+ if (!totalRows) {
1509
+ if (list) {
1510
+ list.innerHTML = '<div class="empty">no active operator lanes or terminal sessions</div>';
1511
+ }
1512
+ return;
1513
+ }
1514
+
1515
+ if (!list) return;
1516
+ const combinedRows = lanes.concat(
1517
+ terminalSessions
1518
+ .map(normalizeTerminalSessionRecord)
1519
+ .filter(Boolean),
1520
+ );
1521
+
1522
+ const lines = combinedRows.map(function(l) {
1523
+ const workItem = l.workItemId && l.workItemTitle
1524
+ ? escapeHtml(l.workItemId) + " · " + escapeHtml(l.workItemTitle)
1525
+ : l.workItemId
1526
+ ? escapeHtml(l.workItemId)
1527
+ : l.workItemTitle
1528
+ ? escapeHtml(l.workItemTitle)
1529
+ : "unassigned";
1530
+ const loc = [l.host ? "host:" + l.host : "", l.location ? "repo:" + l.location : "", l.branch ? "branch:" + l.branch : ""].filter(Boolean).join(" · ");
1531
+ const statusBits = [
1532
+ l.executionStatus ? "exec:" + l.executionStatus : "",
1533
+ l.assignmentStatus ? "assign:" + l.assignmentStatus : "",
1534
+ l.dispatcher ? "dispatch:" + l.dispatcher : "",
1535
+ l.paseoStatus ? "paseo:" + l.paseoStatus : "",
1536
+ ].filter(Boolean).join(" · ");
1537
+ const sessionBits = [
1538
+ l.paseoAgentId ? "agent:" + l.paseoAgentId : "",
1539
+ l.paseoProvider ? "provider:" + l.paseoProvider : "",
1540
+ l.paseoMode ? "mode:" + l.paseoMode : "",
1541
+ l.sessionId ? "session:" + l.sessionId : "",
1542
+ l.pid ? "pid:" + l.pid : "",
1543
+ ].filter(Boolean).join(" · ");
1544
+ const laneType = l.isTerminalSession ? "operator/terminal session" : "operator session";
1545
+
1546
+ return (
1547
+ '<div class="lane-row">' +
1548
+ '<div><span style="color:var(--green);font-size:10px;letter-spacing:0.08em;text-transform:uppercase;">' + escapeHtml(l.operatorLane) + '</span>' +
1549
+ ' <span class="lane-pill">' + laneType + '</span></div>' +
1550
+ '<div style="color:var(--white);" title="' + escapeAttr(workItem) + '">' + workItem + '</div>' +
1551
+ (loc ? '<div class="lane-row-kv">' + loc + '</div>' : '') +
1552
+ (statusBits || sessionBits ? '<div class="lane-row-kv">' + (statusBits ? statusBits : '') + (statusBits && sessionBits ? ' | ' : '') + sessionBits + '</div>' : '') +
1553
+ '</div>'
1554
+ );
1555
+ }).join("");
1556
+
1557
+ list.innerHTML = lines;
1558
+ }
1559
+
1560
+ async function fetchLanes() {
1561
+ const list = document.getElementById("lane-list");
1562
+ const summary = document.getElementById("lane-summary");
1563
+ if (summary) summary.textContent = "...";
1564
+ if (list) list.innerHTML = '<div class="empty">loading lane truth...</div>';
1565
+
1566
+ try {
1567
+ const res = await fetch("/api/lanes");
1568
+ if (!res.ok) throw new Error("HTTP " + res.status);
1569
+ const payload = await res.json();
1570
+ const lanePayload = Array.isArray(payload)
1571
+ ? { lanes: payload, terminalSessions: [], summary: null }
1572
+ : payload;
1573
+ const rows = Array.isArray(lanePayload?.lanes) ? lanePayload.lanes : [];
1574
+ const normalizedLanes = [];
1575
+ for (let i = 0; i < rows.length; i++) {
1576
+ const n = normalizeLaneRecord(rows[i]);
1577
+ if (n) laneRows.push(n);
1578
+ if (n) normalizedLanes.push(n);
1579
+ }
1580
+ laneRows = normalizedLanes;
1581
+ renderLaneSummary({
1582
+ lanes: normalizedLanes,
1583
+ terminalSessions: Array.isArray(lanePayload?.terminalSessions) ? lanePayload.terminalSessions : [],
1584
+ summary: lanePayload?.summary || null,
1585
+ });
1586
+ } catch (err) {
1587
+ if (summary) summary.textContent = "error";
1588
+ if (list) list.innerHTML = '<div class="empty">failed to load operator lanes: ' + escapeHtml(err.message || "unknown") + '</div>';
1589
+ }
1590
+ }
1591
+
1592
+ function renderWorkItems() {
1593
+ const el = document.getElementById("work-item-list");
1594
+ const cntEl = document.getElementById("work-item-count");
1595
+ if (!el) return;
1596
+
1597
+ let items = workItemsCache;
1598
+ if (currentRepoScope !== "All") {
1599
+ // currentRepoScope is the project PATH; derive the basename for area matching.
1600
+ const scopeName = currentRepoScope.split("/").pop() || currentRepoScope;
1601
+ // Filter by repo name matching area or title prefix (best-effort without full area→repo map)
1602
+ items = items.filter(w => {
1603
+ const area = typeof w.area === "string" ? w.area : "";
1604
+ return area.toLowerCase().includes(scopeName.replace("enact-","").toLowerCase());
1605
+ });
1606
+ }
1607
+
1608
+ if (cntEl) cntEl.textContent = items.length + "/" + workItemsCache.length;
1609
+
1610
+ if (!items.length) {
1611
+ const scopeLabel = currentRepoScope === "All" ? "" : (currentRepoScope.split("/").pop() || currentRepoScope);
1612
+ el.innerHTML = '<div class="empty">' + (currentRepoScope !== "All" ? "no items for " + escapeHtml(scopeLabel) : "no work items") + '</div>';
1613
+ return;
1614
+ }
1615
+
1616
+ el.innerHTML = items.map(function(w) {
1617
+ const p = presentationFor(w);
1618
+ const kindColor = w.kind === "Epic" ? "var(--amber)" : w.kind === "Feature" ? "var(--cyan)" : "var(--green)";
1619
+ return (
1620
+ '<div style="padding:4px 6px;border-bottom:1px solid var(--border2);font-size:11px">' +
1621
+ '<div style="display:flex;align-items:center;gap:4px">' +
1622
+ '<span style="font-size:8px;color:' + kindColor + ';border:1px solid;padding:0 3px;border-radius:2px;flex-shrink:0">' + escapeHtml(w.kind) + '</span>' +
1623
+ '<span style="color:var(--white);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1" title="' + escapeAttr(w.title) + '">' + escapeHtml(w.title) + '</span>' +
1624
+ '</div>' +
1625
+ '<div class="badge-row">' + p.bizBadge + p.execBadge + p.reviewBadge + p.prBadge + p.resBadge + '</div>' +
1626
+ '</div>'
1627
+ );
1628
+ }).join("");
1629
+ }
1630
+
1631
+ // ---- Restart stuck work items ----
1632
+ async function restartStuckWorkItems() {
1633
+ if (!confirm("Move all stuck/failed workitems to Todo?")) return;
1634
+ const btn = document.getElementById("restart-stuck-btn");
1635
+ btn.disabled = true;
1636
+ btn.textContent = "⟳ PROCESSING...";
1637
+
1638
+ try {
1639
+ const res = await fetch("/api/workitems/stuck");
1640
+ const data = await res.json();
1641
+ const allWorkItems = [...data.stuckWorkItems, ...data.failedWorkItems];
1642
+
1643
+ let success = 0;
1644
+ let failed = 0;
1645
+
1646
+ for (const workItem of allWorkItems) {
1647
+ try {
1648
+ const moveRes = await fetch("/api/workitems/move-to-todo", {
1649
+ method: "POST",
1650
+ headers: { "Content-Type": "application/json" },
1651
+ body: JSON.stringify({ workItemId: workItem.id })
1652
+ });
1653
+
1654
+ if (moveRes.ok) {
1655
+ success++;
1656
+ addLogLine({ workItemId: "system", stage: "stuck", line: "Moved " + workItem.identifier + " to Todo" });
1657
+ } else {
1658
+ failed++;
1659
+ }
1660
+ } catch (e) {
1661
+ failed++;
1662
+ }
1663
+ }
1664
+
1665
+ addLogLine({ workItemId: "system", stage: "stuck", line: "Restart complete: " + success + " moved, " + failed + " failed" });
1666
+ setTimeout(fetchStuckWorkItems, 1000);
1667
+ } catch (e) {
1668
+ addLogLine({ workItemId: "system", stage: "error", line: "Failed to restart stuck work items: " + e.message });
1669
+ }
1670
+
1671
+ btn.disabled = false;
1672
+ btn.textContent = "↻ RESTART ALL";
1673
+ }
1674
+
1675
+ // ---- Project workItem updates ----
1676
+ function updateProjectWorkItem(projectPath, workItemId, title, priority, status) {
1677
+ const p = projects.find(x => x.path === projectPath);
1678
+ if (!p) return;
1679
+
1680
+ // Get workItemIdentifier from workItemTitleMap
1681
+ const workItemInfo = workItemTitleMap.get(workItemId);
1682
+ const workItemIdentifier = workItemInfo?.workItemIdentifier;
1683
+
1684
+ if (status === "running") {
1685
+ p.queued = p.queued.filter(t => t.id !== workItemId);
1686
+ if (!p.running.find(t => t.id === workItemId)) {
1687
+ p.running.push({ id: workItemId, title, priority, workItemIdentifier });
1688
+ }
1689
+ } else {
1690
+ if (!p.queued.find(t => t.id === workItemId)) {
1691
+ p.queued.push({ id: workItemId, title, priority, workItemIdentifier });
1692
+ }
1693
+ }
1694
+ renderProjects();
1695
+ }
1696
+ function removeProjectWorkItem(projectPath, workItemId) {
1697
+ const p = projects.find(x => x.path === projectPath);
1698
+ if (!p) return;
1699
+ p.running = p.running.filter(t => t.id !== workItemId);
1700
+ p.queued = p.queued.filter(t => t.id !== workItemId);
1701
+ renderProjects();
1702
+ }
1703
+
1704
+ // ---- Toggle project ----
1705
+ async function toggleProject(projectPath, enabled) {
1706
+ const p = projects.find(x => x.path === projectPath);
1707
+ if (p) p.enabled = enabled;
1708
+ renderProjects();
1709
+ try {
1710
+ await fetch("/api/projects/toggle", {
1711
+ method: "POST",
1712
+ headers: { "Content-Type": "application/json" },
1713
+ body: JSON.stringify({ projectPath, enabled }),
1714
+ });
1715
+ } catch(e) {
1716
+ if (p) p.enabled = !enabled;
1717
+ renderProjects();
1718
+ }
1719
+ }
1720
+
1721
+ // ---- Sync Repo Scope Picker Dropdown ----
1722
+ // Keeps the REPO [All v] dropdown in the header in sync with the loaded project list.
1723
+ // Called after projects are fetched or updated so the dropdown always reflects
1724
+ // the same repository set shown in the REPOSITORIES panel.
1725
+ // Options are keyed by project PATH (canonical, unique) so two repos sharing a
1726
+ // directory basename don't collide; the visible label uses name || basename.
1727
+ function syncRepoScopeDropdown() {
1728
+ const sel = document.getElementById("repo-scope-select");
1729
+ if (!sel) return;
1730
+ // Preserve the user's current selection so a refresh doesn't reset to "All".
1731
+ const previousValue = sel.value;
1732
+ // Canonical set of present project paths (keyed identity).
1733
+ const presentPaths = new Set(projects.map(function(p) { return p.path; }).filter(Boolean));
1734
+ // Existing non-All option values (paths) already in the dropdown.
1735
+ const existing = new Set(
1736
+ Array.from(sel.querySelectorAll("option"))
1737
+ .map(function(o) { return o.value; })
1738
+ .filter(function(v) { return v && v !== "All"; })
1739
+ );
1740
+ // Add any project not yet in the dropdown, keyed by path.
1741
+ for (const p of projects) {
1742
+ const path = p.path;
1743
+ if (!path || existing.has(path)) continue;
1744
+ const label = p.name || path.split("/").pop() || path;
1745
+ const opt = document.createElement("option");
1746
+ opt.value = path;
1747
+ opt.textContent = label;
1748
+ sel.appendChild(opt);
1749
+ existing.add(path);
1750
+ }
1751
+ // Remove options for projects that have been unpinned (keep All + still-present paths).
1752
+ Array.from(sel.querySelectorAll("option")).forEach(function(opt) {
1753
+ if (opt.value && opt.value !== "All" && !presentPaths.has(opt.value)) {
1754
+ sel.removeChild(opt);
1755
+ }
1756
+ });
1757
+ // Restore previous selection if that path still exists; otherwise leave as-is
1758
+ // (a removed selection naturally falls back to the first remaining option).
1759
+ if (previousValue === "All" || presentPaths.has(previousValue)) {
1760
+ sel.value = previousValue;
1761
+ }
1762
+ }
1763
+
1764
+ // ---- Render Projects ----
1765
+ function renderProjects() {
1766
+ const el = document.getElementById("project-list");
1767
+ const sumEl = document.getElementById("proj-summary");
1768
+ if (!projects.length) { el.innerHTML = "<div class=\\"empty\\">no repositories</div>"; return; }
1769
+ const on = projects.filter(p => p.enabled).length;
1770
+ if (sumEl) sumEl.textContent = on + "/" + projects.length;
1771
+ syncRepoScopeDropdown();
1772
+
1773
+ el.innerHTML = projects.map(p => {
1774
+ // Use path as key, fall back to __n:name for unmapped projects
1775
+ const key = p.path || ("__n:" + p.name);
1776
+ const expanded = expandedProjects.has(key);
1777
+ const checked = p.enabled ? "checked" : "";
1778
+ const dCls = p.enabled ? "" : " disabled";
1779
+ const eCls = expanded ? " expanded" : "";
1780
+ // PRs always visible (not gated by expand)
1781
+ let prsHtml = "";
1782
+ if (p.prs && p.prs.length) {
1783
+ prsHtml = "<div class=\\"proj-workItems\\">" +
1784
+ "<div class=\\"workItem-sec-label\\">open PRs (" + p.prs.length + ")</div>" +
1785
+ p.prs.map(function(pr) {
1786
+ return "<div class=\\"pr-row\\">" +
1787
+ "<span class=\\"pr-num\\">#" + pr.number + "</span>" +
1788
+ "<span class=\\"pr-branch\\" title=\\"" + escapeAttr(pr.branch) + "\\">" + escapeHtml(pr.branch) + "</span>" +
1789
+ "<span class=\\"pr-title\\" title=\\"" + escapeAttr(pr.title) + "\\">" + escapeHtml(pr.title) + "</span>" +
1790
+ "<span class=\\"pr-age\\">" + fmtAge(pr.updatedAt) + "</span>" +
1791
+ "</div>";
1792
+ }).join("") +
1793
+ "</div>";
1794
+ }
1795
+ let workItemsHtml = "";
1796
+ if (expanded) {
1797
+ const secs = [];
1798
+ if (p.running.length) secs.push(
1799
+ "<div class=\\"workItem-sec-label\\">running</div>" +
1800
+ p.running.map(t => workItemRow(t, "idot-run")).join("")
1801
+ );
1802
+ if (p.queued.length) secs.push(
1803
+ "<div class=\\"workItem-sec-label\\">queued</div>" +
1804
+ p.queued.map(t => workItemRow(t, "idot-que")).join("")
1805
+ );
1806
+ if (p.pending.length) {
1807
+ var stateOrder = ["In Review", "In Progress", "Todo", "Backlog"];
1808
+ var byState = {};
1809
+ for (var ti = 0; ti < p.pending.length; ti++) {
1810
+ var st = p.pending[ti].azdoState || "Todo";
1811
+ if (!byState[st]) byState[st] = [];
1812
+ byState[st].push(p.pending[ti]);
1813
+ }
1814
+ for (var si = 0; si < stateOrder.length; si++) {
1815
+ var sn = stateOrder[si];
1816
+ if (!byState[sn] || !byState[sn].length) continue;
1817
+ secs.push(
1818
+ "<div class=\\"workItem-sec-label\\">" + sn.toLowerCase() + " (" + byState[sn].length + ")</div>" +
1819
+ byState[sn].map(t => workItemRow(t, "idot-pnd")).join("")
1820
+ );
1821
+ }
1822
+ var otherKeys = Object.keys(byState);
1823
+ for (var oi = 0; oi < otherKeys.length; oi++) {
1824
+ if (stateOrder.indexOf(otherKeys[oi]) === -1) {
1825
+ secs.push(
1826
+ "<div class=\\"workItem-sec-label\\">" + otherKeys[oi].toLowerCase() + " (" + byState[otherKeys[oi]].length + ")</div>" +
1827
+ byState[otherKeys[oi]].map(t => workItemRow(t, "idot-pnd")).join("")
1828
+ );
1829
+ }
1830
+ }
1831
+ }
1832
+ if (!secs.length) secs.push("<div class=\\"empty\\" style=\\"padding:4px\\">no work items</div>");
1833
+ // Knowledge graph health info (if cached)
1834
+ var kgData = knowledgeCache[p.name] || knowledgeCache[p.path];
1835
+ if (kgData && kgData.summary) {
1836
+ var s = kgData.summary;
1837
+ secs.push(
1838
+ "<div class=\\"workItem-sec-label\\">code health</div>" +
1839
+ "<div style=\\"padding:2px 8px;font-size:10px;color:var(--mutedStrong)\\">" +
1840
+ "modules:" + s.totalModules + " tests:" + s.totalTestFiles +
1841
+ " untested:" + s.untestedModules.length +
1842
+ " churn:" + (s.avgChurnScore || 0).toFixed(2) +
1843
+ (s.hotModules.length ? " hot:" + s.hotModules.slice(0,3).map(function(m){return m.split("/").pop()}).join(",") : "") +
1844
+ "</div>"
1845
+ );
1846
+ }
1847
+ workItemsHtml = "<div class=\\"proj-workItems\\">" + secs.join("") + "</div>";
1848
+ }
1849
+
1850
+ return (
1851
+ "<div class=\\"proj-card" + dCls + eCls + "\\" data-key=\\"" + escapeAttr(key) + "\\">" +
1852
+ "<div class=\\"proj-hdr\\" data-key=\\"" + escapeAttr(key) + "\\" onclick=\\"handleToggleExpand(this)\\">" +
1853
+ "<span class=\\"proj-arrow\\"></span>" +
1854
+ "<div class=\\"proj-info\\">" +
1855
+ "<div class=\\"proj-name\\">" + escapeHtml(p.name) + "</div>" +
1856
+ "<div class=\\"proj-path\\">" + escapeHtml(p.path) + "</div>" +
1857
+ (p.git ? "<div class=\\"git-info\\">" +
1858
+ "\\u2387 <span class=\\"git-branch-name\\">" + escapeHtml(p.git.branch) + "</span>" +
1859
+ (p.git.hasChanges ? " <span class=\\"git-dirty\\">\\u25CF " + p.git.uncommittedFiles + "</span>" : "") +
1860
+ ((p.git.ahead || p.git.behind) ? " <span class=\\"git-sync\\">" +
1861
+ (p.git.ahead ? "\\u2191" + p.git.ahead : "") +
1862
+ (p.git.behind ? " \\u2193" + p.git.behind : "") +
1863
+ "</span>" : "") +
1864
+ "</div>" : "") +
1865
+ "</div>" +
1866
+ "<div class=\\"proj-counts\\">" +
1867
+ (p.running.length ? "<span class=\\"cnt cnt-run\\">" + p.running.length + "r</span>" : "") +
1868
+ (p.queued.length ? "<span class=\\"cnt cnt-que\\">" + p.queued.length + "q</span>" : "") +
1869
+ (p.pending.length ? "<span class=\\"cnt cnt-pnd\\">" + p.pending.length + "p</span>" : "") +
1870
+ "</div>" +
1871
+ "<div class=\\"proj-toggle\\" onclick=\\"event.stopPropagation()\\" style=\\"display:flex;align-items:center;gap:4px\\">" +
1872
+ "<button class=\\"btn\\" style=\\"font-size:8px;padding:1px 4px;opacity:.5\\" data-path=\\"" + escapeAttr(p.path) + "\\" onclick=\\"handleUnpin(this)\\" title=\\"Remove repository from dashboard panel\\" aria-label=\\"Unpin repository " + escapeAttr(p.name) + "\\">✕</button>" +
1873
+ "<label class=\\"toggle\\" title=\\"" + (p.enabled ? "Disable" : "Enable") + " repository for autonomous agent scheduling\\" aria-label=\\"" + (p.enabled ? "Disable" : "Enable") + " agent scheduling for " + escapeAttr(p.name) + "\\">" +
1874
+ "<input type=\\"checkbox\\" " + checked + " data-path=\\"" + escapeAttr(p.path) + "\\" onchange=\\"handleToggleProject(this)\\">" +
1875
+ "<span class=\\"slider\\"></span>" +
1876
+ "</label>" +
1877
+ "</div>" +
1878
+ "</div>" +
1879
+ prsHtml +
1880
+ workItemsHtml +
1881
+ "</div>"
1882
+ );
1883
+ }).join("");
1884
+ }
1885
+
1886
+ function workItemRow(t, dotClass) {
1887
+ const prio = t.priority || 3;
1888
+ const extraCls = t.azdoState === "Backlog" ? " workItem-backlog" : "";
1889
+ const retryBadge = (t.retryRemainingMs && t.retryRemainingMs > 0)
1890
+ ? "<span class=\\"workItem-retry\\" title=\\"Retry scheduled\\">retry " + escapeHtml(fmtDur(t.retryRemainingMs)) + "</span>"
1891
+ : "";
1892
+ const blockedBadge = (t.executionStatus === "blocked")
1893
+ ? "<span class=\\"workItem-blocked\\" title=\\"" + escapeAttr(t.blockedReason || "Blocked") + "\\">blocked</span>"
1894
+ : "";
1895
+ const moveBtn = t.azdoState === "Backlog" && t.azdoId
1896
+ ? "<button class=\\"move-to-todo-btn\\" data-work-item-id=\\"" + escapeAttr(t.azdoId) + "\\" onclick=\\"handleMoveToTodo(this)\\">→ Todo</button>"
1897
+ : "";
1898
+ return (
1899
+ "<div class=\\"workItem-row" + extraCls + "\\">" +
1900
+ "<span class=\\"idot " + dotClass + "\\"></span>" +
1901
+ "<span class=\\"prio prio-" + Math.min(4, prio) + "\\"></span>" +
1902
+ (t.workItemIdentifier ? "<span class=\\"workItem-id\\">" + escapeHtml(t.workItemIdentifier) + "</span>" : "") +
1903
+ "<span class=\\"workItem-title\\" title=\\"" + escapeAttr(t.title) + "\\">" + escapeHtml(t.title) + "</span>" +
1904
+ retryBadge +
1905
+ blockedBadge +
1906
+ moveBtn +
1907
+ "</div>"
1908
+ );
1909
+ }
1910
+
1911
+ function toggleExpand(key) {
1912
+ if (expandedProjects.has(key)) expandedProjects.delete(key);
1913
+ else expandedProjects.add(key);
1914
+ renderProjects();
1915
+ }
1916
+ function handleToggleExpand(el) {
1917
+ const key = el.getAttribute("data-key");
1918
+ if (key) toggleExpand(key);
1919
+ }
1920
+ function handleToggleProject(el) {
1921
+ const path = el.getAttribute("data-path");
1922
+ if (path) toggleProject(path, el.checked);
1923
+ }
1924
+ async function handleUnpin(el) {
1925
+ const path = el.getAttribute("data-path");
1926
+ if (!path) return;
1927
+ el.disabled = true;
1928
+ try {
1929
+ await fetch("/api/projects/unpin", {
1930
+ method: "POST",
1931
+ headers: { "Content-Type": "application/json" },
1932
+ body: JSON.stringify({ projectPath: path }),
1933
+ });
1934
+ const res = await fetch("/api/projects");
1935
+ projects = await res.json();
1936
+ renderProjects();
1937
+ // Update picker state so re-adding works correctly
1938
+ const item = allLocalProjects.find(function(p) { return p.path === path; });
1939
+ if (item) item.pinned = false;
1940
+ } catch(e) { el.disabled = false; }
1941
+ }
1942
+ async function handleMoveToTodo(el) {
1943
+ const workItemId = el.getAttribute("data-work-item-id");
1944
+ if (!workItemId) return;
1945
+
1946
+ const originalText = el.textContent;
1947
+ el.disabled = true;
1948
+ el.textContent = "Moving...";
1949
+
1950
+ try {
1951
+ const response = await fetch("/api/workitems/move-to-todo", {
1952
+ method: "POST",
1953
+ headers: { "Content-Type": "application/json" },
1954
+ body: JSON.stringify({ workItemId }),
1955
+ });
1956
+
1957
+ if (!response.ok) {
1958
+ throw new Error("Failed to move work item");
1959
+ }
1960
+
1961
+ // Refresh projects to show updated state
1962
+ const res = await fetch("/api/projects");
1963
+ projects = await res.json();
1964
+ renderProjects();
1965
+ } catch(e) {
1966
+ el.disabled = false;
1967
+ el.textContent = originalText;
1968
+ alert("Failed to move work item to Todo: " + e.message);
1969
+ }
1970
+ }
1971
+
1972
+ // ---- Pipeline Stages ----
1973
+ function addStageRow(data) {
1974
+ stageRows.push(data);
1975
+ if (stageRows.length > MAX_STAGE) stageRows = stageRows.slice(-MAX_STAGE);
1976
+ renderStages();
1977
+ }
1978
+ function shortModel(name) {
1979
+ if (!name) return "";
1980
+ if (name.includes("sonnet-4-5")) return "sonnet-4.5";
1981
+ if (name.includes("haiku-4-5")) return "haiku-4.5";
1982
+ if (name.includes("opus-4-6")) return "opus-4.6";
1983
+ if (name.includes("opus-4")) return "opus-4";
1984
+ if (name.includes("sonnet-4")) return "sonnet-4";
1985
+ var parts = name.split("-");
1986
+ return parts[parts.length - 1];
1987
+ }
1988
+ function fmtTokens(n) {
1989
+ if (n == null) return "";
1990
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
1991
+ if (n >= 1000) return (n / 1000).toFixed(1) + "k";
1992
+ return String(n);
1993
+ }
1994
+ function renderStages() {
1995
+ const el = document.getElementById("stage-list");
1996
+ const cnt = document.getElementById("stage-count");
1997
+ if (!stageRows.length) { el.innerHTML = "<div class=\\"empty\\">no pipeline events</div>"; return; }
1998
+ if (cnt) cnt.textContent = stageRows.length + "/" + MAX_STAGE;
1999
+ el.innerHTML = stageRows.slice().reverse().map(r => {
2000
+ const info = r.workItemId ? workItemTitleMap.get(r.workItemId) : null;
2001
+ let workItemLabel = "";
2002
+ if (info) {
2003
+ workItemLabel = info.workItemIdentifier
2004
+ ? info.workItemIdentifier + (info.title ? " " + info.title.slice(0, 22) : "")
2005
+ : (info.title ? info.title.slice(0, 30) : "");
2006
+ } else if (r.workItemId) {
2007
+ workItemLabel = r.workItemId.slice(0, 8);
2008
+ }
2009
+ // repo name from projectPath
2010
+ const projPath = r.workItemId ? workItemProjectMap.get(r.workItemId) : null;
2011
+ const repoName = projPath ? projPath.split("/").pop() : "";
2012
+ // elapsed time
2013
+ const startTs = r.workItemId ? workItemStartMap.get(r.workItemId) : null;
2014
+ let elapsed = "";
2015
+ if (startTs) {
2016
+ const sec = Math.floor((Date.now() - startTs) / 1000);
2017
+ if (sec < 60) elapsed = sec + "s";
2018
+ else if (sec < 3600) elapsed = Math.floor(sec / 60) + "m" + (sec % 60) + "s";
2019
+ else elapsed = Math.floor(sec / 3600) + "h" + Math.floor((sec % 3600) / 60) + "m";
2020
+ }
2021
+ // model/token info (only on complete)
2022
+ var modelStr = r.model ? shortModel(r.model) : "";
2023
+ var tokenStr = "";
2024
+ if (r.inputTokens || r.outputTokens) {
2025
+ tokenStr = fmtTokens(r.inputTokens) + "/" + fmtTokens(r.outputTokens);
2026
+ if (r.costUsd != null) tokenStr += " $" + r.costUsd.toFixed(2);
2027
+ }
2028
+ return (
2029
+ "<div class=\\"stage-row\\">" +
2030
+ "<div class=\\"sdot " + (r.status || "") + "\\"></div>" +
2031
+ "<div class=\\"srepo\\">" + escapeHtml(repoName) + "</div>" +
2032
+ "<div class=\\"sname\\">" + escapeHtml(r.stage) + "</div>" +
2033
+ "<div class=\\"sworkItem\\" title=\\"" + escapeAttr(r.workItemId || "") + "\\">" + escapeHtml(workItemLabel) + "</div>" +
2034
+ "<div class=\\"smodel\\">" + escapeHtml(modelStr) + "</div>" +
2035
+ "<div class=\\"stokens\\">" + escapeHtml(tokenStr) + "</div>" +
2036
+ "<div class=\\"selapsed\\">" + elapsed + "</div>" +
2037
+ "<div class=\\"sstatus\\">" + (r.status || "") + "</div>" +
2038
+ "</div>"
2039
+ );
2040
+ }).join("");
2041
+ el.scrollTop = 0;
2042
+ }
2043
+
2044
+ // ---- Log Tab ----
2045
+ function selectLogTab(workItemId) {
2046
+ selectedLogWorkItemId = workItemId;
2047
+ document.querySelectorAll('.log-tab').forEach(t =>
2048
+ t.classList.toggle('active', t.dataset.workItem === (workItemId ?? 'all'))
2049
+ );
2050
+ renderLog();
2051
+ }
2052
+
2053
+ function updateLogTabs() {
2054
+ const bar = document.getElementById('log-tab-bar');
2055
+ const workItemIds = [...new Set(logLines.map(l => l.workItemId).filter(id => id && id !== 'system'))];
2056
+ // Sort by most recent start time
2057
+ workItemIds.sort((a, b) => (workItemStartMap.get(b) || 0) - (workItemStartMap.get(a) || 0));
2058
+
2059
+ let html = '<button class="log-tab' + (selectedLogWorkItemId === null ? ' active' : '')
2060
+ + '" data-workItem="all" onclick="selectLogTab(null)">ALL</button>';
2061
+
2062
+ for (const tid of workItemIds) {
2063
+ const info = workItemTitleMap.get(tid);
2064
+ const label = info?.workItemIdentifier || tid.slice(0, 8);
2065
+ const isActive = selectedLogWorkItemId === tid;
2066
+ html += '<button class="log-tab' + (isActive ? ' active' : '')
2067
+ + '" data-workItem="' + tid + '" onclick="selectLogTab(\\'' + tid + '\\')">'
2068
+ + escapeHtml(label) + '</button>';
2069
+ }
2070
+ bar.innerHTML = html;
2071
+ }
2072
+
2073
+ // ---- Log ----
2074
+ function addLogLine(data) {
2075
+ data._ts = Date.now();
2076
+ logLines.push(data);
2077
+ if (logLines.length > MAX_LOG) logLines = logLines.slice(-MAX_LOG);
2078
+ updateLogTabs();
2079
+ renderLog();
2080
+ }
2081
+
2082
+ function classifyLog(line) {
2083
+ if (!line) return { cls: "log-spacer", icon: "" };
2084
+ if (line === "───") return { cls: "log-separator", icon: "" };
2085
+ if (/^■ /.test(line)) return { cls: "log-heading2", icon: "■" };
2086
+ if (/^[┌└│]/.test(line)) return { cls: "log-code", icon: "" };
2087
+ if (/^▸ /.test(line)) return { cls: "log-tool", icon: "▸" };
2088
+ if (/^▶|Heartbeat started|Stage started|Iteration [0-9]/.test(line)) return { cls: "log-heading", icon: "▶" };
2089
+ if (/^✓|success=true|approved|completed|Done|Created sub-workItem/.test(line)) return { cls: "log-success", icon: "✓" };
2090
+ if (/^✗|success=false|failed|error|Error|rejected|exceeded/.test(line)) return { cls: "log-fail", icon: "✗" };
2091
+ if (/^⟳|Fetching|Decomposing|Running|Scheduling|Spawning/.test(line)) return { cls: "", icon: "⟳" };
2092
+ if (/^⛔|Blocked|Time window|blocked/.test(line)) return { cls: "log-warn", icon: "⛔" };
2093
+ if (/^—|No workItem|already completed|no log/.test(line)) return { cls: "log-system", icon: "—" };
2094
+ if (/Cost:|\\$[\\.0-9]/.test(line)) return { cls: "", icon: "💲" };
2095
+ if (/Git detected|files changed|filesChanged/.test(line)) return { cls: "", icon: "📁" };
2096
+ if (/Selected [0-9]+ workItems/.test(line)) return { cls: "", icon: "🎯" };
2097
+ if (/Enqueued|executePipeline/.test(line)) return { cls: "", icon: "📋" };
2098
+ if (/Direct path|Project|path found/.test(line)) return { cls: "log-system", icon: "📂" };
2099
+ return { cls: "", icon: "·" };
2100
+ }
2101
+
2102
+ function formatLogText(raw) {
2103
+ if (!raw) return "";
2104
+ // Detect and humanize raw JSON that slipped through
2105
+ const trimmed = raw.trim();
2106
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
2107
+ try {
2108
+ const obj = JSON.parse(trimmed);
2109
+ if (obj.needsDecomposition === false) {
2110
+ const r = obj.reason ? obj.reason.slice(0, 120) : "";
2111
+ raw = "\\u2713 No decomposition needed (" + (obj.totalEstimatedMinutes || "?") + "min) " + r;
2112
+ } else if (obj.needsDecomposition === true && obj.subWorkItems) {
2113
+ raw = "\\uD83D\\uDD00 Decomposed into " + obj.subWorkItems.length + " sub-workItems (total " + (obj.totalEstimatedMinutes || "?") + "min)";
2114
+ } else if (obj.success !== undefined) {
2115
+ raw = (obj.success ? "\\u2713 " : "\\u2717 ") + (obj.summary || obj.error || JSON.stringify(obj).slice(0, 120));
2116
+ }
2117
+ } catch { /* not valid JSON */ }
2118
+ }
2119
+ let t = escapeHtml(raw);
2120
+ // inline bold: **text** → highlighted
2121
+ t = t.replace(/\\*\\*([^*]+)\\*\\*/g, '<span class="lhighlight">$1</span>');
2122
+ // highlight cost figures
2123
+ t = t.replace(/(\\$[\\d.]+)/g, '<span class="lcost">$1</span>');
2124
+ // highlight file counts
2125
+ t = t.replace(/(\\d+ files? changed)/g, '<span class="lfiles">$1</span>');
2126
+ // highlight durations
2127
+ t = t.replace(/(\\d+\\.\\d+s|\\d+ms)/g, '<span class="lcost">$1</span>');
2128
+ // highlight workItem titles in quotes
2129
+ t = t.replace(/(&quot;[^&]+&quot;)/g, '<span class="lhighlight">$1</span>');
2130
+ // highlight workItem identifiers
2131
+ t = t.replace(/(INT-\\d+)/g, '<span class="lhighlight">$1</span>');
2132
+ return t;
2133
+ }
2134
+
2135
+ function fmtLogTime(ts) {
2136
+ if (!ts) return "";
2137
+ const d = new Date(ts);
2138
+ return d.getHours().toString().padStart(2,"0") + ":" +
2139
+ d.getMinutes().toString().padStart(2,"0") + ":" +
2140
+ d.getSeconds().toString().padStart(2,"0");
2141
+ }
2142
+
2143
+ function renderLog() {
2144
+ const el = document.getElementById("log-list");
2145
+ const cnt = document.getElementById("log-count");
2146
+ const filtered = selectedLogWorkItemId === null
2147
+ ? logLines
2148
+ : logLines.filter(l => l.workItemId === selectedLogWorkItemId);
2149
+ if (!filtered.length) {
2150
+ el.innerHTML = "<div class=\\"empty\\">" + (selectedLogWorkItemId ? "no logs for this workItem" : "no log output") + "</div>";
2151
+ if (cnt) cnt.textContent = selectedLogWorkItemId ? filtered.length + "/" + logLines.length : "";
2152
+ return;
2153
+ }
2154
+ if (cnt) cnt.textContent = (selectedLogWorkItemId ? filtered.length + "/" : "") + logLines.length + "/" + MAX_LOG;
2155
+ const atBottom = el.scrollHeight - el.scrollTop <= el.clientHeight + 50;
2156
+ el.innerHTML = filtered.map(l => {
2157
+ const info = l.workItemId ? workItemTitleMap.get(l.workItemId) : null;
2158
+ const tag = info?.workItemIdentifier
2159
+ ? info.workItemIdentifier
2160
+ : (l.workItemId === "system" ? "SYS" : (l.workItemId || "").slice(0, 8));
2161
+ const { cls, icon } = classifyLog(l.line);
2162
+ // spacer/separator use minimal rendering
2163
+ if (cls === "log-spacer") return "<div class=\\"log-line log-spacer\\"></div>";
2164
+ if (cls === "log-separator") return "<div class=\\"log-line log-separator\\"><span class=\\"ltext\\">───────────────────</span></div>";
2165
+ const time = fmtLogTime(l._ts);
2166
+ const stage = l.stage && l.stage !== "heartbeat" ? l.stage : "";
2167
+ // heading2: strip ■ prefix (icon handles it)
2168
+ const displayLine = cls === "log-heading2" ? (l.line || "").replace(/^■ /, "") : l.line;
2169
+ // tool: strip ▸ prefix
2170
+ const displayLine2 = cls === "log-tool" ? (displayLine || "").replace(/^▸ /, "") : displayLine;
2171
+ return (
2172
+ "<div class=\\"log-line " + cls + "\\">" +
2173
+ "<span class=\\"ltime\\">" + time + "</span>" +
2174
+ "<span class=\\"licon\\">" + icon + "</span>" +
2175
+ "<span class=\\"ltag\\" title=\\"" + escapeAttr(l.workItemId || "") + "\\">" + escapeHtml(tag) + "</span>" +
2176
+ (stage ? "<span class=\\"lstage\\">" + escapeHtml(stage) + "</span>" : "") +
2177
+ "<span class=\\"ltext\\">" + formatLogText(displayLine2) + "</span>" +
2178
+ "</div>"
2179
+ );
2180
+ }).join("");
2181
+ if (atBottom) el.scrollTop = el.scrollHeight;
2182
+ }
2183
+
2184
+ // ---- Utils ----
2185
+ function fmtAge(isoDate) {
2186
+ if (!isoDate) return "";
2187
+ var diff = Math.max(0, Date.now() - new Date(isoDate).getTime());
2188
+ var sec = Math.floor(diff / 1000);
2189
+ if (sec < 60) return sec + "s";
2190
+ var min = Math.floor(sec / 60);
2191
+ if (min < 60) return min + "m";
2192
+ var hr = Math.floor(min / 60);
2193
+ if (hr < 24) return hr + "h";
2194
+ var day = Math.floor(hr / 24);
2195
+ if (day < 7) return day + "d";
2196
+ var wk = Math.floor(day / 7);
2197
+ return wk + "w";
2198
+ }
2199
+ function escapeHtml(text) {
2200
+ const d = document.createElement("div"); d.textContent = String(text || ""); return d.innerHTML;
2201
+ }
2202
+ function escapeAttr(text) {
2203
+ return String(text || "").replace(/&/g, "&amp;").replace(/"/g, "&quot;");
2204
+ }
2205
+
2206
+ // ---- Repo Picker ----
2207
+ let allLocalProjects = [];
2208
+ let pickerOpen = false;
2209
+
2210
+ async function openRepoPicker() {
2211
+ if (pickerOpen) return;
2212
+ pickerOpen = true;
2213
+ const overlay = document.getElementById("repo-picker");
2214
+ overlay.style.display = "flex";
2215
+ document.getElementById("repo-search").value = "";
2216
+ document.getElementById("repo-picker-list").innerHTML =
2217
+ "<div class=\\"empty\\">loading...</div>";
2218
+ document.getElementById("repo-search").focus();
2219
+
2220
+ try {
2221
+ fetchScanPaths();
2222
+ const res = await fetch("/api/local-projects");
2223
+ allLocalProjects = await res.json();
2224
+ filterRepos("");
2225
+ } catch(e) {
2226
+ document.getElementById("repo-picker-list").innerHTML =
2227
+ "<div class=\\"empty\\">failed to load: " + escapeHtml(e.message) + "</div>";
2228
+ }
2229
+ }
2230
+
2231
+ function closeRepoPicker() {
2232
+ pickerOpen = false;
2233
+ document.getElementById("repo-picker").style.display = "none";
2234
+ }
2235
+
2236
+ function filterRepos(q) {
2237
+ const list = document.getElementById("repo-picker-list");
2238
+ const filtered = q
2239
+ ? allLocalProjects.filter(p =>
2240
+ p.name.toLowerCase().includes(q.toLowerCase()) ||
2241
+ p.path.toLowerCase().includes(q.toLowerCase()))
2242
+ : allLocalProjects;
2243
+
2244
+ if (!filtered.length) {
2245
+ list.innerHTML = "<div class=\\"empty\\">no results</div>";
2246
+ return;
2247
+ }
2248
+ list.innerHTML = filtered.slice(0, 80).map(p => {
2249
+ const badge = p.pinned ? "<span class=\\"repo-item-badge\\">pinned</span>" : "";
2250
+ return (
2251
+ "<div class=\\"repo-item\\" data-path=\\"" + escapeAttr(p.path) + "\\" onclick=\\"pickRepo(this)\\">" +
2252
+ "<div>" +
2253
+ "<div class=\\"repo-item-name\\">" + escapeHtml(p.name) + "</div>" +
2254
+ "<div class=\\"repo-item-path\\">" + escapeHtml(p.path) + "</div>" +
2255
+ "</div>" +
2256
+ badge +
2257
+ "</div>"
2258
+ );
2259
+ }).join("");
2260
+ }
2261
+
2262
+ async function pickRepo(el) {
2263
+ const path = el.getAttribute("data-path");
2264
+ if (!path) return;
2265
+ el.style.opacity = "0.4";
2266
+ try {
2267
+ await fetch("/api/projects/pin", {
2268
+ method: "POST",
2269
+ headers: { "Content-Type": "application/json" },
2270
+ body: JSON.stringify({ projectPath: path }),
2271
+ });
2272
+ // Refresh project list
2273
+ const res = await fetch("/api/projects");
2274
+ projects = await res.json();
2275
+ renderProjects();
2276
+ // Mark as pinned in local picker list
2277
+ const item = allLocalProjects.find(p => p.path === path);
2278
+ if (item) item.pinned = true;
2279
+ filterRepos(document.getElementById("repo-search").value);
2280
+ } catch(e) {
2281
+ console.error("Pin failed:", e);
2282
+ }
2283
+ el.style.opacity = "1";
2284
+ }
2285
+
2286
+ // ---- Scan Paths ----
2287
+ async function fetchScanPaths() {
2288
+ try {
2289
+ const res = await fetch("/api/scan-paths");
2290
+ if (res.ok) {
2291
+ const data = await res.json();
2292
+ renderScanPaths(data);
2293
+ }
2294
+ } catch(e) {
2295
+ console.error("fetchScanPaths error:", e);
2296
+ }
2297
+ }
2298
+
2299
+ function renderScanPaths(data) {
2300
+ const list = document.getElementById("scan-paths-list");
2301
+ if (!list) return;
2302
+ const rows = [];
2303
+ for (const p of (data.configPaths || [])) {
2304
+ rows.push(
2305
+ "<div class=\\"scan-path-row\\">" +
2306
+ "<span class=\\"path\\">" + escapeHtml(p) + "</span>" +
2307
+ "<button class=\\"scan-path-remove\\" title=\\"remove\\" onclick=\\"removeScanPath('" + escapeAttr(p) + "')\\">✕</button>" +
2308
+ "</div>"
2309
+ );
2310
+ }
2311
+ for (const p of (data.customPaths || [])) {
2312
+ rows.push(
2313
+ "<div class=\\"scan-path-row\\">" +
2314
+ "<span class=\\"path\\">" + escapeHtml(p) + "</span>" +
2315
+ "<button class=\\"scan-path-remove\\" onclick=\\"removeScanPath('" + escapeAttr(p) + "')\\">✕</button>" +
2316
+ "</div>"
2317
+ );
2318
+ }
2319
+ list.innerHTML = rows.length > 0 ? rows.join("") : "<div style=\\"color:var(--mutedStrong);font-size:10px\\">no scan paths configured</div>";
2320
+ }
2321
+
2322
+ async function addScanPath() {
2323
+ const input = document.getElementById("scan-path-input");
2324
+ const path = input.value.trim();
2325
+ if (!path) return;
2326
+ input.value = "";
2327
+ try {
2328
+ await fetch("/api/scan-paths", {
2329
+ method: "POST",
2330
+ headers: { "Content-Type": "application/json" },
2331
+ body: JSON.stringify({ path }),
2332
+ });
2333
+ await fetchScanPaths();
2334
+ // Refresh project list in picker
2335
+ const res = await fetch("/api/local-projects");
2336
+ allLocalProjects = await res.json();
2337
+ filterRepos(document.getElementById("repo-search").value);
2338
+ } catch(e) {
2339
+ console.error("addScanPath error:", e);
2340
+ }
2341
+ }
2342
+
2343
+ async function removeScanPath(path) {
2344
+ try {
2345
+ await fetch("/api/scan-paths/" + encodeURIComponent(path), {
2346
+ method: "DELETE",
2347
+ });
2348
+ await fetchScanPaths();
2349
+ // Refresh project list in picker
2350
+ const res = await fetch("/api/local-projects");
2351
+ allLocalProjects = await res.json();
2352
+ filterRepos(document.getElementById("repo-search").value);
2353
+ } catch(e) {
2354
+ console.error("removeScanPath error:", e);
2355
+ }
2356
+ }
2357
+
2358
+ // ---- Monitors ----
2359
+ var monitorsData = [];
2360
+ async function fetchMonitors() {
2361
+ try {
2362
+ const res = await fetch("/api/monitors");
2363
+ if (res.ok) { monitorsData = await res.json(); renderMonitors(); }
2364
+ } catch {}
2365
+ }
2366
+ function renderMonitors() {
2367
+ renderMonitorsAndProcesses();
2368
+ }
2369
+ function fmtDur(ms) {
2370
+ var s = Math.floor(ms / 1000);
2371
+ var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);
2372
+ if (h >= 24) return Math.floor(h / 24) + "d " + (h % 24) + "h";
2373
+ if (h > 0) return h + "h " + m + "m";
2374
+ return m + "m";
2375
+ }
2376
+
2377
+ // ---- Processes ----
2378
+ var processesData = [];
2379
+ async function fetchProcesses() {
2380
+ try {
2381
+ const res = await fetch("/api/processes");
2382
+ if (res.ok) { processesData = await res.json(); renderMonitorsAndProcesses(); }
2383
+ } catch {}
2384
+ }
2385
+ async function killProcess(pid) {
2386
+ if (!confirm("Kill process PID " + pid + "?")) return;
2387
+ try {
2388
+ await fetch("/api/processes/" + pid, { method: "DELETE" });
2389
+ processesData = processesData.filter(p => p.pid !== pid);
2390
+ renderMonitorsAndProcesses();
2391
+ } catch(e) {
2392
+ addLogLine({ workItemId: "system", stage: "error", line: "Kill failed: " + e.message });
2393
+ }
2394
+ }
2395
+ function procActivityIcon(lastActivityAt) {
2396
+ var ago = (Date.now() - lastActivityAt) / 1000;
2397
+ if (ago < 10) return "\\u26A1";
2398
+ if (ago < 60) return "\\u23F8";
2399
+ return "\\u2757";
2400
+ }
2401
+ function renderMonitorsAndProcesses() {
2402
+ var panel = document.getElementById("monitor-panel");
2403
+ var el = document.getElementById("monitor-list");
2404
+ var countEl = document.getElementById("monitor-count");
2405
+ var hasMonitors = monitorsData.length > 0;
2406
+ var hasProcesses = processesData.length > 0;
2407
+ if (!hasMonitors && !hasProcesses) {
2408
+ el.innerHTML = "<div class=\\"empty\\">no monitors or processes</div>";
2409
+ var counts = [];
2410
+ if (countEl) countEl.textContent = "";
2411
+ return;
2412
+ }
2413
+ var parts = [];
2414
+ if (processesData.length) parts.push(processesData.length + "p");
2415
+ if (monitorsData.length) parts.push(monitorsData.length + "m");
2416
+ if (countEl) countEl.textContent = parts.join(" ");
2417
+ var html = "";
2418
+ // Processes section
2419
+ if (hasProcesses) {
2420
+ html += "<div class=\\"workItem-sec-label\\">processes</div>";
2421
+ html += processesData.map(function(p) {
2422
+ var dur = fmtDur(Date.now() - p.spawnedAt);
2423
+ var act = procActivityIcon(p.lastActivityAt);
2424
+ var modelStr = p.model ? shortModel(p.model) : "";
2425
+ var projName = p.projectPath ? p.projectPath.split("/").pop() : "";
2426
+ return '<div class="proc-row">' +
2427
+ '<span class="proc-pid">' + p.pid + '</span>' +
2428
+ '<span class="proc-stage">' + escapeHtml(p.stage) + '</span>' +
2429
+ '<span class="proc-model">' + escapeHtml(modelStr) + '</span>' +
2430
+ '<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>' +
2431
+ '<span class="proc-activity">' + act + '</span>' +
2432
+ '<span class="proc-dur">' + dur + '</span>' +
2433
+ '<button class="proc-kill" onclick="killProcess(' + p.pid + ')">KILL</button>' +
2434
+ '</div>';
2435
+ }).join("");
2436
+ }
2437
+ // Monitors section
2438
+ if (hasMonitors) {
2439
+ html += "<div class=\\"workItem-sec-label\\">monitors</div>";
2440
+ html += monitorsData.map(function(m) {
2441
+ var stateColor = m.state === "running" ? "var(--green)" : m.state === "completed" ? "var(--cyan)" : m.state === "failed" || m.state === "timeout" ? "var(--red)" : "var(--dim)";
2442
+ var elapsed = m.registeredAt ? fmtDur(Date.now() - m.registeredAt) : "-";
2443
+ var lastOut = m.lastOutput ? escapeHtml(m.lastOutput.slice(0, 80)) : "-";
2444
+ return '<div style="padding:4px 6px;border-bottom:1px solid var(--border);font-size:11px">' +
2445
+ '<div style="display:flex;align-items:center;gap:6px">' +
2446
+ '<span style="color:' + stateColor + ';font-weight:bold">[' + m.state.toUpperCase() + ']</span>' +
2447
+ '<span style="color:var(--green)">' + escapeHtml(m.name) + '</span>' +
2448
+ '<span style="margin-left:auto;color:var(--dim)">' + elapsed + '</span>' +
2449
+ '</div>' +
2450
+ '<div style="color:var(--dim);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeAttr(m.lastOutput || "") + '">' + lastOut + '</div>' +
2451
+ (m.workItemId ? '<div style="color:var(--cyan-dim);font-size:10px;margin-top:1px">' + escapeHtml(m.workItemId) + ' | checks: ' + m.checkCount + '</div>' : '') +
2452
+ '</div>';
2453
+ }).join("");
2454
+ }
2455
+ el.innerHTML = html;
2456
+ }
2457
+
2458
+ // ---- Init ----
2459
+ async function loadInitial() {
2460
+ try {
2461
+ // Phase 1: critical data first
2462
+ const [statsRes, projectsRes] = await Promise.all([
2463
+ fetch("/api/stats"),
2464
+ fetch("/api/projects"),
2465
+ ]);
2466
+ const stats = await statsRes.json();
2467
+ updateStats(stats);
2468
+ document.getElementById("stat-sse").textContent = stats.sseClients ?? "-";
2469
+
2470
+ projects = await projectsRes.json();
2471
+ renderProjects();
2472
+
2473
+ // Phase 2: supplemental data deferred to avoid blocking render
2474
+ loadSupplementalData();
2475
+ } catch(e) {
2476
+ console.error("Init failed:", e);
2477
+ }
2478
+ }
2479
+
2480
+ // Supplemental data deferred load (does not block initial render)
2481
+ async function loadSupplementalData() {
2482
+ try {
2483
+ const [logsRes, stagesRes] = await Promise.all([
2484
+ fetch("/api/logs"),
2485
+ fetch("/api/stages"),
2486
+ ]);
2487
+
2488
+ // Restore logs
2489
+ const logs = await logsRes.json();
2490
+ for (const ev of logs) addLogLine(ev.data);
2491
+
2492
+ // Restore pipeline/workItem events
2493
+ const stages = await stagesRes.json();
2494
+ for (const ev of stages) handleEvent(ev);
2495
+
2496
+ // Load work items from canonical endpoint
2497
+ fetchWorkItems();
2498
+ fetchLanes();
2499
+ } catch(e) {
2500
+ console.error("Supplemental data load failed:", e);
2501
+ }
2502
+ }
2503
+
2504
+ // 성능 최적화: stats + projects 폴링을 60초로 증가 (변화 빈도 낮음)
2505
+ setInterval(async () => {
2506
+ try {
2507
+ const [sRes, pRes] = await Promise.all([fetch("/api/stats"), fetch("/api/projects")]);
2508
+ const stats = await sRes.json();
2509
+ document.getElementById("stat-sse").textContent = stats.sseClients ?? "-";
2510
+ updateStats(stats);
2511
+ const fresh = await pRes.json();
2512
+ fresh.forEach(p => {
2513
+ const local = projects.find(l => l.path === p.path);
2514
+ if (local) p.enabled = local.enabled;
2515
+ });
2516
+ projects = fresh;
2517
+ renderProjects();
2518
+ } catch {}
2519
+ }, 60000);
2520
+
2521
+ // ---- Mobile Tab Navigation ----
2522
+ function switchTab(idx) {
2523
+ const cols = document.querySelectorAll(".main-grid > .col");
2524
+ const tabs = document.querySelectorAll(".tab-bar .tab");
2525
+ cols.forEach((c, i) => c.classList.toggle("mob-active", i === idx));
2526
+ tabs.forEach((t, i) => t.classList.toggle("active", i === idx));
2527
+ }
2528
+ document.querySelector(".tab-bar").addEventListener("click", e => {
2529
+ const tab = e.target.closest(".tab");
2530
+ if (!tab) return;
2531
+ switchTab(parseInt(tab.dataset.tab, 10));
2532
+ });
2533
+ // Activate first tab on load
2534
+ switchTab(0);
2535
+
2536
+ // Knowledge graph data fetcher
2537
+ async function fetchKnowledgeData() {
2538
+ try {
2539
+ const res = await fetch("/api/knowledge");
2540
+ if (res.ok) {
2541
+ const data = await res.json();
2542
+ for (const item of data) {
2543
+ knowledgeCache[item.slug] = item;
2544
+ // Also cache by last segment for name-based lookup
2545
+ const parts = item.slug.split("-");
2546
+ knowledgeCache[parts[parts.length - 1]] = item;
2547
+ }
2548
+ renderProjects();
2549
+ }
2550
+ } catch {}
2551
+ }
2552
+
2553
+ // ---- Quota tracker ----
2554
+ async function fetchQuota() {
2555
+ try {
2556
+ var res = await fetch("/api/quota");
2557
+ if (!res.ok) return;
2558
+ var q = await res.json();
2559
+ if (q.error) return;
2560
+ var el5h = document.getElementById("stat-quota-5h");
2561
+ var el7d = document.getElementById("stat-quota-7d");
2562
+ if (q.five_hour && el5h) {
2563
+ var u5 = Math.round(q.five_hour.utilization);
2564
+ el5h.textContent = u5 + "%";
2565
+ el5h.className = "stat-val " + (u5 >= 80 ? "red" : u5 >= 50 ? "amber" : "cyan");
2566
+ }
2567
+ if (q.seven_day && el7d) {
2568
+ var u7 = Math.round(q.seven_day.utilization);
2569
+ el7d.textContent = u7 + "%";
2570
+ el7d.className = "stat-val " + (u7 >= 80 ? "red" : u7 >= 50 ? "amber" : "cyan");
2571
+ }
2572
+ } catch {}
2573
+ }
2574
+
2575
+ document.addEventListener("click", function(event) {
2576
+ const popover = document.getElementById("theme-popover");
2577
+ const launcher = document.getElementById("theme-launcher-btn");
2578
+ if (!popover || !launcher) return;
2579
+ const target = event.target;
2580
+ if (popover.contains(target) || launcher.contains(target)) return;
2581
+ closeThemePicker();
2582
+ });
2583
+
2584
+ document.addEventListener("keydown", function(event) {
2585
+ if (event.key === "Escape" && isThemePickerOpen()) {
2586
+ closeThemePicker();
2587
+ }
2588
+ });
2589
+
2590
+ bootAppTheme();
2591
+
2592
+ // 성능 최적화: 초기 로드 후 2단계 페칭 (렌더링 블로킹 방지)
2593
+ loadInitial().then(function() {
2594
+ initializeTerminal();
2595
+ connectSSE(true);
2596
+ });
2597
+
2598
+ // Polling intervals
2599
+ setInterval(pollHealth, 10000);
2600
+ setInterval(fetchStuckWorkItems, 60000);
2601
+ setInterval(fetchKnowledgeData, 60000);
2602
+ setInterval(fetchMonitors, 60000);
2603
+ setInterval(fetchProcesses, 30000);
2604
+ setInterval(fetchQuota, 300000);
2605
+ setInterval(fetchWorkItems, 30000); // v2 work-item tree refresh
2606
+ setInterval(fetchLanes, 30000);
2607
+ setInterval(renderSyncAgo, 1000);
2608
+
2609
+ // Deferred startup fetch (after render stabilizes)
2610
+ setTimeout(function() {
2611
+ pollHealth();
2612
+ fetchStuckWorkItems();
2613
+ fetchKnowledgeData();
2614
+ fetchMonitors();
2615
+ fetchProcesses();
2616
+ fetchQuota();
2617
+ fetchWorkItems();
2618
+ fetchLanes();
2619
+ }, 3000);
2620
+
2621
+ // 렌더링 성능: 스테이지 업데이트 폴링 제거 (SSE 이벤트 활용)
2622
+ // setInterval(() => { if (stageRows.length) renderStages(); }, 10000);
2623
+ </script>
2624
+ </body>
2625
+ </html>`;
2626
+ }
2627
+ const DASHBOARD_HTML = createDashboardHtml();
2628
+ export { DASHBOARD_HTML };
2629
+ //# sourceMappingURL=dashboardHtml.js.map