@aion0/forge 0.5.26 → 0.5.27

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 (253) 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 +11 -28
  249. package/app/api/terminal-bell/route.ts +6 -2
  250. package/components/WebTerminal.tsx +36 -2
  251. package/lib/terminal-standalone.ts +19 -2
  252. package/next-env.d.ts +1 -1
  253. package/package.json +1 -1
@@ -0,0 +1,1647 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+
5
+ function SecretInput({ value, onChange, placeholder, className }: {
6
+ value: string;
7
+ onChange: (v: string) => void;
8
+ placeholder?: string;
9
+ className?: string;
10
+ }) {
11
+ const [show, setShow] = useState(false);
12
+ return (
13
+ <div className="relative">
14
+ <input
15
+ type={show ? 'text' : 'password'}
16
+ value={value}
17
+ onChange={e => onChange(e.target.value)}
18
+ placeholder={placeholder}
19
+ className={className}
20
+ />
21
+ <button
22
+ type="button"
23
+ onClick={() => setShow(v => !v)}
24
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
25
+ >
26
+ {show ? '🙈' : '👁'}
27
+ </button>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ // ─── Secret Change Dialog ──────────────────────────────────────
33
+
34
+ function SecretChangeDialog({ field, label, isSet, onSave, onClose }: {
35
+ field: string;
36
+ label: string;
37
+ isSet: boolean;
38
+ onSave: (field: string, adminPassword: string, newValue: string) => Promise<string | null>;
39
+ onClose: () => void;
40
+ }) {
41
+ const [mode, setMode] = useState<'change' | 'clear'>('change');
42
+ const [adminPassword, setAdminPassword] = useState('');
43
+ const [newValue, setNewValue] = useState('');
44
+ const [confirmValue, setConfirmValue] = useState('');
45
+ const [error, setError] = useState('');
46
+ const [saving, setSaving] = useState(false);
47
+
48
+ const canSave = mode === 'clear'
49
+ ? adminPassword.length > 0
50
+ : (adminPassword.length > 0 && newValue.length > 0 && newValue === confirmValue);
51
+
52
+ const handleSave = async () => {
53
+ if (mode === 'change' && newValue !== confirmValue) {
54
+ setError('New values do not match');
55
+ return;
56
+ }
57
+ setSaving(true);
58
+ setError('');
59
+ const err = await onSave(field, adminPassword, mode === 'clear' ? '' : newValue);
60
+ setSaving(false);
61
+ if (err) {
62
+ setError(err);
63
+ } else {
64
+ onClose();
65
+ }
66
+ };
67
+
68
+ const inputClass = "w-full px-2 py-1.5 pr-8 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] font-mono focus:outline-none focus:border-[var(--accent)]";
69
+
70
+ return (
71
+ <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-[60]" onClick={onClose}>
72
+ <div
73
+ className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg w-[380px] p-4 space-y-3"
74
+ onClick={e => e.stopPropagation()}
75
+ >
76
+ <div className="flex items-center justify-between">
77
+ <h3 className="text-xs font-bold">{isSet ? `Change ${label}` : `Set ${label}`}</h3>
78
+ {isSet && (
79
+ <div className="flex bg-[var(--bg-tertiary)] rounded p-0.5">
80
+ <button
81
+ onClick={() => { setMode('change'); setError(''); }}
82
+ className={`text-[10px] px-2 py-0.5 rounded ${mode === 'change' ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-secondary)]'}`}
83
+ >
84
+ Change
85
+ </button>
86
+ <button
87
+ onClick={() => { setMode('clear'); setError(''); }}
88
+ className={`text-[10px] px-2 py-0.5 rounded ${mode === 'clear' ? 'bg-[var(--red)] text-white shadow-sm' : 'text-[var(--text-secondary)]'}`}
89
+ >
90
+ Clear
91
+ </button>
92
+ </div>
93
+ )}
94
+ </div>
95
+
96
+ <div className="space-y-1">
97
+ <label className="text-[10px] text-[var(--text-secondary)]">Admin password (login password)</label>
98
+ <SecretInput
99
+ value={adminPassword}
100
+ onChange={v => { setAdminPassword(v); setError(''); }}
101
+ placeholder="Enter login password to verify"
102
+ className={inputClass}
103
+ />
104
+ </div>
105
+
106
+ {mode === 'change' && (
107
+ <>
108
+ <div className="space-y-1">
109
+ <label className="text-[10px] text-[var(--text-secondary)]">New value</label>
110
+ <SecretInput
111
+ value={newValue}
112
+ onChange={v => { setNewValue(v); setError(''); }}
113
+ placeholder="Enter new value"
114
+ className={inputClass}
115
+ />
116
+ </div>
117
+
118
+ <div className="space-y-1">
119
+ <label className="text-[10px] text-[var(--text-secondary)]">Confirm new value</label>
120
+ <SecretInput
121
+ value={confirmValue}
122
+ onChange={v => { setConfirmValue(v); setError(''); }}
123
+ placeholder="Re-enter new value"
124
+ className={inputClass}
125
+ />
126
+ {confirmValue && newValue !== confirmValue && (
127
+ <p className="text-[9px] text-[var(--red)]">Values do not match</p>
128
+ )}
129
+ </div>
130
+ </>
131
+ )}
132
+
133
+ {mode === 'clear' && (
134
+ <p className="text-[10px] text-[var(--text-secondary)]">
135
+ Enter admin password to verify, then click Clear to remove this value.
136
+ </p>
137
+ )}
138
+
139
+ {error && <p className="text-[10px] text-[var(--red)]">{error}</p>}
140
+
141
+ <div className="flex justify-end gap-2 pt-1">
142
+ <button
143
+ onClick={onClose}
144
+ className="px-3 py-1.5 text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
145
+ >
146
+ Cancel
147
+ </button>
148
+ <button
149
+ onClick={handleSave}
150
+ disabled={!canSave || saving}
151
+ className={`px-3 py-1.5 text-xs text-white rounded hover:opacity-90 disabled:opacity-50 ${mode === 'clear' ? 'bg-[var(--red)]' : 'bg-[var(--accent)]'}`}
152
+ >
153
+ {saving ? 'Saving...' : mode === 'clear' ? 'Clear' : 'Save'}
154
+ </button>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ );
159
+ }
160
+
161
+ // ─── Secret Field Display ──────────────────────────────────────
162
+
163
+ function SecretField({ label, description, isSet, onEdit }: {
164
+ label: string;
165
+ description?: string;
166
+ isSet: boolean;
167
+ onEdit: () => void;
168
+ }) {
169
+ return (
170
+ <div className="space-y-1">
171
+ {description && (
172
+ <label className="text-[10px] text-[var(--text-secondary)]">{description}</label>
173
+ )}
174
+ <div className="flex items-center gap-2">
175
+ <div className="flex-1 px-2 py-1.5 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs font-mono text-[var(--text-secondary)]">
176
+ {isSet ? '••••••••' : <span className="italic">Not set</span>}
177
+ </div>
178
+ <button
179
+ onClick={onEdit}
180
+ className="text-[10px] px-2 py-1 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white transition-colors"
181
+ >
182
+ {isSet ? 'Change' : 'Set'}
183
+ </button>
184
+ </div>
185
+ </div>
186
+ );
187
+ }
188
+
189
+ // ─── Settings Modal ────────────────────────────────────────────
190
+
191
+ interface Settings {
192
+ projectRoots: string[];
193
+ docRoots: string[];
194
+ claudePath: string;
195
+ telegramBotToken: string;
196
+ telegramChatId: string;
197
+ notifyOnComplete: boolean;
198
+ notifyOnFailure: boolean;
199
+ tunnelAutoStart: boolean;
200
+ telegramTunnelPassword: string;
201
+ taskModel: string;
202
+ pipelineModel: string;
203
+ telegramModel: string;
204
+ skipPermissions: boolean;
205
+ notificationRetentionDays: number;
206
+ _secretStatus?: Record<string, boolean>;
207
+ }
208
+
209
+ interface TunnelStatus {
210
+ status: 'stopped' | 'starting' | 'running' | 'error';
211
+ url: string | null;
212
+ error: string | null;
213
+ installed: boolean;
214
+ log: string[];
215
+ }
216
+
217
+ export default function SettingsModal({ onClose }: { onClose: () => void }) {
218
+ const [settings, setSettings] = useState<Settings>({
219
+ projectRoots: [],
220
+ docRoots: [],
221
+ claudePath: '',
222
+ telegramBotToken: '',
223
+ telegramChatId: '',
224
+ notifyOnComplete: true,
225
+ notifyOnFailure: true,
226
+ tunnelAutoStart: false,
227
+ telegramTunnelPassword: '',
228
+ taskModel: 'sonnet',
229
+ pipelineModel: 'sonnet',
230
+ telegramModel: 'sonnet',
231
+ skipPermissions: false,
232
+ notificationRetentionDays: 30,
233
+ });
234
+ const [secretStatus, setSecretStatus] = useState<Record<string, boolean>>({});
235
+ const [newRoot, setNewRoot] = useState('');
236
+ const [newDocRoot, setNewDocRoot] = useState('');
237
+ const [saved, setSaved] = useState(false);
238
+ const [tunnel, setTunnel] = useState<TunnelStatus>({
239
+ status: 'stopped', url: null, error: null, installed: false, log: [],
240
+ });
241
+ const [tunnelLoading, setTunnelLoading] = useState(false);
242
+ const [confirmStopTunnel, setConfirmStopTunnel] = useState(false);
243
+ const [tunnelPasswordPrompt, setTunnelPasswordPrompt] = useState(false);
244
+ const [tunnelPassword, setTunnelPassword] = useState('');
245
+ const [tunnelPasswordError, setTunnelPasswordError] = useState('');
246
+ const [editingSecret, setEditingSecret] = useState<{ field: string; label: string } | null>(null);
247
+ const [hasUnsaved, setHasUnsaved] = useState(false);
248
+ const origSettingsRef = useRef('');
249
+
250
+ const refreshTunnel = useCallback(() => {
251
+ fetch('/api/tunnel').then(r => r.json()).then(setTunnel).catch(() => {});
252
+ }, []);
253
+
254
+ const fetchSettings = useCallback(() => {
255
+ fetch('/api/settings').then(r => r.json()).then((data: Settings) => {
256
+ const status = data._secretStatus || {};
257
+ delete data._secretStatus;
258
+ setSettings(data);
259
+ origSettingsRef.current = JSON.stringify(data);
260
+ setSecretStatus(status);
261
+ });
262
+ }, []);
263
+
264
+ useEffect(() => {
265
+ fetchSettings();
266
+ refreshTunnel();
267
+ }, [fetchSettings, refreshTunnel]);
268
+
269
+ // Poll tunnel status while starting
270
+ useEffect(() => {
271
+ if (tunnel.status !== 'starting') return;
272
+ const id = setInterval(refreshTunnel, 2000);
273
+ return () => clearInterval(id);
274
+ }, [tunnel.status, refreshTunnel]);
275
+
276
+ const save = async () => {
277
+ await fetch('/api/settings', {
278
+ method: 'PUT',
279
+ headers: { 'Content-Type': 'application/json' },
280
+ body: JSON.stringify(settings),
281
+ });
282
+ origSettingsRef.current = JSON.stringify(settings);
283
+ setHasUnsaved(false);
284
+ setSaved(true);
285
+ setTimeout(() => setSaved(false), 2000);
286
+ };
287
+
288
+ // Track unsaved changes
289
+ useEffect(() => {
290
+ if (origSettingsRef.current) {
291
+ setHasUnsaved(JSON.stringify(settings) !== origSettingsRef.current);
292
+ }
293
+ }, [settings]);
294
+
295
+ const saveSecret = async (field: string, adminPassword: string, newValue: string): Promise<string | null> => {
296
+ const res = await fetch('/api/settings', {
297
+ method: 'PUT',
298
+ headers: { 'Content-Type': 'application/json' },
299
+ body: JSON.stringify({ _secretUpdate: { field, adminPassword, newValue } }),
300
+ });
301
+ const data = await res.json();
302
+ if (!data.ok) return data.error || 'Failed to save';
303
+ // Refresh status
304
+ setSecretStatus(prev => ({ ...prev, [field]: !!newValue }));
305
+ return null;
306
+ };
307
+
308
+ const addRoot = () => {
309
+ const path = newRoot.trim();
310
+ if (!path || settings.projectRoots.includes(path)) return;
311
+ setSettings({ ...settings, projectRoots: [...settings.projectRoots, path] });
312
+ setNewRoot('');
313
+ };
314
+
315
+ const removeRoot = (path: string) => {
316
+ setSettings({
317
+ ...settings,
318
+ projectRoots: settings.projectRoots.filter(r => r !== path),
319
+ });
320
+ };
321
+
322
+ return (
323
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => {
324
+ if (hasUnsaved && !confirm('You have unsaved changes. Close anyway?')) return;
325
+ onClose();
326
+ }}>
327
+ <div
328
+ className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg w-[500px] max-h-[80vh] overflow-y-auto p-5 space-y-5"
329
+ onClick={e => e.stopPropagation()}
330
+ >
331
+ <h2 className="text-sm font-bold">Settings</h2>
332
+
333
+ {/* Project Roots */}
334
+ <div className="space-y-2">
335
+ <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
336
+ Project Directories
337
+ </label>
338
+ <p className="text-[10px] text-[var(--text-secondary)]">
339
+ Add directories containing your projects. Each subdirectory is treated as a project.
340
+ </p>
341
+
342
+ {settings.projectRoots.map(root => (
343
+ <div key={root} className="flex items-center gap-2">
344
+ <span className="flex-1 text-xs px-2 py-1.5 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded font-mono truncate">
345
+ {root}
346
+ </span>
347
+ <button
348
+ onClick={() => removeRoot(root)}
349
+ className="text-[10px] px-2 py-1 text-[var(--red)] hover:bg-[var(--red)] hover:text-white rounded transition-colors"
350
+ >
351
+ Remove
352
+ </button>
353
+ </div>
354
+ ))}
355
+
356
+ <div className="flex gap-2">
357
+ <input
358
+ value={newRoot}
359
+ onChange={e => setNewRoot(e.target.value)}
360
+ onKeyDown={e => e.key === 'Enter' && addRoot()}
361
+ placeholder="/Users/you/projects"
362
+ className="flex-1 px-2 py-1.5 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] font-mono focus:outline-none focus:border-[var(--accent)]"
363
+ />
364
+ <button
365
+ onClick={addRoot}
366
+ className="text-[10px] px-3 py-1.5 bg-[var(--accent)] text-white rounded hover:opacity-90"
367
+ >
368
+ Add
369
+ </button>
370
+ </div>
371
+ </div>
372
+
373
+ {/* Document Roots */}
374
+ <div className="space-y-2">
375
+ <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
376
+ Document Directories
377
+ </label>
378
+ <p className="text-[10px] text-[var(--text-secondary)]">
379
+ Markdown document directories (e.g. Obsidian vaults). Shown in the Docs tab.
380
+ </p>
381
+
382
+ {(settings.docRoots || []).map((root: string) => (
383
+ <div key={root} className="flex items-center gap-2">
384
+ <span className="flex-1 text-xs px-2 py-1.5 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded font-mono truncate">
385
+ {root}
386
+ </span>
387
+ <button
388
+ onClick={() => setSettings({ ...settings, docRoots: settings.docRoots.filter((r: string) => r !== root) })}
389
+ className="text-[10px] px-2 py-1 text-[var(--red)] hover:bg-[var(--red)] hover:text-white rounded transition-colors"
390
+ >
391
+ Remove
392
+ </button>
393
+ </div>
394
+ ))}
395
+
396
+ <div className="flex gap-2">
397
+ <input
398
+ value={newDocRoot}
399
+ onChange={e => setNewDocRoot(e.target.value)}
400
+ onKeyDown={e => {
401
+ if (e.key === 'Enter' && newDocRoot.trim()) {
402
+ if (!settings.docRoots.includes(newDocRoot.trim())) {
403
+ setSettings({ ...settings, docRoots: [...(settings.docRoots || []), newDocRoot.trim()] });
404
+ }
405
+ setNewDocRoot('');
406
+ }
407
+ }}
408
+ placeholder="/Users/you/obsidian-vault"
409
+ className="flex-1 px-2 py-1.5 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] font-mono focus:outline-none focus:border-[var(--accent)]"
410
+ />
411
+ <button
412
+ onClick={() => {
413
+ if (newDocRoot.trim() && !settings.docRoots.includes(newDocRoot.trim())) {
414
+ setSettings({ ...settings, docRoots: [...(settings.docRoots || []), newDocRoot.trim()] });
415
+ }
416
+ setNewDocRoot('');
417
+ }}
418
+ className="text-[10px] px-3 py-1.5 bg-[var(--accent)] text-white rounded hover:opacity-90"
419
+ >
420
+ Add
421
+ </button>
422
+ </div>
423
+ <DocsAgentSelect settings={settings} setSettings={setSettings} />
424
+ </div>
425
+
426
+ {/* Agents */}
427
+ <AgentsSection settings={settings} setSettings={setSettings} />
428
+
429
+ {/* Telegram Notifications */}
430
+ <div className="space-y-2">
431
+ <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
432
+ Telegram Notifications
433
+ </label>
434
+ <p className="text-[10px] text-[var(--text-secondary)]">
435
+ Get notified when tasks complete or fail. Create a bot via @BotFather, then send /start to it and use the test button below to get your chat ID.
436
+ </p>
437
+
438
+ <SecretField
439
+ label="Bot Token"
440
+ description="Telegram Bot API token (from @BotFather)"
441
+ isSet={!!secretStatus.telegramBotToken}
442
+ onEdit={() => setEditingSecret({ field: 'telegramBotToken', label: 'Bot Token' })}
443
+
444
+ />
445
+
446
+ <input
447
+ value={settings.telegramChatId}
448
+ onChange={e => setSettings({ ...settings, telegramChatId: e.target.value })}
449
+ placeholder="Chat ID (comma-separated for multiple)"
450
+ className="w-full px-2 py-1.5 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] font-mono focus:outline-none focus:border-[var(--accent)]"
451
+ />
452
+ <p className="text-[9px] text-[var(--text-secondary)]">
453
+ Allowed user IDs (whitelist). Multiple IDs separated by commas. Only these users can interact with the bot.
454
+ </p>
455
+ <div className="flex items-center gap-4">
456
+ <label className="flex items-center gap-1.5 text-[11px] text-[var(--text-secondary)]">
457
+ <input
458
+ type="checkbox"
459
+ checked={settings.notifyOnComplete}
460
+ onChange={e => setSettings({ ...settings, notifyOnComplete: e.target.checked })}
461
+ className="rounded"
462
+ />
463
+ Notify on complete
464
+ </label>
465
+ <label className="flex items-center gap-1.5 text-[11px] text-[var(--text-secondary)]">
466
+ <input
467
+ type="checkbox"
468
+ checked={settings.notifyOnFailure}
469
+ onChange={e => setSettings({ ...settings, notifyOnFailure: e.target.checked })}
470
+ className="rounded"
471
+ />
472
+ Notify on failure
473
+ </label>
474
+ {secretStatus.telegramBotToken && settings.telegramChatId && (
475
+ <button
476
+ type="button"
477
+ onClick={async () => {
478
+ // Save first, then test
479
+ await fetch('/api/settings', {
480
+ method: 'PUT',
481
+ headers: { 'Content-Type': 'application/json' },
482
+ body: JSON.stringify(settings),
483
+ });
484
+ const res = await fetch('/api/notify/test', { method: 'POST' });
485
+ const data = await res.json();
486
+ alert(data.ok ? 'Test message sent!' : `Failed: ${data.error}`);
487
+ }}
488
+ className="text-[10px] px-2 py-0.5 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white transition-colors"
489
+ >
490
+ Test
491
+ </button>
492
+ )}
493
+ </div>
494
+ <TelegramAgentSelect settings={settings} setSettings={setSettings} />
495
+ </div>
496
+
497
+
498
+
499
+ {/* Notification Retention */}
500
+ <div className="space-y-2">
501
+ <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
502
+ Notifications
503
+ </label>
504
+ <div className="flex items-center gap-2">
505
+ <span className="text-[10px] text-[var(--text-secondary)]">Auto-delete after</span>
506
+ <select
507
+ value={settings.notificationRetentionDays || 30}
508
+ onChange={e => setSettings({ ...settings, notificationRetentionDays: Number(e.target.value) })}
509
+ className="text-xs bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-1 text-[var(--text-primary)]"
510
+ >
511
+ <option value={7}>7 days</option>
512
+ <option value={14}>14 days</option>
513
+ <option value={30}>30 days</option>
514
+ <option value={60}>60 days</option>
515
+ <option value={90}>90 days</option>
516
+ </select>
517
+ </div>
518
+ </div>
519
+
520
+ {/* Remote Access (Cloudflare Tunnel) */}
521
+ <div className="space-y-2">
522
+ <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
523
+ Remote Access
524
+ </label>
525
+ <p className="text-[10px] text-[var(--text-secondary)]">
526
+ Expose this instance to the internet via Cloudflare Tunnel. No account needed — generates a temporary public URL.
527
+ {!tunnel.installed && ' First use will download cloudflared (~30MB).'}
528
+ </p>
529
+
530
+ <div className="flex items-center gap-2">
531
+ {tunnel.status === 'stopped' || tunnel.status === 'error' ? (
532
+ tunnelPasswordPrompt ? (
533
+ <div className="flex items-center gap-2">
534
+ <input
535
+ type="password"
536
+ value={tunnelPassword}
537
+ onChange={e => { setTunnelPassword(e.target.value); setTunnelPasswordError(''); }}
538
+ onKeyDown={async e => {
539
+ if (e.key === 'Enter' && tunnelPassword) {
540
+ setTunnelLoading(true);
541
+ setTunnelPasswordError('');
542
+ try {
543
+ const res = await fetch('/api/tunnel', {
544
+ method: 'POST',
545
+ headers: { 'Content-Type': 'application/json' },
546
+ body: JSON.stringify({ action: 'start', password: tunnelPassword }),
547
+ });
548
+ const data = await res.json();
549
+ if (res.status === 403) {
550
+ setTunnelPasswordError('Wrong password');
551
+ } else {
552
+ setTunnel(data);
553
+ setTunnelPasswordPrompt(false);
554
+ setTunnelPassword('');
555
+ }
556
+ } catch {}
557
+ setTunnelLoading(false);
558
+ }
559
+ }}
560
+ placeholder="Login password"
561
+ autoFocus
562
+ className={`w-[140px] text-[10px] px-2 py-1 bg-[var(--bg-tertiary)] border rounded font-mono focus:outline-none ${
563
+ tunnelPasswordError ? 'border-[var(--red)]' : 'border-[var(--border)] focus:border-[var(--accent)]'
564
+ } text-[var(--text-primary)]`}
565
+ />
566
+ <button
567
+ disabled={!tunnelPassword || tunnelLoading}
568
+ onClick={async () => {
569
+ setTunnelLoading(true);
570
+ setTunnelPasswordError('');
571
+ try {
572
+ const res = await fetch('/api/tunnel', {
573
+ method: 'POST',
574
+ headers: { 'Content-Type': 'application/json' },
575
+ body: JSON.stringify({ action: 'start', password: tunnelPassword }),
576
+ });
577
+ const data = await res.json();
578
+ if (res.status === 403) {
579
+ setTunnelPasswordError('Wrong password');
580
+ } else {
581
+ setTunnel(data);
582
+ setTunnelPasswordPrompt(false);
583
+ setTunnelPassword('');
584
+ }
585
+ } catch {}
586
+ setTunnelLoading(false);
587
+ }}
588
+ className="text-[10px] px-2 py-1 bg-[var(--green)] text-black rounded hover:opacity-90 disabled:opacity-50"
589
+ >
590
+ {tunnelLoading ? 'Starting...' : 'Start'}
591
+ </button>
592
+ <button
593
+ onClick={() => { setTunnelPasswordPrompt(false); setTunnelPassword(''); setTunnelPasswordError(''); }}
594
+ className="text-[10px] px-2 py-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
595
+ >
596
+ Cancel
597
+ </button>
598
+ {tunnelPasswordError && <span className="text-[9px] text-[var(--red)]">{tunnelPasswordError}</span>}
599
+ </div>
600
+ ) : (
601
+ <button
602
+ onClick={() => setTunnelPasswordPrompt(true)}
603
+ className="text-[10px] px-3 py-1.5 bg-[var(--green)] text-black rounded hover:opacity-90"
604
+ >
605
+ Start Tunnel
606
+ </button>
607
+ )
608
+ ) : confirmStopTunnel ? (
609
+ <div className="flex items-center gap-2">
610
+ <span className="text-[10px] text-[var(--text-secondary)]">Stop tunnel?</span>
611
+ <button
612
+ onClick={async () => {
613
+ await fetch('/api/tunnel', {
614
+ method: 'POST',
615
+ headers: { 'Content-Type': 'application/json' },
616
+ body: JSON.stringify({ action: 'stop' }),
617
+ });
618
+ refreshTunnel();
619
+ setConfirmStopTunnel(false);
620
+ }}
621
+ className="text-[10px] px-2 py-1 bg-[var(--red)] text-white rounded hover:opacity-90"
622
+ >
623
+ Confirm
624
+ </button>
625
+ <button
626
+ onClick={() => setConfirmStopTunnel(false)}
627
+ className="text-[10px] px-2 py-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
628
+ >
629
+ Cancel
630
+ </button>
631
+ </div>
632
+ ) : (
633
+ <button
634
+ onClick={() => setConfirmStopTunnel(true)}
635
+ className="text-[10px] px-3 py-1.5 bg-[var(--red)] text-white rounded hover:opacity-90"
636
+ >
637
+ Stop Tunnel
638
+ </button>
639
+ )}
640
+
641
+ <span className="text-[10px] text-[var(--text-secondary)]">
642
+ {tunnel.status === 'running' && (
643
+ <span className="text-[var(--green)]">Running</span>
644
+ )}
645
+ {tunnel.status === 'starting' && (
646
+ <span className="text-[var(--yellow)]">Starting...</span>
647
+ )}
648
+ {tunnel.status === 'error' && (
649
+ <span className="text-[var(--red)]">Error</span>
650
+ )}
651
+ {tunnel.status === 'stopped' && 'Stopped'}
652
+ </span>
653
+ </div>
654
+
655
+ {tunnel.url && (
656
+ <div className="flex items-center gap-2">
657
+ <input
658
+ readOnly
659
+ value={tunnel.url}
660
+ className="flex-1 px-2 py-1.5 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--green)] font-mono focus:outline-none cursor-text select-all"
661
+ onClick={e => (e.target as HTMLInputElement).select()}
662
+ />
663
+ <button
664
+ onClick={() => {
665
+ navigator.clipboard.writeText(tunnel.url!);
666
+ }}
667
+ className="text-[10px] px-2 py-1.5 border border-[var(--border)] rounded hover:bg-[var(--bg-tertiary)] transition-colors"
668
+ >
669
+ Copy
670
+ </button>
671
+ </div>
672
+ )}
673
+
674
+ {tunnel.error && (
675
+ <p className="text-[10px] text-[var(--red)]">{tunnel.error}</p>
676
+ )}
677
+
678
+ {tunnel.log.length > 0 && tunnel.status !== 'stopped' && (
679
+ <details className="text-[10px]">
680
+ <summary className="text-[var(--text-secondary)] cursor-pointer hover:text-[var(--text-primary)]">
681
+ Logs ({tunnel.log.length} lines)
682
+ </summary>
683
+ <pre className="mt-1 p-2 bg-[var(--bg-primary)] border border-[var(--border)] rounded text-[9px] text-[var(--text-secondary)] max-h-[120px] overflow-auto font-mono whitespace-pre-wrap">
684
+ {tunnel.log.join('\n')}
685
+ </pre>
686
+ </details>
687
+ )}
688
+
689
+ <label className="flex items-center gap-1.5 text-[11px] text-[var(--text-secondary)]">
690
+ <input
691
+ type="checkbox"
692
+ checked={settings.tunnelAutoStart}
693
+ onChange={e => setSettings({ ...settings, tunnelAutoStart: e.target.checked })}
694
+ className="rounded"
695
+ />
696
+ Auto-start tunnel on server startup
697
+ </label>
698
+
699
+ </div>
700
+
701
+ {/* Display Name */}
702
+ <div className="space-y-2">
703
+ <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
704
+ Display Name
705
+ </label>
706
+ <input
707
+ type="text"
708
+ value={(settings as any).displayName || ''}
709
+ onChange={e => setSettings({ ...settings, displayName: e.target.value } as any)}
710
+ placeholder="Forge"
711
+ className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)]"
712
+ />
713
+ </div>
714
+
715
+ {/* Email */}
716
+ <div className="space-y-2">
717
+ <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
718
+ Email
719
+ </label>
720
+ <input
721
+ type="email"
722
+ value={(settings as any).displayEmail || ''}
723
+ onChange={e => setSettings({ ...settings, displayEmail: e.target.value } as any)}
724
+ placeholder="local@forge"
725
+ className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)]"
726
+ />
727
+ </div>
728
+
729
+ {/* Admin Password */}
730
+ <div className="space-y-2">
731
+ <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
732
+ Admin Password
733
+ </label>
734
+ <p className="text-[10px] text-[var(--text-secondary)]">
735
+ Used for local login, tunnel start, secret changes, and Telegram commands. Remote login requires admin password + session code (generated on tunnel start).
736
+ </p>
737
+ <SecretField
738
+ label="Admin Password"
739
+ isSet={!!secretStatus.telegramTunnelPassword}
740
+ onEdit={() => setEditingSecret({ field: 'telegramTunnelPassword', label: 'Admin Password' })}
741
+ />
742
+ <p className="text-[9px] text-[var(--text-secondary)]">
743
+ Forgot? Run: <code className="text-[var(--accent)]">forge --reset-password</code>
744
+ </p>
745
+ </div>
746
+
747
+ {/* Actions */}
748
+ <div className="flex items-center justify-between pt-2 border-t border-[var(--border)]">
749
+ <span className="text-[10px] text-[var(--green)]">
750
+ {saved ? '✓ Saved' : ''}
751
+ </span>
752
+ <div className="flex gap-2">
753
+ <button
754
+ onClick={onClose}
755
+ className="px-3 py-1.5 text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
756
+ >
757
+ Close
758
+ </button>
759
+ <button
760
+ onClick={save}
761
+ className="px-3 py-1.5 text-xs bg-[var(--accent)] text-white rounded hover:opacity-90"
762
+ >
763
+ Save
764
+ </button>
765
+ </div>
766
+ </div>
767
+ </div>
768
+
769
+ {/* Secret Change Dialog */}
770
+ {editingSecret && (
771
+ <SecretChangeDialog
772
+ field={editingSecret.field}
773
+ label={editingSecret.label}
774
+ isSet={!!secretStatus[editingSecret.field]}
775
+ onSave={saveSecret}
776
+ onClose={() => setEditingSecret(null)}
777
+ />
778
+ )}
779
+ </div>
780
+ );
781
+ }
782
+
783
+ // ─── Agents Configuration Section ─────────────────────────────
784
+
785
+ interface AgentEntry {
786
+ id: string;
787
+ name: string;
788
+ path: string;
789
+ enabled: boolean;
790
+ type: string;
791
+ taskFlags: string;
792
+ interactiveCmd: string;
793
+ resumeFlag: string;
794
+ outputFormat: string;
795
+ models: { terminal: string; task: string; telegram: string; help: string; mobile: string };
796
+ skipPermissionsFlag: string;
797
+ requiresTTY: boolean;
798
+ detected: boolean;
799
+ isProfile?: boolean;
800
+ base?: string;
801
+ backendType?: string;
802
+ }
803
+
804
+ function ProfileRow({ id, cfg, inputClass, onUpdate, onDelete }: {
805
+ id: string; cfg: any; inputClass: string;
806
+ onUpdate: (cfg: any) => void; onDelete: () => void;
807
+ }) {
808
+ const [expanded, setExpanded] = useState(false);
809
+ const isApi = cfg.type === 'api';
810
+ const summary = isApi
811
+ ? `API: ${cfg.provider || '?'} / ${cfg.model || '?'}`
812
+ : `CLI: ${cfg.cliType || cfg.base || '?'} / ${cfg.model || cfg.models?.task || 'default'}`;
813
+ const envStr = cfg.env ? Object.entries(cfg.env).map(([k, v]) => `${k}=${v}`).join('\n') : '';
814
+
815
+ return (
816
+ <div className="mb-1 rounded" style={{ background: 'var(--bg-tertiary)' }}>
817
+ <div className="flex items-center gap-2 px-2 py-1.5 cursor-pointer" onClick={() => setExpanded(!expanded)}>
818
+ <span className="text-[8px] text-[var(--text-secondary)]">{expanded ? '▼' : '▶'}</span>
819
+ <span className="text-[9px] text-[var(--accent)] font-mono w-28 truncate">{id}</span>
820
+ <span className="text-[9px] text-[var(--text-secondary)]">{summary}</span>
821
+ <span className="text-[8px] text-[var(--text-secondary)]">{cfg.name || ''}</span>
822
+ <button onClick={(e) => { e.stopPropagation(); onDelete(); }}
823
+ className="text-[9px] text-gray-500 hover:text-red-400 ml-auto">✕</button>
824
+ </div>
825
+ {expanded && (
826
+ <div className="px-3 pb-2 space-y-1.5 border-t border-[var(--border)]">
827
+ <div className="flex gap-2 mt-1.5">
828
+ <div className="flex-1">
829
+ <label className="text-[8px] text-[var(--text-secondary)]">Name</label>
830
+ <input value={cfg.name || ''} onChange={e => onUpdate({ ...cfg, name: e.target.value })} className={inputClass} />
831
+ </div>
832
+ <div className="flex-1">
833
+ <label className="text-[8px] text-[var(--text-secondary)]">Model</label>
834
+ <input value={cfg.model || ''} onChange={e => onUpdate({ ...cfg, model: e.target.value })}
835
+ list={`profile-model-${id}`} className={inputClass} />
836
+ {(cfg.cliType === 'claude-code' || (!cfg.cliType && !cfg.base && !isApi)) && (
837
+ <datalist id={`profile-model-${id}`}>
838
+ <option value="claude-opus-4-6" />
839
+ <option value="claude-sonnet-4-6" />
840
+ <option value="claude-haiku-4-5-20251001" />
841
+ </datalist>
842
+ )}
843
+ {(cfg.cliType === 'codex' || cfg.base === 'codex') && (
844
+ <datalist id={`profile-model-${id}`}>
845
+ <option value="codex-mini" />
846
+ <option value="o4-mini" />
847
+ <option value="gpt-4o" />
848
+ </datalist>
849
+ )}
850
+ </div>
851
+ </div>
852
+ {isApi ? (
853
+ <div className="flex gap-2">
854
+ <div className="flex-1">
855
+ <label className="text-[8px] text-[var(--text-secondary)]">Provider</label>
856
+ <select value={cfg.provider || 'anthropic'} onChange={e => onUpdate({ ...cfg, provider: e.target.value })} className={inputClass}>
857
+ <option value="anthropic">Anthropic</option>
858
+ <option value="google">Google</option>
859
+ <option value="openai">OpenAI</option>
860
+ <option value="grok">Grok</option>
861
+ </select>
862
+ </div>
863
+ <div className="flex-1">
864
+ <label className="text-[8px] text-[var(--text-secondary)]">API Key (optional)</label>
865
+ <input type="password" value={cfg.apiKey || ''} onChange={e => onUpdate({ ...cfg, apiKey: e.target.value })} className={inputClass} />
866
+ </div>
867
+ </div>
868
+ ) : (
869
+ <>
870
+ <div>
871
+ <label className="text-[8px] text-[var(--text-secondary)]">CLI Type</label>
872
+ <select value={cfg.cliType === 'claude-code' ? 'claude' : (cfg.cliType || cfg.base || 'claude')} onChange={e => onUpdate({ ...cfg, cliType: e.target.value === 'claude' ? 'claude-code' : e.target.value })} className={inputClass}>
873
+ <option value="claude">Claude Code</option>
874
+ <option value="codex">Codex</option>
875
+ <option value="aider">Aider</option>
876
+ <option value="generic">Generic</option>
877
+ </select>
878
+ </div>
879
+ <div>
880
+ <div className="flex items-center gap-2">
881
+ <label className="text-[8px] text-[var(--text-secondary)]">Environment Variables (KEY=VALUE per line)</label>
882
+ {(cfg.cliType || cfg.base) && (
883
+ <button onClick={() => {
884
+ const templates: Record<string, string> = {
885
+ 'claude-code': 'ANTHROPIC_AUTH_TOKEN=\nANTHROPIC_BASE_URL=\nANTHROPIC_SMALL_FAST_MODEL=\nCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=true\nDISABLE_TELEMETRY=true\nDISABLE_ERROR_REPORTING=true\nDISABLE_AUTOUPDATER=true\nDISABLE_NON_ESSENTIAL_MODEL_CALLS=true',
886
+ claude: 'ANTHROPIC_AUTH_TOKEN=\nANTHROPIC_BASE_URL=\nANTHROPIC_SMALL_FAST_MODEL=\nCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=true\nDISABLE_TELEMETRY=true\nDISABLE_ERROR_REPORTING=true\nDISABLE_AUTOUPDATER=true\nDISABLE_NON_ESSENTIAL_MODEL_CALLS=true',
887
+ codex: 'OPENAI_API_KEY=\nOPENAI_BASE_URL=',
888
+ aider: 'ANTHROPIC_API_KEY=\nOPENAI_API_KEY=',
889
+ };
890
+ const tpl = templates[cfg.cliType || cfg.base!];
891
+ if (tpl) {
892
+ const env: Record<string, string> = {};
893
+ for (const line of tpl.split('\n')) {
894
+ const eq = line.indexOf('=');
895
+ if (eq > 0) env[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
896
+ }
897
+ // Merge with existing (don't overwrite filled values)
898
+ const merged = { ...env, ...(cfg.env || {}) };
899
+ onUpdate({ ...cfg, env: merged });
900
+ }
901
+ }} className="text-[7px] px-1.5 py-0.5 rounded bg-[var(--accent)]/10 text-[var(--accent)] hover:bg-[var(--accent)]/20">
902
+ Fill {cfg.cliType === 'claude-code' ? 'claude' : (cfg.cliType || cfg.base)} template
903
+ </button>
904
+ )}
905
+ </div>
906
+ <textarea
907
+ value={envStr}
908
+ onChange={e => {
909
+ const env: Record<string, string> = {};
910
+ for (const line of e.target.value.split('\n')) {
911
+ const eq = line.indexOf('=');
912
+ if (eq > 0) env[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
913
+ }
914
+ onUpdate({ ...cfg, env: Object.keys(env).length > 0 ? env : undefined });
915
+ }}
916
+ rows={5}
917
+ placeholder="ANTHROPIC_AUTH_TOKEN=sk-...\nANTHROPIC_BASE_URL=http://..."
918
+ className={inputClass + ' resize-none font-mono'} />
919
+ </div>
920
+ </>
921
+ )}
922
+ </div>
923
+ )}
924
+ </div>
925
+ );
926
+ }
927
+
928
+ function AddProfileForm({ type, baseAgents, onAdd }: {
929
+ type: 'cli' | 'api';
930
+ baseAgents: AgentEntry[];
931
+ onAdd: (id: string, cfg: any) => void;
932
+ }) {
933
+ const [open, setOpen] = useState(false);
934
+ const [id, setId] = useState('');
935
+ const [name, setName] = useState('');
936
+ const [base, setBase] = useState(baseAgents[0]?.id || 'claude');
937
+ const [model, setModel] = useState('');
938
+ const [provider, setProvider] = useState('anthropic');
939
+ const [envText, setEnvText] = useState('');
940
+ const [apiKey, setApiKey] = useState('');
941
+
942
+ const inputClass = "w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] font-mono focus:outline-none focus:border-[var(--accent)]";
943
+
944
+ // Env var templates per CLI type
945
+ const envTemplates: Record<string, string> = {
946
+ claude: [
947
+ 'ANTHROPIC_AUTH_TOKEN=',
948
+ 'ANTHROPIC_BASE_URL=',
949
+ 'ANTHROPIC_SMALL_FAST_MODEL=',
950
+ 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=true',
951
+ 'DISABLE_TELEMETRY=true',
952
+ 'DISABLE_ERROR_REPORTING=true',
953
+ 'DISABLE_AUTOUPDATER=true',
954
+ 'DISABLE_NON_ESSENTIAL_MODEL_CALLS=true',
955
+ ].join('\n'),
956
+ codex: [
957
+ 'OPENAI_API_KEY=',
958
+ 'OPENAI_BASE_URL=',
959
+ ].join('\n'),
960
+ aider: [
961
+ 'ANTHROPIC_API_KEY=',
962
+ 'OPENAI_API_KEY=',
963
+ ].join('\n'),
964
+ };
965
+
966
+ const fillEnvTemplate = () => {
967
+ const tpl = envTemplates[base] || '';
968
+ if (tpl && (!envText.trim() || confirm('Replace current env vars with template?'))) {
969
+ setEnvText(tpl);
970
+ }
971
+ };
972
+
973
+ if (!open) {
974
+ return (
975
+ <button onClick={() => setOpen(true)}
976
+ className="text-[9px] px-2 py-0.5 border border-dashed border-[var(--border)] text-[var(--text-secondary)] rounded hover:text-[var(--text-primary)] mt-1">
977
+ + {type === 'cli' ? 'CLI Profile' : 'API Profile'}
978
+ </button>
979
+ );
980
+ }
981
+
982
+ const parseEnv = (): Record<string, string> | undefined => {
983
+ if (!envText.trim()) return undefined;
984
+ const env: Record<string, string> = {};
985
+ for (const line of envText.split('\n')) {
986
+ const eq = line.indexOf('=');
987
+ if (eq > 0) {
988
+ env[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
989
+ }
990
+ }
991
+ return Object.keys(env).length > 0 ? env : undefined;
992
+ };
993
+
994
+ const handleAdd = () => {
995
+ if (!id) return;
996
+ if (type === 'cli') {
997
+ onAdd(id, { cliType: base === 'claude' ? 'claude-code' : base, name: name || id, model: model || undefined, env: parseEnv() });
998
+ } else {
999
+ onAdd(id, { type: 'api', name: name || id, provider, model: model || undefined, apiKey: apiKey || undefined });
1000
+ }
1001
+ setOpen(false);
1002
+ setId(''); setName(''); setModel(''); setApiKey(''); setEnvText('');
1003
+ };
1004
+
1005
+ return (
1006
+ <div className="mt-2 p-2 rounded border border-[var(--border)] space-y-1.5" style={{ background: 'var(--bg-secondary)' }}>
1007
+ <div className="text-[9px] text-[var(--text-secondary)] font-semibold">New {type === 'cli' ? 'CLI' : 'API'} Profile</div>
1008
+ <div className="flex gap-2">
1009
+ <div className="flex-1">
1010
+ <label className="text-[8px] text-[var(--text-secondary)]">Profile ID</label>
1011
+ <input value={id} onChange={e => setId(e.target.value.replace(/\s+/g, '-').toLowerCase())} placeholder={type === 'cli' ? 'claude-opus' : 'api-sonnet'} className={inputClass} />
1012
+ </div>
1013
+ <div className="flex-1">
1014
+ <label className="text-[8px] text-[var(--text-secondary)]">Display Name</label>
1015
+ <input value={name} onChange={e => setName(e.target.value)} placeholder="Claude Opus" className={inputClass} />
1016
+ </div>
1017
+ </div>
1018
+ {type === 'cli' ? (<>
1019
+ <div className="flex gap-2">
1020
+ <div className="flex-1">
1021
+ <label className="text-[8px] text-[var(--text-secondary)]">CLI Type</label>
1022
+ <select value={base} onChange={e => setBase(e.target.value)}
1023
+ className={inputClass}>
1024
+ <option value="claude">Claude Code</option>
1025
+ <option value="codex">Codex</option>
1026
+ <option value="aider">Aider</option>
1027
+ <option value="generic">Generic</option>
1028
+ </select>
1029
+ </div>
1030
+ <div className="flex-1">
1031
+ <label className="text-[8px] text-[var(--text-secondary)]">Model</label>
1032
+ <input value={model} onChange={e => setModel(e.target.value)}
1033
+ placeholder={base === 'claude' ? 'claude-sonnet-4-6' : base === 'codex' ? 'codex-mini' : ''}
1034
+ list={`model-list-${base}`} className={inputClass} />
1035
+ {base === 'claude' && (
1036
+ <datalist id="model-list-claude">
1037
+ <option value="claude-opus-4-6" />
1038
+ <option value="claude-sonnet-4-6" />
1039
+ <option value="claude-haiku-4-5-20251001" />
1040
+ </datalist>
1041
+ )}
1042
+ {base === 'codex' && (
1043
+ <datalist id="model-list-codex">
1044
+ <option value="codex-mini" />
1045
+ <option value="o4-mini" />
1046
+ <option value="gpt-4o" />
1047
+ </datalist>
1048
+ )}
1049
+ </div>
1050
+ </div>
1051
+ <div>
1052
+ <div className="flex items-center gap-2">
1053
+ <label className="text-[8px] text-[var(--text-secondary)]">Environment Variables (KEY=VALUE per line)</label>
1054
+ {envTemplates[base] && (
1055
+ <button onClick={fillEnvTemplate} className="text-[7px] px-1.5 py-0.5 rounded bg-[var(--accent)]/10 text-[var(--accent)] hover:bg-[var(--accent)]/20">
1056
+ Fill {base} template
1057
+ </button>
1058
+ )}
1059
+ </div>
1060
+ <textarea value={envText} onChange={e => setEnvText(e.target.value)} rows={5}
1061
+ placeholder={envTemplates[base] || 'KEY=VALUE\nKEY2=VALUE2'}
1062
+ className={inputClass + ' resize-none font-mono'} />
1063
+ </div>
1064
+ </>) : (
1065
+ <>
1066
+ <div className="flex gap-2">
1067
+ <div className="flex-1">
1068
+ <label className="text-[8px] text-[var(--text-secondary)]">Provider</label>
1069
+ <select value={provider} onChange={e => setProvider(e.target.value)} className={inputClass}>
1070
+ <option value="anthropic">Anthropic</option>
1071
+ <option value="google">Google</option>
1072
+ <option value="openai">OpenAI</option>
1073
+ <option value="grok">Grok</option>
1074
+ </select>
1075
+ </div>
1076
+ <div className="flex-1">
1077
+ <label className="text-[8px] text-[var(--text-secondary)]">Model</label>
1078
+ <input value={model} onChange={e => setModel(e.target.value)} placeholder="claude-sonnet-4-6" className={inputClass} />
1079
+ </div>
1080
+ </div>
1081
+ <div>
1082
+ <label className="text-[8px] text-[var(--text-secondary)]">API Key (optional, uses provider key if empty)</label>
1083
+ <input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} placeholder="sk-..." className={inputClass} />
1084
+ </div>
1085
+ </>
1086
+ )}
1087
+ <div className="flex gap-2">
1088
+ <button onClick={handleAdd} disabled={!id} className="text-[10px] px-3 py-1 bg-[var(--accent)] text-white rounded disabled:opacity-50">Add</button>
1089
+ <button onClick={() => setOpen(false)} className="text-[10px] px-3 py-1 border border-[var(--border)] text-[var(--text-secondary)] rounded">Cancel</button>
1090
+ </div>
1091
+ </div>
1092
+ );
1093
+ }
1094
+
1095
+ function AgentsSection({ settings, setSettings }: { settings: any; setSettings: (s: any) => void }) {
1096
+ const [agents, setAgents] = useState<AgentEntry[]>([]);
1097
+ const [loading, setLoading] = useState(true);
1098
+ const [expandedAgent, setExpandedAgent] = useState<string | null>(null);
1099
+ const [showAdd, setShowAdd] = useState(false);
1100
+ const cliDefaults: Record<string, any> = {
1101
+ 'claude-code': { taskFlags: '-p --verbose --output-format stream-json --dangerously-skip-permissions', resumeFlag: '-c', outputFormat: 'stream-json', skipPermissionsFlag: '--dangerously-skip-permissions' },
1102
+ 'codex': { taskFlags: '', resumeFlag: '', outputFormat: 'text', skipPermissionsFlag: '--full-auto' },
1103
+ 'aider': { taskFlags: '--message', resumeFlag: '', outputFormat: 'text', skipPermissionsFlag: '--yes' },
1104
+ 'generic': { taskFlags: '', resumeFlag: '', outputFormat: 'text', skipPermissionsFlag: '' },
1105
+ };
1106
+ const makeNewAgent = (cliType = 'claude-code') => ({
1107
+ id: '', name: '', path: '', interactiveCmd: '',
1108
+ models: { terminal: 'default', task: 'default', telegram: 'default', help: 'default', mobile: 'default' },
1109
+ requiresTTY: false, cliType,
1110
+ ...cliDefaults[cliType],
1111
+ });
1112
+ const [newAgent, setNewAgent] = useState(makeNewAgent());
1113
+
1114
+ // Fetch detected + configured agents
1115
+ useEffect(() => {
1116
+ (async () => {
1117
+ setLoading(true);
1118
+ try {
1119
+ // Fetch both agents and settings together to avoid race condition
1120
+ // (settings prop may not be loaded yet when this effect runs)
1121
+ const [agentsRes, settingsRes] = await Promise.all([
1122
+ fetch('/api/agents'),
1123
+ fetch('/api/settings'),
1124
+ ]);
1125
+ const data = await agentsRes.json();
1126
+ const settingsData = await settingsRes.json();
1127
+ const detected = (data.agents || []) as any[];
1128
+ const configured = settingsData.agents || {};
1129
+
1130
+ const merged: AgentEntry[] = [];
1131
+
1132
+ // Add agents from API (may be detected or configured-only)
1133
+ for (const a of detected) {
1134
+ const cfg = configured[a.id] || {};
1135
+ merged.push({
1136
+ id: a.id,
1137
+ name: cfg.name ?? a.name,
1138
+ path: cfg.path ?? a.path,
1139
+ enabled: cfg.enabled !== false,
1140
+ type: a.type || 'generic',
1141
+ taskFlags: cfg.taskFlags ?? (a.id === 'claude' ? '-p --verbose --output-format stream-json --dangerously-skip-permissions' : cfg.flags?.join(' ') ?? ''),
1142
+ interactiveCmd: cfg.interactiveCmd ?? a.path,
1143
+ resumeFlag: cfg.resumeFlag ?? (a.capabilities?.supportsResume ? '-c' : ''),
1144
+ outputFormat: cfg.outputFormat ?? (a.capabilities?.supportsStreamJson ? 'stream-json' : 'text'),
1145
+ models: cfg.models ?? { terminal: "default", task: "default", telegram: "default", help: "default", mobile: "default" },
1146
+ skipPermissionsFlag: cfg.skipPermissionsFlag ?? a.skipPermissionsFlag ?? "",
1147
+ requiresTTY: cfg.requiresTTY ?? a.capabilities?.requiresTTY ?? false,
1148
+ detected: a.detected !== false,
1149
+ });
1150
+ }
1151
+
1152
+ // Add configured but not detected agents
1153
+ for (const [id, cfg] of Object.entries(configured) as [string, any][]) {
1154
+ if (merged.find(a => a.id === id)) continue;
1155
+ merged.push({
1156
+ id,
1157
+ name: cfg.name ?? id,
1158
+ path: cfg.path ?? '',
1159
+ enabled: cfg.enabled !== false,
1160
+ type: 'generic',
1161
+ taskFlags: cfg.taskFlags ?? cfg.flags?.join(' ') ?? '',
1162
+ interactiveCmd: cfg.interactiveCmd ?? cfg.path ?? '',
1163
+ resumeFlag: cfg.resumeFlag ?? '',
1164
+ outputFormat: cfg.outputFormat ?? 'text',
1165
+ models: cfg.models ?? { terminal: "default", task: "default", telegram: "default", help: "default", mobile: "default" },
1166
+ skipPermissionsFlag: cfg.skipPermissionsFlag ?? '',
1167
+ requiresTTY: cfg.requiresTTY ?? false,
1168
+ detected: false,
1169
+ });
1170
+ }
1171
+
1172
+ setAgents(merged);
1173
+ } catch {}
1174
+ setLoading(false);
1175
+ })();
1176
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1177
+ }, []); // Only fetch once on mount
1178
+
1179
+ const defaultAgent = settings.defaultAgent || 'claude';
1180
+
1181
+ const saveAgentConfig = (updated: AgentEntry[]) => {
1182
+ // Use functional update to avoid stale closure — each call sees the latest settings
1183
+ setSettings((prev: any) => {
1184
+ const agentsCfg: Record<string, any> = { ...(prev.agents || {}) };
1185
+ for (const a of updated) {
1186
+ const existing = agentsCfg[a.id] || {};
1187
+ agentsCfg[a.id] = {
1188
+ ...existing, // preserve profile-specific fields
1189
+ name: a.name,
1190
+ path: a.path,
1191
+ enabled: a.enabled,
1192
+ taskFlags: a.taskFlags,
1193
+ interactiveCmd: a.interactiveCmd,
1194
+ resumeFlag: a.resumeFlag,
1195
+ outputFormat: a.outputFormat,
1196
+ models: a.models,
1197
+ skipPermissionsFlag: a.skipPermissionsFlag,
1198
+ requiresTTY: a.requiresTTY,
1199
+ cliType: (a as any).cliType || existing.cliType,
1200
+ };
1201
+ }
1202
+ const claude = updated.find(a => a.id === 'claude');
1203
+ return { ...prev, agents: agentsCfg, claudePath: claude?.path || prev.claudePath };
1204
+ });
1205
+ };
1206
+
1207
+ const [agentsDirty, setAgentsDirty] = useState(false);
1208
+ const saveTimerRef = useRef<any>(null);
1209
+
1210
+ const debouncedSave = useCallback((updated: AgentEntry[]) => {
1211
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
1212
+ saveTimerRef.current = setTimeout(() => {
1213
+ saveAgentConfig(updated);
1214
+ setAgentsDirty(false);
1215
+ }, 1000); // save after 1s of no changes
1216
+ }, [saveAgentConfig]);
1217
+
1218
+ const updateAgent = (id: string, field: string, value: any) => {
1219
+ const updated = agents.map(a => a.id === id ? { ...a, [field]: value } : a);
1220
+ setAgents(updated);
1221
+ // Sync to settings immediately (no debounce) so global Save always has latest data
1222
+ saveAgentConfig(updated);
1223
+ };
1224
+
1225
+ const saveAgents = () => {
1226
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
1227
+ saveAgentConfig(agents);
1228
+ setAgentsDirty(false);
1229
+ };
1230
+
1231
+ const removeAgent = (id: string) => {
1232
+ if (!confirm(`Remove "${id}" agent?`)) return;
1233
+ const updated = agents.filter(a => a.id !== id);
1234
+ setAgents(updated);
1235
+ // Remove from settings directly (saveAgentConfig only handles add/update, not delete)
1236
+ setSettings((prev: any) => {
1237
+ const agentsCfg = { ...(prev.agents || {}) };
1238
+ delete agentsCfg[id];
1239
+ return { ...prev, agents: agentsCfg };
1240
+ });
1241
+ };
1242
+
1243
+ const addAgent = () => {
1244
+ if (!newAgent.id || !newAgent.path) return;
1245
+ const entry: AgentEntry = {
1246
+ ...newAgent,
1247
+ enabled: true,
1248
+ type: 'generic',
1249
+ detected: false,
1250
+ };
1251
+ const updated = [...agents, entry];
1252
+ setAgents(updated);
1253
+ debouncedSave(updated);
1254
+ setShowAdd(false);
1255
+ setNewAgent(makeNewAgent());
1256
+ };
1257
+
1258
+ const inputClass = "w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] font-mono focus:outline-none focus:border-[var(--accent)]";
1259
+
1260
+ return (
1261
+ <div className="space-y-3">
1262
+ <div className="flex items-center gap-2">
1263
+ <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">Agents</label>
1264
+ <button
1265
+ onClick={async () => {
1266
+ try {
1267
+ const res = await fetch('/api/agents');
1268
+ const data = await res.json();
1269
+ if (data.agents?.length) alert(`Detected: ${data.agents.map((a: any) => a.name).join(', ')}`);
1270
+ else alert('No agents detected');
1271
+ } catch { alert('Detection failed'); }
1272
+ }}
1273
+ className="text-[9px] px-2 py-0.5 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white ml-auto"
1274
+ >Detect</button>
1275
+ <button
1276
+ onClick={() => {
1277
+ // Auto-fill path from detected claude agent when opening Add form
1278
+ const claude = agents.find(a => a.id === 'claude');
1279
+ if (claude?.path && !newAgent.path) setNewAgent((prev: any) => ({ ...prev, path: claude.path, interactiveCmd: claude.path }));
1280
+ setShowAdd(v => !v);
1281
+ }}
1282
+ className="text-[9px] px-2 py-0.5 border border-[var(--border)] text-[var(--text-secondary)] rounded hover:text-[var(--text-primary)]"
1283
+ >+ Add</button>
1284
+ {agentsDirty && (
1285
+ <button
1286
+ onClick={saveAgents}
1287
+ className="text-[9px] px-2 py-0.5 bg-[var(--accent)] text-white rounded"
1288
+ >Save Agents</button>
1289
+ )}
1290
+ </div>
1291
+
1292
+ {/* Default agent selector */}
1293
+ <div className="flex items-center gap-2">
1294
+ <span className="text-[10px] text-[var(--text-secondary)]">Default:</span>
1295
+ <select
1296
+ value={defaultAgent}
1297
+ onChange={e => setSettings({ ...settings, defaultAgent: e.target.value })}
1298
+ className="bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-1 text-xs text-[var(--text-primary)]"
1299
+ >
1300
+ {agents.filter(a => a.enabled).map(a => (
1301
+ <option key={a.id} value={a.id}>{a.name}</option>
1302
+ ))}
1303
+ </select>
1304
+ <span className="text-[9px] text-[var(--text-secondary)]">Used for Task, Terminal, Pipeline, Mobile, Help</span>
1305
+ </div>
1306
+
1307
+ {loading ? (
1308
+ <p className="text-[10px] text-[var(--text-secondary)]">Loading agents...</p>
1309
+ ) : agents.length === 0 ? (
1310
+ <p className="text-[10px] text-[var(--text-secondary)]">No agents detected. Click Detect or Add manually.</p>
1311
+ ) : (
1312
+ <div className="space-y-2">
1313
+ {agents.map(a => (
1314
+ <div key={a.id} className="border border-[var(--border)] rounded-lg overflow-hidden">
1315
+ {/* Agent header */}
1316
+ <div
1317
+ className="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-[var(--bg-tertiary)]"
1318
+ onClick={() => setExpandedAgent(expandedAgent === a.id ? null : a.id)}
1319
+ >
1320
+ <span className={`w-2 h-2 rounded-full shrink-0 ${
1321
+ !a.detected ? 'bg-gray-500' : a.id === defaultAgent ? 'bg-green-500' : 'bg-green-400/60'
1322
+ }`} title={!a.detected ? 'Not installed' : a.id === defaultAgent ? 'Default agent' : 'Installed'} />
1323
+ <span className={`text-xs font-medium ${!a.detected ? 'text-[var(--text-secondary)]' : 'text-[var(--text-primary)]'}`}>{a.name}</span>
1324
+ <span className="text-[9px] text-[var(--text-secondary)] font-mono">{a.id}</span>
1325
+ {a.id === defaultAgent && <span className="text-[8px] px-1 rounded bg-green-500/20 text-green-400">default</span>}
1326
+ {!a.detected && <span className="text-[8px] text-gray-500">not installed</span>}
1327
+ <label className="flex items-center gap-1 ml-auto text-[9px] text-[var(--text-secondary)]" onClick={e => e.stopPropagation()}>
1328
+ <input type="checkbox" checked={a.enabled} onChange={e => updateAgent(a.id, 'enabled', e.target.checked)} className="accent-[var(--accent)]" />
1329
+ Enabled
1330
+ </label>
1331
+ <span className="text-[10px] text-[var(--text-secondary)]">{expandedAgent === a.id ? '▾' : '▸'}</span>
1332
+ </div>
1333
+
1334
+ {/* Agent detail */}
1335
+ {expandedAgent === a.id && (
1336
+ <div className="px-3 py-2 border-t border-[var(--border)] space-y-2 bg-[var(--bg-secondary)]">
1337
+ <div className="flex gap-2">
1338
+ <div className="flex-1">
1339
+ <label className="text-[9px] text-[var(--text-secondary)]">Name</label>
1340
+ <input value={a.name} onChange={e => updateAgent(a.id, 'name', e.target.value)} className={inputClass} />
1341
+ </div>
1342
+ <div className="w-36">
1343
+ <label className="text-[9px] text-[var(--text-secondary)]">CLI Type</label>
1344
+ <select value={(settings.agents?.[a.id] as any)?.cliType || (a.id === 'claude' ? 'claude-code' : a.id === 'codex' ? 'codex' : a.id === 'aider' ? 'aider' : 'generic')}
1345
+ onChange={e => setSettings({ ...settings, agents: { ...settings.agents, [a.id]: { ...(settings.agents?.[a.id] || {}), cliType: e.target.value } } })}
1346
+ className={inputClass}>
1347
+ <option value="claude-code">Claude Code</option>
1348
+ <option value="codex">Codex</option>
1349
+ <option value="aider">Aider</option>
1350
+ <option value="generic">Generic</option>
1351
+ </select>
1352
+ </div>
1353
+ </div>
1354
+ <div>
1355
+ <label className="text-[9px] text-[var(--text-secondary)]">Binary Path</label>
1356
+ <input value={a.path} onChange={e => updateAgent(a.id, 'path', e.target.value)} placeholder="/usr/local/bin/agent" className={inputClass} />
1357
+ </div>
1358
+ <div>
1359
+ <label className="text-[9px] text-[var(--text-secondary)]">Task Flags <span className="text-[8px]">(non-interactive mode, e.g. -p --output-format json)</span></label>
1360
+ <input value={a.taskFlags} onChange={e => updateAgent(a.id, 'taskFlags', e.target.value)} placeholder="-p --verbose" className={inputClass} />
1361
+ </div>
1362
+ <div>
1363
+ <label className="text-[9px] text-[var(--text-secondary)]">Interactive Command <span className="text-[8px]">(terminal startup)</span></label>
1364
+ <input value={a.interactiveCmd} onChange={e => updateAgent(a.id, 'interactiveCmd', e.target.value)} placeholder="claude" className={inputClass} />
1365
+ </div>
1366
+ <div className="flex gap-3">
1367
+ <div className="flex-1">
1368
+ <label className="text-[9px] text-[var(--text-secondary)]">Resume Flag <span className="text-[8px]">(empty = no resume)</span></label>
1369
+ <input value={a.resumeFlag} onChange={e => updateAgent(a.id, 'resumeFlag', e.target.value)} placeholder="-c or --resume" className={inputClass} />
1370
+ </div>
1371
+ <div className="w-32">
1372
+ <label className="text-[9px] text-[var(--text-secondary)]">Output Format</label>
1373
+ <select value={a.outputFormat} onChange={e => updateAgent(a.id, 'outputFormat', e.target.value)} className={inputClass}>
1374
+ <option value="stream-json">stream-json</option>
1375
+ <option value="json">json</option>
1376
+ <option value="text">text</option>
1377
+ </select>
1378
+ </div>
1379
+ </div>
1380
+ {/* Per-scene model config */}
1381
+ <div>
1382
+ <label className="text-[9px] text-[var(--text-secondary)] mb-1 block">
1383
+ Models per scene <span className="text-[8px]">(type or pick from presets below)</span>
1384
+ </label>
1385
+ <div className="grid grid-cols-5 gap-1">
1386
+ {(['terminal', 'task', 'telegram', 'help', 'mobile'] as const).map(scene => (
1387
+ <div key={scene}>
1388
+ <label className="text-[8px] text-[var(--text-secondary)] capitalize">{scene}</label>
1389
+ <input
1390
+ value={a.models[scene]}
1391
+ onChange={e => {
1392
+ const updated = { ...a.models, [scene]: e.target.value };
1393
+ updateAgent(a.id, 'models', updated);
1394
+ }}
1395
+ placeholder="default"
1396
+ className="w-full px-1.5 py-0.5 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[9px] text-[var(--text-primary)] font-mono"
1397
+ />
1398
+ </div>
1399
+ ))}
1400
+ </div>
1401
+ {/* Preset models */}
1402
+ <div className="flex items-center gap-1 mt-1.5 flex-wrap">
1403
+ <span className="text-[8px] text-[var(--text-secondary)]">Presets:</span>
1404
+ {((() => {
1405
+ const ct = (settings.agents?.[a.id] as any)?.cliType || (a.id === 'claude' ? 'claude-code' : a.id === 'codex' ? 'codex' : a.id === 'aider' ? 'aider' : 'generic');
1406
+ if (ct === 'claude-code') return ['default', 'sonnet', 'opus', 'haiku', 'claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5-20251001'];
1407
+ if (ct === 'codex') return ['default', 'o3-mini', 'o4-mini', 'gpt-4.1'];
1408
+ return ['default'];
1409
+ })()).map(preset => (
1410
+ <button
1411
+ key={preset}
1412
+ onClick={() => navigator.clipboard.writeText(preset)}
1413
+ className="text-[8px] px-1 py-0.5 rounded bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]"
1414
+ title={`Click to copy "${preset}"`}
1415
+ >{preset}</button>
1416
+ ))}
1417
+ </div>
1418
+ </div>
1419
+ <div>
1420
+ <label className="text-[9px] text-[var(--text-secondary)]">Auto-approve flag <span className="text-[8px]">(empty = requires manual approval)</span></label>
1421
+ <input value={a.skipPermissionsFlag} onChange={e => updateAgent(a.id, 'skipPermissionsFlag', e.target.value)} placeholder="e.g. --dangerously-skip-permissions" className={inputClass} />
1422
+ <div className="flex gap-1 mt-1">
1423
+ {[
1424
+ { label: 'Claude', flag: '--dangerously-skip-permissions' },
1425
+ { label: 'Codex', flag: '--full-auto' },
1426
+ { label: 'Aider', flag: '--yes' },
1427
+ ].map(p => (
1428
+ <button key={p.label} onClick={() => updateAgent(a.id, 'skipPermissionsFlag', p.flag)}
1429
+ className="text-[8px] px-1 py-0.5 rounded bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
1430
+ >{p.label}: {p.flag}</button>
1431
+ ))}
1432
+ </div>
1433
+ </div>
1434
+ <label className="flex items-center gap-2 text-[9px] text-[var(--text-secondary)] cursor-pointer">
1435
+ <input type="checkbox" checked={a.requiresTTY} onChange={e => updateAgent(a.id, 'requiresTTY', e.target.checked)} className="accent-[var(--accent)]" />
1436
+ Requires terminal environment (TTY)
1437
+ <span className="text-[8px]">— enable for agents that need a terminal to run (e.g. Codex)</span>
1438
+ </label>
1439
+ {a.id !== 'claude' && (
1440
+ <button onClick={() => removeAgent(a.id)} className="text-[9px] text-red-400 hover:underline">Remove Agent</button>
1441
+ )}
1442
+
1443
+ {/* Profile selector */}
1444
+ <div>
1445
+ <label className="text-[9px] text-[var(--text-secondary)]">Profile <span className="text-[8px]">— select to override model, env vars, API endpoint</span></label>
1446
+ <select
1447
+ value={(settings.agents?.[a.id] as any)?.profile || ''}
1448
+ onChange={e => setSettings({ ...settings, agents: { ...settings.agents, [a.id]: { ...(settings.agents?.[a.id] || {}), profile: e.target.value || undefined } } })}
1449
+ className={inputClass}
1450
+ >
1451
+ <option value="">Default (no profile)</option>
1452
+ {Object.entries(settings.agents || {}).filter(([, cfg]: [string, any]) => cfg.base || cfg.type === 'profile').map(([pid, cfg]: [string, any]) => (
1453
+ <option key={pid} value={pid}>{cfg.name || pid}{cfg.model ? ` (${cfg.model})` : ''}</option>
1454
+ ))}
1455
+ </select>
1456
+ </div>
1457
+ </div>
1458
+ )}
1459
+ </div>
1460
+ ))}
1461
+ </div>
1462
+ )}
1463
+
1464
+ {/* Add agent form */}
1465
+ {showAdd && (
1466
+ <div className="border border-[var(--accent)]/30 rounded-lg p-3 space-y-2 bg-[var(--bg-secondary)]">
1467
+ <div className="text-[10px] text-[var(--text-primary)] font-semibold">Add Custom Agent</div>
1468
+ <div className="grid grid-cols-3 gap-2">
1469
+ <div>
1470
+ <label className="text-[9px] text-[var(--text-secondary)]">ID (unique)</label>
1471
+ <input value={newAgent.id} onChange={e => setNewAgent({ ...newAgent, id: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '') })} placeholder="my-agent" className={inputClass} />
1472
+ </div>
1473
+ <div>
1474
+ <label className="text-[9px] text-[var(--text-secondary)]">Display Name</label>
1475
+ <input value={newAgent.name} onChange={e => setNewAgent({ ...newAgent, name: e.target.value })} placeholder="My Agent" className={inputClass} />
1476
+ </div>
1477
+ <div>
1478
+ <label className="text-[9px] text-[var(--text-secondary)]">CLI Type</label>
1479
+ <select value={newAgent.cliType} onChange={e => {
1480
+ const ct = e.target.value;
1481
+ // Auto-fill path from detected agent if available
1482
+ const baseId = ct === 'claude-code' ? 'claude' : ct;
1483
+ const detected = agents.find(a => a.id === baseId);
1484
+ setNewAgent({ ...newAgent, cliType: ct, ...(cliDefaults[ct] || {}), path: detected?.path || newAgent.path, interactiveCmd: detected?.path || newAgent.interactiveCmd });
1485
+ }} className={inputClass}>
1486
+ <option value="claude-code">Claude Code</option>
1487
+ <option value="codex">Codex</option>
1488
+ <option value="aider">Aider</option>
1489
+ <option value="generic">Generic</option>
1490
+ </select>
1491
+ </div>
1492
+ </div>
1493
+ <div className="grid grid-cols-2 gap-2">
1494
+ <div>
1495
+ <label className="text-[9px] text-[var(--text-secondary)]">Binary Path</label>
1496
+ <input value={newAgent.path} onChange={e => setNewAgent({ ...newAgent, path: e.target.value })}
1497
+ placeholder={newAgent.cliType === 'claude-code' ? 'claude' : newAgent.cliType === 'codex' ? 'codex' : newAgent.cliType === 'aider' ? 'aider' : '/usr/local/bin/agent'}
1498
+ className={inputClass} />
1499
+ </div>
1500
+ <div>
1501
+ <label className="text-[9px] text-[var(--text-secondary)]">Task Flags (non-interactive)</label>
1502
+ <input value={newAgent.taskFlags} onChange={e => setNewAgent({ ...newAgent, taskFlags: e.target.value })} placeholder="--prompt" className={inputClass} />
1503
+ </div>
1504
+ </div>
1505
+ <div className="grid grid-cols-3 gap-2">
1506
+ <div>
1507
+ <label className="text-[9px] text-[var(--text-secondary)]">Resume Flag</label>
1508
+ <input value={newAgent.resumeFlag} onChange={e => setNewAgent({ ...newAgent, resumeFlag: e.target.value })} placeholder="-c or --resume" className={inputClass} />
1509
+ </div>
1510
+ <div>
1511
+ <label className="text-[9px] text-[var(--text-secondary)]">Output Format</label>
1512
+ <select value={newAgent.outputFormat} onChange={e => setNewAgent({ ...newAgent, outputFormat: e.target.value })} className={inputClass}>
1513
+ <option value="stream-json">stream-json</option>
1514
+ <option value="json">json</option>
1515
+ <option value="text">text</option>
1516
+ </select>
1517
+ </div>
1518
+ <div>
1519
+ <label className="text-[9px] text-[var(--text-secondary)]">Skip Permissions Flag</label>
1520
+ <input value={newAgent.skipPermissionsFlag} onChange={e => setNewAgent({ ...newAgent, skipPermissionsFlag: e.target.value })} placeholder="--dangerously-skip-permissions" className={inputClass} />
1521
+ </div>
1522
+ </div>
1523
+ <div className="flex gap-2">
1524
+ <button onClick={addAgent} disabled={!newAgent.id || !newAgent.path} className="text-[10px] px-3 py-1 bg-[var(--accent)] text-white rounded disabled:opacity-50">Add</button>
1525
+ <button onClick={() => setShowAdd(false)} className="text-[10px] px-3 py-1 border border-[var(--border)] text-[var(--text-secondary)] rounded">Cancel</button>
1526
+ </div>
1527
+ </div>
1528
+ )}
1529
+
1530
+ {/* ── Profiles Section ── */}
1531
+ <div className="mt-4 pt-3 border-t border-[var(--border)]">
1532
+ <div className="flex items-center gap-2 mb-2">
1533
+ <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">Profiles</label>
1534
+ <span className="text-[8px] text-[var(--text-secondary)]">Shared across workspace and terminal — override model, env vars, API endpoint</span>
1535
+ </div>
1536
+
1537
+ {/* All profiles (CLI + API) */}
1538
+ {Object.entries(settings.agents || {}).filter(([, cfg]: [string, any]) => cfg.base || cfg.type === 'api').map(([id, cfg]: [string, any]) => (
1539
+ <ProfileRow key={id} id={id} cfg={cfg} inputClass={inputClass}
1540
+ onUpdate={(updated) => setSettings({ ...settings, agents: { ...settings.agents, [id]: updated } })}
1541
+ onDelete={() => {
1542
+ const updated = { ...settings.agents };
1543
+ delete updated[id];
1544
+ setSettings({ ...settings, agents: updated });
1545
+ }}
1546
+ />
1547
+ ))}
1548
+
1549
+ <div className="flex gap-2 mt-1">
1550
+ <AddProfileForm type="cli" baseAgents={agents.filter(a => !a.isProfile && a.detected)} onAdd={(id, cfg) => {
1551
+ setSettings({ ...settings, agents: { ...settings.agents, [id]: cfg } });
1552
+ }} />
1553
+ <AddProfileForm type="api" baseAgents={[]} onAdd={(id, cfg) => {
1554
+ setSettings({ ...settings, agents: { ...settings.agents, [id]: cfg } });
1555
+ }} />
1556
+ </div>
1557
+ </div>
1558
+
1559
+ {/* ── Providers Section ── */}
1560
+ <div className="mt-4 pt-3 border-t border-[var(--border)]">
1561
+ <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase mb-2 block">API Providers</label>
1562
+ {['anthropic', 'google', 'openai', 'grok'].map(name => {
1563
+ const provider = settings.providers?.[name] || {};
1564
+ const secretKey = `providers.${name}.apiKey`;
1565
+ const hasKey = (provider.apiKey && provider.apiKey !== '••••••••') || settings._secretStatus?.[secretKey];
1566
+ return (
1567
+ <div key={name} className="flex items-center gap-2 px-2 py-1.5 mb-1 rounded" style={{ background: 'var(--bg-tertiary)' }}>
1568
+ <span className="text-[10px] text-[var(--text-primary)] w-20 font-semibold capitalize">{name}</span>
1569
+ <input
1570
+ type="password"
1571
+ placeholder="API Key"
1572
+ value={provider.apiKey || ''}
1573
+ onChange={e => setSettings({
1574
+ ...settings,
1575
+ providers: { ...settings.providers, [name]: { ...provider, apiKey: e.target.value } }
1576
+ })}
1577
+ className="flex-1 text-[9px] px-2 py-0.5 bg-[var(--bg-secondary)] border border-[var(--border)] rounded text-[var(--text-primary)] font-mono focus:outline-none focus:border-[var(--accent)]"
1578
+ />
1579
+ <span className={`text-[8px] ${hasKey ? 'text-green-400' : 'text-gray-600'}`}>
1580
+ {hasKey ? '● set' : '○'}
1581
+ </span>
1582
+ </div>
1583
+ );
1584
+ })}
1585
+ </div>
1586
+ </div>
1587
+ );
1588
+ }
1589
+
1590
+ // ─── Telegram Agent Selector ──────────────────────────────
1591
+
1592
+ function TelegramAgentSelect({ settings, setSettings }: { settings: any; setSettings: (s: any) => void }) {
1593
+ const [agents, setAgents] = useState<{ id: string; name: string }[]>([]);
1594
+ useEffect(() => {
1595
+ fetch('/api/agents').then(r => r.json())
1596
+ .then(data => setAgents((data.agents || []).filter((a: any) => a.enabled)))
1597
+ .catch(() => {});
1598
+ }, []);
1599
+
1600
+ if (agents.length <= 1) return null;
1601
+
1602
+ return (
1603
+ <div className="flex items-center gap-2 mt-1">
1604
+ <span className="text-[9px] text-[var(--text-secondary)]">Default Agent:</span>
1605
+ <select
1606
+ value={settings.telegramAgent || ''}
1607
+ onChange={e => setSettings({ ...settings, telegramAgent: e.target.value })}
1608
+ className="bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-0.5 text-[10px] text-[var(--text-primary)]"
1609
+ >
1610
+ <option value="">Global default ({settings.defaultAgent || 'claude'})</option>
1611
+ {agents.map(a => (
1612
+ <option key={a.id} value={a.id}>{a.name}</option>
1613
+ ))}
1614
+ </select>
1615
+ <span className="text-[8px] text-[var(--text-secondary)]">Used for /task without @agent</span>
1616
+ </div>
1617
+ );
1618
+ }
1619
+
1620
+ // ─── Docs Agent Selector ──────────────────────────────
1621
+
1622
+ function DocsAgentSelect({ settings, setSettings }: { settings: any; setSettings: (s: any) => void }) {
1623
+ const [agents, setAgents] = useState<{ id: string; name: string }[]>([]);
1624
+ useEffect(() => {
1625
+ fetch('/api/agents').then(r => r.json())
1626
+ .then(data => setAgents((data.agents || []).filter((a: any) => a.enabled)))
1627
+ .catch(() => {});
1628
+ }, []);
1629
+
1630
+ if (agents.length <= 1) return null;
1631
+
1632
+ return (
1633
+ <div className="flex items-center gap-2 mt-1">
1634
+ <span className="text-[9px] text-[var(--text-secondary)]">Docs Agent:</span>
1635
+ <select
1636
+ value={settings.docsAgent || ''}
1637
+ onChange={e => setSettings({ ...settings, docsAgent: e.target.value })}
1638
+ className="bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-0.5 text-[10px] text-[var(--text-primary)]"
1639
+ >
1640
+ <option value="">Global default ({settings.defaultAgent || 'claude'})</option>
1641
+ {agents.map(a => (
1642
+ <option key={a.id} value={a.id}>{a.name}</option>
1643
+ ))}
1644
+ </select>
1645
+ </div>
1646
+ );
1647
+ }