@aion0/forge 0.5.26 → 0.5.28

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 (255) hide show
  1. package/.forge/worktrees/pipeline-4dd8dc2d/CLAUDE.md +86 -0
  2. package/.forge/worktrees/pipeline-4dd8dc2d/README.md +136 -0
  3. package/.forge/worktrees/pipeline-4dd8dc2d/RELEASE_NOTES.md +36 -0
  4. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/agents/route.ts +17 -0
  5. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/[...nextauth]/route.ts +3 -0
  6. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/verify/route.ts +46 -0
  7. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/route.ts +31 -0
  8. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/stream/route.ts +63 -0
  9. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/route.ts +28 -0
  10. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/entries/route.ts +23 -0
  11. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
  12. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/route.ts +37 -0
  13. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/sync/route.ts +17 -0
  14. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-templates/route.ts +145 -0
  15. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/code/route.ts +299 -0
  16. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/[id]/route.ts +62 -0
  17. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/route.ts +40 -0
  18. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/detect-cli/route.ts +46 -0
  19. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/route.ts +176 -0
  20. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/sessions/route.ts +54 -0
  21. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/favorites/route.ts +26 -0
  22. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/route.ts +6 -0
  23. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/run/route.ts +19 -0
  24. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/git/route.ts +149 -0
  25. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/help/route.ts +84 -0
  26. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/issue-scanner/route.ts +116 -0
  27. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/logs/route.ts +100 -0
  28. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/mobile-chat/route.ts +115 -0
  29. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/monitor/route.ts +74 -0
  30. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notifications/route.ts +42 -0
  31. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notify/test/route.ts +33 -0
  32. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/online/route.ts +40 -0
  33. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/[id]/route.ts +41 -0
  34. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/route.ts +90 -0
  35. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/plugins/route.ts +75 -0
  36. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/[...path]/route.ts +64 -0
  37. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/route.ts +156 -0
  38. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-pipelines/route.ts +91 -0
  39. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-sessions/route.ts +61 -0
  40. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/projects/route.ts +26 -0
  41. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/chat/route.ts +64 -0
  42. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/messages/route.ts +9 -0
  43. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/route.ts +17 -0
  44. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/route.ts +20 -0
  45. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/settings/route.ts +64 -0
  46. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/local/route.ts +228 -0
  47. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/route.ts +182 -0
  48. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/smith-templates/route.ts +81 -0
  49. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/status/route.ts +12 -0
  50. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tabs/route.ts +25 -0
  51. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/route.ts +51 -0
  52. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/stream/route.ts +77 -0
  53. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/link/route.ts +37 -0
  54. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/route.ts +44 -0
  55. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/session/route.ts +14 -0
  56. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/telegram/route.ts +23 -0
  57. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/templates/route.ts +6 -0
  58. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-bell/route.ts +39 -0
  59. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-cwd/route.ts +19 -0
  60. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-state/route.ts +15 -0
  61. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tunnel/route.ts +26 -0
  62. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/upgrade/route.ts +43 -0
  63. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/usage/route.ts +20 -0
  64. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/version/route.ts +78 -0
  65. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/watchers/route.ts +33 -0
  66. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/agents/route.ts +35 -0
  67. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/memory/route.ts +23 -0
  68. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/smith/route.ts +22 -0
  69. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/stream/route.ts +31 -0
  70. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/route.ts +79 -0
  71. package/.forge/worktrees/pipeline-4dd8dc2d/app/global-error.tsx +21 -0
  72. package/.forge/worktrees/pipeline-4dd8dc2d/app/globals.css +52 -0
  73. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.ico +0 -0
  74. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.png +0 -0
  75. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.svg +106 -0
  76. package/.forge/worktrees/pipeline-4dd8dc2d/app/layout.tsx +17 -0
  77. package/.forge/worktrees/pipeline-4dd8dc2d/app/login/LoginForm.tsx +96 -0
  78. package/.forge/worktrees/pipeline-4dd8dc2d/app/login/page.tsx +10 -0
  79. package/.forge/worktrees/pipeline-4dd8dc2d/app/mobile/page.tsx +10 -0
  80. package/.forge/worktrees/pipeline-4dd8dc2d/app/page.tsx +22 -0
  81. package/.forge/worktrees/pipeline-4dd8dc2d/bin/forge-server.mjs +484 -0
  82. package/.forge/worktrees/pipeline-4dd8dc2d/check-forge-status.sh +71 -0
  83. package/.forge/worktrees/pipeline-4dd8dc2d/cli/mw.ts +579 -0
  84. package/.forge/worktrees/pipeline-4dd8dc2d/components/BrowserPanel.tsx +175 -0
  85. package/.forge/worktrees/pipeline-4dd8dc2d/components/ChatPanel.tsx +191 -0
  86. package/.forge/worktrees/pipeline-4dd8dc2d/components/ClaudeTerminal.tsx +267 -0
  87. package/.forge/worktrees/pipeline-4dd8dc2d/components/CodeViewer.tsx +787 -0
  88. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationEditor.tsx +411 -0
  89. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationGraphView.tsx +347 -0
  90. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationTerminalView.tsx +303 -0
  91. package/.forge/worktrees/pipeline-4dd8dc2d/components/Dashboard.tsx +807 -0
  92. package/.forge/worktrees/pipeline-4dd8dc2d/components/DashboardWrapper.tsx +9 -0
  93. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryFlowEditor.tsx +491 -0
  94. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryList.tsx +230 -0
  95. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryWorkspace.tsx +589 -0
  96. package/.forge/worktrees/pipeline-4dd8dc2d/components/DocTerminal.tsx +187 -0
  97. package/.forge/worktrees/pipeline-4dd8dc2d/components/DocsViewer.tsx +574 -0
  98. package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpDialog.tsx +169 -0
  99. package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpTerminal.tsx +141 -0
  100. package/.forge/worktrees/pipeline-4dd8dc2d/components/InlinePipelineView.tsx +111 -0
  101. package/.forge/worktrees/pipeline-4dd8dc2d/components/LogViewer.tsx +194 -0
  102. package/.forge/worktrees/pipeline-4dd8dc2d/components/MarkdownContent.tsx +73 -0
  103. package/.forge/worktrees/pipeline-4dd8dc2d/components/MobileView.tsx +385 -0
  104. package/.forge/worktrees/pipeline-4dd8dc2d/components/MonitorPanel.tsx +122 -0
  105. package/.forge/worktrees/pipeline-4dd8dc2d/components/NewSessionModal.tsx +93 -0
  106. package/.forge/worktrees/pipeline-4dd8dc2d/components/NewTaskModal.tsx +492 -0
  107. package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineEditor.tsx +570 -0
  108. package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineView.tsx +1018 -0
  109. package/.forge/worktrees/pipeline-4dd8dc2d/components/PluginsPanel.tsx +472 -0
  110. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectDetail.tsx +1618 -0
  111. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectList.tsx +108 -0
  112. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectManager.tsx +401 -0
  113. package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionList.tsx +74 -0
  114. package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionView.tsx +726 -0
  115. package/.forge/worktrees/pipeline-4dd8dc2d/components/SettingsModal.tsx +1647 -0
  116. package/.forge/worktrees/pipeline-4dd8dc2d/components/SkillsPanel.tsx +969 -0
  117. package/.forge/worktrees/pipeline-4dd8dc2d/components/StatusBar.tsx +99 -0
  118. package/.forge/worktrees/pipeline-4dd8dc2d/components/TabBar.tsx +46 -0
  119. package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskBoard.tsx +113 -0
  120. package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskDetail.tsx +372 -0
  121. package/.forge/worktrees/pipeline-4dd8dc2d/components/TerminalLauncher.tsx +398 -0
  122. package/.forge/worktrees/pipeline-4dd8dc2d/components/TunnelToggle.tsx +206 -0
  123. package/.forge/worktrees/pipeline-4dd8dc2d/components/UsagePanel.tsx +207 -0
  124. package/.forge/worktrees/pipeline-4dd8dc2d/components/WebTerminal.tsx +1743 -0
  125. package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceTree.tsx +221 -0
  126. package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceView.tsx +4048 -0
  127. package/.forge/worktrees/pipeline-4dd8dc2d/dev-test.sh +5 -0
  128. package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Memory_Layer_Design.docx +0 -0
  129. package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Strategy_Research_2026.docx +0 -0
  130. package/.forge/worktrees/pipeline-4dd8dc2d/docs/LOCAL-DEPLOY.md +144 -0
  131. package/.forge/worktrees/pipeline-4dd8dc2d/docs/roadmap-multi-agent-workflow.md +330 -0
  132. package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.png +0 -0
  133. package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.svg +106 -0
  134. package/.forge/worktrees/pipeline-4dd8dc2d/hooks/useSidebarResize.ts +52 -0
  135. package/.forge/worktrees/pipeline-4dd8dc2d/install.sh +29 -0
  136. package/.forge/worktrees/pipeline-4dd8dc2d/instrumentation.ts +35 -0
  137. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/claude-adapter.ts +104 -0
  138. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/generic-adapter.ts +64 -0
  139. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/index.ts +245 -0
  140. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/types.ts +70 -0
  141. package/.forge/worktrees/pipeline-4dd8dc2d/lib/artifacts.ts +106 -0
  142. package/.forge/worktrees/pipeline-4dd8dc2d/lib/auth.ts +62 -0
  143. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/docker.yaml +70 -0
  144. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/http.yaml +66 -0
  145. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/jenkins.yaml +92 -0
  146. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/llm-vision.yaml +85 -0
  147. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/playwright.yaml +111 -0
  148. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/shell-command.yaml +60 -0
  149. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/slack.yaml +48 -0
  150. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/webhook.yaml +56 -0
  151. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-process.ts +361 -0
  152. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-sessions.ts +266 -0
  153. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-templates.ts +227 -0
  154. package/.forge/worktrees/pipeline-4dd8dc2d/lib/cloudflared.ts +424 -0
  155. package/.forge/worktrees/pipeline-4dd8dc2d/lib/crypto.ts +67 -0
  156. package/.forge/worktrees/pipeline-4dd8dc2d/lib/delivery.ts +787 -0
  157. package/.forge/worktrees/pipeline-4dd8dc2d/lib/dirs.ts +99 -0
  158. package/.forge/worktrees/pipeline-4dd8dc2d/lib/flows.ts +86 -0
  159. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-mcp-server.ts +732 -0
  160. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-inbox.md +38 -0
  161. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-send.md +47 -0
  162. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-status.md +32 -0
  163. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-workspace-sync.md +37 -0
  164. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/00-overview.md +40 -0
  165. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +194 -0
  166. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/02-telegram.md +41 -0
  167. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/03-tunnel.md +31 -0
  168. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/04-tasks.md +52 -0
  169. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/05-pipelines.md +460 -0
  170. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/06-skills.md +43 -0
  171. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +73 -0
  172. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/08-rules.md +53 -0
  173. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/09-issue-autofix.md +55 -0
  174. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/10-troubleshooting.md +89 -0
  175. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/11-workspace.md +810 -0
  176. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/CLAUDE.md +62 -0
  177. package/.forge/worktrees/pipeline-4dd8dc2d/lib/init.ts +266 -0
  178. package/.forge/worktrees/pipeline-4dd8dc2d/lib/issue-scanner.ts +298 -0
  179. package/.forge/worktrees/pipeline-4dd8dc2d/lib/logger.ts +79 -0
  180. package/.forge/worktrees/pipeline-4dd8dc2d/lib/notifications.ts +75 -0
  181. package/.forge/worktrees/pipeline-4dd8dc2d/lib/notify.ts +108 -0
  182. package/.forge/worktrees/pipeline-4dd8dc2d/lib/password.ts +97 -0
  183. package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline-scheduler.ts +373 -0
  184. package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline.ts +1565 -0
  185. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/executor.ts +347 -0
  186. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/registry.ts +228 -0
  187. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/types.ts +103 -0
  188. package/.forge/worktrees/pipeline-4dd8dc2d/lib/project-sessions.ts +53 -0
  189. package/.forge/worktrees/pipeline-4dd8dc2d/lib/projects.ts +86 -0
  190. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-manager.ts +156 -0
  191. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-utils.ts +53 -0
  192. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-watcher.ts +345 -0
  193. package/.forge/worktrees/pipeline-4dd8dc2d/lib/settings.ts +195 -0
  194. package/.forge/worktrees/pipeline-4dd8dc2d/lib/skills.ts +458 -0
  195. package/.forge/worktrees/pipeline-4dd8dc2d/lib/task-manager.ts +951 -0
  196. package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-bot.ts +1477 -0
  197. package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-standalone.ts +83 -0
  198. package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-server.ts +70 -0
  199. package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-standalone.ts +438 -0
  200. package/.forge/worktrees/pipeline-4dd8dc2d/lib/usage-scanner.ts +249 -0
  201. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/state-machine.test.ts +388 -0
  202. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/workspace.test.ts +311 -0
  203. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-bus.ts +416 -0
  204. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-worker.ts +655 -0
  205. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/api-backend.ts +262 -0
  206. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/cli-backend.ts +491 -0
  207. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/index.ts +84 -0
  208. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/manager.ts +136 -0
  209. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/orchestrator.ts +3415 -0
  210. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/persistence.ts +309 -0
  211. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/presets.ts +649 -0
  212. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/requests.ts +287 -0
  213. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/session-monitor.ts +240 -0
  214. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/skill-installer.ts +275 -0
  215. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/smith-memory.ts +498 -0
  216. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/types.ts +241 -0
  217. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/watch-manager.ts +560 -0
  218. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace-standalone.ts +978 -0
  219. package/.forge/worktrees/pipeline-4dd8dc2d/middleware.ts +51 -0
  220. package/.forge/worktrees/pipeline-4dd8dc2d/next.config.ts +26 -0
  221. package/.forge/worktrees/pipeline-4dd8dc2d/package.json +74 -0
  222. package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-lock.yaml +3719 -0
  223. package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-workspace.yaml +1 -0
  224. package/.forge/worktrees/pipeline-4dd8dc2d/postcss.config.mjs +7 -0
  225. package/.forge/worktrees/pipeline-4dd8dc2d/publish.sh +133 -0
  226. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/README.md +66 -0
  227. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/results/.gitignore +2 -0
  228. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/run.ts +635 -0
  229. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/task.md +26 -0
  230. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
  231. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/setup.sh +19 -0
  232. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/task.md +48 -0
  233. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/validator.sh +69 -0
  234. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
  235. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/task.md +30 -0
  236. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
  237. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/verify-usage.ts +178 -0
  238. package/.forge/worktrees/pipeline-4dd8dc2d/src/config/index.ts +129 -0
  239. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/db/database.ts +259 -0
  240. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/memory/strategy.ts +32 -0
  241. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/chat.ts +65 -0
  242. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/registry.ts +60 -0
  243. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/session/manager.ts +190 -0
  244. package/.forge/worktrees/pipeline-4dd8dc2d/src/types/index.ts +129 -0
  245. package/.forge/worktrees/pipeline-4dd8dc2d/start.sh +32 -0
  246. package/.forge/worktrees/pipeline-4dd8dc2d/templates/smith-lead.json +45 -0
  247. package/.forge/worktrees/pipeline-4dd8dc2d/tsconfig.json +42 -0
  248. package/RELEASE_NOTES.md +10 -29
  249. package/app/api/terminal-bell/route.ts +6 -2
  250. package/app/api/terminal-cwd/route.ts +7 -4
  251. package/components/CodeViewer.tsx +3 -31
  252. package/components/Dashboard.tsx +34 -20
  253. package/components/WebTerminal.tsx +36 -2
  254. package/lib/terminal-standalone.ts +19 -2
  255. package/package.json +1 -1
