@aion0/forge 0.5.26 → 0.5.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/.forge/worktrees/pipeline-4dd8dc2d/CLAUDE.md +86 -0
  2. package/.forge/worktrees/pipeline-4dd8dc2d/README.md +136 -0
  3. package/.forge/worktrees/pipeline-4dd8dc2d/RELEASE_NOTES.md +36 -0
  4. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/agents/route.ts +17 -0
  5. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/[...nextauth]/route.ts +3 -0
  6. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/verify/route.ts +46 -0
  7. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/route.ts +31 -0
  8. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/stream/route.ts +63 -0
  9. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/route.ts +28 -0
  10. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/entries/route.ts +23 -0
  11. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
  12. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/route.ts +37 -0
  13. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/sync/route.ts +17 -0
  14. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-templates/route.ts +145 -0
  15. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/code/route.ts +299 -0
  16. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/[id]/route.ts +62 -0
  17. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/route.ts +40 -0
  18. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/detect-cli/route.ts +46 -0
  19. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/route.ts +176 -0
  20. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/sessions/route.ts +54 -0
  21. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/favorites/route.ts +26 -0
  22. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/route.ts +6 -0
  23. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/run/route.ts +19 -0
  24. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/git/route.ts +149 -0
  25. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/help/route.ts +84 -0
  26. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/issue-scanner/route.ts +116 -0
  27. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/logs/route.ts +100 -0
  28. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/mobile-chat/route.ts +115 -0
  29. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/monitor/route.ts +74 -0
  30. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notifications/route.ts +42 -0
  31. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notify/test/route.ts +33 -0
  32. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/online/route.ts +40 -0
  33. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/[id]/route.ts +41 -0
  34. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/route.ts +90 -0
  35. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/plugins/route.ts +75 -0
  36. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/[...path]/route.ts +64 -0
  37. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/route.ts +156 -0
  38. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-pipelines/route.ts +91 -0
  39. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-sessions/route.ts +61 -0
  40. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/projects/route.ts +26 -0
  41. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/chat/route.ts +64 -0
  42. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/messages/route.ts +9 -0
  43. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/route.ts +17 -0
  44. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/route.ts +20 -0
  45. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/settings/route.ts +64 -0
  46. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/local/route.ts +228 -0
  47. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/route.ts +182 -0
  48. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/smith-templates/route.ts +81 -0
  49. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/status/route.ts +12 -0
  50. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tabs/route.ts +25 -0
  51. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/route.ts +51 -0
  52. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/stream/route.ts +77 -0
  53. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/link/route.ts +37 -0
  54. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/route.ts +44 -0
  55. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/session/route.ts +14 -0
  56. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/telegram/route.ts +23 -0
  57. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/templates/route.ts +6 -0
  58. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-bell/route.ts +39 -0
  59. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-cwd/route.ts +19 -0
  60. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-state/route.ts +15 -0
  61. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tunnel/route.ts +26 -0
  62. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/upgrade/route.ts +43 -0
  63. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/usage/route.ts +20 -0
  64. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/version/route.ts +78 -0
  65. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/watchers/route.ts +33 -0
  66. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/agents/route.ts +35 -0
  67. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/memory/route.ts +23 -0
  68. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/smith/route.ts +22 -0
  69. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/stream/route.ts +31 -0
  70. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/route.ts +79 -0
  71. package/.forge/worktrees/pipeline-4dd8dc2d/app/global-error.tsx +21 -0
  72. package/.forge/worktrees/pipeline-4dd8dc2d/app/globals.css +52 -0
  73. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.ico +0 -0
  74. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.png +0 -0
  75. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.svg +106 -0
  76. package/.forge/worktrees/pipeline-4dd8dc2d/app/layout.tsx +17 -0
  77. package/.forge/worktrees/pipeline-4dd8dc2d/app/login/LoginForm.tsx +96 -0
  78. package/.forge/worktrees/pipeline-4dd8dc2d/app/login/page.tsx +10 -0
  79. package/.forge/worktrees/pipeline-4dd8dc2d/app/mobile/page.tsx +10 -0
  80. package/.forge/worktrees/pipeline-4dd8dc2d/app/page.tsx +22 -0
  81. package/.forge/worktrees/pipeline-4dd8dc2d/bin/forge-server.mjs +484 -0
  82. package/.forge/worktrees/pipeline-4dd8dc2d/check-forge-status.sh +71 -0
  83. package/.forge/worktrees/pipeline-4dd8dc2d/cli/mw.ts +579 -0
  84. package/.forge/worktrees/pipeline-4dd8dc2d/components/BrowserPanel.tsx +175 -0
  85. package/.forge/worktrees/pipeline-4dd8dc2d/components/ChatPanel.tsx +191 -0
  86. package/.forge/worktrees/pipeline-4dd8dc2d/components/ClaudeTerminal.tsx +267 -0
  87. package/.forge/worktrees/pipeline-4dd8dc2d/components/CodeViewer.tsx +787 -0
  88. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationEditor.tsx +411 -0
  89. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationGraphView.tsx +347 -0
  90. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationTerminalView.tsx +303 -0
  91. package/.forge/worktrees/pipeline-4dd8dc2d/components/Dashboard.tsx +807 -0
  92. package/.forge/worktrees/pipeline-4dd8dc2d/components/DashboardWrapper.tsx +9 -0
  93. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryFlowEditor.tsx +491 -0
  94. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryList.tsx +230 -0
  95. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryWorkspace.tsx +589 -0
  96. package/.forge/worktrees/pipeline-4dd8dc2d/components/DocTerminal.tsx +187 -0
  97. package/.forge/worktrees/pipeline-4dd8dc2d/components/DocsViewer.tsx +574 -0
  98. package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpDialog.tsx +169 -0
  99. package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpTerminal.tsx +141 -0
  100. package/.forge/worktrees/pipeline-4dd8dc2d/components/InlinePipelineView.tsx +111 -0
  101. package/.forge/worktrees/pipeline-4dd8dc2d/components/LogViewer.tsx +194 -0
  102. package/.forge/worktrees/pipeline-4dd8dc2d/components/MarkdownContent.tsx +73 -0
  103. package/.forge/worktrees/pipeline-4dd8dc2d/components/MobileView.tsx +385 -0
  104. package/.forge/worktrees/pipeline-4dd8dc2d/components/MonitorPanel.tsx +122 -0
  105. package/.forge/worktrees/pipeline-4dd8dc2d/components/NewSessionModal.tsx +93 -0
  106. package/.forge/worktrees/pipeline-4dd8dc2d/components/NewTaskModal.tsx +492 -0
  107. package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineEditor.tsx +570 -0
  108. package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineView.tsx +1018 -0
  109. package/.forge/worktrees/pipeline-4dd8dc2d/components/PluginsPanel.tsx +472 -0
  110. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectDetail.tsx +1618 -0
  111. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectList.tsx +108 -0
  112. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectManager.tsx +401 -0
  113. package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionList.tsx +74 -0
  114. package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionView.tsx +726 -0
  115. package/.forge/worktrees/pipeline-4dd8dc2d/components/SettingsModal.tsx +1647 -0
  116. package/.forge/worktrees/pipeline-4dd8dc2d/components/SkillsPanel.tsx +969 -0
  117. package/.forge/worktrees/pipeline-4dd8dc2d/components/StatusBar.tsx +99 -0
  118. package/.forge/worktrees/pipeline-4dd8dc2d/components/TabBar.tsx +46 -0
  119. package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskBoard.tsx +113 -0
  120. package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskDetail.tsx +372 -0
  121. package/.forge/worktrees/pipeline-4dd8dc2d/components/TerminalLauncher.tsx +398 -0
  122. package/.forge/worktrees/pipeline-4dd8dc2d/components/TunnelToggle.tsx +206 -0
  123. package/.forge/worktrees/pipeline-4dd8dc2d/components/UsagePanel.tsx +207 -0
  124. package/.forge/worktrees/pipeline-4dd8dc2d/components/WebTerminal.tsx +1743 -0
  125. package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceTree.tsx +221 -0
  126. package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceView.tsx +4048 -0
  127. package/.forge/worktrees/pipeline-4dd8dc2d/dev-test.sh +5 -0
  128. package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Memory_Layer_Design.docx +0 -0
  129. package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Strategy_Research_2026.docx +0 -0
  130. package/.forge/worktrees/pipeline-4dd8dc2d/docs/LOCAL-DEPLOY.md +144 -0
  131. package/.forge/worktrees/pipeline-4dd8dc2d/docs/roadmap-multi-agent-workflow.md +330 -0
  132. package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.png +0 -0
  133. package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.svg +106 -0
  134. package/.forge/worktrees/pipeline-4dd8dc2d/hooks/useSidebarResize.ts +52 -0
  135. package/.forge/worktrees/pipeline-4dd8dc2d/install.sh +29 -0
  136. package/.forge/worktrees/pipeline-4dd8dc2d/instrumentation.ts +35 -0
  137. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/claude-adapter.ts +104 -0
  138. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/generic-adapter.ts +64 -0
  139. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/index.ts +245 -0
  140. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/types.ts +70 -0
  141. package/.forge/worktrees/pipeline-4dd8dc2d/lib/artifacts.ts +106 -0
  142. package/.forge/worktrees/pipeline-4dd8dc2d/lib/auth.ts +62 -0
  143. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/docker.yaml +70 -0
  144. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/http.yaml +66 -0
  145. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/jenkins.yaml +92 -0
  146. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/llm-vision.yaml +85 -0
  147. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/playwright.yaml +111 -0
  148. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/shell-command.yaml +60 -0
  149. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/slack.yaml +48 -0
  150. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/webhook.yaml +56 -0
  151. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-process.ts +361 -0
  152. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-sessions.ts +266 -0
  153. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-templates.ts +227 -0
  154. package/.forge/worktrees/pipeline-4dd8dc2d/lib/cloudflared.ts +424 -0
  155. package/.forge/worktrees/pipeline-4dd8dc2d/lib/crypto.ts +67 -0
  156. package/.forge/worktrees/pipeline-4dd8dc2d/lib/delivery.ts +787 -0
  157. package/.forge/worktrees/pipeline-4dd8dc2d/lib/dirs.ts +99 -0
  158. package/.forge/worktrees/pipeline-4dd8dc2d/lib/flows.ts +86 -0
  159. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-mcp-server.ts +732 -0
  160. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-inbox.md +38 -0
  161. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-send.md +47 -0
  162. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-status.md +32 -0
  163. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-workspace-sync.md +37 -0
  164. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/00-overview.md +40 -0
  165. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +194 -0
  166. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/02-telegram.md +41 -0
  167. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/03-tunnel.md +31 -0
  168. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/04-tasks.md +52 -0
  169. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/05-pipelines.md +460 -0
  170. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/06-skills.md +43 -0
  171. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +73 -0
  172. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/08-rules.md +53 -0
  173. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/09-issue-autofix.md +55 -0
  174. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/10-troubleshooting.md +89 -0
  175. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/11-workspace.md +810 -0
  176. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/CLAUDE.md +62 -0
  177. package/.forge/worktrees/pipeline-4dd8dc2d/lib/init.ts +266 -0
  178. package/.forge/worktrees/pipeline-4dd8dc2d/lib/issue-scanner.ts +298 -0
  179. package/.forge/worktrees/pipeline-4dd8dc2d/lib/logger.ts +79 -0
  180. package/.forge/worktrees/pipeline-4dd8dc2d/lib/notifications.ts +75 -0
  181. package/.forge/worktrees/pipeline-4dd8dc2d/lib/notify.ts +108 -0
  182. package/.forge/worktrees/pipeline-4dd8dc2d/lib/password.ts +97 -0
  183. package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline-scheduler.ts +373 -0
  184. package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline.ts +1565 -0
  185. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/executor.ts +347 -0
  186. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/registry.ts +228 -0
  187. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/types.ts +103 -0
  188. package/.forge/worktrees/pipeline-4dd8dc2d/lib/project-sessions.ts +53 -0
  189. package/.forge/worktrees/pipeline-4dd8dc2d/lib/projects.ts +86 -0
  190. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-manager.ts +156 -0
  191. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-utils.ts +53 -0
  192. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-watcher.ts +345 -0
  193. package/.forge/worktrees/pipeline-4dd8dc2d/lib/settings.ts +195 -0
  194. package/.forge/worktrees/pipeline-4dd8dc2d/lib/skills.ts +458 -0
  195. package/.forge/worktrees/pipeline-4dd8dc2d/lib/task-manager.ts +951 -0
  196. package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-bot.ts +1477 -0
  197. package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-standalone.ts +83 -0
  198. package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-server.ts +70 -0
  199. package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-standalone.ts +438 -0
  200. package/.forge/worktrees/pipeline-4dd8dc2d/lib/usage-scanner.ts +249 -0
  201. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/state-machine.test.ts +388 -0
  202. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/workspace.test.ts +311 -0
  203. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-bus.ts +416 -0
  204. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-worker.ts +655 -0
  205. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/api-backend.ts +262 -0
  206. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/cli-backend.ts +491 -0
  207. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/index.ts +84 -0
  208. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/manager.ts +136 -0
  209. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/orchestrator.ts +3415 -0
  210. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/persistence.ts +309 -0
  211. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/presets.ts +649 -0
  212. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/requests.ts +287 -0
  213. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/session-monitor.ts +240 -0
  214. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/skill-installer.ts +275 -0
  215. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/smith-memory.ts +498 -0
  216. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/types.ts +241 -0
  217. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/watch-manager.ts +560 -0
  218. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace-standalone.ts +978 -0
  219. package/.forge/worktrees/pipeline-4dd8dc2d/middleware.ts +51 -0
  220. package/.forge/worktrees/pipeline-4dd8dc2d/next.config.ts +26 -0
  221. package/.forge/worktrees/pipeline-4dd8dc2d/package.json +74 -0
  222. package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-lock.yaml +3719 -0
  223. package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-workspace.yaml +1 -0
  224. package/.forge/worktrees/pipeline-4dd8dc2d/postcss.config.mjs +7 -0
  225. package/.forge/worktrees/pipeline-4dd8dc2d/publish.sh +133 -0
  226. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/README.md +66 -0
  227. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/results/.gitignore +2 -0
  228. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/run.ts +635 -0
  229. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/task.md +26 -0
  230. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
  231. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/setup.sh +19 -0
  232. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/task.md +48 -0
  233. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/validator.sh +69 -0
  234. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
  235. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/task.md +30 -0
  236. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
  237. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/verify-usage.ts +178 -0
  238. package/.forge/worktrees/pipeline-4dd8dc2d/src/config/index.ts +129 -0
  239. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/db/database.ts +259 -0
  240. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/memory/strategy.ts +32 -0
  241. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/chat.ts +65 -0
  242. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/registry.ts +60 -0
  243. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/session/manager.ts +190 -0
  244. package/.forge/worktrees/pipeline-4dd8dc2d/src/types/index.ts +129 -0
  245. package/.forge/worktrees/pipeline-4dd8dc2d/start.sh +32 -0
  246. package/.forge/worktrees/pipeline-4dd8dc2d/templates/smith-lead.json +45 -0
  247. package/.forge/worktrees/pipeline-4dd8dc2d/tsconfig.json +42 -0
  248. package/RELEASE_NOTES.md +10 -29
  249. package/app/api/terminal-bell/route.ts +6 -2
  250. package/app/api/terminal-cwd/route.ts +7 -4
  251. package/components/CodeViewer.tsx +3 -31
  252. package/components/Dashboard.tsx +34 -20
  253. package/components/WebTerminal.tsx +36 -2
  254. package/lib/terminal-standalone.ts +19 -2
  255. package/package.json +1 -1
@@ -0,0 +1,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
+ }