@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,787 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef, lazy, Suspense } from 'react';
4
+ import type { WebTerminalHandle, WebTerminalProps } from './WebTerminal';
5
+ import { useSidebarResize } from '@/hooks/useSidebarResize';
6
+
7
+ const WebTerminal = lazy(() => import('./WebTerminal'));
8
+
9
+ interface FileNode {
10
+ name: string;
11
+ path: string;
12
+ type: 'file' | 'dir';
13
+ children?: FileNode[];
14
+ }
15
+
16
+ // โ”€โ”€โ”€ File Tree โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
17
+
18
+ type GitStatusMap = Map<string, string>; // path โ†’ status
19
+ type GitRepoMap = Map<string, { branch: string; remote: string }>; // dir name โ†’ repo info
20
+
21
+ function TreeNode({ node, depth, selected, onSelect, gitMap, repoMap }: {
22
+ node: FileNode;
23
+ depth: number;
24
+ selected: string | null;
25
+ onSelect: (path: string) => void;
26
+ gitMap: GitStatusMap;
27
+ repoMap: GitRepoMap;
28
+ }) {
29
+ // Auto-expand if selected file is under this directory
30
+ const containsSelected = selected ? selected.startsWith(node.path + '/') : false;
31
+ const [manualExpanded, setManualExpanded] = useState<boolean | null>(null);
32
+ const expanded = manualExpanded ?? (depth < 1 || containsSelected);
33
+
34
+ if (node.type === 'dir') {
35
+ const dirHasChanges = node.children?.some(c => hasGitChanges(c, gitMap));
36
+ const repo = repoMap.get(node.name);
37
+ return (
38
+ <div>
39
+ <button
40
+ onClick={() => setManualExpanded(v => v === null ? !expanded : !v)}
41
+ className="w-full text-left flex items-center gap-1 px-1 py-0.5 hover:bg-[var(--bg-tertiary)] rounded text-xs group"
42
+ style={{ paddingLeft: depth * 12 + 4 }}
43
+ title={repo ? `${repo.branch} ยท ${repo.remote.replace(/^https?:\/\//, '').replace(/^git@github\.com:/, 'github.com/').replace(/\.git$/, '')}` : undefined}
44
+ >
45
+ <span className="text-[10px] text-[var(--text-secondary)] w-3">{expanded ? 'โ–พ' : 'โ–ธ'}</span>
46
+ <span className={dirHasChanges ? 'text-yellow-400' : 'text-[var(--text-primary)]'}>{node.name}</span>
47
+ {repo && (
48
+ <span className="text-[8px] text-[var(--accent)] opacity-60 group-hover:opacity-100 ml-auto shrink-0">{repo.branch}</span>
49
+ )}
50
+ </button>
51
+ {expanded && node.children?.map(child => (
52
+ <TreeNode key={child.path} node={child} depth={depth + 1} selected={selected} onSelect={onSelect} gitMap={gitMap} repoMap={repoMap} />
53
+ ))}
54
+ </div>
55
+ );
56
+ }
57
+
58
+ const isSelected = selected === node.path;
59
+ const gitStatus = gitMap.get(node.path);
60
+ const gitColor = gitStatus
61
+ ? gitStatus.includes('M') ? 'text-yellow-400'
62
+ : gitStatus.includes('D') ? 'text-red-400'
63
+ : 'text-green-400' // A, ?, new
64
+ : '';
65
+
66
+ return (
67
+ <button
68
+ onClick={() => onSelect(node.path)}
69
+ className={`w-full text-left flex items-center gap-1 px-1 py-0.5 rounded text-xs truncate ${
70
+ isSelected ? 'bg-[var(--accent)]/20 text-[var(--accent)]'
71
+ : gitColor ? `hover:bg-[var(--bg-tertiary)] ${gitColor}`
72
+ : 'hover:bg-[var(--bg-tertiary)] text-[var(--text-secondary)]'
73
+ }`}
74
+ style={{ paddingLeft: depth * 12 + 16 }}
75
+ title={node.path}
76
+ >
77
+ {node.name}
78
+ </button>
79
+ );
80
+ }
81
+
82
+ function hasGitChanges(node: FileNode, gitMap: GitStatusMap): boolean {
83
+ if (node.type === 'file') return gitMap.has(node.path);
84
+ return node.children?.some(c => hasGitChanges(c, gitMap)) || false;
85
+ }
86
+
87
+ function flattenTree(nodes: FileNode[]): FileNode[] {
88
+ const result: FileNode[] = [];
89
+ for (const node of nodes) {
90
+ if (node.type === 'file') result.push(node);
91
+ if (node.children) result.push(...flattenTree(node.children));
92
+ }
93
+ return result;
94
+ }
95
+
96
+ const LANG_MAP: Record<string, string> = {
97
+ ts: 'TypeScript', tsx: 'TypeScript (JSX)', js: 'JavaScript', jsx: 'JavaScript (JSX)',
98
+ py: 'Python', go: 'Go', rs: 'Rust', java: 'Java', kt: 'Kotlin',
99
+ css: 'CSS', scss: 'SCSS', html: 'HTML', json: 'JSON', yaml: 'YAML', yml: 'YAML',
100
+ md: 'Markdown', sh: 'Shell', sql: 'SQL', toml: 'TOML', xml: 'XML',
101
+ };
102
+
103
+ // โ”€โ”€โ”€ Simple syntax highlighting โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
104
+
105
+ const KEYWORDS = new Set([
106
+ 'import', 'export', 'from', 'const', 'let', 'var', 'function', 'return',
107
+ 'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue',
108
+ 'class', 'extends', 'new', 'this', 'super', 'typeof', 'instanceof',
109
+ 'try', 'catch', 'finally', 'throw', 'async', 'await', 'yield',
110
+ 'default', 'interface', 'type', 'enum', 'implements', 'readonly',
111
+ 'public', 'private', 'protected', 'static', 'abstract',
112
+ 'true', 'false', 'null', 'undefined', 'void',
113
+ 'def', 'self', 'None', 'True', 'False', 'class', 'lambda', 'with', 'as', 'in', 'not', 'and', 'or',
114
+ 'func', 'package', 'struct', 'go', 'defer', 'select', 'chan', 'map', 'range',
115
+ 'final', 'synchronized', 'volatile', 'transient', 'native',
116
+ 'throws', 'int', 'long', 'double', 'float', 'char', 'byte', 'short', 'boolean',
117
+ 'override',
118
+ 'val', 'object', 'trait', 'sealed', 'implicit', 'lazy', 'match',
119
+ ]);
120
+
121
+ function highlightLine(line: string, lang: string): React.ReactNode {
122
+ if (!line) return ' ';
123
+
124
+ // Comments
125
+ const commentIdx = lang === 'py' ? line.indexOf('#') :
126
+ line.indexOf('//');
127
+ if (commentIdx === 0 || (commentIdx > 0 && /^\s*$/.test(line.slice(0, commentIdx)))) {
128
+ return <span className="text-gray-500 italic">{line}</span>;
129
+ }
130
+
131
+ // Tokenize with regex
132
+ const parts: React.ReactNode[] = [];
133
+ let lastIdx = 0;
134
+
135
+ const regex = /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(\b\d+\.?\d*\b)|(\/\/.*$|#.*$)|(\b[A-Z_][A-Z_0-9]+\b)|(\b\w+\b)/g;
136
+ let match;
137
+
138
+ while ((match = regex.exec(line)) !== null) {
139
+ // Text before match
140
+ if (match.index > lastIdx) {
141
+ parts.push(line.slice(lastIdx, match.index));
142
+ }
143
+
144
+ if (match[1]) {
145
+ // String
146
+ parts.push(<span key={match.index} className="text-green-400">{match[0]}</span>);
147
+ } else if (match[2]) {
148
+ // Number
149
+ parts.push(<span key={match.index} className="text-orange-300">{match[0]}</span>);
150
+ } else if (match[3]) {
151
+ // Comment
152
+ parts.push(<span key={match.index} className="text-gray-500 italic">{match[0]}</span>);
153
+ } else if (match[4]) {
154
+ // CONSTANT
155
+ parts.push(<span key={match.index} className="text-cyan-300">{match[0]}</span>);
156
+ } else if (match[5] && KEYWORDS.has(match[5])) {
157
+ // Keyword
158
+ parts.push(<span key={match.index} className="text-purple-400">{match[0]}</span>);
159
+ } else {
160
+ parts.push(match[0]);
161
+ }
162
+ lastIdx = match.index + match[0].length;
163
+ }
164
+
165
+ if (lastIdx < line.length) {
166
+ parts.push(line.slice(lastIdx));
167
+ }
168
+
169
+ return parts.length > 0 ? <>{parts}</> : line;
170
+ }
171
+
172
+ // โ”€โ”€โ”€ Main Component โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
173
+
174
+ export default function CodeViewer({ terminalRef }: { terminalRef: React.RefObject<WebTerminalHandle | null> }) {
175
+ const [currentDir, setCurrentDir] = useState<string | null>(null);
176
+ const [dirName, setDirName] = useState('');
177
+ const [tree, setTree] = useState<FileNode[]>([]);
178
+ const [gitBranch, setGitBranch] = useState('');
179
+ const [gitChanges, setGitChanges] = useState<{ path: string; status: string }[]>([]);
180
+ const [gitRepos, setGitRepos] = useState<{ name: string; branch: string; remote: string; changes: { path: string; status: string }[] }[]>([]);
181
+ const [showGit, setShowGit] = useState(false);
182
+ const [selectedFile, setSelectedFile] = useState<string | null>(null);
183
+ const [content, setContent] = useState<string | null>(null);
184
+ const [language, setLanguage] = useState('');
185
+ const [loading, setLoading] = useState(false);
186
+ const [fileWarning, setFileWarning] = useState<{ type: 'binary' | 'large' | 'tooLarge'; label: string; fileType?: string } | null>(null);
187
+ const [search, setSearch] = useState('');
188
+ const [diffContent, setDiffContent] = useState<string | null>(null);
189
+ const [diffFile, setDiffFile] = useState<string | null>(null);
190
+ const [viewMode, setViewMode] = useState<'file' | 'diff'>('file');
191
+ const [sidebarOpen, setSidebarOpen] = useState(true);
192
+ const [codeOpen, setCodeOpen] = useState(false);
193
+ const [editing, setEditing] = useState(false);
194
+ const [editContent, setEditContent] = useState('');
195
+ const [saving, setSaving] = useState(false);
196
+
197
+ const handleCodeOpenChange = useCallback((open: boolean) => {
198
+ setCodeOpen(open);
199
+ }, []);
200
+ const [terminalHeight, setTerminalHeight] = useState(300);
201
+ const [activeSession, setActiveSession] = useState<string | null>(null);
202
+ const [taskNotification, setTaskNotification] = useState<{ id: string; status: string; prompt: string; sessionId?: string } | null>(null);
203
+ const dragRef = useRef<{ startY: number; startH: number } | null>(null);
204
+ const { sidebarWidth, onSidebarDragStart } = useSidebarResize({ defaultWidth: 224, minWidth: 120, maxWidth: 480 });
205
+ const lastDirRef = useRef<string | null>(null);
206
+ const lastTaskCheckRef = useRef<string>('');
207
+
208
+ // When active terminal session changes, query its cwd
209
+ useEffect(() => {
210
+ if (!activeSession) return;
211
+ let cancelled = false;
212
+
213
+ const fetchCwd = async () => {
214
+ try {
215
+ const res = await fetch(`/api/terminal-cwd?session=${encodeURIComponent(activeSession)}`);
216
+ const data = await res.json();
217
+ if (!cancelled && data.path && data.path !== lastDirRef.current) {
218
+ lastDirRef.current = data.path;
219
+ setCurrentDir(data.path);
220
+ setSelectedFile(null);
221
+ setContent(null);
222
+ }
223
+ } catch {}
224
+ };
225
+
226
+ fetchCwd();
227
+ // Poll cwd every 5s (user might cd to a different directory)
228
+ const timer = setInterval(fetchCwd, 5000);
229
+ return () => { cancelled = true; clearInterval(timer); };
230
+ }, [activeSession]);
231
+
232
+ // Fetch file tree when directory changes
233
+ useEffect(() => {
234
+ if (!currentDir) return;
235
+ const fetchDir = () => {
236
+ fetch(`/api/code?dir=${encodeURIComponent(currentDir)}`)
237
+ .then(r => r.json())
238
+ .then(data => {
239
+ setTree(data.tree || []);
240
+ setDirName(data.dirName || currentDir.split('/').pop() || '');
241
+ setGitBranch(data.gitBranch || '');
242
+ setGitChanges(data.gitChanges || []);
243
+ setGitRepos(data.gitRepos || []);
244
+ })
245
+ .catch(() => setTree([]));
246
+ };
247
+ fetchDir();
248
+ }, [currentDir]);
249
+
250
+ // Poll for task completions in the current project
251
+ useEffect(() => {
252
+ if (!currentDir) return;
253
+ const dirName = currentDir.split('/').pop() || '';
254
+ const check = async () => {
255
+ try {
256
+ const res = await fetch('/api/tasks?status=done');
257
+ const tasks = await res.json();
258
+ if (!Array.isArray(tasks) || tasks.length === 0) return;
259
+ const latest = tasks.find((t: any) => t.projectPath === currentDir || t.projectName === dirName);
260
+ if (latest && latest.id !== lastTaskCheckRef.current && latest.completedAt) {
261
+ // Only notify if completed in the last 30s
262
+ const age = Date.now() - new Date(latest.completedAt).getTime();
263
+ if (age < 30_000) {
264
+ lastTaskCheckRef.current = latest.id;
265
+ setTaskNotification({
266
+ id: latest.id,
267
+ status: latest.status,
268
+ prompt: latest.prompt,
269
+ sessionId: latest.conversationId,
270
+ });
271
+ setTimeout(() => setTaskNotification(null), 15_000);
272
+ }
273
+ }
274
+ } catch {}
275
+ };
276
+ const timer = setInterval(check, 5000);
277
+ return () => clearInterval(timer);
278
+ }, [currentDir]);
279
+
280
+ // Build git status map for tree coloring
281
+ const gitMap: GitStatusMap = new Map(gitChanges.map(g => [g.path, g.status]));
282
+ const repoMap: GitRepoMap = new Map(gitRepos.filter(r => r.name !== '.').map(r => [r.name, { branch: r.branch, remote: r.remote }]));
283
+
284
+ const openFile = useCallback(async (path: string, forceLoad?: boolean) => {
285
+ if (!currentDir) return;
286
+ setSelectedFile(path);
287
+ setViewMode('file');
288
+ setFileWarning(null);
289
+ setLoading(true);
290
+
291
+ const url = `/api/code?dir=${encodeURIComponent(currentDir)}&file=${encodeURIComponent(path)}${forceLoad ? '&force=1' : ''}`;
292
+ const res = await fetch(url);
293
+ const data = await res.json();
294
+
295
+ if (data.binary) {
296
+ setContent(null);
297
+ setFileWarning({ type: 'binary', label: data.sizeLabel, fileType: data.fileType });
298
+ } else if (data.tooLarge) {
299
+ setContent(null);
300
+ setFileWarning({ type: 'tooLarge', label: data.sizeLabel });
301
+ } else if (data.large && !forceLoad) {
302
+ setContent(null);
303
+ setFileWarning({ type: 'large', label: data.sizeLabel });
304
+ setLanguage(data.language || '');
305
+ } else {
306
+ setContent(data.content || null);
307
+ setLanguage(data.language || '');
308
+ }
309
+ setLoading(false);
310
+ }, [currentDir]);
311
+
312
+ const openDiff = useCallback(async (path: string) => {
313
+ if (!currentDir) return;
314
+ setDiffFile(path);
315
+ setViewMode('diff');
316
+ setLoading(true);
317
+ const res = await fetch(`/api/code?dir=${encodeURIComponent(currentDir)}&diff=${encodeURIComponent(path)}`);
318
+ const data = await res.json();
319
+ setDiffContent(data.diff || null);
320
+ setLoading(false);
321
+ }, [currentDir]);
322
+
323
+ // Open file and auto-expand its parent dirs in tree
324
+ const locateFile = useCallback((path: string) => {
325
+ setSearch(''); // clear search so tree is visible
326
+ openFile(path);
327
+ }, [openFile]);
328
+
329
+ const allFiles = flattenTree(tree);
330
+ const filtered = search
331
+ ? allFiles.filter(f => f.name.toLowerCase().includes(search.toLowerCase()) || f.path.toLowerCase().includes(search.toLowerCase()))
332
+ : null;
333
+
334
+ const onDragStart = (e: React.MouseEvent) => {
335
+ e.preventDefault();
336
+ dragRef.current = { startY: e.clientY, startH: terminalHeight };
337
+ const onMove = (ev: MouseEvent) => {
338
+ if (!dragRef.current) return;
339
+ const delta = ev.clientY - dragRef.current.startY;
340
+ setTerminalHeight(Math.max(100, Math.min(600, dragRef.current.startH + delta)));
341
+ };
342
+ const onUp = () => {
343
+ dragRef.current = null;
344
+ window.removeEventListener('mousemove', onMove);
345
+ window.removeEventListener('mouseup', onUp);
346
+ };
347
+ window.addEventListener('mousemove', onMove);
348
+ window.addEventListener('mouseup', onUp);
349
+ };
350
+
351
+ // Git operations
352
+ const [commitMsg, setCommitMsg] = useState('');
353
+ const [gitLoading, setGitLoading] = useState(false);
354
+ const [gitResult, setGitResult] = useState<{ ok?: boolean; error?: string } | null>(null);
355
+
356
+ const gitAction = useCallback(async (action: string, extra?: any) => {
357
+ if (!currentDir) return;
358
+ setGitLoading(true);
359
+ setGitResult(null);
360
+ try {
361
+ const res = await fetch('/api/git', {
362
+ method: 'POST',
363
+ headers: { 'Content-Type': 'application/json' },
364
+ body: JSON.stringify({ action, dir: currentDir, ...extra }),
365
+ });
366
+ const data = await res.json();
367
+ setGitResult(data);
368
+ // Refresh git status
369
+ if (data.ok) {
370
+ const r = await fetch(`/api/code?dir=${encodeURIComponent(currentDir)}`);
371
+ const d = await r.json();
372
+ setGitChanges(d.gitChanges || []);
373
+ setGitRepos(d.gitRepos || []);
374
+ setGitBranch(d.gitBranch || '');
375
+ if (action === 'commit') setCommitMsg('');
376
+ }
377
+ } catch (e: any) {
378
+ setGitResult({ error: e.message });
379
+ }
380
+ setGitLoading(false);
381
+ setTimeout(() => setGitResult(null), 5000);
382
+ }, [currentDir]);
383
+
384
+ const refreshAll = useCallback(() => {
385
+ if (!currentDir) return;
386
+ // Refresh tree + git
387
+ fetch(`/api/code?dir=${encodeURIComponent(currentDir)}`)
388
+ .then(r => r.json())
389
+ .then(data => {
390
+ setTree(data.tree || []);
391
+ setDirName(data.dirName || currentDir.split('/').pop() || '');
392
+ setGitBranch(data.gitBranch || '');
393
+ setGitChanges(data.gitChanges || []);
394
+ setGitRepos(data.gitRepos || []);
395
+ })
396
+ .catch(() => {});
397
+ // Refresh open file
398
+ if (selectedFile) openFile(selectedFile);
399
+ }, [currentDir, selectedFile, openFile]);
400
+
401
+ const handleActiveSession = useCallback((session: string | null) => {
402
+ setActiveSession(session);
403
+ }, []);
404
+
405
+ return (
406
+ <div className="flex-1 flex flex-col min-h-0 min-w-0 overflow-hidden">
407
+ {/* Task completion notification */}
408
+ {taskNotification && (
409
+ <div className="shrink-0 px-3 py-1.5 bg-green-900/30 border-b border-green-800/50 flex items-center gap-2 text-xs">
410
+ <span className="text-green-400">{taskNotification.status === 'done' ? 'โœ…' : 'โŒ'}</span>
411
+ <span className="text-green-300 truncate">Task {taskNotification.id}: {taskNotification.prompt.slice(0, 60)}</span>
412
+ {taskNotification.sessionId && (
413
+ <button
414
+ onClick={() => {
415
+ // Send claude --resume to the active terminal
416
+ // The tmux display-message from backend already showed the notification
417
+ setTaskNotification(null);
418
+ }}
419
+ className="ml-auto text-[10px] text-green-400 hover:text-white shrink-0"
420
+ >
421
+ Dismiss
422
+ </button>
423
+ )}
424
+ </div>
425
+ )}
426
+
427
+ {/* Terminal + Browser โ€” main area */}
428
+ <div className={codeOpen ? 'shrink-0' : 'flex-1'} style={codeOpen ? { height: terminalHeight } : undefined}>
429
+ <Suspense fallback={<div className="h-full flex items-center justify-center text-[var(--text-secondary)] text-xs">Loading...</div>}>
430
+ <WebTerminal ref={terminalRef} onActiveSession={handleActiveSession} onCodeOpenChange={handleCodeOpenChange} />
431
+ </Suspense>
432
+ </div>
433
+
434
+ {/* Resize handle */}
435
+ {codeOpen && (
436
+ <div
437
+ onMouseDown={onDragStart}
438
+ className="h-1 bg-[var(--border)] cursor-row-resize hover:bg-[var(--accent)]/50 shrink-0"
439
+ />
440
+ )}
441
+
442
+ {/* File browser + code viewer โ€” bottom */}
443
+ {codeOpen && <div className="flex-1 flex min-h-0 min-w-0 overflow-hidden">
444
+ {/* Sidebar */}
445
+ {sidebarOpen && (
446
+ <aside style={{ width: sidebarWidth }} className="flex flex-col shrink-0 overflow-hidden">
447
+ {/* Directory name + git */}
448
+ <div className="px-3 py-2 border-b border-[var(--border)]">
449
+ <div className="flex items-center gap-2">
450
+ <span className="text-[11px] font-semibold text-[var(--text-primary)] truncate">
451
+ {dirName || 'No directory'}
452
+ </span>
453
+ {gitBranch && (
454
+ <span className="text-[9px] text-[var(--accent)] bg-[var(--accent)]/10 px-1.5 py-0.5 rounded shrink-0">
455
+ {gitBranch}
456
+ </span>
457
+ )}
458
+ <button
459
+ onClick={refreshAll}
460
+ className="text-[9px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] ml-auto shrink-0"
461
+ title="Refresh files & git status"
462
+ >
463
+ โ†ป
464
+ </button>
465
+ </div>
466
+ {gitRepos.find(r => r.name === '.')?.remote && (
467
+ <div className="text-[9px] text-[var(--text-secondary)] truncate mt-0.5" title={gitRepos.find(r => r.name === '.')!.remote}>
468
+ {gitRepos.find(r => r.name === '.')!.remote.replace(/^https?:\/\//, '').replace(/^git@github\.com:/, 'github.com/').replace(/\.git$/, '')}
469
+ </div>
470
+ )}
471
+ {gitChanges.length > 0 && (
472
+ <button
473
+ onClick={() => setShowGit(v => !v)}
474
+ className="text-[10px] text-yellow-500 hover:text-yellow-400 mt-1 block"
475
+ >
476
+ {gitChanges.length} changes {showGit ? 'โ–พ' : 'โ–ธ'}
477
+ </button>
478
+ )}
479
+ </div>
480
+
481
+ {/* Git changes โ€” grouped by repo */}
482
+ {showGit && gitChanges.length > 0 && (
483
+ <div className="border-b border-[var(--border)] max-h-48 overflow-y-auto">
484
+ {gitRepos.map(repo => (
485
+ <div key={repo.name}>
486
+ {/* Repo header โ€” only show if multiple repos */}
487
+ {gitRepos.length > 1 && (
488
+ <div className="px-2 py-1 text-[9px] text-[var(--text-secondary)] bg-[var(--bg-tertiary)] sticky top-0" title={repo.remote}>
489
+ <div className="flex items-center gap-1.5">
490
+ <span className="font-semibold text-[var(--text-primary)]">{repo.name}</span>
491
+ <span className="text-[var(--accent)]">{repo.branch}</span>
492
+ <span className="ml-auto">{repo.changes.length}</span>
493
+ </div>
494
+ {repo.remote && (
495
+ <div className="text-[8px] truncate mt-0.5">{repo.remote.replace(/^https?:\/\//, '').replace(/\.git$/, '')}</div>
496
+ )}
497
+ </div>
498
+ )}
499
+ {repo.changes.map(g => (
500
+ <div
501
+ key={g.path}
502
+ className={`flex items-center px-2 py-1 text-xs hover:bg-[var(--bg-tertiary)] ${
503
+ diffFile === g.path && viewMode === 'diff' ? 'bg-[var(--accent)]/10' : ''
504
+ }`}
505
+ >
506
+ <span className={`text-[10px] font-mono w-4 shrink-0 ${
507
+ g.status.includes('M') ? 'text-yellow-500' :
508
+ g.status.includes('A') || g.status.includes('?') ? 'text-green-500' :
509
+ g.status.includes('D') ? 'text-red-500' :
510
+ 'text-[var(--text-secondary)]'
511
+ }`}>
512
+ {g.status.includes('?') ? '+' : g.status[0]}
513
+ </span>
514
+ <button
515
+ onClick={() => openDiff(g.path)}
516
+ className="flex-1 text-left truncate text-[var(--text-secondary)] hover:text-[var(--text-primary)] ml-1 group relative"
517
+ title={`${g.path}${gitRepos.length > 1 ? ` (${repo.name} ยท ${repo.branch})` : ''}`}
518
+ >
519
+ {gitRepos.length > 1 ? g.path.replace(repo.name + '/', '') : g.path}
520
+ </button>
521
+ <button
522
+ onClick={(e) => { e.stopPropagation(); locateFile(g.path); }}
523
+ className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 px-1.5 py-0.5 rounded shrink-0"
524
+ title="Locate in file tree"
525
+ >
526
+ file
527
+ </button>
528
+ </div>
529
+ ))}
530
+ </div>
531
+ ))}
532
+ </div>
533
+ )}
534
+
535
+ {/* Search */}
536
+ <div className="p-2 border-b border-[var(--border)]">
537
+ <input
538
+ type="text"
539
+ placeholder="Search..."
540
+ value={search}
541
+ onChange={e => setSearch(e.target.value)}
542
+ 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)]"
543
+ />
544
+ </div>
545
+
546
+ {/* Tree */}
547
+ <div className="flex-1 overflow-y-auto p-1">
548
+ {!currentDir ? (
549
+ <div className="text-xs text-[var(--text-secondary)] p-2">Open a terminal to see files</div>
550
+ ) : filtered ? (
551
+ filtered.length === 0 ? (
552
+ <div className="text-xs text-[var(--text-secondary)] p-2">No matches</div>
553
+ ) : (
554
+ filtered.map(f => (
555
+ <button
556
+ key={f.path}
557
+ onClick={() => { openFile(f.path); setSearch(''); }}
558
+ className={`w-full text-left px-2 py-1 rounded text-xs truncate ${
559
+ selectedFile === f.path ? 'bg-[var(--accent)]/20 text-[var(--accent)]' : 'hover:bg-[var(--bg-tertiary)] text-[var(--text-secondary)]'
560
+ }`}
561
+ title={f.path}
562
+ >
563
+ <span className="text-[var(--text-primary)]">{f.name}</span>
564
+ <span className="text-[9px] text-[var(--text-secondary)] ml-1">{f.path.split('/').slice(0, -1).join('/')}</span>
565
+ </button>
566
+ ))
567
+ )
568
+ ) : (
569
+ tree.map(node => (
570
+ <TreeNode key={node.path} node={node} depth={0} selected={selectedFile} onSelect={openFile} gitMap={gitMap} repoMap={repoMap} />
571
+ ))
572
+ )}
573
+ </div>
574
+
575
+ {/* Git actions โ€” bottom of sidebar */}
576
+ {currentDir && (gitChanges.length > 0 || gitRepos.length > 0) && (
577
+ <div className="border-t border-[var(--border)] shrink-0 p-2 space-y-1.5">
578
+ <div className="flex gap-1.5">
579
+ <input
580
+ value={commitMsg}
581
+ onChange={e => setCommitMsg(e.target.value)}
582
+ onKeyDown={e => e.key === 'Enter' && commitMsg.trim() && gitAction('commit', { message: commitMsg.trim() })}
583
+ placeholder="Commit message..."
584
+ className="flex-1 text-[10px] bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-1 text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
585
+ />
586
+ <button
587
+ onClick={() => commitMsg.trim() && gitAction('commit', { message: commitMsg.trim() })}
588
+ disabled={gitLoading || !commitMsg.trim() || gitChanges.length === 0}
589
+ className="text-[9px] px-2 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90 disabled:opacity-50 shrink-0"
590
+ >
591
+ Commit
592
+ </button>
593
+ </div>
594
+ <div className="flex gap-1.5">
595
+ <button
596
+ onClick={() => gitAction('push')}
597
+ disabled={gitLoading}
598
+ className="flex-1 text-[9px] py-1 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white disabled:opacity-50"
599
+ >
600
+ Push
601
+ </button>
602
+ <button
603
+ onClick={() => gitAction('pull')}
604
+ disabled={gitLoading}
605
+ className="flex-1 text-[9px] py-1 text-[var(--text-secondary)] border border-[var(--border)] rounded hover:text-[var(--text-primary)] hover:border-[var(--text-secondary)] disabled:opacity-50"
606
+ >
607
+ Pull
608
+ </button>
609
+ </div>
610
+ {gitResult && (
611
+ <div className={`text-[9px] ${gitResult.ok ? 'text-green-400' : 'text-red-400'}`}>
612
+ {gitResult.ok ? 'โœ… Done' : `โŒ ${gitResult.error}`}
613
+ </div>
614
+ )}
615
+ </div>
616
+ )}
617
+ </aside>
618
+ )}
619
+
620
+ {/* Sidebar resize handle */}
621
+ {sidebarOpen && (
622
+ <div
623
+ onMouseDown={onSidebarDragStart}
624
+ className="w-1 bg-[var(--border)] cursor-col-resize shrink-0 hover:bg-[var(--accent)]/50 transition-colors"
625
+ />
626
+ )}
627
+
628
+ {/* Code viewer */}
629
+ <main className="flex-1 flex flex-col min-w-0 overflow-hidden" style={{ width: 0 }}>
630
+ <div className="px-3 py-1.5 border-b border-[var(--border)] shrink-0 flex items-center gap-2">
631
+ <button
632
+ onClick={() => setSidebarOpen(v => !v)}
633
+ className="text-[10px] px-1.5 py-0.5 text-gray-400 hover:text-white hover:bg-[var(--bg-tertiary)] rounded"
634
+ >
635
+ {sidebarOpen ? 'โ—€' : 'โ–ถ'}
636
+ </button>
637
+ {viewMode === 'diff' && diffFile ? (
638
+ <>
639
+ <span className="text-xs font-semibold text-yellow-400 truncate">{diffFile}</span>
640
+ <span className="text-[9px] text-[var(--text-secondary)] ml-auto">diff</span>
641
+ </>
642
+ ) : selectedFile ? (
643
+ <>
644
+ <span className="text-xs font-semibold text-[var(--text-primary)] truncate">{selectedFile}</span>
645
+ {language && (
646
+ <span className="text-[9px] text-[var(--text-secondary)] ml-auto mr-2">{LANG_MAP[language] || language}</span>
647
+ )}
648
+ {content !== null && !editing && (
649
+ <button
650
+ onClick={() => { setEditing(true); setEditContent(content); }}
651
+ className="text-[9px] px-2 py-0.5 border border-[var(--border)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:border-[var(--text-secondary)] shrink-0 ml-auto"
652
+ >
653
+ Edit
654
+ </button>
655
+ )}
656
+ {editing && (
657
+ <>
658
+ <button
659
+ disabled={saving}
660
+ onClick={async () => {
661
+ if (!currentDir || !selectedFile) return;
662
+ setSaving(true);
663
+ await fetch('/api/code', {
664
+ method: 'PUT',
665
+ headers: { 'Content-Type': 'application/json' },
666
+ body: JSON.stringify({ dir: currentDir, file: selectedFile, content: editContent }),
667
+ });
668
+ setContent(editContent);
669
+ setEditing(false);
670
+ setSaving(false);
671
+ }}
672
+ className="text-[9px] px-2 py-0.5 bg-[var(--accent)] text-white rounded hover:opacity-90 disabled:opacity-50 shrink-0 ml-auto"
673
+ >
674
+ {saving ? 'Saving...' : 'Save'}
675
+ </button>
676
+ <button
677
+ onClick={() => setEditing(false)}
678
+ className="text-[9px] px-2 py-0.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] shrink-0"
679
+ >
680
+ Cancel
681
+ </button>
682
+ </>
683
+ )}
684
+ </>
685
+ ) : (
686
+ <span className="text-xs text-[var(--text-secondary)]">{dirName || 'Code'}</span>
687
+ )}
688
+ </div>
689
+
690
+ {loading ? (
691
+ <div className="flex-1 flex items-center justify-center">
692
+ <div className="text-xs text-[var(--text-secondary)]">Loading...</div>
693
+ </div>
694
+ ) : fileWarning ? (
695
+ <div className="flex-1 flex items-center justify-center">
696
+ <div className="text-center space-y-3 p-6">
697
+ {fileWarning.type === 'binary' && (
698
+ <>
699
+ <div className="text-3xl">๐Ÿšซ</div>
700
+ <p className="text-sm text-[var(--text-primary)]">Binary file cannot be displayed</p>
701
+ <p className="text-xs text-[var(--text-secondary)]">{fileWarning.fileType?.toUpperCase()} ยท {fileWarning.label}</p>
702
+ </>
703
+ )}
704
+ {fileWarning.type === 'tooLarge' && (
705
+ <>
706
+ <div className="text-3xl">โš ๏ธ</div>
707
+ <p className="text-sm text-[var(--text-primary)]">File too large to display</p>
708
+ <p className="text-xs text-[var(--text-secondary)]">{fileWarning.label} โ€” exceeds 2 MB limit</p>
709
+ </>
710
+ )}
711
+ {fileWarning.type === 'large' && (
712
+ <>
713
+ <div className="text-3xl">๐Ÿ“„</div>
714
+ <p className="text-sm text-[var(--text-primary)]">Large file: {fileWarning.label}</p>
715
+ <p className="text-xs text-[var(--text-secondary)]">This file may slow down the browser</p>
716
+ <button
717
+ onClick={() => selectedFile && openFile(selectedFile, true)}
718
+ className="text-xs px-4 py-1.5 bg-[var(--accent)] text-white rounded hover:opacity-90 mt-2"
719
+ >
720
+ Open anyway
721
+ </button>
722
+ </>
723
+ )}
724
+ </div>
725
+ </div>
726
+ ) : viewMode === 'diff' && diffContent ? (
727
+ <div className="flex-1 overflow-auto bg-[var(--bg-primary)]">
728
+ <pre className="p-4 text-[12px] leading-[1.5] font-mono whitespace-pre" style={{ fontFamily: 'Menlo, Monaco, "Courier New", monospace', tabSize: 2, overflow: 'auto', maxWidth: 0, minWidth: '100%' }}>
729
+ {diffContent.split('\n').map((line, i) => {
730
+ const color = line.startsWith('+') ? 'text-green-400 bg-green-900/20'
731
+ : line.startsWith('-') ? 'text-red-400 bg-red-900/20'
732
+ : line.startsWith('@@') ? 'text-cyan-400'
733
+ : line.startsWith('diff') || line.startsWith('index') ? 'text-[var(--text-secondary)]'
734
+ : 'text-[var(--text-primary)]';
735
+ return (
736
+ <div key={i} className={`${color} px-2`}>
737
+ {line || ' '}
738
+ </div>
739
+ );
740
+ })}
741
+ </pre>
742
+ </div>
743
+ ) : selectedFile && content !== null ? (
744
+ editing ? (
745
+ <div className="flex-1 overflow-hidden flex flex-col">
746
+ <textarea
747
+ value={editContent}
748
+ onChange={e => setEditContent(e.target.value)}
749
+ onKeyDown={e => {
750
+ // Tab key inserts 2 spaces
751
+ if (e.key === 'Tab') {
752
+ e.preventDefault();
753
+ const ta = e.target as HTMLTextAreaElement;
754
+ const start = ta.selectionStart;
755
+ const end = ta.selectionEnd;
756
+ setEditContent(editContent.slice(0, start) + ' ' + editContent.slice(end));
757
+ setTimeout(() => { ta.selectionStart = ta.selectionEnd = start + 2; }, 0);
758
+ }
759
+ }}
760
+ className="flex-1 w-full p-4 bg-[var(--bg-primary)] text-[var(--text-primary)] text-[12px] leading-[1.5] font-mono resize-none focus:outline-none"
761
+ style={{ fontFamily: 'Menlo, Monaco, "Courier New", monospace', tabSize: 2 }}
762
+ spellCheck={false}
763
+ />
764
+ </div>
765
+ ) : (
766
+ <div className="flex-1 overflow-auto bg-[var(--bg-primary)]">
767
+ <pre className="p-4 text-[12px] leading-[1.5] font-mono text-[var(--text-primary)] whitespace-pre" style={{ fontFamily: 'Menlo, Monaco, "Courier New", monospace', tabSize: 2, overflow: 'auto', maxWidth: 0, minWidth: '100%' }}>
768
+ {content.split('\n').map((line, i) => (
769
+ <div key={i} className="flex hover:bg-[var(--bg-tertiary)]/50">
770
+ <span className="select-none text-[var(--text-secondary)]/40 text-right pr-4 w-10 shrink-0">{i + 1}</span>
771
+ <span className="whitespace-pre">{highlightLine(line, language)}</span>
772
+ </div>
773
+ ))}
774
+ </pre>
775
+ </div>
776
+ )
777
+ ) : (
778
+ <div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">
779
+ <p className="text-xs">{currentDir ? 'Select a file to view' : 'Terminal will show files for its working directory'}</p>
780
+ </div>
781
+ )}
782
+ </main>
783
+ </div>}
784
+
785
+ </div>
786
+ );
787
+ }