@@ -0,0 +1,1018 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef, lazy, Suspense } from 'react';
4
+ import { useSidebarResize } from '@/hooks/useSidebarResize';
5
+ import type { TaskLogEntry } from '@/src/types';
6
+
7
+ const PipelineEditor = lazy(() => import('./PipelineEditor'));
8
+ const ConversationEditor = lazy(() => import('./ConversationEditor'));
9
+
10
+ // ─── Live Task Log Hook ──────────────────────────────────
11
+ // Subscribes to SSE stream for a running task, returns live log entries
12
+ function useTaskStream(taskId: string | undefined, isRunning: boolean) {
13
+ const [log, setLog] = useState<TaskLogEntry[]>([]);
14
+ const [status, setStatus] = useState<string>('');
15
+
16
+ useEffect(() => {
17
+ if (!taskId || !isRunning) { setLog([]); return; }
18
+
19
+ const es = new EventSource(`/api/tasks/${taskId}/stream`);
20
+ es.onmessage = (event) => {
21
+ try {
22
+ const data = JSON.parse(event.data);
23
+ if (data.type === 'log') setLog(prev => [...prev, data.entry]);
24
+ else if (data.type === 'status') setStatus(data.status);
25
+ else if (data.type === 'complete' && data.task) setLog(data.task.log);
26
+ } catch {}
27
+ };
28
+ es.onerror = () => es.close();
29
+ return () => es.close();
30
+ }, [taskId, isRunning]);
31
+
32
+ return { log, status };
33
+ }
34
+
35
+ // ─── Compact log renderer ─────────────────────────────────
36
+ function LiveLog({ log, maxHeight = 200 }: { log: TaskLogEntry[]; maxHeight?: number }) {
37
+ const endRef = useRef<HTMLDivElement>(null);
38
+ useEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [log]);
39
+
40
+ if (log.length === 0) return <div className="text-[10px] text-[var(--text-secondary)] italic">Starting...</div>;
41
+
42
+ return (
43
+ <div className="overflow-y-auto text-[9px] font-mono leading-relaxed space-y-0.5" style={{ maxHeight }}>
44
+ {log.slice(-50).map((entry, i) => (
45
+ <div key={i} className={
46
+ entry.type === 'result' ? 'text-green-400' :
47
+ entry.subtype === 'error' ? 'text-red-400' :
48
+ entry.type === 'system' ? 'text-yellow-400/70' :
49
+ 'text-[var(--text-secondary)]'
50
+ }>
51
+ {entry.type === 'assistant' && entry.subtype === 'tool_use'
52
+ ? `⚙ ${entry.tool || 'tool'}: ${entry.content.slice(0, 80)}${entry.content.length > 80 ? '...' : ''}`
53
+ : entry.content.slice(0, 200)}{entry.content.length > 200 ? '...' : ''}
54
+ </div>
55
+ ))}
56
+ <div ref={endRef} />
57
+ </div>
58
+ );
59
+ }
60
+
61
+ interface WorkflowNode {
62
+ id: string;
63
+ project: string;
64
+ prompt: string;
65
+ mode?: 'claude' | 'shell';
66
+ agent?: string;
67
+ branch?: string;
68
+ dependsOn: string[];
69
+ outputs: { name: string; extract: string }[];
70
+ routes: { condition: string; next: string }[];
71
+ maxIterations: number;
72
+ }
73
+
74
+ interface Workflow {
75
+ name: string;
76
+ type?: 'dag' | 'conversation';
77
+ description?: string;
78
+ builtin?: boolean;
79
+ vars: Record<string, string>;
80
+ input: Record<string, string>;
81
+ nodes: Record<string, WorkflowNode>;
82
+ conversation?: {
83
+ agents: { id: string; agent: string; role: string }[];
84
+ maxRounds: number;
85
+ stopCondition?: string;
86
+ initialPrompt: string;
87
+ };
88
+ }
89
+
90
+ interface PipelineNodeState {
91
+ status: 'pending' | 'running' | 'done' | 'failed' | 'skipped';
92
+ taskId?: string;
93
+ outputs: Record<string, string>;
94
+ iterations: number;
95
+ startedAt?: string;
96
+ completedAt?: string;
97
+ error?: string;
98
+ }
99
+
100
+ interface ConversationMessage {
101
+ round: number;
102
+ agentId: string;
103
+ agentName: string;
104
+ content: string;
105
+ timestamp: string;
106
+ taskId?: string;
107
+ status: 'pending' | 'running' | 'done' | 'failed';
108
+ }
109
+
110
+ interface Pipeline {
111
+ id: string;
112
+ workflowName: string;
113
+ type?: 'dag' | 'conversation';
114
+ status: 'running' | 'done' | 'failed' | 'cancelled';
115
+ input: Record<string, string>;
116
+ vars: Record<string, string>;
117
+ nodes: Record<string, PipelineNodeState>;
118
+ nodeOrder: string[];
119
+ createdAt: string;
120
+ completedAt?: string;
121
+ conversation?: {
122
+ config: {
123
+ agents: { id: string; agent: string; role: string; project?: string }[];
124
+ maxRounds: number;
125
+ stopCondition?: string;
126
+ initialPrompt: string;
127
+ };
128
+ messages: ConversationMessage[];
129
+ currentRound: number;
130
+ currentAgentIndex: number;
131
+ };
132
+ }
133
+
134
+ const STATUS_ICON: Record<string, string> = {
135
+ pending: '⏳',
136
+ running: '🔄',
137
+ done: '✅',
138
+ failed: '❌',
139
+ skipped: '⏭',
140
+ };
141
+
142
+ const STATUS_COLOR: Record<string, string> = {
143
+ pending: 'text-gray-400',
144
+ running: 'text-yellow-400',
145
+ done: 'text-green-400',
146
+ failed: 'text-red-400',
147
+ skipped: 'text-gray-500',
148
+ };
149
+
150
+ // ─── DAG Node Card with live logs ─────────────────────────
151
+
152
+ function DagNodeCard({ nodeId, node, nodeDef, onViewTask }: {
153
+ nodeId: string;
154
+ node: PipelineNodeState;
155
+ nodeDef?: WorkflowNode;
156
+ onViewTask?: (taskId: string) => void;
157
+ }) {
158
+ const isRunning = node.status === 'running';
159
+ const { log } = useTaskStream(node.taskId, isRunning);
160
+
161
+ return (
162
+ <div className={`border rounded-lg p-3 ${
163
+ isRunning ? 'border-yellow-500/50 bg-yellow-500/5' :
164
+ node.status === 'done' ? 'border-green-500/30 bg-green-500/5' :
165
+ node.status === 'failed' ? 'border-red-500/30 bg-red-500/5' :
166
+ 'border-[var(--border)]'
167
+ }`}>
168
+ <div className="flex items-center gap-2">
169
+ <span className={STATUS_COLOR[node.status]}>{STATUS_ICON[node.status]}</span>
170
+ <span className="text-xs font-semibold text-[var(--text-primary)]">{nodeId}</span>
171
+ {nodeDef && nodeDef.mode !== 'shell' && (
172
+ <span className="text-[8px] px-1 rounded bg-purple-500/20 text-purple-400">{nodeDef.agent || 'default'}</span>
173
+ )}
174
+ {node.taskId && (
175
+ <button onClick={() => onViewTask?.(node.taskId!)} className="text-[9px] text-[var(--accent)] font-mono hover:underline">
176
+ task:{node.taskId}
177
+ </button>
178
+ )}
179
+ {node.iterations > 1 && <span className="text-[9px] text-yellow-400">iter {node.iterations}</span>}
180
+ <span className="text-[9px] text-[var(--text-secondary)] ml-auto">{node.status}</span>
181
+ </div>
182
+
183
+ {/* Live log for running nodes */}
184
+ {isRunning && (
185
+ <div className="mt-2 p-2 bg-[var(--bg-tertiary)] rounded">
186
+ <LiveLog log={log} maxHeight={160} />
187
+ </div>
188
+ )}
189
+
190
+ {node.error && <div className="text-[10px] text-red-400 mt-1">{node.error}</div>}
191
+
192
+ {/* Outputs */}
193
+ {Object.keys(node.outputs).length > 0 && (
194
+ <div className="mt-2 space-y-1">
195
+ {Object.entries(node.outputs).map(([key, val]) => (
196
+ <details key={key} className="text-[10px]">
197
+ <summary className="cursor-pointer text-[var(--accent)]">output: {key} ({val.length} chars)</summary>
198
+ <pre className="mt-1 p-2 bg-[var(--bg-tertiary)] rounded text-[9px] text-[var(--text-secondary)] max-h-32 overflow-auto whitespace-pre-wrap">
199
+ {val.slice(0, 1000)}{val.length > 1000 ? '...' : ''}
200
+ </pre>
201
+ </details>
202
+ ))}
203
+ </div>
204
+ )}
205
+
206
+ {node.startedAt && (
207
+ <div className="text-[8px] text-[var(--text-secondary)] mt-1">
208
+ {`Started: ${new Date(node.startedAt).toLocaleTimeString()}`}
209
+ {node.completedAt && ` · Done: ${new Date(node.completedAt).toLocaleTimeString()}`}
210
+ </div>
211
+ )}
212
+ </div>
213
+ );
214
+ }
215
+
216
+ // ─── Agent color palette for conversation bubbles ────────
217
+ const AGENT_COLORS = [
218
+ { bg: 'bg-blue-500/10', border: 'border-blue-500/30', badge: 'bg-blue-500/20 text-blue-400', dot: 'text-blue-400' },
219
+ { bg: 'bg-purple-500/10', border: 'border-purple-500/30', badge: 'bg-purple-500/20 text-purple-400', dot: 'text-purple-400' },
220
+ { bg: 'bg-green-500/10', border: 'border-green-500/30', badge: 'bg-green-500/20 text-green-400', dot: 'text-green-400' },
221
+ { bg: 'bg-orange-500/10', border: 'border-orange-500/30', badge: 'bg-orange-500/20 text-orange-400', dot: 'text-orange-400' },
222
+ { bg: 'bg-pink-500/10', border: 'border-pink-500/30', badge: 'bg-pink-500/20 text-pink-400', dot: 'text-pink-400' },
223
+ ];
224
+
225
+ function ConversationMessageBubble({ msg, colors, agentDef, isLeft, onViewTask }: {
226
+ msg: ConversationMessage; colors: typeof AGENT_COLORS[0]; agentDef?: { id: string; role: string };
227
+ isLeft: boolean; onViewTask?: (taskId: string) => void;
228
+ }) {
229
+ const isRunning = msg.status === 'running';
230
+ const { log } = useTaskStream(msg.taskId, isRunning);
231
+
232
+ return (
233
+ <div className={`flex ${isLeft ? 'justify-start' : 'justify-end'}`}>
234
+ <div className={`max-w-[85%] border rounded-lg p-3 ${colors.bg} ${colors.border}`}>
235
+ {/* Agent header */}
236
+ <div className="flex items-center gap-2 mb-1.5">
237
+ <span className={`text-[9px] px-1.5 py-0.5 rounded font-medium ${colors.badge}`}>{msg.agentName}</span>
238
+ <span className="text-[8px] text-[var(--text-secondary)]">
239
+ {msg.agentId}{agentDef?.role ? ` — ${agentDef.role.slice(0, 40)}${agentDef.role.length > 40 ? '...' : ''}` : ''}
240
+ </span>
241
+ {isRunning && <span className="text-[8px] text-yellow-400 animate-pulse">● running</span>}
242
+ <span className="text-[8px] text-[var(--text-secondary)] ml-auto">R{msg.round}</span>
243
+ </div>
244
+
245
+ {/* Content */}
246
+ {isRunning ? (
247
+ <LiveLog log={log} maxHeight={250} />
248
+ ) : msg.status === 'failed' ? (
249
+ <div className="text-[10px] text-red-400">{msg.content || 'Failed'}</div>
250
+ ) : (
251
+ <div className="text-[10px] text-[var(--text-primary)] whitespace-pre-wrap leading-relaxed">
252
+ {msg.content.slice(0, 3000)}{msg.content.length > 3000 ? '\n\n[... truncated]' : ''}
253
+ </div>
254
+ )}
255
+
256
+ {/* Footer */}
257
+ <div className="flex items-center gap-2 mt-1.5">
258
+ {msg.taskId && (
259
+ <button onClick={() => onViewTask?.(msg.taskId!)} className="text-[8px] text-[var(--accent)] font-mono hover:underline">
260
+ task:{msg.taskId}
261
+ </button>
262
+ )}
263
+ <span className="text-[7px] text-[var(--text-secondary)] ml-auto">{new Date(msg.timestamp).toLocaleTimeString()}</span>
264
+ </div>
265
+ </div>
266
+ </div>
267
+ );
268
+ }
269
+
270
+ const ConversationGraphView = lazy(() => import('./ConversationGraphView'));
271
+ const ConversationTerminalView = lazy(() => import('./ConversationTerminalView'));
272
+
273
+ function ConversationView({ pipeline, onViewTask }: { pipeline: Pipeline; onViewTask?: (taskId: string) => void }) {
274
+ const conv = pipeline.conversation!;
275
+ const { config, messages, currentRound } = conv;
276
+ const [injectText, setInjectText] = useState('');
277
+ const [injectTarget, setInjectTarget] = useState(config.agents[0]?.id || '');
278
+ const [injecting, setInjecting] = useState(false);
279
+ const [viewMode, setViewMode] = useState<'terminal' | 'graph' | 'chat'>('terminal');
280
+ const scrollRef = useRef<HTMLDivElement>(null);
281
+
282
+ // Assign stable colors per agent
283
+ const agentColorMap: Record<string, typeof AGENT_COLORS[0]> = {};
284
+ config.agents.forEach((a, i) => {
285
+ agentColorMap[a.id] = AGENT_COLORS[i % AGENT_COLORS.length];
286
+ });
287
+
288
+ // Auto-scroll on new messages
289
+ useEffect(() => {
290
+ scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' });
291
+ }, [messages.length]);
292
+
293
+ const handleInject = async () => {
294
+ if (!injectText.trim() || injecting) return;
295
+ setInjecting(true);
296
+ try {
297
+ await fetch(`/api/pipelines/${pipeline.id}`, {
298
+ method: 'POST',
299
+ headers: { 'Content-Type': 'application/json' },
300
+ body: JSON.stringify({ action: 'inject', agentId: injectTarget, message: injectText }),
301
+ });
302
+ setInjectText('');
303
+ } catch {}
304
+ setInjecting(false);
305
+ };
306
+
307
+ return (
308
+ <div className="flex-1 flex flex-col overflow-hidden">
309
+ {/* Conversation info bar */}
310
+ <div className="px-4 py-2 border-b border-[var(--border)] bg-[var(--bg-tertiary)]/50 shrink-0">
311
+ <div className="flex items-center gap-3">
312
+ <span className="text-[9px] px-1.5 py-0.5 rounded bg-[var(--accent)]/20 text-[var(--accent)] font-medium">Conversation</span>
313
+ <span className="text-[9px] text-[var(--text-secondary)]">Round {currentRound}/{config.maxRounds}</span>
314
+ <div className="flex items-center gap-2 ml-auto">
315
+ {/* View mode toggle */}
316
+ <div className="flex border border-[var(--border)] rounded overflow-hidden">
317
+ {(['terminal', 'graph', 'chat'] as const).map(mode => (
318
+ <button
319
+ key={mode}
320
+ onClick={() => setViewMode(mode)}
321
+ className={`text-[8px] px-2 py-0.5 capitalize ${viewMode === mode ? 'bg-[var(--accent)]/20 text-[var(--accent)]' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'}`}
322
+ >{mode}</button>
323
+ ))}
324
+ </div>
325
+ {config.agents.map(a => {
326
+ const colors = agentColorMap[a.id];
327
+ const isRunning = messages.some(m => m.agentId === a.id && m.status === 'running');
328
+ return (
329
+ <span key={a.id} className={`text-[8px] px-1.5 py-0.5 rounded ${colors.badge} ${isRunning ? 'ring-1 ring-yellow-400/50' : ''}`}>
330
+ {isRunning ? '● ' : ''}{a.id} ({a.agent})
331
+ </span>
332
+ );
333
+ })}
334
+ </div>
335
+ </div>
336
+ {config.stopCondition && (
337
+ <div className="text-[8px] text-[var(--text-secondary)] mt-1">Stop: {config.stopCondition}</div>
338
+ )}
339
+ </div>
340
+
341
+ {/* Terminal / Graph / Chat view */}
342
+ {viewMode === 'terminal' ? (
343
+ <div className="flex-1 min-h-0">
344
+ <Suspense fallback={<div className="flex items-center justify-center h-full text-xs text-[var(--text-secondary)]">Loading...</div>}>
345
+ <ConversationTerminalView pipeline={pipeline} onViewTask={onViewTask} />
346
+ </Suspense>
347
+ </div>
348
+ ) : viewMode === 'graph' ? (
349
+ <div className="flex-1 min-h-0">
350
+ <Suspense fallback={<div className="flex items-center justify-center h-full text-xs text-[var(--text-secondary)]">Loading graph...</div>}>
351
+ <ConversationGraphView pipeline={pipeline} />
352
+ </Suspense>
353
+ </div>
354
+ ) : (
355
+ <>
356
+ {/* Initial prompt */}
357
+ <div className="px-4 pt-3">
358
+ <div className="border border-[var(--border)] rounded-lg p-3 bg-[var(--bg-tertiary)]/50">
359
+ <div className="text-[9px] text-[var(--text-secondary)] font-medium mb-1">Initial Prompt</div>
360
+ <div className="text-[11px] text-[var(--text-primary)] whitespace-pre-wrap">{config.initialPrompt}</div>
361
+ </div>
362
+ </div>
363
+
364
+ {/* Messages — chat-like view with live logs */}
365
+ <div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-3 space-y-3">
366
+ {messages.map((msg, i) => {
367
+ const colors = agentColorMap[msg.agentId] || AGENT_COLORS[0];
368
+ const agentDef = config.agents.find(a => a.id === msg.agentId);
369
+ const isLeft = config.agents.indexOf(agentDef!) % 2 === 0;
370
+
371
+ return (
372
+ <ConversationMessageBubble
373
+ key={`${msg.taskId || i}-${msg.status}`}
374
+ msg={msg}
375
+ colors={colors}
376
+ agentDef={agentDef}
377
+ isLeft={isLeft}
378
+ onViewTask={onViewTask}
379
+ />
380
+ );
381
+ })}
382
+
383
+ {/* Completion indicator */}
384
+ {pipeline.status === 'done' && (
385
+ <div className="flex justify-center py-2">
386
+ <span className="text-[10px] px-3 py-1 rounded-full bg-green-500/10 text-green-400 border border-green-500/30">
387
+ Conversation complete — {messages.length} messages in {Math.max(...messages.map(m => m.round), 0)} rounds
388
+ </span>
389
+ </div>
390
+ )}
391
+ {pipeline.status === 'failed' && (
392
+ <div className="flex justify-center py-2">
393
+ <span className="text-[10px] px-3 py-1 rounded-full bg-red-500/10 text-red-400 border border-red-500/30">
394
+ Conversation failed
395
+ </span>
396
+ </div>
397
+ )}
398
+ </div>
399
+ </>
400
+ )}
401
+
402
+ {/* Inject command bar */}
403
+ {pipeline.status === 'running' && (
404
+ <div className="px-4 py-2 border-t border-[var(--border)] bg-[var(--bg-tertiary)]/50 shrink-0">
405
+ <div className="flex items-center gap-2">
406
+ <select
407
+ value={injectTarget}
408
+ onChange={e => setInjectTarget(e.target.value)}
409
+ className="text-[10px] bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-1 text-[var(--text-primary)]"
410
+ >
411
+ {config.agents.map(a => (
412
+ <option key={a.id} value={a.id}>@{a.id}</option>
413
+ ))}
414
+ </select>
415
+ <input
416
+ value={injectText}
417
+ onChange={e => setInjectText(e.target.value)}
418
+ onKeyDown={e => e.key === 'Enter' && !e.shiftKey && handleInject()}
419
+ placeholder="Send instruction to agent..."
420
+ className="flex-1 text-[10px] bg-[var(--bg-primary)] border border-[var(--border)] rounded px-2 py-1 text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
421
+ />
422
+ <button
423
+ onClick={handleInject}
424
+ disabled={!injectText.trim() || injecting}
425
+ className="text-[10px] px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90 disabled:opacity-50"
426
+ >Send</button>
427
+ </div>
428
+ </div>
429
+ )}
430
+ </div>
431
+ );
432
+ }
433
+
434
+ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandled }: { onViewTask?: (taskId: string) => void; focusPipelineId?: string | null; onFocusHandled?: () => void }) {
435
+ const { sidebarWidth, onSidebarDragStart } = useSidebarResize({ defaultWidth: 256, minWidth: 140, maxWidth: 480 });
436
+ const [pipelines, setPipelines] = useState<Pipeline[]>([]);
437
+ const [workflows, setWorkflows] = useState<Workflow[]>([]);
438
+ const [selectedPipeline, setSelectedPipeline] = useState<Pipeline | null>(null);
439
+ const [activeWorkflow, setActiveWorkflow] = useState<string | null>(null); // selected workflow in left panel
440
+ const [showCreate, setShowCreate] = useState(false);
441
+ const [selectedWorkflow, setSelectedWorkflow] = useState<string>('');
442
+ const [projects, setProjects] = useState<{ name: string; path: string }[]>([]);
443
+ const [inputValues, setInputValues] = useState<Record<string, string>>({});
444
+ const [creating, setCreating] = useState(false);
445
+ const [showEditor, setShowEditor] = useState(false);
446
+ const [editorYaml, setEditorYaml] = useState<string | undefined>(undefined);
447
+ const [editorIsConversation, setEditorIsConversation] = useState(false);
448
+ const [showImport, setShowImport] = useState(false);
449
+ const [importYaml, setImportYaml] = useState('');
450
+ const [agents, setAgents] = useState<{ id: string; name: string; detected?: boolean }[]>([]);
451
+
452
+ const fetchData = useCallback(async () => {
453
+ const [pRes, wRes, projRes, agentRes] = await Promise.all([
454
+ fetch('/api/pipelines'),
455
+ fetch('/api/pipelines?type=workflows'),
456
+ fetch('/api/projects'),
457
+ fetch('/api/agents'),
458
+ ]);
459
+ const pData = await pRes.json();
460
+ const wData = await wRes.json();
461
+ const projData = await projRes.json();
462
+ const agentData = await agentRes.json();
463
+ if (Array.isArray(pData)) setPipelines(pData);
464
+ if (Array.isArray(wData)) setWorkflows(wData);
465
+ if (Array.isArray(projData)) setProjects(projData.map((p: any) => ({ name: p.name, path: p.path })));
466
+ if (Array.isArray(agentData?.agents)) setAgents(agentData.agents);
467
+ }, []);
468
+
469
+ useEffect(() => {
470
+ fetchData();
471
+ const timer = setInterval(fetchData, 5000);
472
+ return () => clearInterval(timer);
473
+ }, [fetchData]);
474
+
475
+ // Focus on a specific pipeline (from external navigation)
476
+ useEffect(() => {
477
+ if (!focusPipelineId || pipelines.length === 0) return;
478
+ const target = pipelines.find(p => p.id === focusPipelineId);
479
+ if (target) {
480
+ setSelectedPipeline(target);
481
+ setShowEditor(false);
482
+ onFocusHandled?.();
483
+ }
484
+ }, [focusPipelineId, pipelines, onFocusHandled]);
485
+
486
+ // Refresh selected pipeline
487
+ useEffect(() => {
488
+ if (!selectedPipeline || selectedPipeline.status !== 'running') return;
489
+ const timer = setInterval(async () => {
490
+ const res = await fetch(`/api/pipelines/${selectedPipeline.id}`);
491
+ const data = await res.json();
492
+ if (data.id) setSelectedPipeline(data);
493
+ }, 3000);
494
+ return () => clearInterval(timer);
495
+ }, [selectedPipeline?.id, selectedPipeline?.status]);
496
+
497
+ const handleCreate = async () => {
498
+ if (!selectedWorkflow) return;
499
+ setCreating(true);
500
+ try {
501
+ const res = await fetch('/api/pipelines', {
502
+ method: 'POST',
503
+ headers: { 'Content-Type': 'application/json' },
504
+ body: JSON.stringify({ workflow: selectedWorkflow, input: inputValues }),
505
+ });
506
+ const data = await res.json();
507
+ if (data.id) {
508
+ setSelectedPipeline(data);
509
+ setShowCreate(false);
510
+ setInputValues({});
511
+ fetchData();
512
+ }
513
+ } catch {}
514
+ setCreating(false);
515
+ };
516
+
517
+ const handleCancel = async (id: string) => {
518
+ await fetch(`/api/pipelines/${id}`, {
519
+ method: 'POST',
520
+ headers: { 'Content-Type': 'application/json' },
521
+ body: JSON.stringify({ action: 'cancel' }),
522
+ });
523
+ fetchData();
524
+ if (selectedPipeline?.id === id) {
525
+ const res = await fetch(`/api/pipelines/${id}`);
526
+ setSelectedPipeline(await res.json());
527
+ }
528
+ };
529
+
530
+ const handleDelete = async (id: string) => {
531
+ if (!confirm('Delete this pipeline?')) return;
532
+ await fetch(`/api/pipelines/${id}`, {
533
+ method: 'POST',
534
+ headers: { 'Content-Type': 'application/json' },
535
+ body: JSON.stringify({ action: 'delete' }),
536
+ });
537
+ if (selectedPipeline?.id === id) setSelectedPipeline(null);
538
+ fetchData();
539
+ };
540
+
541
+ const generateConversationTemplate = () => {
542
+ const detectedAgents = agents.filter(a => a.detected);
543
+ const agentEntries = detectedAgents.length >= 2
544
+ ? detectedAgents.slice(0, 2)
545
+ : [{ id: 'claude', name: 'Claude Code' }, { id: 'claude', name: 'Claude Code' }];
546
+
547
+ return `name: my-conversation
548
+ type: conversation
549
+ description: "Multi-agent collaboration"
550
+ input:
551
+ project: "Project name"
552
+ task: "Task description"
553
+ agents:
554
+ - id: designer
555
+ agent: ${agentEntries[0].id}
556
+ role: "You are a software architect. Design the solution and review implementations."
557
+ - id: builder
558
+ agent: ${agentEntries[1].id}
559
+ role: "You are a developer. Implement what the designer proposes."
560
+ max_rounds: 5
561
+ stop_condition: "both agents say DONE"
562
+ initial_prompt: "{{input.task}}"
563
+ `;
564
+ };
565
+
566
+ const currentWorkflow = workflows.find(w => w.name === selectedWorkflow);
567
+
568
+ return (
569
+ <div className="flex-1 flex min-h-0">
570
+ {/* Left — Workflow list */}
571
+ <aside style={{ width: sidebarWidth }} className="flex flex-col shrink-0 overflow-hidden">
572
+ <div className="px-3 py-2 border-b border-[var(--border)] flex items-center gap-1.5">
573
+ <span className="text-[11px] font-semibold text-[var(--text-primary)] flex-1">Workflows</span>
574
+ <button
575
+ onClick={() => setShowImport(v => !v)}
576
+ className="text-[9px] text-green-400 hover:underline"
577
+ >Import</button>
578
+ <button
579
+ onClick={() => { setEditorYaml(undefined); setEditorIsConversation(false); setShowEditor(true); }}
580
+ className="text-[9px] text-[var(--accent)] hover:underline"
581
+ >+ DAG</button>
582
+ <button
583
+ onClick={() => { setImportYaml(generateConversationTemplate()); setShowImport(true); }}
584
+ className="text-[9px] text-purple-400 hover:underline"
585
+ >+ Conversation</button>
586
+ </div>
587
+
588
+ {/* Import form */}
589
+ {showImport && (
590
+ <div className="p-3 border-b border-[var(--border)] space-y-2">
591
+ <textarea
592
+ value={importYaml}
593
+ onChange={e => setImportYaml(e.target.value)}
594
+ placeholder="Paste YAML workflow here..."
595
+ className="w-full h-40 text-xs font-mono bg-[var(--bg-tertiary)] border border-[var(--border)] rounded p-2 text-[var(--text-primary)] resize-none focus:outline-none focus:border-[var(--accent)]"
596
+ spellCheck={false}
597
+ />
598
+ <div className="flex gap-2">
599
+ <button
600
+ onClick={async () => {
601
+ if (!importYaml.trim()) return;
602
+ try {
603
+ const res = await fetch('/api/pipelines', {
604
+ method: 'POST',
605
+ headers: { 'Content-Type': 'application/json' },
606
+ body: JSON.stringify({ action: 'save-workflow', yaml: importYaml }),
607
+ });
608
+ const data = await res.json();
609
+ if (data.ok) {
610
+ setShowImport(false);
611
+ setImportYaml('');
612
+ fetchData();
613
+ alert(`Workflow "${data.name}" imported successfully`);
614
+ } else {
615
+ alert(`Import failed: ${data.error}`);
616
+ }
617
+ } catch { alert('Import failed'); }
618
+ }}
619
+ disabled={!importYaml.trim()}
620
+ className="text-[10px] px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90 disabled:opacity-50"
621
+ >Save Workflow</button>
622
+ <button
623
+ onClick={() => { setShowImport(false); setImportYaml(''); }}
624
+ className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
625
+ >Cancel</button>
626
+ </div>
627
+ </div>
628
+ )}
629
+
630
+ {/* Create form */}
631
+ {showCreate && (
632
+ <div className="p-3 border-b border-[var(--border)] space-y-2">
633
+ <select
634
+ value={selectedWorkflow}
635
+ onChange={e => { setSelectedWorkflow(e.target.value); setInputValues({}); }}
636
+ className="w-full text-xs bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-1.5 text-[var(--text-primary)]"
637
+ >
638
+ <option value="">Select workflow...</option>
639
+ {workflows.map(w => (
640
+ <option key={w.name} value={w.name}>{w.builtin ? '[Built-in] ' : ''}{w.name}{w.description ? ` — ${w.description}` : ''}</option>
641
+ ))}
642
+ </select>
643
+
644
+ {/* Input fields — project fields get a dropdown */}
645
+ {currentWorkflow && Object.keys(currentWorkflow.input).length > 0 && (
646
+ <div className="space-y-1.5">
647
+ {Object.entries(currentWorkflow.input).map(([key, desc]) => (
648
+ <div key={key}>
649
+ <label className="text-[9px] text-[var(--text-secondary)]">{key}: {desc}</label>
650
+ {key.toLowerCase() === 'project' ? (
651
+ <select
652
+ value={inputValues[key] || ''}
653
+ onChange={e => setInputValues(prev => ({ ...prev, [key]: e.target.value }))}
654
+ className="w-full text-xs bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-1.5 text-[var(--text-primary)]"
655
+ >
656
+ <option value="">Select project...</option>
657
+ {projects.map(p => <option key={p.path} value={p.name}>{p.name}</option>)}
658
+ </select>
659
+ ) : (
660
+ <input
661
+ value={inputValues[key] || ''}
662
+ onChange={e => setInputValues(prev => ({ ...prev, [key]: e.target.value }))}
663
+ className="w-full text-xs bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-1 text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
664
+ />
665
+ )}
666
+ </div>
667
+ ))}
668
+ </div>
669
+ )}
670
+
671
+ {/* Workflow preview */}
672
+ {currentWorkflow && (
673
+ <div className="text-[9px] text-[var(--text-secondary)] space-y-0.5">
674
+ {Object.entries(currentWorkflow.nodes).map(([id, node]) => (
675
+ <div key={id} className="flex items-center gap-1">
676
+ <span className="text-[var(--accent)]">{id}</span>
677
+ {node.dependsOn.length > 0 && <span>← {node.dependsOn.join(', ')}</span>}
678
+ <span className="text-[var(--text-secondary)] truncate ml-auto">{node.project}</span>
679
+ </div>
680
+ ))}
681
+ </div>
682
+ )}
683
+
684
+ <button
685
+ onClick={handleCreate}
686
+ disabled={!selectedWorkflow || creating}
687
+ className="w-full text-[10px] px-2 py-1.5 bg-[var(--accent)] text-white rounded hover:opacity-90 disabled:opacity-50"
688
+ >
689
+ {creating ? 'Starting...' : 'Start Pipeline'}
690
+ </button>
691
+
692
+ {workflows.length === 0 && (
693
+ <p className="text-[9px] text-[var(--text-secondary)]">
694
+ No workflows found. Create YAML files in ~/.forge/flows/
695
+ </p>
696
+ )}
697
+ </div>
698
+ )}
699
+
700
+ {/* Workflow list + execution history */}
701
+ <div className="flex-1 overflow-y-auto">
702
+ {workflows.map(w => {
703
+ const isActive = activeWorkflow === w.name;
704
+ const runs = pipelines.filter(p => p.workflowName === w.name);
705
+ return (
706
+ <div key={w.name}>
707
+ <div
708
+ onClick={() => { setActiveWorkflow(isActive ? null : w.name); setSelectedPipeline(null); }}
709
+ className={`w-full text-left px-3 py-2 border-b border-[var(--border)]/30 flex items-center gap-2 cursor-pointer ${
710
+ isActive ? 'bg-[var(--accent)]/10 border-l-2 border-l-[var(--accent)]' : 'hover:bg-[var(--bg-tertiary)] border-l-2 border-l-transparent'
711
+ }`}
712
+ >
713
+ <span className="text-[8px] text-[var(--text-secondary)]">{isActive ? '▾' : '▸'}</span>
714
+ {w.builtin && <span className="text-[7px] text-[var(--text-secondary)]">⚙</span>}
715
+ <span className="text-[11px] text-[var(--text-primary)] truncate flex-1">{w.name}</span>
716
+ {runs.length > 0 && <span className="text-[8px] text-[var(--text-secondary)]">{runs.length}</span>}
717
+ <button
718
+ onClick={async (e) => {
719
+ e.stopPropagation();
720
+ setSelectedWorkflow(w.name);
721
+ setInputValues({});
722
+ setShowCreate(true);
723
+ setActiveWorkflow(w.name);
724
+ }}
725
+ className="text-[8px] text-[var(--accent)] hover:underline shrink-0"
726
+ title="Run this workflow"
727
+ >Run</button>
728
+ <button
729
+ onClick={async (e) => {
730
+ e.stopPropagation();
731
+ try {
732
+ const res = await fetch(`/api/pipelines?type=workflow-yaml&name=${encodeURIComponent(w.name)}`);
733
+ const data = await res.json();
734
+ setEditorYaml(data.yaml || undefined);
735
+ setEditorIsConversation(w.type === 'conversation' || (data.yaml || '').includes('type: conversation'));
736
+ } catch { setEditorYaml(undefined); setEditorIsConversation(false); }
737
+ setShowEditor(true);
738
+ }}
739
+ className="text-[8px] text-green-400 hover:underline shrink-0"
740
+ title={w.builtin ? 'View YAML' : 'Edit'}
741
+ >{w.builtin ? 'View' : 'Edit'}</button>
742
+ </div>
743
+ {/* Execution history for this workflow */}
744
+ {isActive && (
745
+ <div className="bg-[var(--bg-tertiary)]/50">
746
+ {runs.length === 0 ? (
747
+ <div className="px-4 py-2 text-[9px] text-[var(--text-secondary)]">No runs yet</div>
748
+ ) : (
749
+ runs.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, 20).map(p => (
750
+ <button
751
+ key={p.id}
752
+ onClick={() => setSelectedPipeline(p)}
753
+ className={`w-full text-left px-4 py-1.5 border-b border-[var(--border)]/20 hover:bg-[var(--bg-tertiary)] ${
754
+ selectedPipeline?.id === p.id ? 'bg-[var(--accent)]/5' : ''
755
+ }`}
756
+ >
757
+ <div className="flex items-center gap-1.5">
758
+ <span className={`text-[9px] ${STATUS_COLOR[p.status]}`}>●</span>
759
+ <span className="text-[9px] text-[var(--text-secondary)] font-mono">{p.id.slice(0, 8)}</span>
760
+ {p.type === 'conversation' ? (
761
+ <span className="text-[7px] px-1 rounded bg-[var(--accent)]/15 text-[var(--accent)]">
762
+ R{p.conversation?.currentRound || 0}/{p.conversation?.config.maxRounds || '?'}
763
+ </span>
764
+ ) : (
765
+ <div className="flex gap-0.5 ml-1">
766
+ {p.nodeOrder.map(nodeId => (
767
+ <span key={nodeId} className={`text-[8px] ${STATUS_COLOR[p.nodes[nodeId]?.status || 'pending']}`}>
768
+ {STATUS_ICON[p.nodes[nodeId]?.status || 'pending']}
769
+ </span>
770
+ ))}
771
+ </div>
772
+ )}
773
+ <span className="text-[8px] text-[var(--text-secondary)] ml-auto">
774
+ {new Date(p.createdAt).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
775
+ </span>
776
+ </div>
777
+ </button>
778
+ ))
779
+ )}
780
+ </div>
781
+ )}
782
+ </div>
783
+ );
784
+ })}
785
+ {workflows.length === 0 && (
786
+ <div className="p-4 text-center text-xs text-[var(--text-secondary)]">
787
+ No workflows. Click Import or + New to create one.
788
+ </div>
789
+ )}
790
+ </div>
791
+ </aside>
792
+
793
+ {/* Sidebar resize handle */}
794
+ <div
795
+ onMouseDown={onSidebarDragStart}
796
+ className="w-1 bg-[var(--border)] cursor-col-resize shrink-0 hover:bg-[var(--accent)]/50 transition-colors"
797
+ />
798
+
799
+ {/* Right — Pipeline detail / Editor */}
800
+ <main className="flex-1 flex flex-col min-w-0 overflow-hidden">
801
+ {showEditor ? (
802
+ editorIsConversation ? (
803
+ <Suspense fallback={<div className="flex-1 flex items-center justify-center text-xs text-[var(--text-secondary)]">Loading editor...</div>}>
804
+ <ConversationEditor
805
+ initialYaml={editorYaml || ''}
806
+ onSave={async (yaml) => {
807
+ await fetch('/api/pipelines', {
808
+ method: 'POST',
809
+ headers: { 'Content-Type': 'application/json' },
810
+ body: JSON.stringify({ action: 'save-workflow', yaml }),
811
+ });
812
+ setShowEditor(false);
813
+ fetchData();
814
+ }}
815
+ onClose={() => setShowEditor(false)}
816
+ />
817
+ </Suspense>
818
+ ) : (
819
+ <Suspense fallback={<div className="flex-1 flex items-center justify-center text-xs text-[var(--text-secondary)]">Loading editor...</div>}>
820
+ <PipelineEditor
821
+ initialYaml={editorYaml}
822
+ onSave={async (yaml) => {
823
+ await fetch('/api/pipelines', {
824
+ method: 'POST',
825
+ headers: { 'Content-Type': 'application/json' },
826
+ body: JSON.stringify({ action: 'save-workflow', yaml }),
827
+ });
828
+ setShowEditor(false);
829
+ fetchData();
830
+ }}
831
+ onClose={() => setShowEditor(false)}
832
+ />
833
+ </Suspense>
834
+ )
835
+ ) : selectedPipeline ? (
836
+ <>
837
+ {/* Header */}
838
+ <div className="px-4 py-3 border-b border-[var(--border)] shrink-0">
839
+ <div className="flex items-center gap-2">
840
+ <span className={`text-sm ${STATUS_COLOR[selectedPipeline.status]}`}>
841
+ {STATUS_ICON[selectedPipeline.status]}
842
+ </span>
843
+ <span className="text-sm font-semibold text-[var(--text-primary)]">{selectedPipeline.workflowName}</span>
844
+ {selectedPipeline.type === 'conversation' && (
845
+ <span className="text-[8px] px-1.5 py-0.5 rounded bg-[var(--accent)]/20 text-[var(--accent)]">conversation</span>
846
+ )}
847
+ <span className="text-[10px] text-[var(--text-secondary)] font-mono">{selectedPipeline.id}</span>
848
+ <div className="flex items-center gap-2 ml-auto">
849
+ {selectedPipeline.status === 'running' && (
850
+ <button
851
+ onClick={() => handleCancel(selectedPipeline.id)}
852
+ className="text-[10px] px-2 py-0.5 text-red-400 border border-red-400/30 rounded hover:bg-red-400 hover:text-white"
853
+ >
854
+ Cancel
855
+ </button>
856
+ )}
857
+ <button
858
+ onClick={() => handleDelete(selectedPipeline.id)}
859
+ className="text-[10px] px-2 py-0.5 text-[var(--text-secondary)] hover:text-red-400"
860
+ >
861
+ Delete
862
+ </button>
863
+ </div>
864
+ </div>
865
+ <div className="text-[9px] text-[var(--text-secondary)] mt-1">
866
+ Started: {new Date(selectedPipeline.createdAt).toLocaleString()}
867
+ {selectedPipeline.completedAt && ` · Completed: ${new Date(selectedPipeline.completedAt).toLocaleString()}`}
868
+ </div>
869
+ {Object.keys(selectedPipeline.input).length > 0 && (
870
+ <div className="text-[9px] text-[var(--text-secondary)] mt-1">
871
+ Input: {Object.entries(selectedPipeline.input).map(([k, v]) => `${k}="${v}"`).join(', ')}
872
+ </div>
873
+ )}
874
+ </div>
875
+
876
+ {/* Conversation or DAG visualization */}
877
+ {selectedPipeline.type === 'conversation' && selectedPipeline.conversation ? (
878
+ <ConversationView
879
+ pipeline={selectedPipeline}
880
+ onViewTask={onViewTask}
881
+ />
882
+ ) : (
883
+ <div className="p-4 space-y-2 overflow-y-auto">
884
+ {selectedPipeline.nodeOrder.map((nodeId, idx) => {
885
+ const node = selectedPipeline.nodes[nodeId];
886
+ const wf = workflows.find(w => w.name === selectedPipeline.workflowName);
887
+ const nodeDef = wf?.nodes?.[nodeId];
888
+ return (
889
+ <div key={nodeId}>
890
+ {idx > 0 && (
891
+ <div className="flex items-center pl-5 py-1">
892
+ <div className="w-px h-4 bg-[var(--border)]" />
893
+ </div>
894
+ )}
895
+ <DagNodeCard nodeId={nodeId} node={node} nodeDef={nodeDef} onViewTask={onViewTask} />
896
+ </div>
897
+ );
898
+ })}
899
+ </div>
900
+ )}
901
+ </>
902
+ ) : activeWorkflow ? (() => {
903
+ const w = workflows.find(wf => wf.name === activeWorkflow);
904
+ if (!w) return null;
905
+ const nodeEntries = Object.entries(w.nodes);
906
+ return (
907
+ <div className="flex-1 flex flex-col overflow-y-auto">
908
+ {/* Workflow header */}
909
+ <div className="px-4 py-3 border-b border-[var(--border)] shrink-0">
910
+ <div className="flex items-center gap-2">
911
+ <span className="text-sm font-semibold text-[var(--text-primary)]">{w.name}</span>
912
+ {w.type === 'conversation' && <span className="text-[8px] px-1.5 py-0.5 rounded bg-[var(--accent)]/20 text-[var(--accent)]">conversation</span>}
913
+ {w.builtin && <span className="text-[8px] px-1.5 py-0.5 rounded bg-[var(--bg-tertiary)] text-[var(--text-secondary)]">built-in</span>}
914
+ <div className="ml-auto flex gap-2">
915
+ <button
916
+ onClick={() => { setSelectedWorkflow(w.name); setInputValues({}); setShowCreate(true); }}
917
+ className="text-[10px] px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90"
918
+ >Run</button>
919
+ <button
920
+ onClick={async () => {
921
+ try {
922
+ const res = await fetch(`/api/pipelines?type=workflow-yaml&name=${encodeURIComponent(w.name)}`);
923
+ const data = await res.json();
924
+ setEditorYaml(data.yaml || undefined);
925
+ setEditorIsConversation(w.type === 'conversation' || (data.yaml || '').includes('type: conversation'));
926
+ } catch { setEditorYaml(undefined); setEditorIsConversation(false); }
927
+ setShowEditor(true);
928
+ }}
929
+ className="text-[10px] px-3 py-1 border border-[var(--border)] text-[var(--text-secondary)] rounded hover:text-[var(--text-primary)]"
930
+ >{w.builtin ? 'View YAML' : 'Edit'}</button>
931
+ </div>
932
+ </div>
933
+ {w.description && <p className="text-[10px] text-[var(--text-secondary)] mt-1">{w.description}</p>}
934
+ {Object.keys(w.input).length > 0 && (
935
+ <div className="mt-2 flex flex-wrap gap-1">
936
+ {Object.entries(w.input).map(([k, v]) => (
937
+ <span key={k} className="text-[8px] px-1.5 py-0.5 rounded bg-[var(--accent)]/10 text-[var(--accent)]">{k}</span>
938
+ ))}
939
+ </div>
940
+ )}
941
+ </div>
942
+
943
+ {/* Conversation or Node flow visualization */}
944
+ {w.type === 'conversation' && w.conversation ? (
945
+ <div className="p-4 space-y-3">
946
+ {/* Initial prompt */}
947
+ <div className="border border-[var(--border)] rounded-lg p-3 bg-[var(--bg-tertiary)]">
948
+ <div className="text-[9px] text-[var(--text-secondary)] font-medium mb-1">Initial Prompt</div>
949
+ <p className="text-[10px] text-[var(--text-primary)]">{w.conversation.initialPrompt}</p>
950
+ </div>
951
+ {/* Agents */}
952
+ <div className="text-[9px] text-[var(--text-secondary)] font-medium">Agents ({w.conversation.agents.length})</div>
953
+ <div className="space-y-2">
954
+ {w.conversation.agents.map((a, i) => {
955
+ const colors = AGENT_COLORS[i % AGENT_COLORS.length];
956
+ return (
957
+ <div key={a.id} className={`border rounded-lg p-3 ${colors.bg} ${colors.border}`}>
958
+ <div className="flex items-center gap-2">
959
+ <span className={`text-[9px] px-1.5 py-0.5 rounded font-medium ${colors.badge}`}>{a.agent}</span>
960
+ <span className="text-[11px] font-semibold text-[var(--text-primary)]">{a.id}</span>
961
+ </div>
962
+ {a.role && <p className="text-[9px] text-[var(--text-secondary)] mt-1">{a.role}</p>}
963
+ </div>
964
+ );
965
+ })}
966
+ </div>
967
+ {/* Config */}
968
+ <div className="text-[9px] text-[var(--text-secondary)] space-y-0.5">
969
+ <div>Max rounds: {w.conversation.maxRounds}</div>
970
+ {w.conversation.stopCondition && <div>Stop: {w.conversation.stopCondition}</div>}
971
+ </div>
972
+ </div>
973
+ ) : (
974
+ <div className="p-4 space-y-2">
975
+ {nodeEntries.map(([nodeId, node], i) => (
976
+ <div key={nodeId}>
977
+ {/* Connection line */}
978
+ {i > 0 && (
979
+ <div className="flex items-center justify-center py-1">
980
+ <div className="w-px h-4 bg-[var(--border)]" />
981
+ </div>
982
+ )}
983
+ {/* Node card */}
984
+ <div className="border border-[var(--border)] rounded-lg p-3 bg-[var(--bg-tertiary)]">
985
+ <div className="flex items-center gap-2">
986
+ <span className={`text-[9px] px-1.5 py-0.5 rounded font-medium ${
987
+ node.mode === 'shell' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-purple-500/20 text-purple-400'
988
+ }`}>{node.mode === 'shell' ? 'shell' : (node.agent || 'default')}</span>
989
+ <span className="text-[11px] font-semibold text-[var(--text-primary)]">{nodeId}</span>
990
+ {node.project && <span className="text-[9px] text-[var(--text-secondary)] ml-auto">{node.project}</span>}
991
+ </div>
992
+ {node.dependsOn.length > 0 && (
993
+ <div className="text-[8px] text-[var(--text-secondary)] mt-1">depends: {node.dependsOn.join(', ')}</div>
994
+ )}
995
+ <p className="text-[9px] text-[var(--text-secondary)] mt-1 line-clamp-2">{node.prompt.slice(0, 120)}{node.prompt.length > 120 ? '...' : ''}</p>
996
+ {node.outputs.length > 0 && (
997
+ <div className="flex gap-1 mt-1">
998
+ {node.outputs.map(o => (
999
+ <span key={o.name} className="text-[7px] px-1 rounded bg-green-500/10 text-green-400">{o.name}</span>
1000
+ ))}
1001
+ </div>
1002
+ )}
1003
+ </div>
1004
+ </div>
1005
+ ))}
1006
+ </div>
1007
+ )}
1008
+ </div>
1009
+ );
1010
+ })() : (
1011
+ <div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">
1012
+ <p className="text-xs">Select a workflow to view details or run</p>
1013
+ </div>
1014
+ )}
1015
+ </main>
1016
+ </div>
1017
+ );
1018
+ }