@dyyz1993/create-agent 1.9.0

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 (358) hide show
  1. package/package.json +25 -0
  2. package/src/__tests__/cli.test.ts +85 -0
  3. package/src/__tests__/copy.test.ts +168 -0
  4. package/src/__tests__/templates.test.ts +87 -0
  5. package/src/cli.ts +65 -0
  6. package/src/commands/create.ts +49 -0
  7. package/src/commands/list.ts +12 -0
  8. package/src/commands/status.ts +85 -0
  9. package/src/commands/workspace.ts +129 -0
  10. package/src/lib/copy.ts +285 -0
  11. package/src/lib/templates.ts +57 -0
  12. package/src/lib/types.ts +6 -0
  13. package/templates/agent/.env.example +7 -0
  14. package/templates/agent/LICENSE +21 -0
  15. package/templates/agent/README.md +87 -0
  16. package/templates/agent/bun.lock +1279 -0
  17. package/templates/agent/electrobun.config.ts +27 -0
  18. package/templates/agent/eslint.config.mjs +63 -0
  19. package/templates/agent/llms.txt +24 -0
  20. package/templates/agent/package-lock.json +3670 -0
  21. package/templates/agent/package.json +59 -0
  22. package/templates/agent/postcss.config.js +6 -0
  23. package/templates/agent/scripts/dev.ts +138 -0
  24. package/templates/agent/src/__tests__/server-config.test.ts +59 -0
  25. package/templates/agent/src/bun/index.ts +73 -0
  26. package/templates/agent/src/gateway/__tests__/ws-handler.test.ts +48 -0
  27. package/templates/agent/src/gateway/http-routes.ts +1 -0
  28. package/templates/agent/src/gateway/ipc-transport.ts +68 -0
  29. package/templates/agent/src/gateway/ws-handler.ts +86 -0
  30. package/templates/agent/src/mainview/App.tsx +41 -0
  31. package/templates/agent/src/mainview/__tests__/components/ChatPanel.test.tsx +114 -0
  32. package/templates/agent/src/mainview/__tests__/components/ExplorerSidebar.test.tsx +141 -0
  33. package/templates/agent/src/mainview/__tests__/components/MessageBubble.test.tsx +72 -0
  34. package/templates/agent/src/mainview/__tests__/hooks/use-input-history.test.ts +125 -0
  35. package/templates/agent/src/mainview/__tests__/hooks/use-rpc-init.test.ts +217 -0
  36. package/templates/agent/src/mainview/__tests__/hooks/use-sidebar-resize.test.ts +127 -0
  37. package/templates/agent/src/mainview/__tests__/i18n/i18n.test.ts +48 -0
  38. package/templates/agent/src/mainview/__tests__/setup.ts +37 -0
  39. package/templates/agent/src/mainview/__tests__/stores/use-chat-store.test.ts +83 -0
  40. package/templates/agent/src/mainview/__tests__/stores/use-sidebar-store.test.ts +85 -0
  41. package/templates/agent/src/mainview/components/activity-bar/ActivityBar.tsx +36 -0
  42. package/templates/agent/src/mainview/components/activity-bar/MobileTabBar.tsx +36 -0
  43. package/templates/agent/src/mainview/components/activity-bar/__tests__/ActivityBar.test.tsx +44 -0
  44. package/templates/agent/src/mainview/components/activity-bar/__tests__/MobileTabBar.test.tsx +44 -0
  45. package/templates/agent/src/mainview/components/bash/BashPanel.tsx +206 -0
  46. package/templates/agent/src/mainview/components/bash/__tests__/BashPanel.test.tsx +103 -0
  47. package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +102 -0
  48. package/templates/agent/src/mainview/components/chat/MessageBubble.tsx +86 -0
  49. package/templates/agent/src/mainview/components/common/ErrorBoundary.tsx +58 -0
  50. package/templates/agent/src/mainview/components/common/LanguageSwitcher.tsx +14 -0
  51. package/templates/agent/src/mainview/components/common/ThemeToggle.tsx +24 -0
  52. package/templates/agent/src/mainview/components/common/__tests__/ErrorBoundary.test.tsx +74 -0
  53. package/templates/agent/src/mainview/components/common/__tests__/LanguageSwitcher.test.tsx +44 -0
  54. package/templates/agent/src/mainview/components/common/__tests__/ThemeToggle.test.tsx +41 -0
  55. package/templates/agent/src/mainview/components/debug/DebugPanel.tsx +120 -0
  56. package/templates/agent/src/mainview/components/debug/__tests__/DebugPanel.test.tsx +102 -0
  57. package/templates/agent/src/mainview/components/diff/DiffViewerPanel.tsx +106 -0
  58. package/templates/agent/src/mainview/components/diff/__tests__/DiffViewerPanel.test.tsx +73 -0
  59. package/templates/agent/src/mainview/components/explorer/ConfirmDialog.tsx +53 -0
  60. package/templates/agent/src/mainview/components/explorer/ContextMenu.tsx +82 -0
  61. package/templates/agent/src/mainview/components/explorer/ExplorerSidebar.tsx +221 -0
  62. package/templates/agent/src/mainview/components/explorer/InlineInput.tsx +55 -0
  63. package/templates/agent/src/mainview/components/explorer/TreeNodeItem.tsx +121 -0
  64. package/templates/agent/src/mainview/components/explorer/__tests__/ConfirmDialog.test.tsx +37 -0
  65. package/templates/agent/src/mainview/components/explorer/__tests__/ContextMenu.test.tsx +49 -0
  66. package/templates/agent/src/mainview/components/explorer/__tests__/InlineInput.test.tsx +48 -0
  67. package/templates/agent/src/mainview/components/explorer/__tests__/TreeNodeItem.test.tsx +146 -0
  68. package/templates/agent/src/mainview/components/feed/FeedPanel.tsx +239 -0
  69. package/templates/agent/src/mainview/components/feed/__tests__/FeedPanel.test.tsx +108 -0
  70. package/templates/agent/src/mainview/components/file-preview/FilePreviewOverlay.tsx +56 -0
  71. package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +99 -0
  72. package/templates/agent/src/mainview/components/file-preview/__tests__/FilePreviewOverlay.test.tsx +96 -0
  73. package/templates/agent/src/mainview/components/file-preview/__tests__/VirtualizedCodeView.test.tsx +58 -0
  74. package/templates/agent/src/mainview/components/git/GitBranchSelector.tsx +98 -0
  75. package/templates/agent/src/mainview/components/git/GitCommitInput.tsx +60 -0
  76. package/templates/agent/src/mainview/components/git/GitPanel.tsx +528 -0
  77. package/templates/agent/src/mainview/components/git/__tests__/GitBranchSelector.test.tsx +76 -0
  78. package/templates/agent/src/mainview/components/git/__tests__/GitCommitInput.test.tsx +91 -0
  79. package/templates/agent/src/mainview/components/git/__tests__/GitPanel.test.tsx +174 -0
  80. package/templates/agent/src/mainview/components/layout/AppLayout.tsx +170 -0
  81. package/templates/agent/src/mainview/components/layout/__tests__/AppLayout.test.tsx +163 -0
  82. package/templates/agent/src/mainview/components/rules/RulesPanel.tsx +117 -0
  83. package/templates/agent/src/mainview/components/rules/__tests__/RulesPanel.test.tsx +91 -0
  84. package/templates/agent/src/mainview/components/search/SearchPanel.tsx +404 -0
  85. package/templates/agent/src/mainview/components/search/__tests__/SearchPanel.test.tsx +50 -0
  86. package/templates/agent/src/mainview/components/sidebar/PinButton.tsx +20 -0
  87. package/templates/agent/src/mainview/components/sidebar/__tests__/PinButton.test.tsx +48 -0
  88. package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +141 -0
  89. package/templates/agent/src/mainview/components/todo/__tests__/TodoPanel.test.tsx +91 -0
  90. package/templates/agent/src/mainview/global.d.ts +13 -0
  91. package/templates/agent/src/mainview/hooks/__tests__/use-breakpoint.test.ts +147 -0
  92. package/templates/agent/src/mainview/hooks/use-breakpoint.ts +34 -0
  93. package/templates/agent/src/mainview/hooks/use-input-history.ts +84 -0
  94. package/templates/agent/src/mainview/hooks/use-rpc-init.ts +61 -0
  95. package/templates/agent/src/mainview/hooks/use-sidebar-resize.ts +40 -0
  96. package/templates/agent/src/mainview/index.css +61 -0
  97. package/templates/agent/src/mainview/index.html +12 -0
  98. package/templates/agent/src/mainview/lib/api-client.ts +192 -0
  99. package/templates/agent/src/mainview/lib/i18n/index.ts +30 -0
  100. package/templates/agent/src/mainview/lib/i18n/locales/en.json +157 -0
  101. package/templates/agent/src/mainview/lib/i18n/locales/zh.json +157 -0
  102. package/templates/agent/src/mainview/lib/rpc-cache.ts +94 -0
  103. package/templates/agent/src/mainview/main.tsx +24 -0
  104. package/templates/agent/src/mainview/stores/__tests__/use-app-store.test.ts +86 -0
  105. package/templates/agent/src/mainview/stores/__tests__/use-bash-store.test.ts +99 -0
  106. package/templates/agent/src/mainview/stores/__tests__/use-connection-store.test.ts +26 -0
  107. package/templates/agent/src/mainview/stores/__tests__/use-explorer-store.test.ts +130 -0
  108. package/templates/agent/src/mainview/stores/__tests__/use-feed-store.test.ts +132 -0
  109. package/templates/agent/src/mainview/stores/__tests__/use-git-store.test.ts +161 -0
  110. package/templates/agent/src/mainview/stores/__tests__/use-locale-store.test.ts +32 -0
  111. package/templates/agent/src/mainview/stores/__tests__/use-log-store.test.ts +28 -0
  112. package/templates/agent/src/mainview/stores/__tests__/use-notification-store.test.ts +102 -0
  113. package/templates/agent/src/mainview/stores/__tests__/use-rules-store.test.ts +75 -0
  114. package/templates/agent/src/mainview/stores/__tests__/use-theme-store.test.ts +59 -0
  115. package/templates/agent/src/mainview/stores/__tests__/use-todo-store.test.ts +75 -0
  116. package/templates/agent/src/mainview/stores/message-batcher.ts +25 -0
  117. package/templates/agent/src/mainview/stores/use-app-store.ts +99 -0
  118. package/templates/agent/src/mainview/stores/use-bash-store.ts +93 -0
  119. package/templates/agent/src/mainview/stores/use-chat-store.ts +37 -0
  120. package/templates/agent/src/mainview/stores/use-connection-store.ts +42 -0
  121. package/templates/agent/src/mainview/stores/use-explorer-store.ts +320 -0
  122. package/templates/agent/src/mainview/stores/use-feed-store.ts +129 -0
  123. package/templates/agent/src/mainview/stores/use-git-store.ts +291 -0
  124. package/templates/agent/src/mainview/stores/use-locale-store.ts +27 -0
  125. package/templates/agent/src/mainview/stores/use-log-store.ts +16 -0
  126. package/templates/agent/src/mainview/stores/use-notification-store.ts +82 -0
  127. package/templates/agent/src/mainview/stores/use-rules-store.ts +54 -0
  128. package/templates/agent/src/mainview/stores/use-sidebar-store.ts +99 -0
  129. package/templates/agent/src/mainview/stores/use-theme-store.ts +46 -0
  130. package/templates/agent/src/mainview/stores/use-todo-store.ts +54 -0
  131. package/templates/agent/src/mainview/types/index.ts +33 -0
  132. package/templates/agent/src/mainview/utils/constants.ts +1 -0
  133. package/templates/agent/src/mainview/utils/drop-handler.ts +150 -0
  134. package/templates/agent/src/mainview/utils/file-icon.tsx +24 -0
  135. package/templates/agent/src/mainview/utils/file-utils.ts +35 -0
  136. package/templates/agent/src/server-config.ts +43 -0
  137. package/templates/agent/src/server.ts +89 -0
  138. package/templates/agent/src/shared/handlers/__tests__/bash-handler.test.ts +135 -0
  139. package/templates/agent/src/shared/handlers/__tests__/chat-handler.test.ts +77 -0
  140. package/templates/agent/src/shared/handlers/__tests__/feed-handler.test.ts +108 -0
  141. package/templates/agent/src/shared/handlers/__tests__/file-handler.test.ts +100 -0
  142. package/templates/agent/src/shared/handlers/__tests__/system-handler.test.ts +55 -0
  143. package/templates/agent/src/shared/handlers/__tests__/timer-handler.test.ts +77 -0
  144. package/templates/agent/src/shared/handlers/bash.ts +89 -0
  145. package/templates/agent/src/shared/handlers/chat.ts +179 -0
  146. package/templates/agent/src/shared/handlers/feed.ts +53 -0
  147. package/templates/agent/src/shared/handlers/file.ts +99 -0
  148. package/templates/agent/src/shared/handlers/git.ts +263 -0
  149. package/templates/agent/src/shared/handlers/index.ts +10 -0
  150. package/templates/agent/src/shared/handlers/rules.ts +45 -0
  151. package/templates/agent/src/shared/handlers/system.ts +27 -0
  152. package/templates/agent/src/shared/handlers/timer.ts +34 -0
  153. package/templates/agent/src/shared/handlers/todo.ts +45 -0
  154. package/templates/agent/src/shared/lib/__tests__/bash-security.test.ts +125 -0
  155. package/templates/agent/src/shared/lib/__tests__/logger.test.ts +92 -0
  156. package/templates/agent/src/shared/lib/__tests__/path-security.test.ts +77 -0
  157. package/templates/agent/src/shared/lib/bash-security.ts +64 -0
  158. package/templates/agent/src/shared/lib/logger.ts +130 -0
  159. package/templates/agent/src/shared/lib/path-security.ts +38 -0
  160. package/templates/agent/src/shared/lib/port-registry.ts +103 -0
  161. package/templates/agent/src/shared/modules/bash.ts +19 -0
  162. package/templates/agent/src/shared/modules/chat.ts +20 -0
  163. package/templates/agent/src/shared/modules/feed.ts +30 -0
  164. package/templates/agent/src/shared/modules/file.ts +40 -0
  165. package/templates/agent/src/shared/modules/git.ts +81 -0
  166. package/templates/agent/src/shared/modules/rules.ts +25 -0
  167. package/templates/agent/src/shared/modules/system.ts +8 -0
  168. package/templates/agent/src/shared/modules/timer.ts +11 -0
  169. package/templates/agent/src/shared/modules/todo.ts +27 -0
  170. package/templates/agent/src/shared/register-all-handlers.ts +36 -0
  171. package/templates/agent/src/shared/rpc-schema.ts +35 -0
  172. package/templates/agent/tailwind.config.js +15 -0
  173. package/templates/agent/tsconfig.json +26 -0
  174. package/templates/agent/vite.config.ts +3 -0
  175. package/templates/agent/vitest.config.ts +3 -0
  176. package/templates/chat/.env.example +6 -0
  177. package/templates/chat/LICENSE +21 -0
  178. package/templates/chat/README.md +56 -0
  179. package/templates/chat/bun.lock +1203 -0
  180. package/templates/chat/electrobun.config.ts +27 -0
  181. package/templates/chat/eslint.config.mjs +56 -0
  182. package/templates/chat/llms.txt +24 -0
  183. package/templates/chat/package-lock.json +3670 -0
  184. package/templates/chat/package.json +58 -0
  185. package/templates/chat/postcss.config.js +6 -0
  186. package/templates/chat/scripts/dev.ts +138 -0
  187. package/templates/chat/src/__tests__/server-config.test.ts +59 -0
  188. package/templates/chat/src/bun/index.ts +72 -0
  189. package/templates/chat/src/gateway/__tests__/ws-handler.test.ts +48 -0
  190. package/templates/chat/src/gateway/http-routes.ts +1 -0
  191. package/templates/chat/src/gateway/ipc-transport.ts +68 -0
  192. package/templates/chat/src/gateway/ws-handler.ts +86 -0
  193. package/templates/chat/src/mainview/App.tsx +108 -0
  194. package/templates/chat/src/mainview/__tests__/components/ChatPanel.test.tsx +76 -0
  195. package/templates/chat/src/mainview/__tests__/components/MessageBubble.test.tsx +48 -0
  196. package/templates/chat/src/mainview/__tests__/i18n/i18n.test.ts +48 -0
  197. package/templates/chat/src/mainview/__tests__/setup-verify.test.ts +8 -0
  198. package/templates/chat/src/mainview/__tests__/setup.ts +37 -0
  199. package/templates/chat/src/mainview/__tests__/stores/use-app-store.test.ts +46 -0
  200. package/templates/chat/src/mainview/__tests__/stores/use-chat-store.test.ts +104 -0
  201. package/templates/chat/src/mainview/__tests__/stores/use-connection-store.test.ts +35 -0
  202. package/templates/chat/src/mainview/__tests__/stores/use-log-store.test.ts +45 -0
  203. package/templates/chat/src/mainview/__tests__/stores/use-notification-store.test.ts +102 -0
  204. package/templates/chat/src/mainview/__tests__/stores/use-sidebar-store.test.ts +89 -0
  205. package/templates/chat/src/mainview/components/activity-bar/ActivityBar.tsx +26 -0
  206. package/templates/chat/src/mainview/components/activity-bar/MobileTabBar.tsx +20 -0
  207. package/templates/chat/src/mainview/components/chat/ChatPanel.tsx +66 -0
  208. package/templates/chat/src/mainview/components/chat/MessageBubble.tsx +85 -0
  209. package/templates/chat/src/mainview/components/common/LanguageSwitcher.tsx +15 -0
  210. package/templates/chat/src/mainview/components/common/ThemeToggle.tsx +24 -0
  211. package/templates/chat/src/mainview/components/sidebar/PinButton.tsx +20 -0
  212. package/templates/chat/src/mainview/global.d.ts +13 -0
  213. package/templates/chat/src/mainview/hooks/use-breakpoint.ts +34 -0
  214. package/templates/chat/src/mainview/hooks/use-input-history.ts +84 -0
  215. package/templates/chat/src/mainview/index.css +61 -0
  216. package/templates/chat/src/mainview/index.html +12 -0
  217. package/templates/chat/src/mainview/lib/api-client.ts +175 -0
  218. package/templates/chat/src/mainview/lib/i18n/index.ts +30 -0
  219. package/templates/chat/src/mainview/lib/i18n/locales/en.json +157 -0
  220. package/templates/chat/src/mainview/lib/i18n/locales/zh.json +157 -0
  221. package/templates/chat/src/mainview/main.tsx +20 -0
  222. package/templates/chat/src/mainview/stores/__tests__/use-locale-store.test.ts +32 -0
  223. package/templates/chat/src/mainview/stores/__tests__/use-theme-store.test.ts +59 -0
  224. package/templates/chat/src/mainview/stores/message-batcher.ts +25 -0
  225. package/templates/chat/src/mainview/stores/use-app-store.ts +50 -0
  226. package/templates/chat/src/mainview/stores/use-chat-store.ts +37 -0
  227. package/templates/chat/src/mainview/stores/use-connection-store.ts +42 -0
  228. package/templates/chat/src/mainview/stores/use-locale-store.ts +27 -0
  229. package/templates/chat/src/mainview/stores/use-log-store.ts +16 -0
  230. package/templates/chat/src/mainview/stores/use-notification-store.ts +82 -0
  231. package/templates/chat/src/mainview/stores/use-sidebar-store.ts +98 -0
  232. package/templates/chat/src/mainview/stores/use-theme-store.ts +46 -0
  233. package/templates/chat/src/mainview/types/index.ts +8 -0
  234. package/templates/chat/src/mainview/utils/constants.ts +1 -0
  235. package/templates/chat/src/server-config.ts +42 -0
  236. package/templates/chat/src/server.ts +89 -0
  237. package/templates/chat/src/shared/__tests__/chat-handler.test.ts +71 -0
  238. package/templates/chat/src/shared/__tests__/system-handler.test.ts +70 -0
  239. package/templates/chat/src/shared/handlers/chat.ts +186 -0
  240. package/templates/chat/src/shared/handlers/index.ts +2 -0
  241. package/templates/chat/src/shared/handlers/system.ts +27 -0
  242. package/templates/chat/src/shared/lib/logger.ts +130 -0
  243. package/templates/chat/src/shared/lib/path-security.ts +38 -0
  244. package/templates/chat/src/shared/lib/port-registry.ts +103 -0
  245. package/templates/chat/src/shared/modules/chat.ts +20 -0
  246. package/templates/chat/src/shared/modules/system.ts +8 -0
  247. package/templates/chat/src/shared/register-all-handlers.ts +36 -0
  248. package/templates/chat/src/shared/rpc-schema.ts +11 -0
  249. package/templates/chat/tailwind.config.js +15 -0
  250. package/templates/chat/tsconfig.json +26 -0
  251. package/templates/chat/vite.config.ts +3 -0
  252. package/templates/chat/vitest.config.ts +3 -0
  253. package/templates/general/.env.example +7 -0
  254. package/templates/general/LICENSE +21 -0
  255. package/templates/general/README.md +113 -0
  256. package/templates/general/bun.lock +1266 -0
  257. package/templates/general/electrobun.config.ts +27 -0
  258. package/templates/general/eslint.config.mjs +56 -0
  259. package/templates/general/llms.txt +24 -0
  260. package/templates/general/package-lock.json +3670 -0
  261. package/templates/general/package.json +59 -0
  262. package/templates/general/postcss.config.js +6 -0
  263. package/templates/general/scripts/dev.ts +138 -0
  264. package/templates/general/src/__tests__/server-config.test.ts +59 -0
  265. package/templates/general/src/bun/index.ts +72 -0
  266. package/templates/general/src/gateway/http-routes.ts +1 -0
  267. package/templates/general/src/gateway/ipc-transport.ts +68 -0
  268. package/templates/general/src/gateway/ws-handler.ts +86 -0
  269. package/templates/general/src/mainview/App.tsx +40 -0
  270. package/templates/general/src/mainview/__tests__/components/ChatPanel.test.tsx +82 -0
  271. package/templates/general/src/mainview/__tests__/components/FeedPanel.test.tsx +109 -0
  272. package/templates/general/src/mainview/__tests__/components/GitPanel.test.tsx +151 -0
  273. package/templates/general/src/mainview/__tests__/i18n/i18n.test.ts +48 -0
  274. package/templates/general/src/mainview/__tests__/setup-verify.test.ts +8 -0
  275. package/templates/general/src/mainview/__tests__/setup.ts +37 -0
  276. package/templates/general/src/mainview/components/activity-bar/ActivityBar.tsx +40 -0
  277. package/templates/general/src/mainview/components/activity-bar/MobileTabBar.tsx +29 -0
  278. package/templates/general/src/mainview/components/chat/ChatPanel.tsx +66 -0
  279. package/templates/general/src/mainview/components/chat/MessageBubble.tsx +85 -0
  280. package/templates/general/src/mainview/components/common/LanguageSwitcher.tsx +15 -0
  281. package/templates/general/src/mainview/components/common/ThemeToggle.tsx +24 -0
  282. package/templates/general/src/mainview/components/debug/DebugPanel.tsx +176 -0
  283. package/templates/general/src/mainview/components/diff/DiffViewerPanel.tsx +108 -0
  284. package/templates/general/src/mainview/components/explorer/ConfirmDialog.tsx +52 -0
  285. package/templates/general/src/mainview/components/explorer/ContextMenu.tsx +82 -0
  286. package/templates/general/src/mainview/components/explorer/ExplorerSidebar.tsx +225 -0
  287. package/templates/general/src/mainview/components/explorer/InlineInput.tsx +55 -0
  288. package/templates/general/src/mainview/components/explorer/TreeNodeItem.tsx +121 -0
  289. package/templates/general/src/mainview/components/feed/FeedPanel.tsx +248 -0
  290. package/templates/general/src/mainview/components/file-preview/FilePreviewOverlay.tsx +55 -0
  291. package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +99 -0
  292. package/templates/general/src/mainview/components/git/GitBranchSelector.tsx +93 -0
  293. package/templates/general/src/mainview/components/git/GitCommitInput.tsx +60 -0
  294. package/templates/general/src/mainview/components/git/GitPanel.tsx +533 -0
  295. package/templates/general/src/mainview/components/layout/AppLayout.tsx +146 -0
  296. package/templates/general/src/mainview/components/search/SearchPanel.tsx +405 -0
  297. package/templates/general/src/mainview/components/sidebar/PinButton.tsx +20 -0
  298. package/templates/general/src/mainview/global.d.ts +13 -0
  299. package/templates/general/src/mainview/hooks/use-breakpoint.ts +34 -0
  300. package/templates/general/src/mainview/hooks/use-input-history.ts +84 -0
  301. package/templates/general/src/mainview/hooks/use-rpc-init.ts +61 -0
  302. package/templates/general/src/mainview/hooks/use-sidebar-resize.ts +40 -0
  303. package/templates/general/src/mainview/index.css +61 -0
  304. package/templates/general/src/mainview/index.html +12 -0
  305. package/templates/general/src/mainview/lib/api-client.ts +175 -0
  306. package/templates/general/src/mainview/lib/i18n/index.ts +30 -0
  307. package/templates/general/src/mainview/lib/i18n/locales/en.json +157 -0
  308. package/templates/general/src/mainview/lib/i18n/locales/zh.json +157 -0
  309. package/templates/general/src/mainview/main.tsx +20 -0
  310. package/templates/general/src/mainview/stores/__tests__/use-chat-store.test.ts +104 -0
  311. package/templates/general/src/mainview/stores/__tests__/use-connection-store.test.ts +35 -0
  312. package/templates/general/src/mainview/stores/__tests__/use-locale-store.test.ts +32 -0
  313. package/templates/general/src/mainview/stores/__tests__/use-log-store.test.ts +45 -0
  314. package/templates/general/src/mainview/stores/__tests__/use-theme-store.test.ts +59 -0
  315. package/templates/general/src/mainview/stores/message-batcher.ts +25 -0
  316. package/templates/general/src/mainview/stores/use-app-store.ts +104 -0
  317. package/templates/general/src/mainview/stores/use-chat-store.ts +37 -0
  318. package/templates/general/src/mainview/stores/use-connection-store.ts +42 -0
  319. package/templates/general/src/mainview/stores/use-explorer-store.ts +315 -0
  320. package/templates/general/src/mainview/stores/use-feed-store.ts +129 -0
  321. package/templates/general/src/mainview/stores/use-git-store.ts +284 -0
  322. package/templates/general/src/mainview/stores/use-locale-store.ts +27 -0
  323. package/templates/general/src/mainview/stores/use-log-store.ts +16 -0
  324. package/templates/general/src/mainview/stores/use-notification-store.ts +82 -0
  325. package/templates/general/src/mainview/stores/use-sidebar-store.ts +99 -0
  326. package/templates/general/src/mainview/stores/use-theme-store.ts +46 -0
  327. package/templates/general/src/mainview/types/index.ts +33 -0
  328. package/templates/general/src/mainview/utils/constants.ts +1 -0
  329. package/templates/general/src/mainview/utils/drop-handler.ts +150 -0
  330. package/templates/general/src/mainview/utils/file-icon.tsx +24 -0
  331. package/templates/general/src/mainview/utils/file-utils.ts +35 -0
  332. package/templates/general/src/server-config.ts +42 -0
  333. package/templates/general/src/server.ts +89 -0
  334. package/templates/general/src/shared/handlers/__tests__/chat.test.ts +183 -0
  335. package/templates/general/src/shared/handlers/__tests__/system.test.ts +94 -0
  336. package/templates/general/src/shared/handlers/__tests__/timer.test.ts +113 -0
  337. package/templates/general/src/shared/handlers/chat.ts +179 -0
  338. package/templates/general/src/shared/handlers/feed.ts +53 -0
  339. package/templates/general/src/shared/handlers/file.ts +99 -0
  340. package/templates/general/src/shared/handlers/git.ts +263 -0
  341. package/templates/general/src/shared/handlers/index.ts +7 -0
  342. package/templates/general/src/shared/handlers/system.ts +27 -0
  343. package/templates/general/src/shared/handlers/timer.ts +34 -0
  344. package/templates/general/src/shared/lib/logger.ts +130 -0
  345. package/templates/general/src/shared/lib/path-security.ts +38 -0
  346. package/templates/general/src/shared/lib/port-registry.ts +103 -0
  347. package/templates/general/src/shared/modules/chat.ts +20 -0
  348. package/templates/general/src/shared/modules/feed.ts +30 -0
  349. package/templates/general/src/shared/modules/file.ts +40 -0
  350. package/templates/general/src/shared/modules/git.ts +81 -0
  351. package/templates/general/src/shared/modules/system.ts +8 -0
  352. package/templates/general/src/shared/modules/timer.ts +11 -0
  353. package/templates/general/src/shared/register-all-handlers.ts +36 -0
  354. package/templates/general/src/shared/rpc-schema.ts +31 -0
  355. package/templates/general/tailwind.config.js +15 -0
  356. package/templates/general/tsconfig.json +26 -0
  357. package/templates/general/vite.config.ts +3 -0
  358. package/templates/general/vitest.config.ts +3 -0
