@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.
- package/.forge/worktrees/pipeline-4dd8dc2d/CLAUDE.md +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/README.md +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/RELEASE_NOTES.md +36 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/agents/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/[...nextauth]/route.ts +3 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/verify/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/stream/route.ts +63 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/route.ts +28 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/entries/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/sync/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-templates/route.ts +145 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/code/route.ts +299 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/[id]/route.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/detect-cli/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/route.ts +176 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/sessions/route.ts +54 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/favorites/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/run/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/git/route.ts +149 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/help/route.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/issue-scanner/route.ts +116 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/logs/route.ts +100 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/mobile-chat/route.ts +115 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/monitor/route.ts +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notifications/route.ts +42 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notify/test/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/online/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/[id]/route.ts +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/route.ts +90 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/plugins/route.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/[...path]/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/route.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-pipelines/route.ts +91 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-sessions/route.ts +61 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/projects/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/chat/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/messages/route.ts +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/settings/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/local/route.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/route.ts +182 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/smith-templates/route.ts +81 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/status/route.ts +12 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tabs/route.ts +25 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/route.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/stream/route.ts +77 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/link/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/route.ts +44 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/session/route.ts +14 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/telegram/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/templates/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-bell/route.ts +39 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-cwd/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-state/route.ts +15 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tunnel/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/upgrade/route.ts +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/usage/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/version/route.ts +78 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/watchers/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/agents/route.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/memory/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/smith/route.ts +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/stream/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/route.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/global-error.tsx +21 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/globals.css +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.ico +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/layout.tsx +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/LoginForm.tsx +96 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/mobile/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/page.tsx +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/bin/forge-server.mjs +484 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/check-forge-status.sh +71 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/cli/mw.ts +579 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/BrowserPanel.tsx +175 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ChatPanel.tsx +191 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ClaudeTerminal.tsx +267 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/CodeViewer.tsx +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationEditor.tsx +411 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationGraphView.tsx +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationTerminalView.tsx +303 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/Dashboard.tsx +807 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DashboardWrapper.tsx +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryFlowEditor.tsx +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryList.tsx +230 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryWorkspace.tsx +589 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocTerminal.tsx +187 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocsViewer.tsx +574 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpDialog.tsx +169 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpTerminal.tsx +141 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/InlinePipelineView.tsx +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/LogViewer.tsx +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MarkdownContent.tsx +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MobileView.tsx +385 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MonitorPanel.tsx +122 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewSessionModal.tsx +93 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewTaskModal.tsx +492 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineEditor.tsx +570 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineView.tsx +1018 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PluginsPanel.tsx +472 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectDetail.tsx +1618 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectList.tsx +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectManager.tsx +401 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionList.tsx +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionView.tsx +726 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SettingsModal.tsx +1647 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SkillsPanel.tsx +969 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/StatusBar.tsx +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TabBar.tsx +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskBoard.tsx +113 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskDetail.tsx +372 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TerminalLauncher.tsx +398 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TunnelToggle.tsx +206 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/UsagePanel.tsx +207 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WebTerminal.tsx +1743 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceTree.tsx +221 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceView.tsx +4048 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/dev-test.sh +5 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Memory_Layer_Design.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Strategy_Research_2026.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/LOCAL-DEPLOY.md +144 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/roadmap-multi-agent-workflow.md +330 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/hooks/useSidebarResize.ts +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/install.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/instrumentation.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/claude-adapter.ts +104 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/generic-adapter.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/index.ts +245 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/types.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/artifacts.ts +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/auth.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/docker.yaml +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/http.yaml +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/jenkins.yaml +92 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/llm-vision.yaml +85 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/playwright.yaml +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/shell-command.yaml +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/slack.yaml +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/webhook.yaml +56 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-process.ts +361 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-sessions.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-templates.ts +227 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/cloudflared.ts +424 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/crypto.ts +67 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/delivery.ts +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/dirs.ts +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/flows.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-mcp-server.ts +732 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-inbox.md +38 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-send.md +47 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-status.md +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/00-overview.md +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/02-telegram.md +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/03-tunnel.md +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/04-tasks.md +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/05-pipelines.md +460 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/06-skills.md +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/08-rules.md +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/09-issue-autofix.md +55 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/10-troubleshooting.md +89 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/11-workspace.md +810 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/CLAUDE.md +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/init.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/issue-scanner.ts +298 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/logger.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notifications.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notify.ts +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/password.ts +97 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline-scheduler.ts +373 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline.ts +1565 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/executor.ts +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/registry.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/types.ts +103 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/project-sessions.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/projects.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-manager.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-utils.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-watcher.ts +345 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/settings.ts +195 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/skills.ts +458 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/task-manager.ts +951 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-bot.ts +1477 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-standalone.ts +83 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-server.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-standalone.ts +438 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/usage-scanner.ts +249 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-bus.ts +416 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-worker.ts +655 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/api-backend.ts +262 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/cli-backend.ts +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/index.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/manager.ts +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/orchestrator.ts +3415 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/persistence.ts +309 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/presets.ts +649 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/requests.ts +287 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/session-monitor.ts +240 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/skill-installer.ts +275 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/smith-memory.ts +498 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/types.ts +241 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/watch-manager.ts +560 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace-standalone.ts +978 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/middleware.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/next.config.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/package.json +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-lock.yaml +3719 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-workspace.yaml +1 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/postcss.config.mjs +7 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/publish.sh +133 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/README.md +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/results/.gitignore +2 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/run.ts +635 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/task.md +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/setup.sh +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/task.md +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/validator.sh +69 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/task.md +30 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/verify-usage.ts +178 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/config/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/db/database.ts +259 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/memory/strategy.ts +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/chat.ts +65 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/registry.ts +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/session/manager.ts +190 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/types/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/start.sh +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/templates/smith-lead.json +45 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/tsconfig.json +42 -0
- package/RELEASE_NOTES.md +11 -28
- package/app/api/terminal-bell/route.ts +6 -2
- package/components/WebTerminal.tsx +36 -2
- package/lib/terminal-standalone.ts +19 -2
- package/next-env.d.ts +1 -1
- 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
|
+
}
|