@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,969 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, lazy, Suspense } from 'react';
|
|
4
|
+
import { useSidebarResize } from '@/hooks/useSidebarResize';
|
|
5
|
+
|
|
6
|
+
const PluginsPanel = lazy(() => import('./PluginsPanel'));
|
|
7
|
+
|
|
8
|
+
type ItemType = 'skill' | 'command';
|
|
9
|
+
|
|
10
|
+
interface Skill {
|
|
11
|
+
name: string;
|
|
12
|
+
type: ItemType;
|
|
13
|
+
displayName: string;
|
|
14
|
+
description: string;
|
|
15
|
+
author: string;
|
|
16
|
+
version: string;
|
|
17
|
+
tags: string[];
|
|
18
|
+
score: number;
|
|
19
|
+
rating: number;
|
|
20
|
+
sourceUrl: string;
|
|
21
|
+
installedGlobal: boolean;
|
|
22
|
+
installedVersion: string;
|
|
23
|
+
hasUpdate: boolean;
|
|
24
|
+
installedProjects: string[];
|
|
25
|
+
deletedRemotely: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ProjectInfo {
|
|
29
|
+
path: string;
|
|
30
|
+
name: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Skill File Tree (collapsible directories) ──────────
|
|
34
|
+
|
|
35
|
+
interface TreeNode {
|
|
36
|
+
name: string;
|
|
37
|
+
path: string;
|
|
38
|
+
type: 'file' | 'dir';
|
|
39
|
+
children: TreeNode[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildTree(files: { name: string; path: string; type: string }[]): TreeNode[] {
|
|
43
|
+
const root: TreeNode[] = [];
|
|
44
|
+
const dirMap = new Map<string, TreeNode>();
|
|
45
|
+
|
|
46
|
+
for (const f of files) {
|
|
47
|
+
const parts = f.path.split('/');
|
|
48
|
+
if (f.type === 'dir') {
|
|
49
|
+
const node: TreeNode = { name: f.name.replace(/\/$/, ''), path: f.path, type: 'dir', children: [] };
|
|
50
|
+
dirMap.set(f.path, node);
|
|
51
|
+
// Find parent
|
|
52
|
+
const parentPath = parts.slice(0, -1).join('/');
|
|
53
|
+
const parent = parentPath ? dirMap.get(parentPath) : null;
|
|
54
|
+
if (parent) parent.children.push(node);
|
|
55
|
+
else root.push(node);
|
|
56
|
+
} else {
|
|
57
|
+
const node: TreeNode = { name: f.name, path: f.path, type: 'file', children: [] };
|
|
58
|
+
const parentPath = parts.slice(0, -1).join('/');
|
|
59
|
+
const parent = parentPath ? dirMap.get(parentPath) : null;
|
|
60
|
+
if (parent) parent.children.push(node);
|
|
61
|
+
else root.push(node);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return root;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function SkillFileTree({ files, activeFile, onSelect }: {
|
|
68
|
+
files: { name: string; path: string; type: string }[];
|
|
69
|
+
activeFile: string | null;
|
|
70
|
+
onSelect: (path: string) => void;
|
|
71
|
+
}) {
|
|
72
|
+
const tree = buildTree(files);
|
|
73
|
+
return <TreeNodeList nodes={tree} depth={0} activeFile={activeFile} onSelect={onSelect} />;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function TreeNodeList({ nodes, depth, activeFile, onSelect }: {
|
|
77
|
+
nodes: TreeNode[]; depth: number; activeFile: string | null; onSelect: (path: string) => void;
|
|
78
|
+
}) {
|
|
79
|
+
const [expanded, setExpanded] = useState<Set<string>>(new Set(
|
|
80
|
+
// Auto-expand first level
|
|
81
|
+
nodes.filter(n => n.type === 'dir').map(n => n.path)
|
|
82
|
+
));
|
|
83
|
+
|
|
84
|
+
const toggle = (path: string) => {
|
|
85
|
+
setExpanded(prev => {
|
|
86
|
+
const next = new Set(prev);
|
|
87
|
+
next.has(path) ? next.delete(path) : next.add(path);
|
|
88
|
+
return next;
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
{nodes.map(node => (
|
|
95
|
+
node.type === 'dir' ? (
|
|
96
|
+
<div key={node.path}>
|
|
97
|
+
<button
|
|
98
|
+
onClick={() => toggle(node.path)}
|
|
99
|
+
className="w-full text-left px-1 py-0.5 text-[9px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] flex items-center gap-0.5"
|
|
100
|
+
style={{ paddingLeft: `${depth * 10 + 4}px` }}
|
|
101
|
+
>
|
|
102
|
+
<span className="text-[8px]">{expanded.has(node.path) ? '▼' : '▶'}</span>
|
|
103
|
+
<span>📁 {node.name}</span>
|
|
104
|
+
</button>
|
|
105
|
+
{expanded.has(node.path) && (
|
|
106
|
+
<TreeNodeList nodes={node.children} depth={depth + 1} activeFile={activeFile} onSelect={onSelect} />
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
) : (
|
|
110
|
+
<button
|
|
111
|
+
key={node.path}
|
|
112
|
+
onClick={() => onSelect(node.path)}
|
|
113
|
+
className={`w-full text-left py-0.5 text-[10px] truncate ${
|
|
114
|
+
activeFile === node.path
|
|
115
|
+
? 'bg-[var(--accent)]/15 text-[var(--accent)]'
|
|
116
|
+
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] hover:text-[var(--text-primary)]'
|
|
117
|
+
}`}
|
|
118
|
+
style={{ paddingLeft: `${depth * 10 + 14}px` }}
|
|
119
|
+
title={node.path}
|
|
120
|
+
>
|
|
121
|
+
{node.name}
|
|
122
|
+
</button>
|
|
123
|
+
)
|
|
124
|
+
))}
|
|
125
|
+
</>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default function SkillsPanel({ projectFilter }: { projectFilter?: string }) {
|
|
130
|
+
const { sidebarWidth, onSidebarDragStart } = useSidebarResize({ defaultWidth: 224, minWidth: 140, maxWidth: 400 });
|
|
131
|
+
const [skills, setSkills] = useState<Skill[]>([]);
|
|
132
|
+
const [projects, setProjects] = useState<ProjectInfo[]>([]);
|
|
133
|
+
const [syncing, setSyncing] = useState(false);
|
|
134
|
+
const [loading, setLoading] = useState(true);
|
|
135
|
+
const [installTarget, setInstallTarget] = useState<{ skill: string; show: boolean }>({ skill: '', show: false });
|
|
136
|
+
const [typeFilter, setTypeFilter] = useState<'all' | 'skill' | 'command' | 'local' | 'rules' | 'plugins'>('all');
|
|
137
|
+
const [localItems, setLocalItems] = useState<{ name: string; type: string; scope: string; fileCount: number; projectPath?: string }[]>([]);
|
|
138
|
+
// Rules (CLAUDE.md templates)
|
|
139
|
+
const [rulesTemplates, setRulesTemplates] = useState<{ id: string; name: string; description: string; tags: string[]; builtin: boolean; isDefault: boolean; content: string }[]>([]);
|
|
140
|
+
const [rulesProjects, setRulesProjects] = useState<{ name: string; path: string }[]>([]);
|
|
141
|
+
const [rulesSelectedTemplate, setRulesSelectedTemplate] = useState<string | null>(null);
|
|
142
|
+
const [rulesEditing, setRulesEditing] = useState(false);
|
|
143
|
+
const [rulesEditId, setRulesEditId] = useState('');
|
|
144
|
+
const [rulesEditName, setRulesEditName] = useState('');
|
|
145
|
+
const [rulesEditDesc, setRulesEditDesc] = useState('');
|
|
146
|
+
const [rulesEditContent, setRulesEditContent] = useState('');
|
|
147
|
+
const [rulesEditDefault, setRulesEditDefault] = useState(false);
|
|
148
|
+
const [rulesShowNew, setRulesShowNew] = useState(false);
|
|
149
|
+
const [rulesBatchProjects, setRulesBatchProjects] = useState<Set<string>>(new Set());
|
|
150
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
151
|
+
const [collapsedLocalSections, setCollapsedLocalSections] = useState<Set<string>>(new Set());
|
|
152
|
+
const [expandedSkill, setExpandedSkill] = useState<string | null>(null);
|
|
153
|
+
const [skillFiles, setSkillFiles] = useState<{ name: string; path: string; type: string }[]>([]);
|
|
154
|
+
const [activeFile, setActiveFile] = useState<string | null>(null);
|
|
155
|
+
const [fileContent, setFileContent] = useState<string>('');
|
|
156
|
+
|
|
157
|
+
const fetchSkills = useCallback(async () => {
|
|
158
|
+
try {
|
|
159
|
+
const [registryRes, localRes] = await Promise.all([
|
|
160
|
+
fetch('/api/skills'),
|
|
161
|
+
fetch('/api/skills/local?action=scan&all=1'),
|
|
162
|
+
]);
|
|
163
|
+
const data = await registryRes.json();
|
|
164
|
+
setSkills(data.skills || []);
|
|
165
|
+
setProjects(data.projects || []);
|
|
166
|
+
const localData = await localRes.json();
|
|
167
|
+
// Filter out items already in registry
|
|
168
|
+
const registryNames = new Set((data.skills || []).map((s: any) => s.name));
|
|
169
|
+
setLocalItems((localData.items || []).filter((i: any) => !registryNames.has(i.name)));
|
|
170
|
+
} catch {}
|
|
171
|
+
setLoading(false);
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
useEffect(() => { fetchSkills(); }, [fetchSkills]);
|
|
175
|
+
|
|
176
|
+
const fetchRules = useCallback(async () => {
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetch('/api/claude-templates?action=list');
|
|
179
|
+
const data = await res.json();
|
|
180
|
+
setRulesTemplates(data.templates || []);
|
|
181
|
+
setRulesProjects(data.projects || []);
|
|
182
|
+
} catch {}
|
|
183
|
+
}, []);
|
|
184
|
+
|
|
185
|
+
useEffect(() => { if (typeFilter === 'rules') fetchRules(); }, [typeFilter, fetchRules]);
|
|
186
|
+
|
|
187
|
+
const saveRule = async () => {
|
|
188
|
+
if (!rulesEditId || !rulesEditName || !rulesEditContent) return;
|
|
189
|
+
await fetch('/api/claude-templates', {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
192
|
+
body: JSON.stringify({ action: 'save', id: rulesEditId, name: rulesEditName, description: rulesEditDesc, tags: [], content: rulesEditContent, isDefault: rulesEditDefault }),
|
|
193
|
+
});
|
|
194
|
+
setRulesEditing(false);
|
|
195
|
+
setRulesShowNew(false);
|
|
196
|
+
fetchRules();
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const deleteRule = async (id: string) => {
|
|
200
|
+
if (!confirm(`Delete template "${id}"?`)) return;
|
|
201
|
+
await fetch('/api/claude-templates', {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: { 'Content-Type': 'application/json' },
|
|
204
|
+
body: JSON.stringify({ action: 'delete', id }),
|
|
205
|
+
});
|
|
206
|
+
if (rulesSelectedTemplate === id) setRulesSelectedTemplate(null);
|
|
207
|
+
fetchRules();
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const toggleDefault = async (id: string, isDefault: boolean) => {
|
|
211
|
+
await fetch('/api/claude-templates', {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
headers: { 'Content-Type': 'application/json' },
|
|
214
|
+
body: JSON.stringify({ action: 'set-default', id, isDefault }),
|
|
215
|
+
});
|
|
216
|
+
fetchRules();
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const batchInject = async (templateId: string) => {
|
|
220
|
+
const projects = [...rulesBatchProjects];
|
|
221
|
+
if (!projects.length) return;
|
|
222
|
+
await fetch('/api/claude-templates', {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: { 'Content-Type': 'application/json' },
|
|
225
|
+
body: JSON.stringify({ action: 'inject', templateId, projects }),
|
|
226
|
+
});
|
|
227
|
+
setRulesBatchProjects(new Set());
|
|
228
|
+
fetchRules();
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const [syncProgress, setSyncProgress] = useState('');
|
|
232
|
+
const sync = async () => {
|
|
233
|
+
setSyncing(true);
|
|
234
|
+
setSyncProgress('');
|
|
235
|
+
try {
|
|
236
|
+
let enrichedTotal = 0;
|
|
237
|
+
let total = 0;
|
|
238
|
+
// Loop: each call enriches a batch of info.json, continue until all done
|
|
239
|
+
for (let round = 0; round < 20; round++) { // safety limit
|
|
240
|
+
setSyncProgress(total > 0 ? `${Math.min(enrichedTotal, total)}/${total}` : '');
|
|
241
|
+
const res = await fetch('/api/skills', {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: { 'Content-Type': 'application/json' },
|
|
244
|
+
body: JSON.stringify({ action: 'sync' }),
|
|
245
|
+
});
|
|
246
|
+
const data = await res.json();
|
|
247
|
+
if (data.error) {
|
|
248
|
+
alert(`Sync error: ${data.error}`);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
total = data.total || 0;
|
|
252
|
+
enrichedTotal += data.enriched || 0;
|
|
253
|
+
await fetchSkills();
|
|
254
|
+
// If remaining is 0 or enriched nothing, we're done
|
|
255
|
+
if (!data.remaining || data.enriched === 0) break;
|
|
256
|
+
}
|
|
257
|
+
} catch (err: any) {
|
|
258
|
+
alert(`Sync failed: ${err.message || 'Network error'}`);
|
|
259
|
+
} finally {
|
|
260
|
+
setSyncing(false);
|
|
261
|
+
setSyncProgress('');
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const install = async (name: string, target: string) => {
|
|
266
|
+
await fetch('/api/skills', {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
headers: { 'Content-Type': 'application/json' },
|
|
269
|
+
body: JSON.stringify({ action: 'install', name, target }),
|
|
270
|
+
});
|
|
271
|
+
setInstallTarget({ skill: '', show: false });
|
|
272
|
+
fetchSkills();
|
|
273
|
+
alert(`"${name}" installed. Restart Claude in terminal to apply.`);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const toggleDetail = async (name: string) => {
|
|
277
|
+
if (expandedSkill === name) {
|
|
278
|
+
setExpandedSkill(null);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
setExpandedSkill(name);
|
|
282
|
+
setSkillFiles([]);
|
|
283
|
+
setActiveFile(null);
|
|
284
|
+
setFileContent('');
|
|
285
|
+
// Fetch file list from GitHub API
|
|
286
|
+
try {
|
|
287
|
+
const res = await fetch(`/api/skills?action=files&name=${encodeURIComponent(name)}`);
|
|
288
|
+
const data = await res.json();
|
|
289
|
+
const files = data.files || [];
|
|
290
|
+
setSkillFiles(files);
|
|
291
|
+
// Auto-select skill.md if exists, otherwise first file
|
|
292
|
+
const defaultFile = files.find((f: any) => f.name === 'skill.md') || files.find((f: any) => f.type === 'file');
|
|
293
|
+
if (defaultFile) loadFile(name, defaultFile.path);
|
|
294
|
+
} catch { setSkillFiles([]); }
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const loadFile = async (skillName: string, filePath: string, isLocalItem?: boolean, localType?: string, localProject?: string) => {
|
|
298
|
+
setActiveFile(filePath);
|
|
299
|
+
setFileContent('Loading...');
|
|
300
|
+
try {
|
|
301
|
+
let res;
|
|
302
|
+
if (isLocalItem) {
|
|
303
|
+
const projectParam = localProject ? `&project=${encodeURIComponent(localProject)}` : '';
|
|
304
|
+
res = await fetch(`/api/skills/local?action=read&name=${encodeURIComponent(skillName)}&type=${localType || 'command'}&path=${encodeURIComponent(filePath)}${projectParam}`);
|
|
305
|
+
} else {
|
|
306
|
+
res = await fetch(`/api/skills?action=file&name=${encodeURIComponent(skillName)}&path=${encodeURIComponent(filePath)}`);
|
|
307
|
+
}
|
|
308
|
+
const data = await res.json();
|
|
309
|
+
setFileContent(data.content || '(Empty)');
|
|
310
|
+
} catch { setFileContent('(Failed to load)'); }
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const uninstall = async (name: string, target: string) => {
|
|
314
|
+
await fetch('/api/skills', {
|
|
315
|
+
method: 'POST',
|
|
316
|
+
headers: { 'Content-Type': 'application/json' },
|
|
317
|
+
body: JSON.stringify({ action: 'uninstall', name, target }),
|
|
318
|
+
});
|
|
319
|
+
fetchSkills();
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Filter by project, type, and search
|
|
323
|
+
const q = searchQuery.toLowerCase();
|
|
324
|
+
const filtered = (typeFilter === 'local' ? [] : skills
|
|
325
|
+
.filter(s => projectFilter ? (s.installedGlobal || s.installedProjects.includes(projectFilter)) : true)
|
|
326
|
+
.filter(s => typeFilter === 'all' ? true : s.type === typeFilter)
|
|
327
|
+
.filter(s => !q || s.name.toLowerCase().includes(q) || s.displayName.toLowerCase().includes(q) || s.description.toLowerCase().includes(q)
|
|
328
|
+
|| s.author.toLowerCase().includes(q) || s.tags.some(t => t.toLowerCase().includes(q)))
|
|
329
|
+
).sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
330
|
+
|
|
331
|
+
const filteredLocal = localItems
|
|
332
|
+
.filter(item => typeFilter === 'local' || typeFilter === 'all' || item.type === typeFilter)
|
|
333
|
+
.filter(item => !q || item.name.toLowerCase().includes(q))
|
|
334
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
335
|
+
|
|
336
|
+
// Group local items by scope
|
|
337
|
+
const localGroups = new Map<string, typeof localItems>();
|
|
338
|
+
for (const item of filteredLocal) {
|
|
339
|
+
const key = item.scope;
|
|
340
|
+
if (!localGroups.has(key)) localGroups.set(key, []);
|
|
341
|
+
localGroups.get(key)!.push(item);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const toggleLocalSection = (section: string) => {
|
|
345
|
+
setCollapsedLocalSections(prev => {
|
|
346
|
+
const next = new Set(prev);
|
|
347
|
+
if (next.has(section)) next.delete(section);
|
|
348
|
+
else next.add(section);
|
|
349
|
+
return next;
|
|
350
|
+
});
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const skillCount = skills.filter(s => s.type === 'skill').length;
|
|
354
|
+
const commandCount = skills.filter(s => s.type === 'command').length;
|
|
355
|
+
const localCount = localItems.length;
|
|
356
|
+
|
|
357
|
+
if (loading) {
|
|
358
|
+
return <div className="p-4 text-xs text-[var(--text-secondary)]">Loading skills...</div>;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<div className="flex-1 flex flex-col min-h-0">
|
|
363
|
+
{/* Header */}
|
|
364
|
+
<div className="flex items-center justify-between px-4 py-2 border-b border-[var(--border)] shrink-0">
|
|
365
|
+
<div className="flex items-center gap-2">
|
|
366
|
+
<span className="text-xs font-semibold text-[var(--text-primary)]">Marketplace</span>
|
|
367
|
+
<div className="flex items-center bg-[var(--bg-tertiary)] rounded p-0.5">
|
|
368
|
+
{([['all', `All (${skills.length})`], ['skill', `Skills (${skillCount})`], ['command', `Commands (${commandCount})`], ['local', `Local (${localCount})`], ['rules', 'Rules'], ['plugins', 'Plugins']] as const).map(([value, label]) => (
|
|
369
|
+
<button
|
|
370
|
+
key={value}
|
|
371
|
+
onClick={() => setTypeFilter(value)}
|
|
372
|
+
className={`text-[9px] px-2 py-0.5 rounded transition-colors ${
|
|
373
|
+
typeFilter === value
|
|
374
|
+
? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm'
|
|
375
|
+
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
|
376
|
+
}`}
|
|
377
|
+
>
|
|
378
|
+
{label}
|
|
379
|
+
</button>
|
|
380
|
+
))}
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
<span className="text-[8px] px-1.5 py-0.5 rounded bg-blue-500/15 text-blue-400">Claude Code</span>
|
|
384
|
+
<button
|
|
385
|
+
onClick={sync}
|
|
386
|
+
disabled={syncing}
|
|
387
|
+
className="text-[9px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] disabled:opacity-50"
|
|
388
|
+
>
|
|
389
|
+
{syncing ? `Syncing${syncProgress ? ` ${syncProgress}` : '...'}` : 'Sync'}
|
|
390
|
+
</button>
|
|
391
|
+
</div>
|
|
392
|
+
{/* Search — hide on rules tab */}
|
|
393
|
+
{typeFilter !== 'rules' && typeFilter !== 'plugins' && <div className="px-3 py-1.5 border-b border-[var(--border)] shrink-0">
|
|
394
|
+
<input
|
|
395
|
+
type="text"
|
|
396
|
+
value={searchQuery}
|
|
397
|
+
onChange={e => setSearchQuery(e.target.value)}
|
|
398
|
+
placeholder="Search skills & commands..."
|
|
399
|
+
className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)] placeholder:text-[var(--text-secondary)] focus:outline-none focus:border-[var(--accent)]"
|
|
400
|
+
/>
|
|
401
|
+
</div>}
|
|
402
|
+
|
|
403
|
+
{typeFilter === 'rules' || typeFilter === 'plugins' ? null : skills.length === 0 ? (
|
|
404
|
+
<div className="flex-1 flex flex-col items-center justify-center gap-2 text-[var(--text-secondary)]">
|
|
405
|
+
<p className="text-xs">No skills yet</p>
|
|
406
|
+
<button onClick={sync} className="text-xs px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90">
|
|
407
|
+
Sync from Registry
|
|
408
|
+
</button>
|
|
409
|
+
</div>
|
|
410
|
+
) : (
|
|
411
|
+
<div className="flex-1 flex min-h-0">
|
|
412
|
+
{/* Left: skill list */}
|
|
413
|
+
<div style={{ width: sidebarWidth }} className="overflow-y-auto shrink-0">
|
|
414
|
+
{/* Registry items */}
|
|
415
|
+
{filtered.map(skill => {
|
|
416
|
+
const isInstalled = skill.installedGlobal || skill.installedProjects.length > 0;
|
|
417
|
+
const isActive = expandedSkill === skill.name;
|
|
418
|
+
return (
|
|
419
|
+
<div
|
|
420
|
+
key={skill.name}
|
|
421
|
+
className={`px-3 py-2.5 border-b border-[var(--border)]/50 cursor-pointer ${
|
|
422
|
+
isActive ? 'bg-[var(--accent)]/10 border-l-2 border-l-[var(--accent)]' : 'hover:bg-[var(--bg-tertiary)] border-l-2 border-l-transparent'
|
|
423
|
+
}`}
|
|
424
|
+
onClick={() => toggleDetail(skill.name)}
|
|
425
|
+
>
|
|
426
|
+
<div className="flex items-center gap-2">
|
|
427
|
+
<span className="text-[11px] font-semibold text-[var(--text-primary)] truncate flex-1">{skill.displayName}</span>
|
|
428
|
+
<span className="text-[8px] text-[var(--text-secondary)] font-mono shrink-0">v{skill.version}</span>
|
|
429
|
+
{skill.rating > 0 && (
|
|
430
|
+
<span className="text-[8px] text-[var(--yellow)] shrink-0" title={`Rating: ${skill.rating}/5`}>
|
|
431
|
+
{'★'.repeat(Math.round(skill.rating))}{'☆'.repeat(5 - Math.round(skill.rating))}
|
|
432
|
+
</span>
|
|
433
|
+
)}
|
|
434
|
+
{skill.score > 0 && !skill.rating && (
|
|
435
|
+
<span className="text-[8px] text-[var(--text-secondary)] shrink-0">{skill.score}pt</span>
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
438
|
+
<p className="text-[9px] text-[var(--text-secondary)] mt-0.5 line-clamp-1">{skill.description}</p>
|
|
439
|
+
<div className="flex items-center gap-1.5 mt-1">
|
|
440
|
+
<span className={`text-[7px] px-1 rounded font-medium ${
|
|
441
|
+
skill.type === 'skill' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
|
|
442
|
+
}`}>{skill.type === 'skill' ? 'SKILL' : 'CMD'}</span>
|
|
443
|
+
<span className="text-[8px] text-[var(--text-secondary)]">{skill.author}</span>
|
|
444
|
+
{skill.tags.slice(0, 3).map(t => (
|
|
445
|
+
<span key={t} className="text-[7px] px-1 rounded bg-[var(--bg-tertiary)] text-[var(--text-secondary)]">{t}</span>
|
|
446
|
+
))}
|
|
447
|
+
{skill.deletedRemotely && <span className="text-[8px] text-[var(--red)] ml-auto">deleted remotely</span>}
|
|
448
|
+
{!skill.deletedRemotely && skill.hasUpdate && <span className="text-[8px] text-[var(--yellow)] ml-auto">update</span>}
|
|
449
|
+
{!skill.deletedRemotely && isInstalled && !skill.hasUpdate && <span className="text-[8px] text-[var(--green)] ml-auto">installed</span>}
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
453
|
+
})}
|
|
454
|
+
{/* Local items — collapsible by scope group */}
|
|
455
|
+
{(typeFilter === 'all' || typeFilter === 'local') && filteredLocal.length > 0 && (
|
|
456
|
+
<>
|
|
457
|
+
{/* Local section header — collapsible */}
|
|
458
|
+
{typeFilter !== 'local' && (
|
|
459
|
+
<button
|
|
460
|
+
onClick={() => toggleLocalSection('__local__')}
|
|
461
|
+
className="w-full px-3 py-1 text-[8px] text-[var(--text-secondary)] uppercase bg-[var(--bg-tertiary)] border-b border-[var(--border)]/50 flex items-center gap-1 hover:text-[var(--text-primary)]"
|
|
462
|
+
>
|
|
463
|
+
<span>{collapsedLocalSections.has('__local__') ? '▸' : '▾'}</span>
|
|
464
|
+
Local ({filteredLocal.length})
|
|
465
|
+
</button>
|
|
466
|
+
)}
|
|
467
|
+
{(typeFilter === 'local' || !collapsedLocalSections.has('__local__')) && (
|
|
468
|
+
<>
|
|
469
|
+
{[...localGroups.entries()].sort(([a], [b]) => a === 'global' ? -1 : b === 'global' ? 1 : a.localeCompare(b)).map(([scope, items]) => (
|
|
470
|
+
<div key={scope}>
|
|
471
|
+
{/* Scope group header — collapsible */}
|
|
472
|
+
<button
|
|
473
|
+
onClick={() => toggleLocalSection(scope)}
|
|
474
|
+
className="w-full px-3 py-1 text-[8px] text-[var(--text-secondary)] border-b border-[var(--border)]/30 flex items-center gap-1.5 hover:bg-[var(--bg-tertiary)]"
|
|
475
|
+
>
|
|
476
|
+
<span className="text-[7px]">{collapsedLocalSections.has(scope) ? '▸' : '▾'}</span>
|
|
477
|
+
<span className={scope === 'global' ? 'text-green-400' : 'text-[var(--accent)]'}>{scope}</span>
|
|
478
|
+
<span className="text-[var(--text-secondary)]">({items.length})</span>
|
|
479
|
+
</button>
|
|
480
|
+
{!collapsedLocalSections.has(scope) && items.map(item => {
|
|
481
|
+
const key = `local:${item.name}:${item.scope}`;
|
|
482
|
+
const isActive = expandedSkill === key;
|
|
483
|
+
const projectParam = item.projectPath ? encodeURIComponent(item.projectPath) : '';
|
|
484
|
+
return (
|
|
485
|
+
<div
|
|
486
|
+
key={key}
|
|
487
|
+
className={`px-3 py-2 border-b border-[var(--border)]/50 cursor-pointer ${
|
|
488
|
+
isActive ? 'bg-[var(--accent)]/10 border-l-2 border-l-[var(--accent)]' : 'hover:bg-[var(--bg-tertiary)] border-l-2 border-l-transparent'
|
|
489
|
+
}`}
|
|
490
|
+
onClick={() => {
|
|
491
|
+
if (expandedSkill === key) { setExpandedSkill(null); return; }
|
|
492
|
+
setExpandedSkill(key);
|
|
493
|
+
setSkillFiles([]);
|
|
494
|
+
setActiveFile(null);
|
|
495
|
+
setFileContent('');
|
|
496
|
+
const fetchUrl = `/api/skills/local?action=files&name=${encodeURIComponent(item.name)}&type=${item.type}${projectParam ? `&project=${projectParam}` : ''}`;
|
|
497
|
+
fetch(fetchUrl)
|
|
498
|
+
.then(r => r.json())
|
|
499
|
+
.then(d => {
|
|
500
|
+
const files = (d.files || []).map((f: any) => ({ name: f.path.split('/').pop(), path: f.path, type: 'file' }));
|
|
501
|
+
setSkillFiles(files);
|
|
502
|
+
const first = files.find((f: any) => f.name?.endsWith('.md'));
|
|
503
|
+
if (first) {
|
|
504
|
+
setActiveFile(first.path);
|
|
505
|
+
fetch(`/api/skills/local?action=read&name=${encodeURIComponent(item.name)}&type=${item.type}&path=${encodeURIComponent(first.path)}${projectParam ? `&project=${projectParam}` : ''}`)
|
|
506
|
+
.then(r => r.json())
|
|
507
|
+
.then(rd => setFileContent(rd.content || ''))
|
|
508
|
+
.catch(() => {});
|
|
509
|
+
}
|
|
510
|
+
})
|
|
511
|
+
.catch(() => {});
|
|
512
|
+
}}
|
|
513
|
+
>
|
|
514
|
+
<div className="flex items-center gap-2">
|
|
515
|
+
<span className={`text-[7px] px-1 rounded font-medium ${
|
|
516
|
+
item.type === 'skill' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
|
|
517
|
+
}`}>{item.type === 'skill' ? 'S' : 'C'}</span>
|
|
518
|
+
<span className="text-[10px] text-[var(--text-primary)] truncate flex-1">{item.name}</span>
|
|
519
|
+
<span className="text-[8px] text-[var(--text-secondary)]">{item.fileCount}</span>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
523
|
+
})}
|
|
524
|
+
</div>
|
|
525
|
+
))}
|
|
526
|
+
</>
|
|
527
|
+
)}
|
|
528
|
+
</>
|
|
529
|
+
)}
|
|
530
|
+
</div>
|
|
531
|
+
|
|
532
|
+
{/* Sidebar resize handle */}
|
|
533
|
+
<div
|
|
534
|
+
onMouseDown={onSidebarDragStart}
|
|
535
|
+
className="w-1 bg-[var(--border)] cursor-col-resize shrink-0 hover:bg-[var(--accent)]/50 transition-colors"
|
|
536
|
+
/>
|
|
537
|
+
|
|
538
|
+
{/* Right: detail panel */}
|
|
539
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
540
|
+
{expandedSkill ? (() => {
|
|
541
|
+
const isLocal = expandedSkill.startsWith('local:');
|
|
542
|
+
// Key format: "local:<name>:<scope>" — extract name (could contain colons in scope)
|
|
543
|
+
const localParts = isLocal ? expandedSkill.slice(6).split(':') : [];
|
|
544
|
+
const itemName = isLocal ? localParts[0] : expandedSkill;
|
|
545
|
+
const localScope = isLocal ? localParts.slice(1).join(':') : '';
|
|
546
|
+
const skill = isLocal ? null : skills.find(s => s.name === expandedSkill);
|
|
547
|
+
const localItem = isLocal ? localItems.find(i => i.name === itemName && i.scope === localScope) : null;
|
|
548
|
+
if (!skill && !localItem) return null;
|
|
549
|
+
const isInstalled = skill ? (skill.installedGlobal || skill.installedProjects.length > 0) : true;
|
|
550
|
+
return (
|
|
551
|
+
<>
|
|
552
|
+
{/* Skill header */}
|
|
553
|
+
<div className="px-4 py-2 border-b border-[var(--border)] shrink-0">
|
|
554
|
+
<div className="flex items-center gap-2">
|
|
555
|
+
<span className="text-sm font-semibold text-[var(--text-primary)]">{skill?.displayName || localItem?.name || itemName}</span>
|
|
556
|
+
<span className={`text-[8px] px-1.5 py-0.5 rounded font-medium ${
|
|
557
|
+
(skill?.type || localItem?.type) === 'skill' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
|
|
558
|
+
}`}>{(skill?.type || localItem?.type) === 'skill' ? 'Skill' : 'Command'}</span>
|
|
559
|
+
{isLocal && <span className="text-[7px] px-1 rounded bg-green-500/10 text-green-400">local</span>}
|
|
560
|
+
{skill?.deletedRemotely && <span className="text-[7px] px-1.5 py-0.5 rounded bg-red-500/20 text-red-400 font-medium">Deleted remotely</span>}
|
|
561
|
+
{skill && !skill.deletedRemotely && <span className="text-[9px] text-[var(--text-secondary)] font-mono">v{skill.version}</span>}
|
|
562
|
+
{skill?.installedVersion && skill.installedVersion !== skill.version && (
|
|
563
|
+
<span className="text-[9px] text-[var(--yellow)] font-mono">installed: v{skill.installedVersion}</span>
|
|
564
|
+
)}
|
|
565
|
+
{skill && skill.rating > 0 && (
|
|
566
|
+
<span className="text-[9px] text-[var(--yellow)]" title={`Rating: ${skill.rating}/5`}>
|
|
567
|
+
{'★'.repeat(Math.round(skill.rating))}{'☆'.repeat(5 - Math.round(skill.rating))}
|
|
568
|
+
</span>
|
|
569
|
+
)}
|
|
570
|
+
{skill && skill.score > 0 && <span className="text-[9px] text-[var(--text-secondary)]">{skill.score}pt</span>}
|
|
571
|
+
|
|
572
|
+
{/* Update button */}
|
|
573
|
+
{skill?.hasUpdate && !skill.deletedRemotely && (
|
|
574
|
+
<button
|
|
575
|
+
onClick={async () => {
|
|
576
|
+
if (skill.installedGlobal) await install(skill.name, 'global');
|
|
577
|
+
for (const pp of skill.installedProjects) await install(skill.name, pp);
|
|
578
|
+
}}
|
|
579
|
+
className="text-[9px] px-2 py-1 bg-[var(--yellow)]/20 text-[var(--yellow)] border border-[var(--yellow)]/50 rounded hover:bg-[var(--yellow)]/30 transition-colors"
|
|
580
|
+
>
|
|
581
|
+
Update
|
|
582
|
+
</button>
|
|
583
|
+
)}
|
|
584
|
+
|
|
585
|
+
{/* Delete button for skills removed from remote registry */}
|
|
586
|
+
{skill?.deletedRemotely && (
|
|
587
|
+
<button
|
|
588
|
+
onClick={async () => {
|
|
589
|
+
if (!confirm(`"${skill.name}" was deleted from the remote repository.\n\nDelete the local installation as well?`)) return;
|
|
590
|
+
await fetch('/api/skills', {
|
|
591
|
+
method: 'POST',
|
|
592
|
+
headers: { 'Content-Type': 'application/json' },
|
|
593
|
+
body: JSON.stringify({ action: 'purge-deleted', name: skill.name }),
|
|
594
|
+
});
|
|
595
|
+
setExpandedSkill(null);
|
|
596
|
+
fetchSkills();
|
|
597
|
+
}}
|
|
598
|
+
className="text-[9px] px-2 py-1 bg-red-500/20 text-red-400 border border-red-500/40 rounded hover:bg-red-500/30 transition-colors ml-auto"
|
|
599
|
+
>
|
|
600
|
+
Delete local
|
|
601
|
+
</button>
|
|
602
|
+
)}
|
|
603
|
+
|
|
604
|
+
{/* Local item actions: install to other projects, delete */}
|
|
605
|
+
{isLocal && localItem && (
|
|
606
|
+
<>
|
|
607
|
+
<div className="relative ml-auto">
|
|
608
|
+
<button
|
|
609
|
+
onClick={() => setInstallTarget(prev =>
|
|
610
|
+
prev.skill === itemName && prev.show ? { skill: '', show: false } : { skill: itemName, show: true }
|
|
611
|
+
)}
|
|
612
|
+
className="text-[9px] px-2 py-1 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white transition-colors"
|
|
613
|
+
>
|
|
614
|
+
Install to...
|
|
615
|
+
</button>
|
|
616
|
+
{installTarget.skill === itemName && installTarget.show && (
|
|
617
|
+
<>
|
|
618
|
+
<div className="fixed inset-0 z-40" onClick={() => setInstallTarget({ skill: '', show: false })} />
|
|
619
|
+
<div className="absolute right-0 top-7 w-[200px] bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg shadow-xl z-50 py-1">
|
|
620
|
+
<button
|
|
621
|
+
onClick={async () => {
|
|
622
|
+
const res = await fetch('/api/skills/local', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
623
|
+
body: JSON.stringify({ action: 'install-local', name: itemName, type: localItem.type, sourceProject: localItem.projectPath, target: 'global', force: true }) });
|
|
624
|
+
const data = await res.json();
|
|
625
|
+
if (!data.ok) alert(data.error);
|
|
626
|
+
else alert(`"${itemName}" installed globally. Restart Claude to apply.`);
|
|
627
|
+
setInstallTarget({ skill: '', show: false });
|
|
628
|
+
fetchSkills();
|
|
629
|
+
}}
|
|
630
|
+
className="w-full text-left text-[10px] px-3 py-1.5 hover:bg-[var(--bg-tertiary)] text-[var(--text-primary)]"
|
|
631
|
+
>Global (~/.claude)</button>
|
|
632
|
+
<div className="border-t border-[var(--border)] my-0.5" />
|
|
633
|
+
{projects.map(p => (
|
|
634
|
+
<button
|
|
635
|
+
key={p.path}
|
|
636
|
+
onClick={async () => {
|
|
637
|
+
const res = await fetch('/api/skills/local', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
638
|
+
body: JSON.stringify({ action: 'install-local', name: itemName, type: localItem.type, sourceProject: localItem.projectPath, target: p.path, force: true }) });
|
|
639
|
+
const data = await res.json();
|
|
640
|
+
if (!data.ok) alert(data.error);
|
|
641
|
+
else alert(`"${itemName}" installed to ${p.name}. Restart Claude to apply.`);
|
|
642
|
+
setInstallTarget({ skill: '', show: false });
|
|
643
|
+
fetchSkills();
|
|
644
|
+
}}
|
|
645
|
+
className="w-full text-left text-[10px] px-3 py-1.5 hover:bg-[var(--bg-tertiary)] text-[var(--text-primary)] truncate"
|
|
646
|
+
title={p.path}
|
|
647
|
+
>{p.name}</button>
|
|
648
|
+
))}
|
|
649
|
+
</div>
|
|
650
|
+
</>
|
|
651
|
+
)}
|
|
652
|
+
</div>
|
|
653
|
+
<button
|
|
654
|
+
onClick={async () => {
|
|
655
|
+
if (!confirm(`Delete "${itemName}" from ${localScope}?`)) return;
|
|
656
|
+
await fetch('/api/skills/local', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
657
|
+
body: JSON.stringify({ action: 'delete-local', name: itemName, type: localItem.type, project: localItem.projectPath }) });
|
|
658
|
+
setExpandedSkill(null);
|
|
659
|
+
fetchSkills();
|
|
660
|
+
}}
|
|
661
|
+
className="text-[9px] text-[var(--red)] hover:underline"
|
|
662
|
+
>Delete</button>
|
|
663
|
+
</>
|
|
664
|
+
)}
|
|
665
|
+
|
|
666
|
+
{/* Install dropdown — registry items only (not deleted remotely) */}
|
|
667
|
+
{skill && !skill.deletedRemotely && <div className="relative ml-auto">
|
|
668
|
+
<button
|
|
669
|
+
onClick={() => setInstallTarget(prev =>
|
|
670
|
+
prev.skill === skill.name && prev.show ? { skill: '', show: false } : { skill: skill.name, show: true }
|
|
671
|
+
)}
|
|
672
|
+
className="text-[9px] px-2 py-1 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white transition-colors"
|
|
673
|
+
>
|
|
674
|
+
Install
|
|
675
|
+
</button>
|
|
676
|
+
{installTarget.skill === skill.name && installTarget.show && (
|
|
677
|
+
<>
|
|
678
|
+
<div className="fixed inset-0 z-40" onClick={() => setInstallTarget({ skill: '', show: false })} />
|
|
679
|
+
<div className="absolute right-0 top-7 w-[180px] bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg shadow-xl z-50 py-1">
|
|
680
|
+
<button
|
|
681
|
+
onClick={() => install(skill.name, 'global')}
|
|
682
|
+
className={`w-full text-left text-[10px] px-3 py-1.5 hover:bg-[var(--bg-tertiary)] ${
|
|
683
|
+
skill.installedGlobal ? 'text-[var(--green)]' : 'text-[var(--text-primary)]'
|
|
684
|
+
}`}
|
|
685
|
+
>
|
|
686
|
+
{skill.installedGlobal ? '✓ ' : ''}Global (~/.claude)
|
|
687
|
+
</button>
|
|
688
|
+
<div className="border-t border-[var(--border)] my-0.5" />
|
|
689
|
+
{projects.map(p => {
|
|
690
|
+
const inst = skill.installedProjects.includes(p.path);
|
|
691
|
+
return (
|
|
692
|
+
<button
|
|
693
|
+
key={p.path}
|
|
694
|
+
onClick={() => install(skill.name, p.path)}
|
|
695
|
+
className={`w-full text-left text-[10px] px-3 py-1.5 hover:bg-[var(--bg-tertiary)] truncate ${
|
|
696
|
+
inst ? 'text-[var(--green)]' : 'text-[var(--text-primary)]'
|
|
697
|
+
}`}
|
|
698
|
+
title={p.path}
|
|
699
|
+
>
|
|
700
|
+
{inst ? '✓ ' : ''}{p.name}
|
|
701
|
+
</button>
|
|
702
|
+
);
|
|
703
|
+
})}
|
|
704
|
+
</div>
|
|
705
|
+
</>
|
|
706
|
+
)}
|
|
707
|
+
</div>}
|
|
708
|
+
</div>
|
|
709
|
+
<p className="text-[10px] text-[var(--text-secondary)] mt-0.5">{skill?.description || ''}</p>
|
|
710
|
+
{skill?.author && (
|
|
711
|
+
<div className="text-[9px] text-[var(--text-secondary)] mt-1">By {skill.author}</div>
|
|
712
|
+
)}
|
|
713
|
+
{skill?.tags && skill.tags.length > 0 && (
|
|
714
|
+
<div className="flex flex-wrap gap-1 mt-1">
|
|
715
|
+
{skill.tags.map(t => (
|
|
716
|
+
<span key={t} className="text-[8px] px-1.5 py-0.5 rounded bg-[var(--bg-tertiary)] text-[var(--text-secondary)]">{t}</span>
|
|
717
|
+
))}
|
|
718
|
+
</div>
|
|
719
|
+
)}
|
|
720
|
+
{skill?.sourceUrl && (
|
|
721
|
+
<a href={skill.sourceUrl} target="_blank" rel="noopener noreferrer" className="text-[9px] text-[var(--accent)] hover:underline mt-0.5 block truncate">{skill.sourceUrl.replace(/^https?:\/\//, '').slice(0, 60)}</a>
|
|
722
|
+
)}
|
|
723
|
+
{/* Installed indicators */}
|
|
724
|
+
{skill && isInstalled && (
|
|
725
|
+
<div className="flex items-center gap-2 mt-1">
|
|
726
|
+
{skill.installedGlobal && (
|
|
727
|
+
<span className="flex items-center gap-1 text-[8px] text-[var(--green)]">
|
|
728
|
+
Global
|
|
729
|
+
<button onClick={() => { if (confirm(`Uninstall "${skill.name}" from global?`)) uninstall(skill.name, 'global'); }} className="text-[var(--text-secondary)] hover:text-[var(--red)]">x</button>
|
|
730
|
+
</span>
|
|
731
|
+
)}
|
|
732
|
+
{skill.installedProjects.map(pp => (
|
|
733
|
+
<span key={pp} className="flex items-center gap-1 text-[8px] text-[var(--accent)]">
|
|
734
|
+
{pp.split('/').pop()}
|
|
735
|
+
<button onClick={() => { if (confirm(`Uninstall "${skill.name}" from ${pp.split('/').pop()}?`)) uninstall(skill.name, pp); }} className="text-[var(--text-secondary)] hover:text-[var(--red)]">x</button>
|
|
736
|
+
</span>
|
|
737
|
+
))}
|
|
738
|
+
</div>
|
|
739
|
+
)}
|
|
740
|
+
</div>
|
|
741
|
+
|
|
742
|
+
{/* File browser */}
|
|
743
|
+
<div className="flex-1 flex min-h-0 overflow-hidden">
|
|
744
|
+
{/* File tree */}
|
|
745
|
+
<div className="w-36 border-r border-[var(--border)] overflow-y-auto shrink-0">
|
|
746
|
+
{skillFiles.length === 0 ? (
|
|
747
|
+
<div className="p-2 text-[9px] text-[var(--text-secondary)]">Loading...</div>
|
|
748
|
+
) : (
|
|
749
|
+
<SkillFileTree
|
|
750
|
+
files={skillFiles}
|
|
751
|
+
activeFile={activeFile}
|
|
752
|
+
onSelect={(path) => loadFile(itemName, path, isLocal, localItem?.type, localItem?.projectPath)}
|
|
753
|
+
/>
|
|
754
|
+
)}
|
|
755
|
+
{skill?.sourceUrl && (
|
|
756
|
+
<div className="border-t border-[var(--border)] p-2">
|
|
757
|
+
<a
|
|
758
|
+
href={skill.sourceUrl.replace(/\/blob\/main\/.*/, '')}
|
|
759
|
+
target="_blank"
|
|
760
|
+
rel="noopener noreferrer"
|
|
761
|
+
className="text-[9px] text-[var(--accent)] hover:underline"
|
|
762
|
+
>
|
|
763
|
+
GitHub
|
|
764
|
+
</a>
|
|
765
|
+
</div>
|
|
766
|
+
)}
|
|
767
|
+
</div>
|
|
768
|
+
{/* File content */}
|
|
769
|
+
<div className="flex-1 flex flex-col" style={{ width: 0 }}>
|
|
770
|
+
{activeFile && (
|
|
771
|
+
<div className="px-3 py-1 border-b border-[var(--border)] text-[9px] text-[var(--text-secondary)] font-mono shrink-0 truncate">
|
|
772
|
+
{activeFile}
|
|
773
|
+
</div>
|
|
774
|
+
)}
|
|
775
|
+
<div className="flex-1 overflow-auto">
|
|
776
|
+
<pre className="p-3 text-[11px] text-[var(--text-primary)] font-mono whitespace-pre-wrap break-all">
|
|
777
|
+
{fileContent}
|
|
778
|
+
</pre>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
</>
|
|
783
|
+
);
|
|
784
|
+
})() : (
|
|
785
|
+
<div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">
|
|
786
|
+
<p className="text-xs">Select a skill to view details</p>
|
|
787
|
+
</div>
|
|
788
|
+
)}
|
|
789
|
+
</div>
|
|
790
|
+
</div>
|
|
791
|
+
)}
|
|
792
|
+
|
|
793
|
+
{/* Rules (CLAUDE.md Templates) — full-page view */}
|
|
794
|
+
{typeFilter === 'rules' && (
|
|
795
|
+
<div className="flex-1 flex min-h-0">
|
|
796
|
+
{/* Left: template list */}
|
|
797
|
+
<div className="w-56 border-r border-[var(--border)] overflow-y-auto shrink-0 flex flex-col">
|
|
798
|
+
<div className="px-3 py-1.5 border-b border-[var(--border)] flex items-center justify-between">
|
|
799
|
+
<span className="text-[9px] text-[var(--text-secondary)] uppercase">Rule Templates</span>
|
|
800
|
+
<button
|
|
801
|
+
onClick={() => { setRulesShowNew(true); setRulesEditing(true); setRulesEditId(''); setRulesEditName(''); setRulesEditDesc(''); setRulesEditContent(''); setRulesEditDefault(false); setRulesSelectedTemplate(null); }}
|
|
802
|
+
className="text-[9px] text-[var(--accent)] hover:underline"
|
|
803
|
+
>+ New</button>
|
|
804
|
+
</div>
|
|
805
|
+
<div className="flex-1 overflow-y-auto">
|
|
806
|
+
{rulesTemplates.map(t => {
|
|
807
|
+
const isActive = rulesSelectedTemplate === t.id;
|
|
808
|
+
return (
|
|
809
|
+
<div
|
|
810
|
+
key={t.id}
|
|
811
|
+
className={`px-3 py-2 border-b border-[var(--border)]/50 cursor-pointer ${
|
|
812
|
+
isActive ? 'bg-[var(--accent)]/10 border-l-2 border-l-[var(--accent)]' : 'hover:bg-[var(--bg-tertiary)] border-l-2 border-l-transparent'
|
|
813
|
+
}`}
|
|
814
|
+
onClick={() => { setRulesSelectedTemplate(t.id); setRulesEditing(false); setRulesShowNew(false); }}
|
|
815
|
+
>
|
|
816
|
+
<div className="flex items-center gap-1.5">
|
|
817
|
+
<span className="text-[10px] text-[var(--text-primary)] truncate flex-1">{t.name}</span>
|
|
818
|
+
{t.builtin && <span className="text-[7px] text-[var(--text-secondary)]">built-in</span>}
|
|
819
|
+
<button
|
|
820
|
+
onClick={(e) => { e.stopPropagation(); toggleDefault(t.id, !t.isDefault); }}
|
|
821
|
+
className={`text-[7px] px-1 rounded ${t.isDefault ? 'bg-[var(--accent)]/20 text-[var(--accent)]' : 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'}`}
|
|
822
|
+
title={t.isDefault ? 'Default: auto-applied to new projects' : 'Click to set as default'}
|
|
823
|
+
>{t.isDefault ? 'default' : 'set default'}</button>
|
|
824
|
+
</div>
|
|
825
|
+
<p className="text-[8px] text-[var(--text-secondary)] mt-0.5 line-clamp-1">{t.description}</p>
|
|
826
|
+
</div>
|
|
827
|
+
);
|
|
828
|
+
})}
|
|
829
|
+
</div>
|
|
830
|
+
</div>
|
|
831
|
+
|
|
832
|
+
{/* Right: template detail / editor / batch apply */}
|
|
833
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
834
|
+
{rulesShowNew || rulesEditing ? (
|
|
835
|
+
/* Edit / New form */
|
|
836
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
837
|
+
<div className="px-4 py-2 border-b border-[var(--border)] shrink-0">
|
|
838
|
+
<div className="text-[11px] font-semibold text-[var(--text-primary)]">{rulesShowNew ? 'New Rule Template' : 'Edit Template'}</div>
|
|
839
|
+
</div>
|
|
840
|
+
<div className="flex-1 overflow-auto p-4 space-y-3">
|
|
841
|
+
<div className="flex gap-2">
|
|
842
|
+
<input
|
|
843
|
+
type="text"
|
|
844
|
+
value={rulesEditId}
|
|
845
|
+
onChange={e => setRulesEditId(e.target.value.replace(/[^a-z0-9-]/g, ''))}
|
|
846
|
+
placeholder="template-id (kebab-case)"
|
|
847
|
+
disabled={!rulesShowNew}
|
|
848
|
+
className="flex-1 px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)] font-mono disabled:opacity-50"
|
|
849
|
+
/>
|
|
850
|
+
<input
|
|
851
|
+
type="text"
|
|
852
|
+
value={rulesEditName}
|
|
853
|
+
onChange={e => setRulesEditName(e.target.value)}
|
|
854
|
+
placeholder="Display Name"
|
|
855
|
+
className="flex-1 px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)]"
|
|
856
|
+
/>
|
|
857
|
+
</div>
|
|
858
|
+
<input
|
|
859
|
+
type="text"
|
|
860
|
+
value={rulesEditDesc}
|
|
861
|
+
onChange={e => setRulesEditDesc(e.target.value)}
|
|
862
|
+
placeholder="Description"
|
|
863
|
+
className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)]"
|
|
864
|
+
/>
|
|
865
|
+
<textarea
|
|
866
|
+
value={rulesEditContent}
|
|
867
|
+
onChange={e => setRulesEditContent(e.target.value)}
|
|
868
|
+
placeholder="Template content (markdown)..."
|
|
869
|
+
className="w-full flex-1 min-h-[200px] p-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] font-mono text-[var(--text-primary)] resize-none"
|
|
870
|
+
spellCheck={false}
|
|
871
|
+
/>
|
|
872
|
+
<div className="flex items-center gap-3">
|
|
873
|
+
<label className="flex items-center gap-1.5 text-[10px] text-[var(--text-secondary)] cursor-pointer">
|
|
874
|
+
<input type="checkbox" checked={rulesEditDefault} onChange={e => setRulesEditDefault(e.target.checked)} className="accent-[var(--accent)]" />
|
|
875
|
+
Auto-apply to new projects
|
|
876
|
+
</label>
|
|
877
|
+
<div className="flex gap-2 ml-auto">
|
|
878
|
+
<button onClick={saveRule} className="text-[10px] px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90">Save</button>
|
|
879
|
+
<button onClick={() => { setRulesEditing(false); setRulesShowNew(false); }} className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]">Cancel</button>
|
|
880
|
+
</div>
|
|
881
|
+
</div>
|
|
882
|
+
</div>
|
|
883
|
+
</div>
|
|
884
|
+
) : rulesSelectedTemplate ? (() => {
|
|
885
|
+
const tmpl = rulesTemplates.find(t => t.id === rulesSelectedTemplate);
|
|
886
|
+
if (!tmpl) return null;
|
|
887
|
+
return (
|
|
888
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
889
|
+
{/* Template header */}
|
|
890
|
+
<div className="px-4 py-2 border-b border-[var(--border)] shrink-0">
|
|
891
|
+
<div className="flex items-center gap-2">
|
|
892
|
+
<span className="text-sm font-semibold text-[var(--text-primary)]">{tmpl.name}</span>
|
|
893
|
+
{tmpl.builtin && <span className="text-[8px] px-1 rounded bg-[var(--bg-tertiary)] text-[var(--text-secondary)]">built-in</span>}
|
|
894
|
+
<div className="ml-auto flex gap-1.5">
|
|
895
|
+
<button
|
|
896
|
+
onClick={() => { setRulesEditing(true); setRulesShowNew(false); setRulesEditId(tmpl.id); setRulesEditName(tmpl.name); setRulesEditDesc(tmpl.description); setRulesEditContent(tmpl.content); setRulesEditDefault(tmpl.isDefault); }}
|
|
897
|
+
className="text-[9px] text-[var(--accent)] hover:underline"
|
|
898
|
+
>Edit</button>
|
|
899
|
+
{!tmpl.builtin && (
|
|
900
|
+
<button onClick={() => deleteRule(tmpl.id)} className="text-[9px] text-[var(--red)] hover:underline">Delete</button>
|
|
901
|
+
)}
|
|
902
|
+
</div>
|
|
903
|
+
</div>
|
|
904
|
+
<p className="text-[9px] text-[var(--text-secondary)] mt-0.5">{tmpl.description}</p>
|
|
905
|
+
</div>
|
|
906
|
+
|
|
907
|
+
{/* Content + batch apply */}
|
|
908
|
+
<div className="flex-1 flex min-h-0 overflow-hidden">
|
|
909
|
+
{/* Template content */}
|
|
910
|
+
<div className="flex-1 min-w-0 overflow-auto">
|
|
911
|
+
<pre className="p-3 text-[11px] font-mono text-[var(--text-primary)] whitespace-pre-wrap break-words">
|
|
912
|
+
{tmpl.content}
|
|
913
|
+
</pre>
|
|
914
|
+
</div>
|
|
915
|
+
|
|
916
|
+
{/* Batch apply panel */}
|
|
917
|
+
<div className="w-48 border-l border-[var(--border)] overflow-y-auto shrink-0 flex flex-col">
|
|
918
|
+
<div className="px-2 py-1.5 border-b border-[var(--border)] text-[9px] text-[var(--text-secondary)] uppercase">Apply to Projects</div>
|
|
919
|
+
<div className="flex-1 overflow-y-auto">
|
|
920
|
+
{rulesProjects.map(p => (
|
|
921
|
+
<label key={p.path} className="flex items-center gap-1.5 px-2 py-1 hover:bg-[var(--bg-tertiary)] cursor-pointer">
|
|
922
|
+
<input
|
|
923
|
+
type="checkbox"
|
|
924
|
+
checked={rulesBatchProjects.has(p.path)}
|
|
925
|
+
onChange={() => {
|
|
926
|
+
setRulesBatchProjects(prev => {
|
|
927
|
+
const next = new Set(prev);
|
|
928
|
+
if (next.has(p.path)) next.delete(p.path); else next.add(p.path);
|
|
929
|
+
return next;
|
|
930
|
+
});
|
|
931
|
+
}}
|
|
932
|
+
className="accent-[var(--accent)]"
|
|
933
|
+
/>
|
|
934
|
+
<span className="text-[9px] text-[var(--text-primary)] truncate">{p.name}</span>
|
|
935
|
+
</label>
|
|
936
|
+
))}
|
|
937
|
+
</div>
|
|
938
|
+
{rulesBatchProjects.size > 0 && (
|
|
939
|
+
<div className="p-2 border-t border-[var(--border)]">
|
|
940
|
+
<button
|
|
941
|
+
onClick={() => batchInject(tmpl.id)}
|
|
942
|
+
className="w-full text-[9px] px-2 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90"
|
|
943
|
+
>
|
|
944
|
+
Apply to {rulesBatchProjects.size} project{rulesBatchProjects.size > 1 ? 's' : ''}
|
|
945
|
+
</button>
|
|
946
|
+
</div>
|
|
947
|
+
)}
|
|
948
|
+
</div>
|
|
949
|
+
</div>
|
|
950
|
+
</div>
|
|
951
|
+
);
|
|
952
|
+
})() : (
|
|
953
|
+
<div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">
|
|
954
|
+
<p className="text-xs">Select a template or create a new one</p>
|
|
955
|
+
</div>
|
|
956
|
+
)}
|
|
957
|
+
</div>
|
|
958
|
+
</div>
|
|
959
|
+
)}
|
|
960
|
+
|
|
961
|
+
{/* Plugins — full-page view */}
|
|
962
|
+
{typeFilter === 'plugins' && (
|
|
963
|
+
<Suspense fallback={<div className="p-4 text-xs text-[var(--text-secondary)]">Loading...</div>}>
|
|
964
|
+
<PluginsPanel />
|
|
965
|
+
</Suspense>
|
|
966
|
+
)}
|
|
967
|
+
</div>
|
|
968
|
+
);
|
|
969
|
+
}
|