@@ -0,0 +1,55 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ interface InlineInputProps {
4
+ defaultValue?: string;
5
+ depth: number;
6
+ onSubmit: (value: string) => void;
7
+ onCancel: () => void;
8
+ }
9
+
10
+ export function InlineInput({ defaultValue = "", depth, onSubmit, onCancel }: InlineInputProps) {
11
+ const ref = useRef<HTMLInputElement>(null);
12
+ const committed = useRef(false);
13
+
14
+ useEffect(() => {
15
+ const el = ref.current;
16
+ if (!el) return;
17
+ el.focus();
18
+ // Select name part (before extension) if there's a dot
19
+ const dotIndex = defaultValue.lastIndexOf(".");
20
+ if (dotIndex > 0) {
21
+ el.setSelectionRange(0, dotIndex);
22
+ } else {
23
+ el.select();
24
+ }
25
+ }, [defaultValue]);
26
+
27
+ const handleKeyDown = (e: React.KeyboardEvent) => {
28
+ if (e.key === "Enter") {
29
+ const val = ref.current?.value.trim();
30
+ committed.current = true;
31
+ if (val) onSubmit(val);
32
+ else onCancel();
33
+ } else if (e.key === "Escape") {
34
+ committed.current = true;
35
+ onCancel();
36
+ }
37
+ };
38
+
39
+ const handleBlur = () => {
40
+ if (!committed.current) onCancel();
41
+ };
42
+
43
+ return (
44
+ <div style={{ paddingLeft: `${depth * 16 + 24}px` }} className="py-0.5 pr-2">
45
+ <input
46
+ ref={ref}
47
+ type="text"
48
+ defaultValue={defaultValue}
49
+ onKeyDown={handleKeyDown}
50
+ onBlur={handleBlur}
51
+ className="w-full px-1 py-0.5 text-xs bg-[var(--color-bg-tertiary)] text-[var(--color-text-primary)] border border-[var(--color-accent)] rounded outline-none"
52
+ />
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,121 @@
1
+ import { memo } from "react";
2
+ import { ChevronRight, ChevronDown } from "lucide-react";
3
+ import type { TreeNode, EditingNode } from "../../types";
4
+ import { getFileIcon } from "../../utils/file-icon";
5
+ import { InlineInput } from "./InlineInput";
6
+
7
+ interface TreeNodeItemProps {
8
+ node: TreeNode;
9
+ depth: number;
10
+ selectedPath: string | null;
11
+ editingNode: EditingNode | null;
12
+ onToggle: (path: string) => void;
13
+ onOpenFile: (node: TreeNode) => void;
14
+ onContextMenu: (e: React.MouseEvent, node: TreeNode) => void;
15
+ onSubmitEdit: (value: string) => void;
16
+ onCancelEdit: () => void;
17
+ }
18
+
19
+ function TreeNodeItemInner({
20
+ node,
21
+ depth,
22
+ selectedPath,
23
+ editingNode,
24
+ onToggle,
25
+ onOpenFile,
26
+ onContextMenu,
27
+ onSubmitEdit,
28
+ onCancelEdit,
29
+ }: TreeNodeItemProps) {
30
+ const isDir = node.type === "directory";
31
+ const isSelected = selectedPath === node.path;
32
+ const isRenaming = editingNode?.path === node.path && editingNode.type === "rename";
33
+ const isAddingChild =
34
+ isDir &&
35
+ node.expanded &&
36
+ editingNode?.path === node.path &&
37
+ (editingNode.type === "newFile" || editingNode.type === "newDir");
38
+
39
+ return (
40
+ <li>
41
+ <div
42
+ className={`flex items-center gap-1.5 px-2 py-0.5 text-xs rounded cursor-pointer transition-colors ${
43
+ isSelected ? "bg-[var(--color-accent)]/30 text-[var(--color-text-primary)]" : "hover:bg-[var(--color-bg-hover)]"
44
+ }`}
45
+ style={{ paddingLeft: `${depth * 16 + 8}px` }}
46
+ onClick={() => isDir ? onToggle(node.path) : onOpenFile(node)}
47
+ onContextMenu={(e) => {
48
+ e.preventDefault();
49
+ e.stopPropagation();
50
+ onContextMenu(e, node);
51
+ }}
52
+ >
53
+ {isDir ? (
54
+ node.expanded ? (
55
+ <ChevronDown className="w-3 h-3 text-[var(--color-text-placeholder)] shrink-0" />
56
+ ) : (
57
+ <ChevronRight className="w-3 h-3 text-[var(--color-text-placeholder)] shrink-0" />
58
+ )
59
+ ) : (
60
+ <span className="w-3 shrink-0" />
61
+ )}
62
+ {getFileIcon(node)}
63
+ {isRenaming ? (
64
+ <InlineInput
65
+ defaultValue={node.name}
66
+ depth={depth}
67
+ onSubmit={onSubmitEdit}
68
+ onCancel={onCancelEdit}
69
+ />
70
+ ) : (
71
+ <span className={`truncate ${isDir ? "text-blue-300 font-medium" : "text-[var(--color-text-secondary)]"}`}>
72
+ {node.name}
73
+ </span>
74
+ )}
75
+ </div>
76
+ {isDir && node.expanded && node.children && (
77
+ <ul>
78
+ {node.children.map((child) => (
79
+ <TreeNodeItem
80
+ key={child.path}
81
+ node={child}
82
+ depth={depth + 1}
83
+ selectedPath={selectedPath}
84
+ editingNode={editingNode}
85
+ onToggle={onToggle}
86
+ onOpenFile={onOpenFile}
87
+ onContextMenu={onContextMenu}
88
+ onSubmitEdit={onSubmitEdit}
89
+ onCancelEdit={onCancelEdit}
90
+ />
91
+ ))}
92
+ {isAddingChild && (
93
+ <InlineInput
94
+ depth={depth + 1}
95
+ onSubmit={onSubmitEdit}
96
+ onCancel={onCancelEdit}
97
+ />
98
+ )}
99
+ </ul>
100
+ )}
101
+ </li>
102
+ );
103
+ }
104
+
105
+ export const TreeNodeItem = memo(TreeNodeItemInner, (prev, next) => {
106
+ // Fast path: same node reference and no editing state change
107
+ if (
108
+ prev.node === next.node &&
109
+ prev.selectedPath === next.selectedPath &&
110
+ prev.editingNode === next.editingNode &&
111
+ prev.depth === next.depth &&
112
+ prev.onToggle === next.onToggle &&
113
+ prev.onOpenFile === next.onOpenFile &&
114
+ prev.onContextMenu === next.onContextMenu &&
115
+ prev.onSubmitEdit === next.onSubmitEdit &&
116
+ prev.onCancelEdit === next.onCancelEdit
117
+ ) {
118
+ return true;
119
+ }
120
+ return false;
121
+ });
@@ -0,0 +1,248 @@
1
+ import { useEffect } from "react";
2
+ import { Rss, Send } from "lucide-react";
3
+ import { useTranslation } from "react-i18next";
4
+ import { apiClient } from "../../lib/api-client";
5
+ import { useLogStore } from "../../stores/use-log-store";
6
+ import { useConnectionStore } from "../../stores/use-connection-store";
7
+ import { useFeedStore, useEventStreamStore, type SubEventType } from "../../stores/use-feed-store";
8
+ import type { FeedCategory } from "../../../shared/modules/feed";
9
+
10
+ const CATEGORIES: FeedCategory[] = ["tech", "news", "general"];
11
+ const EVENT_TYPES: SubEventType[] = ["feed.update", "chat.message", "timer.tick"];
12
+ const FILTER_PRESETS: Record<SubEventType, string[]> = {
13
+ "feed.update": ["", '{"category":"tech"}', '{"category":"news"}', '{"author":"alice"}'],
14
+ "chat.message": ["", '{"role":"user"}', '{"role":"assistant"}'],
15
+ "timer.tick": ["", '{"channel":"default"}'],
16
+ };
17
+
18
+ const CATEGORY_COLORS: Record<FeedCategory, string> = {
19
+ tech: "bg-blue-600/30 text-blue-300",
20
+ news: "bg-amber-600/30 text-amber-300",
21
+ general: "bg-green-600/30 text-green-300",
22
+ };
23
+
24
+ export function FeedPanel() {
25
+ const { t } = useTranslation();
26
+ const posts = useFeedStore((s) => s.posts);
27
+ const author = useFeedStore((s) => s.author);
28
+ const category = useFeedStore((s) => s.category);
29
+ const content = useFeedStore((s) => s.content);
30
+ const loading = useFeedStore((s) => s.loading);
31
+ const setAuthor = useFeedStore((s) => s.setAuthor);
32
+ const setCategory = useFeedStore((s) => s.setCategory);
33
+ const setContent = useFeedStore((s) => s.setContent);
34
+ const createPost = useFeedStore((s) => s.createPost);
35
+ const loadPosts = useFeedStore((s) => s.loadPosts);
36
+ const addPost = useFeedStore((s) => s.addPost);
37
+ const addLog = useLogStore((s) => s.addLog);
38
+ const ready = useConnectionStore((s) => s.ready);
39
+
40
+ // Event stream state
41
+ const entries = useEventStreamStore((s) => s.entries);
42
+ const activeEventType = useEventStreamStore((s) => s.activeEventType);
43
+ const activeFilter = useEventStreamStore((s) => s.activeFilter);
44
+ const subscribed = useEventStreamStore((s) => s.subscribed);
45
+ const setActiveEventType = useEventStreamStore((s) => s.setActiveEventType);
46
+ const setActiveFilter = useEventStreamStore((s) => s.setActiveFilter);
47
+ const handleSubscribe = useEventStreamStore((s) => s.handleSubscribe);
48
+ const handleUnsubscribe = useEventStreamStore((s) => s.handleUnsubscribe);
49
+
50
+ // On ready: subscribe to feed.update (no filter) + load posts
51
+ useEffect(() => {
52
+ if (!ready) return;
53
+
54
+ let subId: string | null = null;
55
+
56
+ const setup = async () => {
57
+ // Subscribe to all feed updates (no filter) to populate feed list
58
+ subId = await apiClient.subscribe("feed.update", (payload) => {
59
+ addPost(payload as typeof posts[number]);
60
+ }, {});
61
+ addLog("Subscribed to feed.update (no filter)");
62
+
63
+ await loadPosts();
64
+ };
65
+ setup();
66
+
67
+ return () => {
68
+ if (subId) apiClient.unsubscribe(subId);
69
+ };
70
+ }, [ready, addLog, loadPosts, addPost]);
71
+
72
+ return (
73
+ <div className="flex-1 flex flex-col overflow-hidden">
74
+ {/* Header */}
75
+ <div className="px-4 py-2 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border-primary)] flex items-center justify-between flex-shrink-0">
76
+ <h2 className="text-sm font-semibold flex items-center gap-1.5">
77
+ <Rss className="w-4 h-4 text-[var(--color-text-accent)]" />
78
+ {t("feed.title")}
79
+ {posts.length > 0 && (
80
+ <span className="ml-1 px-2 py-0.5 bg-[var(--color-accent)]/30 text-[var(--color-text-accent)] rounded text-[10px]">
81
+ {posts.length}
82
+ </span>
83
+ )}
84
+ </h2>
85
+ </div>
86
+
87
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
88
+ {/* Create Post */}
89
+ <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
90
+ <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">{t("feed.newPost")}</div>
91
+ <div className="flex gap-2">
92
+ <input
93
+ type="text"
94
+ value={author}
95
+ onChange={(e) => setAuthor(e.target.value)}
96
+ placeholder={t("feed.authorPlaceholder")}
97
+ className="w-24 px-2 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none"
98
+ />
99
+ <select
100
+ value={category}
101
+ onChange={(e) => setCategory(e.target.value as FeedCategory)}
102
+ className="px-2 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)]"
103
+ >
104
+ {CATEGORIES.map((c) => (
105
+ <option key={c} value={c}>{c}</option>
106
+ ))}
107
+ </select>
108
+ </div>
109
+ <div className="flex gap-2">
110
+ <input
111
+ type="text"
112
+ value={content}
113
+ onChange={(e) => setContent(e.target.value)}
114
+ onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && createPost()}
115
+ placeholder={t("feed.postPlaceholder")}
116
+ className="flex-1 px-3 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none"
117
+ />
118
+ <button
119
+ onClick={createPost}
120
+ disabled={loading || !content.trim()}
121
+ className="px-3 py-1.5 text-xs bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 rounded transition-colors flex items-center gap-1"
122
+ >
123
+ <Send className="w-3 h-3" />
124
+ {t("feed.post")}
125
+ </button>
126
+ </div>
127
+ </div>
128
+
129
+ {/* Subscription Panel */}
130
+ <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
131
+ <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">{t("feed.subscribeWithFilter")}</div>
132
+ <div className="flex gap-2 items-center">
133
+ <select
134
+ value={activeEventType}
135
+ onChange={(e) => {
136
+ setActiveEventType(e.target.value as SubEventType);
137
+ setActiveFilter("");
138
+ }}
139
+ className="px-2 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)]"
140
+ >
141
+ {EVENT_TYPES.map((t) => (
142
+ <option key={t} value={t}>{t}</option>
143
+ ))}
144
+ </select>
145
+ <input
146
+ type="text"
147
+ value={activeFilter}
148
+ onChange={(e) => setActiveFilter(e.target.value)}
149
+ placeholder={t("feed.filterPlaceholder")}
150
+ className="flex-1 px-2 py-1.5 text-xs bg-[var(--color-bg-tertiary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none font-mono"
151
+ />
152
+ {!subscribed ? (
153
+ <button
154
+ onClick={handleSubscribe}
155
+ className="px-3 py-1.5 text-xs bg-green-600 hover:bg-green-700 rounded transition-colors whitespace-nowrap"
156
+ >
157
+ {t("feed.subscribe")}
158
+ </button>
159
+ ) : (
160
+ <button
161
+ onClick={handleUnsubscribe}
162
+ className="px-3 py-1.5 text-xs bg-red-600 hover:bg-red-700 rounded transition-colors whitespace-nowrap"
163
+ >
164
+ {t("feed.unsubscribe")}
165
+ </button>
166
+ )}
167
+ </div>
168
+ {/* Filter presets */}
169
+ <div className="flex gap-1 flex-wrap">
170
+ {FILTER_PRESETS[activeEventType].map((preset) => (
171
+ <button
172
+ key={preset}
173
+ onClick={() => setActiveFilter(preset)}
174
+ className={`px-2 py-0.5 text-[10px] rounded border transition-colors ${
175
+ activeFilter === preset
176
+ ? "border-[var(--color-accent)] bg-[var(--color-accent)]/30 text-[var(--color-text-accent)]"
177
+ : "border-[var(--color-border-secondary)] text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]"
178
+ }`}
179
+ >
180
+ {preset || t("feed.noFilter")}
181
+ </button>
182
+ ))}
183
+ </div>
184
+ </div>
185
+
186
+ {/* Event Stream */}
187
+ {entries.length > 0 && (
188
+ <div className="bg-[var(--color-bg-secondary)] rounded-lg p-3 space-y-2">
189
+ <div className="flex items-center justify-between">
190
+ <div className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider">
191
+ {t("feed.eventStream")}
192
+ </div>
193
+ {subscribed && (
194
+ <span className="px-1.5 py-0.5 bg-green-600/30 text-green-400 rounded text-[10px] animate-pulse">
195
+ {t("feed.live")}
196
+ </span>
197
+ )}
198
+ </div>
199
+ <div className="max-h-48 overflow-y-auto space-y-1">
200
+ {entries.slice(-20).map((entry) => (
201
+ <div key={entry.id} className="bg-[var(--color-bg-tertiary)] rounded px-2 py-1.5 text-[11px] font-mono">
202
+ <div className="flex items-center gap-2 text-[var(--color-text-tertiary)]">
203
+ <span className="text-[var(--color-text-accent)]">{entry.eventType}</span>
204
+ <span className="text-[10px]">
205
+ {Object.keys(entry.filter).length > 0
206
+ ? `filter: ${JSON.stringify(entry.filter)}`
207
+ : t("feed.noFilter")}
208
+ </span>
209
+ <span className="text-[10px] ml-auto">
210
+ {new Date(entry.timestamp).toLocaleTimeString()}
211
+ </span>
212
+ </div>
213
+ <pre className="text-cyan-300 mt-0.5 whitespace-pre-wrap break-all">
214
+ {JSON.stringify(entry.payload, null, 0)}
215
+ </pre>
216
+ </div>
217
+ ))}
218
+ </div>
219
+ </div>
220
+ )}
221
+
222
+ {/* Feed Posts */}
223
+ {posts.length === 0 ? (
224
+ <div className="flex items-center justify-center py-8 text-[var(--color-text-placeholder)] text-xs">
225
+ {t("feed.noPostsYet")}
226
+ </div>
227
+ ) : (
228
+ <div className="space-y-2">
229
+ {posts.slice().reverse().map((post) => (
230
+ <div key={post.id} className="bg-[var(--color-bg-secondary)] rounded-lg p-3">
231
+ <div className="flex items-center gap-2 mb-1">
232
+ <span className="text-xs font-medium text-[var(--color-text-primary)]">{post.author}</span>
233
+ <span className={`px-1.5 py-0.5 rounded text-[10px] ${CATEGORY_COLORS[post.category]}`}>
234
+ {post.category}
235
+ </span>
236
+ <span className="text-[10px] text-[var(--color-text-placeholder)] ml-auto">
237
+ {new Date(post.timestamp).toLocaleTimeString()}
238
+ </span>
239
+ </div>
240
+ <div className="text-xs text-[var(--color-text-secondary)]">{post.content}</div>
241
+ </div>
242
+ ))}
243
+ </div>
244
+ )}
245
+ </div>
246
+ </div>
247
+ );
248
+ }
@@ -0,0 +1,55 @@
1
+ import { FileText, X } from "lucide-react";
2
+ import type { FilePreview } from "../../types";
3
+ import { formatSize } from "../../utils/file-utils";
4
+ import { VirtualizedCodeView } from "./VirtualizedCodeView";
5
+
6
+ interface FilePreviewOverlayProps {
7
+ preview: FilePreview;
8
+ loading: boolean;
9
+ onClose: () => void;
10
+ }
11
+
12
+ export function FilePreviewOverlay({ preview, loading, onClose }: FilePreviewOverlayProps) {
13
+ return (
14
+ <div className="absolute inset-0 z-10 bg-[var(--color-bg-primary)]/95 flex flex-col overflow-hidden">
15
+ <div className="flex items-center justify-between px-4 py-2 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border-primary)] flex-shrink-0">
16
+ <div className="flex items-center gap-2">
17
+ <FileText className="w-4 h-4 text-[var(--color-text-tertiary)]" />
18
+ <span className="text-sm font-medium text-[var(--color-text-secondary)]">{preview.name}</span>
19
+ {preview.size > 0 && (
20
+ <span className="text-xs text-[var(--color-text-placeholder)]">{formatSize(preview.size)}</span>
21
+ )}
22
+ {preview.totalLines != null && (
23
+ <span className="text-xs text-[var(--color-text-placeholder)]">{preview.totalLines} lines</span>
24
+ )}
25
+ </div>
26
+ <button
27
+ onClick={onClose}
28
+ className="text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)] p-1 rounded hover:bg-[var(--color-bg-hover)] transition-colors"
29
+ >
30
+ <X className="w-4 h-4" />
31
+ </button>
32
+ </div>
33
+
34
+ {/* File content — flex-col so VirtualizedCodeView's flex-1 works */}
35
+ <div className="flex-1 min-h-0 flex flex-col">
36
+ {loading ? (
37
+ <div className="flex items-center justify-center h-full text-[var(--color-text-tertiary)] text-sm">
38
+ <div className="w-5 h-5 border-2 border-[var(--color-text-accent)] border-t-transparent rounded-full animate-spin mr-2" />
39
+ Loading...
40
+ </div>
41
+ ) : preview.isImage && preview.imageUrl ? (
42
+ <div className="flex items-center justify-center h-full p-4 bg-[#1a1a2e]">
43
+ <img
44
+ src={preview.imageUrl}
45
+ alt={preview.name}
46
+ className="max-w-full max-h-full object-contain rounded"
47
+ />
48
+ </div>
49
+ ) : preview.content ? (
50
+ <VirtualizedCodeView code={preview.content} filename={preview.name} />
51
+ ) : null}
52
+ </div>
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,99 @@
1
+ import { useRef, useMemo } from "react";
2
+ import { useVirtualizer } from "@tanstack/react-virtual";
3
+ import { Highlight, themes } from "prism-react-renderer";
4
+ import { getLanguage } from "../../utils/file-utils";
5
+
6
+ interface VirtualizedCodeViewProps {
7
+ code: string;
8
+ filename: string;
9
+ }
10
+
11
+ /** Lines longer than this skip syntax highlighting (plain text instead) */
12
+ const LONG_LINE_THRESHOLD = 5000;
13
+
14
+ export function VirtualizedCodeView({ code, filename }: VirtualizedCodeViewProps) {
15
+ const parentRef = useRef<HTMLDivElement>(null);
16
+ const language = getLanguage(filename);
17
+ const lines = useMemo(() => code.split("\n"), [code]);
18
+
19
+ // Detect minified files: avg line length > 500 chars
20
+ const avgLineLength = code.length / Math.max(lines.length, 1);
21
+ const usePlainText = !language || avgLineLength > 500;
22
+
23
+ const virtualizer = useVirtualizer({
24
+ count: lines.length,
25
+ getScrollElement: () => parentRef.current,
26
+ estimateSize: () => 20,
27
+ overscan: 20,
28
+ });
29
+
30
+ // --- Plain text path: no Prism tokenization ---
31
+ if (usePlainText) {
32
+ return (
33
+ <div ref={parentRef} className="flex-1 min-h-0 overflow-auto" style={{ background: "#011627" }}>
34
+ <div
35
+ style={{ height: `${virtualizer.getTotalSize()}px`, width: "100%", position: "relative" }}
36
+ >
37
+ {virtualizer.getVirtualItems().map((vr) => (
38
+ <div
39
+ key={vr.key}
40
+ style={{
41
+ position: "absolute", top: 0, left: 0, width: "100%",
42
+ height: `${vr.size}px`, transform: `translateY(${vr.start}px)`,
43
+ }}
44
+ className="flex text-xs leading-5 font-mono"
45
+ >
46
+ <span className="inline-block w-10 text-right pr-4 text-gray-600 select-none shrink-0">
47
+ {vr.index + 1}
48
+ </span>
49
+ <span className="flex-1 text-gray-300 whitespace-pre">{lines[vr.index]}</span>
50
+ </div>
51
+ ))}
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ // --- Highlighted path: tokenize ONCE for the whole file, virtualize rendering ---
58
+ return (
59
+ <Highlight theme={themes.nightOwl} code={code} language={language}>
60
+ {({ tokens, getTokenProps }) => (
61
+ <div ref={parentRef} className="flex-1 min-h-0 overflow-auto" style={{ background: "#011627" }}>
62
+ <div
63
+ style={{ height: `${virtualizer.getTotalSize()}px`, width: "100%", position: "relative" }}
64
+ >
65
+ {virtualizer.getVirtualItems().map((vr) => {
66
+ const lineTokens = tokens[vr.index];
67
+ const lineText = lines[vr.index];
68
+ const isLongLine = (lineText?.length ?? 0) > LONG_LINE_THRESHOLD;
69
+
70
+ return (
71
+ <div
72
+ key={vr.key}
73
+ style={{
74
+ position: "absolute", top: 0, left: 0, width: "100%",
75
+ height: `${vr.size}px`, transform: `translateY(${vr.start}px)`,
76
+ }}
77
+ className="flex text-xs leading-5 font-mono"
78
+ >
79
+ <span className="inline-block w-10 text-right pr-4 text-gray-600 select-none shrink-0">
80
+ {vr.index + 1}
81
+ </span>
82
+ {isLongLine ? (
83
+ <span className="flex-1 text-gray-300 whitespace-pre">{lineText}</span>
84
+ ) : (
85
+ <span className="flex-1">
86
+ {lineTokens.map((token, key) => (
87
+ <span key={key} {...getTokenProps({ token })} />
88
+ ))}
89
+ </span>
90
+ )}
91
+ </div>
92
+ );
93
+ })}
94
+ </div>
95
+ </div>
96
+ )}
97
+ </Highlight>
98
+ );
99
+ }
@@ -0,0 +1,93 @@
1
+ import { useEffect, useCallback, useRef } from "react";
2
+ import { GitBranch as BranchIcon, Check } from "lucide-react";
3
+ import { useGitStore, type GitBranch } from "../../stores/use-git-store";
4
+ import { useExplorerStore } from "../../stores/use-explorer-store";
5
+
6
+ /**
7
+ * Branch selector popup — lists local/remote branches with checkout action.
8
+ * Self-contained: only depends on useGitStore + useExplorerStore.
9
+ */
10
+ interface GitBranchSelectorProps {
11
+ onClose: () => void;
12
+ }
13
+
14
+ export function GitBranchSelector({ onClose }: GitBranchSelectorProps) {
15
+ const branches = useGitStore((s) => s.branches);
16
+ const loadingBranches = useGitStore((s) => s.loadingBranches);
17
+ const loadingAction = useGitStore((s) => s.loadingAction);
18
+ const fetchBranches = useGitStore((s) => s.fetchBranches);
19
+ const checkout = useGitStore((s) => s.checkout);
20
+ const currentPath = useExplorerStore((s) => s.currentPath);
21
+ const ref = useRef<HTMLDivElement>(null);
22
+
23
+ useEffect(() => {
24
+ fetchBranches(currentPath);
25
+ }, [fetchBranches, currentPath]);
26
+
27
+ useEffect(() => {
28
+ const handleClick = (e: MouseEvent) => {
29
+ if (ref.current && !ref.current.contains(e.target as Node)) onClose();
30
+ };
31
+ const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
32
+ document.addEventListener("mousedown", handleClick);
33
+ document.addEventListener("keydown", handleKey);
34
+ return () => {
35
+ document.removeEventListener("mousedown", handleClick);
36
+ document.removeEventListener("keydown", handleKey);
37
+ };
38
+ }, [onClose]);
39
+
40
+ const handleCheckout = useCallback((branch: GitBranch) => {
41
+ if (branch.isCurrent) return;
42
+ const name = branch.isRemote ? branch.name.split("/").slice(1).join("/") : branch.name;
43
+ if (!name) return;
44
+ checkout(currentPath, name);
45
+ onClose();
46
+ }, [checkout, currentPath, onClose]);
47
+
48
+ const localBranches = branches.filter((b) => !b.isRemote);
49
+ const remoteBranches = branches.filter((b) => b.isRemote);
50
+
51
+ const renderBranch = (b: GitBranch) => (
52
+ <button
53
+ key={b.name}
54
+ className={`w-full text-left px-3 py-1.5 text-xs flex items-center gap-2 transition-colors ${
55
+ b.isCurrent ? "text-[var(--color-text-accent)]" : "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)]"
56
+ }`}
57
+ onClick={() => handleCheckout(b)}
58
+ disabled={loadingAction === "checkout"}
59
+ >
60
+ {b.isCurrent && <Check className="w-3 h-3 shrink-0" />}
61
+ {!b.isCurrent && <span className="w-3 shrink-0" />}
62
+ <BranchIcon className="w-3 h-3 shrink-0" />
63
+ <span className="truncate">{b.isRemote ? b.name.split("/").slice(1).join("/") : b.name}</span>
64
+ </button>
65
+ );
66
+
67
+ return (
68
+ <div
69
+ ref={ref}
70
+ className="fixed z-50 w-56 max-h-64 overflow-y-auto bg-[var(--color-bg-secondary)] border border-[var(--color-border-secondary)] rounded-md shadow-xl py-1"
71
+ style={{ /* positioned by parent via absolute */ }}
72
+ >
73
+ {loadingBranches ? (
74
+ <div className="text-[var(--color-text-placeholder)] text-xs text-center py-4">Loading branches...</div>
75
+ ) : (
76
+ <>
77
+ {localBranches.length > 0 && (
78
+ <>
79
+ <div className="px-3 py-1 text-[10px] uppercase tracking-wide text-[var(--color-text-placeholder)] font-semibold">Local</div>
80
+ {localBranches.map(renderBranch)}
81
+ </>
82
+ )}
83
+ {remoteBranches.length > 0 && (
84
+ <>
85
+ <div className="px-3 py-1 mt-1 text-[10px] uppercase tracking-wide text-[var(--color-text-placeholder)] font-semibold border-t border-[var(--color-border-primary)] pt-1">Remote</div>
86
+ {remoteBranches.map(renderBranch)}
87
+ </>
88
+ )}
89
+ </>
90
+ )}
91
+ </div>
92
+ );
93
+ }