@dyyz1993/create-agent 2.1.0 → 2.1.2

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 (349) hide show
  1. package/package.json +5 -4
  2. package/src/__tests__/cli.test.ts +2 -0
  3. package/src/__tests__/copy.test.ts +110 -122
  4. package/src/__tests__/create-args.test.ts +113 -0
  5. package/src/__tests__/templates.test.ts +29 -3
  6. package/src/__tests__/update.test.ts +129 -0
  7. package/src/cli.ts +30 -32
  8. package/src/commands/create.ts +83 -24
  9. package/src/commands/update.ts +174 -0
  10. package/src/commands/workspace.ts +26 -6
  11. package/src/lib/copy.ts +233 -93
  12. package/src/lib/templates.ts +12 -0
  13. package/templates/agent/.husky/commit-msg +4 -0
  14. package/templates/agent/.husky/pre-commit +11 -0
  15. package/templates/agent/.husky/pre-push +6 -0
  16. package/templates/agent/.prettierignore +6 -0
  17. package/templates/agent/.prettierrc +9 -0
  18. package/templates/agent/CHANGELOG.md +15 -0
  19. package/templates/agent/commitlint.config.js +8 -0
  20. package/templates/agent/package.json +19 -4
  21. package/templates/agent/src/gateway/__tests__/ws-handler-token.test.ts +53 -39
  22. package/templates/agent/src/mainview/App.tsx +14 -12
  23. package/templates/agent/src/mainview/__tests__/hooks/use-rpc-init.test.ts +194 -190
  24. package/templates/agent/src/mainview/components/bash/BashPanel.tsx +210 -189
  25. package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +45 -20
  26. package/templates/agent/src/mainview/components/common/ThemeToggle.tsx +41 -21
  27. package/templates/agent/src/mainview/components/debug/DebugPanel.tsx +181 -114
  28. package/templates/agent/src/mainview/components/debug/__tests__/DebugPanel.test.tsx +80 -80
  29. package/templates/agent/src/mainview/components/diff/__tests__/DiffViewerPanel.test.tsx +61 -58
  30. package/templates/agent/src/mainview/components/feed/FeedPanel.tsx +43 -41
  31. package/templates/agent/src/mainview/components/file-preview/FilePreviewOverlay.tsx +69 -50
  32. package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +31 -18
  33. package/templates/agent/src/mainview/components/git/GitPanel.tsx +731 -490
  34. package/templates/agent/src/mainview/components/git/__tests__/GitPanel.test.tsx +157 -139
  35. package/templates/agent/src/mainview/components/layout/AppLayout.tsx +210 -161
  36. package/templates/agent/src/mainview/components/layout/__tests__/AppLayout.test.tsx +130 -122
  37. package/templates/agent/src/mainview/components/rules/RulesPanel.tsx +119 -109
  38. package/templates/agent/src/mainview/components/search/SearchPanel.tsx +123 -117
  39. package/templates/agent/src/mainview/components/search/__tests__/SearchPanel.test.tsx +64 -44
  40. package/templates/agent/src/mainview/components/sidebar/__tests__/PinButton.test.tsx +38 -38
  41. package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +40 -38
  42. package/templates/agent/src/mainview/components/todo/__tests__/TodoPanel.test.tsx +72 -72
  43. package/templates/agent/src/mainview/lib/i18n/locales/en.json +175 -155
  44. package/templates/agent/src/mainview/lib/i18n/locales/zh.json +173 -155
  45. package/templates/agent/src/mainview/stores/use-app-store.ts +96 -86
  46. package/templates/agent/src/mainview/stores/use-explorer-store.ts +292 -275
  47. package/templates/agent/src/mainview/types/index.ts +23 -22
  48. package/templates/agent/src/mainview/utils/file-icon.tsx +17 -21
  49. package/templates/agent/src/shared/handlers/chat.ts +53 -53
  50. package/templates/agent/src/shared/handlers/debug.ts +13 -3
  51. package/templates/browser-agent/.env.example +19 -0
  52. package/templates/browser-agent/.husky/commit-msg +4 -0
  53. package/templates/browser-agent/.husky/pre-commit +11 -0
  54. package/templates/browser-agent/.husky/pre-push +6 -0
  55. package/templates/browser-agent/.prettierignore +6 -0
  56. package/templates/browser-agent/.prettierrc +9 -0
  57. package/templates/browser-agent/AGENTS.md +396 -0
  58. package/templates/browser-agent/CHANGELOG.md +15 -0
  59. package/templates/browser-agent/LICENSE +21 -0
  60. package/templates/browser-agent/README.md +103 -0
  61. package/templates/browser-agent/commitlint.config.js +8 -0
  62. package/templates/browser-agent/electrobun.config.ts +27 -0
  63. package/templates/browser-agent/electron/main.js +46 -0
  64. package/templates/browser-agent/electron/preload.js +5 -0
  65. package/templates/browser-agent/electron-builder.json +40 -0
  66. package/templates/browser-agent/eslint.config.mjs +79 -0
  67. package/templates/browser-agent/llms.txt +24 -0
  68. package/templates/browser-agent/package.json +140 -0
  69. package/templates/browser-agent/postcss.config.js +6 -0
  70. package/templates/browser-agent/scripts/dev.ts +138 -0
  71. package/templates/browser-agent/src/__tests__/hybrid-mode.test.ts +126 -0
  72. package/templates/browser-agent/src/__tests__/server-config-security.test.ts +55 -0
  73. package/templates/browser-agent/src/__tests__/server-config.test.ts +59 -0
  74. package/templates/browser-agent/src/bun/index.ts +130 -0
  75. package/templates/browser-agent/src/bun/three.d.ts +1 -0
  76. package/templates/browser-agent/src/gateway/http-routes.ts +1 -0
  77. package/templates/browser-agent/src/gateway/ipc-transport.ts +68 -0
  78. package/templates/browser-agent/src/gateway/sse-transport.ts +221 -0
  79. package/templates/browser-agent/src/mainview/App.tsx +45 -0
  80. package/templates/browser-agent/src/mainview/__tests__/hooks/use-rpc-init.test.ts +202 -0
  81. package/templates/browser-agent/src/mainview/__tests__/hooks/use-sidebar-resize.test.ts +127 -0
  82. package/templates/browser-agent/src/mainview/__tests__/i18n/i18n.test.ts +49 -0
  83. package/templates/browser-agent/src/mainview/__tests__/setup.ts +40 -0
  84. package/templates/browser-agent/src/mainview/__tests__/stores/use-chat-store.test.ts +73 -0
  85. package/templates/browser-agent/src/mainview/__tests__/stores/use-sidebar-store.test.ts +61 -0
  86. package/templates/browser-agent/src/mainview/__tests__/theme-variables.test.ts +36 -0
  87. package/templates/browser-agent/src/mainview/components/assets/AssetsPanel.tsx +139 -0
  88. package/templates/browser-agent/src/mainview/components/chat/ChatPanel.tsx +219 -0
  89. package/templates/browser-agent/src/mainview/components/chat/CommandBar.tsx +184 -0
  90. package/templates/browser-agent/src/mainview/components/chat/MessageBubble.tsx +249 -0
  91. package/templates/browser-agent/src/mainview/components/chat/ToolPicker.tsx +115 -0
  92. package/templates/browser-agent/src/mainview/components/common/ErrorBoundary.tsx +1 -0
  93. package/templates/browser-agent/src/mainview/components/common/LanguageSwitcher.tsx +15 -0
  94. package/templates/browser-agent/src/mainview/components/common/ThemeToggle.tsx +45 -0
  95. package/templates/browser-agent/src/mainview/components/common/__tests__/ErrorBoundary.test.tsx +74 -0
  96. package/templates/browser-agent/src/mainview/components/common/__tests__/LanguageSwitcher.test.tsx +44 -0
  97. package/templates/browser-agent/src/mainview/components/common/__tests__/ThemeToggle.test.tsx +41 -0
  98. package/templates/browser-agent/src/mainview/components/dev/NetworkPanel.tsx +155 -0
  99. package/templates/browser-agent/src/mainview/components/layout/AppLayout.tsx +311 -0
  100. package/templates/browser-agent/src/mainview/components/onboarding/SetupWizard.tsx +310 -0
  101. package/templates/browser-agent/src/mainview/components/process/ProcessPanel.tsx +329 -0
  102. package/templates/browser-agent/src/mainview/components/sidebar/SessionSidebar.tsx +122 -0
  103. package/templates/browser-agent/src/mainview/components/sidebar/SkillSidebar.tsx +115 -0
  104. package/templates/browser-agent/src/mainview/components/topbar/TopBar.tsx +350 -0
  105. package/templates/browser-agent/src/mainview/global.d.ts +13 -0
  106. package/templates/browser-agent/src/mainview/hooks/__tests__/use-breakpoint.test.ts +151 -0
  107. package/templates/browser-agent/src/mainview/hooks/use-agent-chat.ts +157 -0
  108. package/templates/browser-agent/src/mainview/hooks/use-breakpoint.ts +68 -0
  109. package/templates/browser-agent/src/mainview/hooks/use-rpc-init.ts +71 -0
  110. package/templates/browser-agent/src/mainview/hooks/use-sidebar-resize.ts +40 -0
  111. package/templates/browser-agent/src/mainview/index.css +71 -0
  112. package/templates/browser-agent/src/mainview/index.html +12 -0
  113. package/templates/browser-agent/src/mainview/lib/api-client.ts +397 -0
  114. package/templates/browser-agent/src/mainview/lib/i18n/index.ts +30 -0
  115. package/templates/browser-agent/src/mainview/lib/i18n/locales/en.json +130 -0
  116. package/templates/browser-agent/src/mainview/lib/i18n/locales/zh.json +128 -0
  117. package/templates/browser-agent/src/mainview/lib/network-bus.ts +92 -0
  118. package/templates/browser-agent/src/mainview/lib/rpc-cache.ts +94 -0
  119. package/templates/browser-agent/src/mainview/main.tsx +18 -0
  120. package/templates/browser-agent/src/mainview/stores/__tests__/use-connection-store.test.ts +26 -0
  121. package/templates/browser-agent/src/mainview/stores/__tests__/use-locale-store.test.ts +32 -0
  122. package/templates/browser-agent/src/mainview/stores/__tests__/use-log-store.test.ts +28 -0
  123. package/templates/browser-agent/src/mainview/stores/__tests__/use-theme-store.test.ts +59 -0
  124. package/templates/browser-agent/src/mainview/stores/message-batcher.ts +25 -0
  125. package/templates/browser-agent/src/mainview/stores/use-asset-store.ts +65 -0
  126. package/templates/browser-agent/src/mainview/stores/use-chat-store.ts +200 -0
  127. package/templates/browser-agent/src/mainview/stores/use-connection-store.ts +173 -0
  128. package/templates/browser-agent/src/mainview/stores/use-locale-store.ts +27 -0
  129. package/templates/browser-agent/src/mainview/stores/use-log-store.ts +16 -0
  130. package/templates/browser-agent/src/mainview/stores/use-network-panel-store.ts +18 -0
  131. package/templates/browser-agent/src/mainview/stores/use-record-store.ts +147 -0
  132. package/templates/browser-agent/src/mainview/stores/use-session-store.ts +151 -0
  133. package/templates/browser-agent/src/mainview/stores/use-sidebar-store.ts +161 -0
  134. package/templates/browser-agent/src/mainview/stores/use-theme-store.ts +46 -0
  135. package/templates/browser-agent/src/mainview/stores/use-view-store.ts +20 -0
  136. package/templates/browser-agent/src/mainview/types/index.ts +6 -0
  137. package/templates/browser-agent/src/server-config.ts +45 -0
  138. package/templates/browser-agent/src/server.ts +78 -0
  139. package/templates/browser-agent/src/shared/handlers/__tests__/chat-concurrency.test.ts +127 -0
  140. package/templates/browser-agent/src/shared/handlers/__tests__/chat-handler.test.ts +77 -0
  141. package/templates/browser-agent/src/shared/handlers/__tests__/file-handler.test.ts +100 -0
  142. package/templates/browser-agent/src/shared/handlers/__tests__/system-handler.test.ts +55 -0
  143. package/templates/browser-agent/src/shared/handlers/__tests__/timer-handler.test.ts +77 -0
  144. package/templates/browser-agent/src/shared/handlers/browser.ts +596 -0
  145. package/templates/browser-agent/src/shared/handlers/chat.ts +213 -0
  146. package/templates/browser-agent/src/shared/handlers/file.ts +99 -0
  147. package/templates/browser-agent/src/shared/handlers/index.ts +7 -0
  148. package/templates/browser-agent/src/shared/handlers/session.ts +167 -0
  149. package/templates/browser-agent/src/shared/handlers/system.ts +27 -0
  150. package/templates/browser-agent/src/shared/handlers/timer.ts +34 -0
  151. package/templates/browser-agent/src/shared/http-routes.ts +437 -0
  152. package/templates/browser-agent/src/shared/lib/__tests__/logger.test.ts +92 -0
  153. package/templates/browser-agent/src/shared/lib/__tests__/path-security.test.ts +77 -0
  154. package/templates/browser-agent/src/shared/lib/__tests__/web-server.test.ts +203 -0
  155. package/templates/browser-agent/src/shared/lib/agent.ts +384 -0
  156. package/templates/browser-agent/src/shared/lib/cdp.ts +448 -0
  157. package/templates/browser-agent/src/shared/lib/generate.ts +111 -0
  158. package/templates/browser-agent/src/shared/lib/logger.ts +152 -0
  159. package/templates/browser-agent/src/shared/lib/mock-stream.ts +147 -0
  160. package/templates/browser-agent/src/shared/lib/path-security.ts +38 -0
  161. package/templates/browser-agent/src/shared/lib/port-registry.ts +103 -0
  162. package/templates/browser-agent/src/shared/lib/web-server.ts +127 -0
  163. package/templates/browser-agent/src/shared/lib/xhs-extract.ts +71 -0
  164. package/templates/browser-agent/src/shared/modules/browser.ts +86 -0
  165. package/templates/browser-agent/src/shared/modules/chat.ts +20 -0
  166. package/templates/browser-agent/src/shared/modules/file.ts +40 -0
  167. package/templates/browser-agent/src/shared/modules/session.ts +55 -0
  168. package/templates/browser-agent/src/shared/modules/system.ts +8 -0
  169. package/templates/browser-agent/src/shared/modules/timer.ts +11 -0
  170. package/templates/browser-agent/src/shared/register-all-handlers.ts +36 -0
  171. package/templates/browser-agent/src/shared/rpc-schema.ts +34 -0
  172. package/templates/browser-agent/tailwind.config.js +15 -0
  173. package/templates/browser-agent/tsconfig.ipc.json +14 -0
  174. package/templates/browser-agent/tsconfig.json +36 -0
  175. package/templates/browser-agent/vite.config.ts +3 -0
  176. package/templates/browser-agent/vitest.config.ts +3 -0
  177. package/templates/chat/.husky/commit-msg +4 -0
  178. package/templates/chat/.husky/pre-commit +11 -0
  179. package/templates/chat/.husky/pre-push +6 -0
  180. package/templates/chat/CHANGELOG.md +15 -0
  181. package/templates/chat/commitlint.config.js +8 -0
  182. package/templates/chat/package.json +19 -4
  183. package/templates/chat/src/mainview/__tests__/hooks/use-input-history.test.ts +125 -0
  184. package/templates/chat/src/mainview/__tests__/setup.ts +32 -29
  185. package/templates/chat/src/mainview/components/chat/ChatPanel.tsx +80 -55
  186. package/templates/chat/src/mainview/components/common/ThemeToggle.tsx +40 -20
  187. package/templates/chat/src/mainview/lib/i18n/locales/en.json +175 -155
  188. package/templates/chat/src/mainview/lib/i18n/locales/zh.json +173 -155
  189. package/templates/chat/src/shared/handlers/chat.ts +17 -19
  190. package/templates/chat/src/shared/handlers/debug.ts +12 -2
  191. package/templates/cowork/.env.example +8 -0
  192. package/templates/cowork/.prettierignore +6 -0
  193. package/templates/cowork/.prettierrc +9 -0
  194. package/templates/cowork/AGENTS.md +236 -0
  195. package/templates/cowork/CHANGELOG.md +15 -0
  196. package/templates/cowork/LICENSE +21 -0
  197. package/templates/cowork/README.md +103 -0
  198. package/templates/cowork/commitlint.config.js +8 -0
  199. package/templates/cowork/docs/CODE-VIEW-PLAN.md +637 -0
  200. package/templates/cowork/docs/CODE-VIEW-V2.md +196 -0
  201. package/templates/cowork/docs/CODE-VIEW-V4.md +150 -0
  202. package/templates/cowork/electrobun.config.ts +27 -0
  203. package/templates/cowork/eslint.config.mjs +79 -0
  204. package/templates/cowork/llms.txt +24 -0
  205. package/templates/cowork/package.json +87 -0
  206. package/templates/cowork/postcss.config.js +6 -0
  207. package/templates/cowork/scripts/dev.ts +138 -0
  208. package/templates/cowork/src/__tests__/hybrid-mode.test.ts +126 -0
  209. package/templates/cowork/src/__tests__/server-config-security.test.ts +55 -0
  210. package/templates/cowork/src/__tests__/server-config.test.ts +59 -0
  211. package/templates/cowork/src/bun/index.ts +130 -0
  212. package/templates/cowork/src/bun/three.d.ts +1 -0
  213. package/templates/cowork/src/gateway/http-routes.ts +1 -0
  214. package/templates/cowork/src/gateway/ipc-transport.ts +68 -0
  215. package/templates/cowork/src/gateway/sse-transport.ts +231 -0
  216. package/templates/cowork/src/mainview/App.tsx +45 -0
  217. package/templates/cowork/src/mainview/__tests__/hooks/use-rpc-init.test.ts +202 -0
  218. package/templates/cowork/src/mainview/__tests__/hooks/use-sidebar-resize.test.ts +127 -0
  219. package/templates/cowork/src/mainview/__tests__/i18n/i18n.test.ts +49 -0
  220. package/templates/cowork/src/mainview/__tests__/setup.ts +40 -0
  221. package/templates/cowork/src/mainview/__tests__/stores/use-chat-store.test.ts +73 -0
  222. package/templates/cowork/src/mainview/__tests__/stores/use-sidebar-store.test.ts +61 -0
  223. package/templates/cowork/src/mainview/__tests__/theme-variables.test.ts +36 -0
  224. package/templates/cowork/src/mainview/components/chat/TaskChat.tsx +311 -0
  225. package/templates/cowork/src/mainview/components/chat/__tests__/localhost-links.test.ts +76 -0
  226. package/templates/cowork/src/mainview/components/common/ErrorBoundary.tsx +1 -0
  227. package/templates/cowork/src/mainview/components/common/LanguageSwitcher.tsx +15 -0
  228. package/templates/cowork/src/mainview/components/common/ThemeToggle.tsx +45 -0
  229. package/templates/cowork/src/mainview/components/common/__tests__/ErrorBoundary.test.tsx +74 -0
  230. package/templates/cowork/src/mainview/components/common/__tests__/LanguageSwitcher.test.tsx +44 -0
  231. package/templates/cowork/src/mainview/components/common/__tests__/ThemeToggle.test.tsx +41 -0
  232. package/templates/cowork/src/mainview/components/dev/NetworkPanel.tsx +155 -0
  233. package/templates/cowork/src/mainview/components/layout/AppLayout.tsx +216 -0
  234. package/templates/cowork/src/mainview/components/right/ArtifactsPanel.tsx +54 -0
  235. package/templates/cowork/src/mainview/components/right/ContextPanel.tsx +133 -0
  236. package/templates/cowork/src/mainview/components/right/ElementPicker.tsx +206 -0
  237. package/templates/cowork/src/mainview/components/right/PreviewBlock.tsx +155 -0
  238. package/templates/cowork/src/mainview/components/right/ProgressPanel.tsx +85 -0
  239. package/templates/cowork/src/mainview/components/sidebar/TaskSidebar.tsx +211 -0
  240. package/templates/cowork/src/mainview/components/topbar/TopBar.tsx +86 -0
  241. package/templates/cowork/src/mainview/global.d.ts +13 -0
  242. package/templates/cowork/src/mainview/hooks/__tests__/use-breakpoint.test.ts +123 -0
  243. package/templates/cowork/src/mainview/hooks/use-breakpoint.ts +53 -0
  244. package/templates/cowork/src/mainview/hooks/use-rpc-init.ts +26 -0
  245. package/templates/cowork/src/mainview/hooks/use-sidebar-resize.ts +40 -0
  246. package/templates/cowork/src/mainview/index.css +89 -0
  247. package/templates/cowork/src/mainview/index.html +12 -0
  248. package/templates/cowork/src/mainview/lib/api-client.ts +404 -0
  249. package/templates/cowork/src/mainview/lib/i18n/index.ts +30 -0
  250. package/templates/cowork/src/mainview/lib/i18n/locales/en.json +130 -0
  251. package/templates/cowork/src/mainview/lib/i18n/locales/zh.json +128 -0
  252. package/templates/cowork/src/mainview/lib/network-bus.ts +92 -0
  253. package/templates/cowork/src/mainview/lib/rpc-cache.ts +94 -0
  254. package/templates/cowork/src/mainview/main.tsx +18 -0
  255. package/templates/cowork/src/mainview/stores/__tests__/use-connection-store.test.ts +26 -0
  256. package/templates/cowork/src/mainview/stores/__tests__/use-locale-store.test.ts +32 -0
  257. package/templates/cowork/src/mainview/stores/__tests__/use-log-store.test.ts +28 -0
  258. package/templates/cowork/src/mainview/stores/__tests__/use-preview-store.test.ts +193 -0
  259. package/templates/cowork/src/mainview/stores/__tests__/use-sidebar-store.test.ts +81 -0
  260. package/templates/cowork/src/mainview/stores/__tests__/use-theme-store.test.ts +59 -0
  261. package/templates/cowork/src/mainview/stores/message-batcher.ts +25 -0
  262. package/templates/cowork/src/mainview/stores/use-chat-store.ts +198 -0
  263. package/templates/cowork/src/mainview/stores/use-connection-store.ts +168 -0
  264. package/templates/cowork/src/mainview/stores/use-context-store.ts +68 -0
  265. package/templates/cowork/src/mainview/stores/use-locale-store.ts +27 -0
  266. package/templates/cowork/src/mainview/stores/use-log-store.ts +16 -0
  267. package/templates/cowork/src/mainview/stores/use-network-panel-store.ts +18 -0
  268. package/templates/cowork/src/mainview/stores/use-output-store.ts +47 -0
  269. package/templates/cowork/src/mainview/stores/use-preview-store.ts +97 -0
  270. package/templates/cowork/src/mainview/stores/use-sidebar-store.ts +272 -0
  271. package/templates/cowork/src/mainview/stores/use-task-store.ts +86 -0
  272. package/templates/cowork/src/mainview/stores/use-theme-store.ts +46 -0
  273. package/templates/cowork/src/mainview/stores/use-view-store.ts +29 -0
  274. package/templates/cowork/src/mainview/types/index.ts +6 -0
  275. package/templates/cowork/src/server-config.ts +44 -0
  276. package/templates/cowork/src/server.ts +78 -0
  277. package/templates/cowork/src/shared/handlers/__tests__/chat-concurrency.test.ts +127 -0
  278. package/templates/cowork/src/shared/handlers/__tests__/chat-handler.test.ts +77 -0
  279. package/templates/cowork/src/shared/handlers/__tests__/file-handler.test.ts +100 -0
  280. package/templates/cowork/src/shared/handlers/__tests__/preview-handler.test.ts +172 -0
  281. package/templates/cowork/src/shared/handlers/__tests__/system-handler.test.ts +55 -0
  282. package/templates/cowork/src/shared/handlers/__tests__/timer-handler.test.ts +77 -0
  283. package/templates/cowork/src/shared/handlers/chat.ts +213 -0
  284. package/templates/cowork/src/shared/handlers/context.ts +72 -0
  285. package/templates/cowork/src/shared/handlers/file.ts +99 -0
  286. package/templates/cowork/src/shared/handlers/index.ts +9 -0
  287. package/templates/cowork/src/shared/handlers/output.ts +59 -0
  288. package/templates/cowork/src/shared/handlers/preview.ts +81 -0
  289. package/templates/cowork/src/shared/handlers/system.ts +27 -0
  290. package/templates/cowork/src/shared/handlers/task.ts +101 -0
  291. package/templates/cowork/src/shared/handlers/timer.ts +34 -0
  292. package/templates/cowork/src/shared/http-routes.ts +444 -0
  293. package/templates/cowork/src/shared/lib/__tests__/logger.test.ts +92 -0
  294. package/templates/cowork/src/shared/lib/__tests__/path-security.test.ts +77 -0
  295. package/templates/cowork/src/shared/lib/__tests__/web-server.test.ts +203 -0
  296. package/templates/cowork/src/shared/lib/logger.ts +152 -0
  297. package/templates/cowork/src/shared/lib/path-security.ts +38 -0
  298. package/templates/cowork/src/shared/lib/port-registry.ts +103 -0
  299. package/templates/cowork/src/shared/lib/web-server.ts +127 -0
  300. package/templates/cowork/src/shared/modules/chat.ts +20 -0
  301. package/templates/cowork/src/shared/modules/context.ts +32 -0
  302. package/templates/cowork/src/shared/modules/file.ts +40 -0
  303. package/templates/cowork/src/shared/modules/output.ts +20 -0
  304. package/templates/cowork/src/shared/modules/preview.ts +44 -0
  305. package/templates/cowork/src/shared/modules/system.ts +8 -0
  306. package/templates/cowork/src/shared/modules/task.ts +31 -0
  307. package/templates/cowork/src/shared/modules/timer.ts +11 -0
  308. package/templates/cowork/src/shared/register-all-handlers.ts +36 -0
  309. package/templates/cowork/src/shared/rpc-schema.ts +38 -0
  310. package/templates/cowork/tailwind.config.js +15 -0
  311. package/templates/cowork/test-upload.txt +0 -0
  312. package/templates/cowork/tsconfig.ipc.json +14 -0
  313. package/templates/cowork/tsconfig.json +36 -0
  314. package/templates/cowork/vite.config.ts +3 -0
  315. package/templates/cowork/vitest.config.ts +3 -0
  316. package/templates/general/.husky/commit-msg +4 -0
  317. package/templates/general/.husky/pre-commit +11 -0
  318. package/templates/general/.husky/pre-push +6 -0
  319. package/templates/general/CHANGELOG.md +15 -0
  320. package/templates/general/commitlint.config.js +8 -0
  321. package/templates/general/package.json +19 -4
  322. package/templates/general/src/mainview/__tests__/hooks/use-input-history.test.ts +125 -0
  323. package/templates/general/src/mainview/__tests__/setup.ts +32 -29
  324. package/templates/general/src/mainview/components/chat/ChatPanel.tsx +80 -55
  325. package/templates/general/src/mainview/components/common/ThemeToggle.tsx +40 -20
  326. package/templates/general/src/mainview/components/debug/DebugPanel.tsx +170 -156
  327. package/templates/general/src/mainview/components/debug/__tests__/DebugPanel.test.tsx +169 -0
  328. package/templates/general/src/mainview/components/explorer/ConfirmDialog.tsx +42 -42
  329. package/templates/general/src/mainview/components/explorer/ContextMenu.tsx +72 -67
  330. package/templates/general/src/mainview/components/feed/FeedPanel.tsx +7 -5
  331. package/templates/general/src/mainview/components/file-preview/FilePreviewOverlay.tsx +64 -45
  332. package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +24 -7
  333. package/templates/general/src/mainview/components/git/GitPanel.tsx +700 -473
  334. package/templates/general/src/mainview/components/layout/AppLayout.tsx +123 -114
  335. package/templates/general/src/mainview/components/search/SearchPanel.tsx +15 -19
  336. package/templates/general/src/mainview/lib/i18n/locales/en.json +175 -155
  337. package/templates/general/src/mainview/lib/i18n/locales/zh.json +173 -155
  338. package/templates/general/src/mainview/stores/__tests__/use-explorer-store.test.ts +259 -0
  339. package/templates/general/src/mainview/stores/__tests__/use-feed-store.test.ts +160 -0
  340. package/templates/general/src/mainview/stores/__tests__/use-git-store.test.ts +313 -0
  341. package/templates/general/src/mainview/stores/__tests__/use-notification-store.test.ts +115 -0
  342. package/templates/general/src/mainview/stores/__tests__/use-sidebar-store.test.ts +120 -0
  343. package/templates/general/src/mainview/stores/use-explorer-store.ts +277 -260
  344. package/templates/general/src/mainview/types/index.ts +21 -20
  345. package/templates/general/src/shared/handlers/chat.ts +1 -1
  346. package/templates/general/src/shared/handlers/debug.ts +12 -2
  347. package/templates/shared/components/ErrorBoundary.tsx +0 -1
  348. package/templates/shared/vite-base.config.ts +8 -7
  349. /package/templates/{shared → browser-agent}/test-upload.txt +0 -0
@@ -1,9 +1,21 @@
1
1
  import { memo, useEffect, useCallback, useState, useRef } from "react";
2
2
  import {
3
- GitBranch, RefreshCw, FileQuestion, Plus, Minus, Pencil,
4
- ChevronRight, ChevronDown, Eye, FileText, Copy,
5
- Upload, Download, ChevronUp, ChevronDown as BranchChevron,
6
- FolderTree,
3
+ GitBranch,
4
+ RefreshCw,
5
+ FileQuestion,
6
+ Plus,
7
+ Minus,
8
+ Pencil,
9
+ ChevronRight,
10
+ ChevronDown,
11
+ Eye,
12
+ FileText,
13
+ Copy,
14
+ Upload,
15
+ Download,
16
+ ChevronUp,
17
+ ChevronDown as BranchChevron,
18
+ FolderTree,
7
19
  } from "lucide-react";
8
20
  import { useTranslation } from "react-i18next";
9
21
  import { useGitStore, type GitFileChange, type GitCommit } from "../../stores/use-git-store";
@@ -16,518 +28,733 @@ import { PinButton } from "../sidebar/PinButton";
16
28
  /* ── Helpers ────────────────────────────────────────────── */
17
29
 
18
30
  function statusIcon(status: GitFileChange["status"]) {
19
- switch (status) {
20
- case "added": return <Plus className="w-3 h-3 text-[var(--color-text-success)]" />;
21
- case "deleted": return <Minus className="w-3 h-3 text-[var(--color-text-error)]" />;
22
- case "modified": return <Pencil className="w-3 h-3 text-yellow-400" />;
23
- default: return <FileQuestion className="w-3 h-3 text-[var(--color-text-tertiary)]" />;
24
- }
31
+ switch (status) {
32
+ case "added":
33
+ return <Plus className="w-3 h-3 text-[var(--color-text-success)]" />;
34
+ case "deleted":
35
+ return <Minus className="w-3 h-3 text-[var(--color-text-error)]" />;
36
+ case "modified":
37
+ return <Pencil className="w-3 h-3 text-[var(--color-text-warning)]" />;
38
+ default:
39
+ return <FileQuestion className="w-3 h-3 text-[var(--color-text-tertiary)]" />;
40
+ }
25
41
  }
26
42
 
27
43
  function statusLabel(status: GitFileChange["status"]) {
28
- switch (status) {
29
- case "added": return "A";
30
- case "deleted": return "D";
31
- case "modified": return "M";
32
- case "renamed": return "R";
33
- case "copied": return "C";
34
- }
44
+ switch (status) {
45
+ case "added":
46
+ return "A";
47
+ case "deleted":
48
+ return "D";
49
+ case "modified":
50
+ return "M";
51
+ case "renamed":
52
+ return "R";
53
+ case "copied":
54
+ return "C";
55
+ }
35
56
  }
36
57
 
37
- function relativeTime(dateStr: string, t: (key: string, options?: Record<string, unknown>) => string): string {
38
- const d = new Date(dateStr);
39
- const now = new Date();
40
- const diff = now.getTime() - d.getTime();
41
- const mins = Math.floor(diff / 60000);
42
- if (mins < 1) return t("git.justNow");
43
- if (mins < 60) return t("git.minutesAgo", { count: mins });
44
- const hours = Math.floor(mins / 60);
45
- if (hours < 24) return t("git.hoursAgo", { count: hours });
46
- const days = Math.floor(hours / 24);
47
- if (days < 30) return t("git.daysAgo", { count: days });
48
- return d.toLocaleDateString();
58
+ function relativeTime(
59
+ dateStr: string,
60
+ t: (key: string, options?: Record<string, unknown>) => string
61
+ ): string {
62
+ const d = new Date(dateStr);
63
+ const now = new Date();
64
+ const diff = now.getTime() - d.getTime();
65
+ const mins = Math.floor(diff / 60000);
66
+ if (mins < 1) return t("git.justNow");
67
+ if (mins < 60) return t("git.minutesAgo", { count: mins });
68
+ const hours = Math.floor(mins / 60);
69
+ if (hours < 24) return t("git.hoursAgo", { count: hours });
70
+ const days = Math.floor(hours / 24);
71
+ if (days < 30) return t("git.daysAgo", { count: days });
72
+ return d.toLocaleDateString();
49
73
  }
50
74
 
51
75
  /* ── Sub-components ─────────────────────────────────────── */
52
76
 
53
77
  /* Working file item (staged / changed) with stage/unstage button */
54
78
  interface FileItemProps {
55
- path: string;
56
- status: GitFileChange["status"];
57
- isSelected: boolean;
58
- isStaged?: boolean;
59
- onClick: (filePath: string, staged?: boolean) => void;
60
- onContextMenu: (e: React.MouseEvent, filePath: string, isStaged?: boolean) => void;
61
- onStageToggle: (filePath: string, isStaged?: boolean) => void;
79
+ path: string;
80
+ status: GitFileChange["status"];
81
+ isSelected: boolean;
82
+ isStaged?: boolean;
83
+ onClick: (filePath: string, staged?: boolean) => void;
84
+ onContextMenu: (e: React.MouseEvent, filePath: string, isStaged?: boolean) => void;
85
+ onStageToggle: (filePath: string, isStaged?: boolean) => void;
62
86
  }
63
87
 
64
88
  const FileItem = memo(function FileItem({
65
- path, status, isSelected, isStaged, onClick, onContextMenu, onStageToggle,
89
+ path,
90
+ status,
91
+ isSelected,
92
+ isStaged,
93
+ onClick,
94
+ onContextMenu,
95
+ onStageToggle,
66
96
  }: FileItemProps) {
67
- return (
68
- <div
69
- className={`group flex items-center gap-1.5 px-2 py-0.5 text-xs rounded cursor-pointer transition-colors ${
70
- isSelected ? "bg-[var(--color-accent)]/30 text-[var(--color-text-primary)]" : "hover:bg-[var(--color-bg-hover)] text-[var(--color-text-secondary)]"
71
- }`}
72
- onClick={() => onClick(path, isStaged)}
73
- onContextMenu={(e) => { e.preventDefault(); onContextMenu(e, path, isStaged); }}
74
- >
75
- {statusIcon(status)}
76
- <span className="truncate flex-1">{path.split("/").pop()}</span>
77
- <span className="text-[var(--color-text-placeholder)] text-[10px]">{statusLabel(status)}</span>
78
- <button
79
- className={`opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded hover:bg-[var(--color-bg-active)] ${
80
- isStaged ? "text-orange-400 hover:text-orange-300" : "text-[var(--color-text-success)] hover:text-green-300"
81
- }`}
82
- onClick={(e) => { e.stopPropagation(); onStageToggle(path, isStaged); }}
83
- title={isStaged ? "Unstage" : "Stage"}
84
- >
85
- {isStaged ? <ChevronUp className="w-3 h-3" /> : <Plus className="w-3 h-3" />}
86
- </button>
87
- </div>
88
- );
97
+ return (
98
+ <div
99
+ className={`group flex items-center gap-1.5 px-2 py-0.5 text-xs rounded cursor-pointer transition-colors ${
100
+ isSelected
101
+ ? "bg-[var(--color-accent)]/30 text-[var(--color-text-primary)]"
102
+ : "hover:bg-[var(--color-bg-hover)] text-[var(--color-text-secondary)]"
103
+ }`}
104
+ onClick={() => onClick(path, isStaged)}
105
+ onContextMenu={(e) => {
106
+ e.preventDefault();
107
+ onContextMenu(e, path, isStaged);
108
+ }}
109
+ >
110
+ {statusIcon(status)}
111
+ <span className="truncate flex-1">{path.split("/").pop()}</span>
112
+ <span className="text-[var(--color-text-placeholder)] text-[10px]">
113
+ {statusLabel(status)}
114
+ </span>
115
+ <button
116
+ className={`opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded hover:bg-[var(--color-bg-active)] ${
117
+ isStaged
118
+ ? "text-[var(--color-text-warning)] hover:text-[var(--color-text-warning)]"
119
+ : "text-[var(--color-text-success)] hover:text-[var(--color-text-success)]"
120
+ }`}
121
+ onClick={(e) => {
122
+ e.stopPropagation();
123
+ onStageToggle(path, isStaged);
124
+ }}
125
+ title={isStaged ? "Unstage" : "Stage"}
126
+ >
127
+ {isStaged ? <ChevronUp className="w-3 h-3" /> : <Plus className="w-3 h-3" />}
128
+ </button>
129
+ </div>
130
+ );
89
131
  });
90
132
 
91
133
  /* Untracked item with stage button */
92
134
  interface UntrackedItemProps {
93
- path: string;
94
- isSelected: boolean;
95
- onClick: (filePath: string) => void;
96
- onContextMenu: (e: React.MouseEvent, filePath: string) => void;
97
- onStage: (filePath: string) => void;
135
+ path: string;
136
+ isSelected: boolean;
137
+ onClick: (filePath: string) => void;
138
+ onContextMenu: (e: React.MouseEvent, filePath: string) => void;
139
+ onStage: (filePath: string) => void;
98
140
  }
99
141
 
100
142
  const UntrackedItem = memo(function UntrackedItem({
101
- path, isSelected, onClick, onContextMenu, onStage,
143
+ path,
144
+ isSelected,
145
+ onClick,
146
+ onContextMenu,
147
+ onStage,
102
148
  }: UntrackedItemProps) {
103
- return (
104
- <div
105
- className={`group flex items-center gap-1.5 px-2 py-0.5 text-xs rounded cursor-pointer transition-colors ${
106
- isSelected ? "bg-[var(--color-accent)]/30 text-[var(--color-text-primary)]" : "hover:bg-[var(--color-bg-hover)] text-[var(--color-text-tertiary)]"
107
- }`}
108
- onClick={() => onClick(path)}
109
- onContextMenu={(e) => { e.preventDefault(); onContextMenu(e, path); }}
110
- >
111
- <FileQuestion className="w-3 h-3 text-[var(--color-text-placeholder)]" />
112
- <span className="truncate flex-1">{path.split("/").pop()}</span>
113
- <span className="text-gray-600 text-[10px]">U</span>
114
- <button
115
- className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded text-[var(--color-text-success)] hover:text-green-300 hover:bg-[var(--color-bg-active)]"
116
- onClick={(e) => { e.stopPropagation(); onStage(path); }}
117
- title="Stage"
118
- >
119
- <Plus className="w-3 h-3" />
120
- </button>
121
- </div>
122
- );
149
+ return (
150
+ <div
151
+ className={`group flex items-center gap-1.5 px-2 py-0.5 text-xs rounded cursor-pointer transition-colors ${
152
+ isSelected
153
+ ? "bg-[var(--color-accent)]/30 text-[var(--color-text-primary)]"
154
+ : "hover:bg-[var(--color-bg-hover)] text-[var(--color-text-tertiary)]"
155
+ }`}
156
+ onClick={() => onClick(path)}
157
+ onContextMenu={(e) => {
158
+ e.preventDefault();
159
+ onContextMenu(e, path);
160
+ }}
161
+ >
162
+ <FileQuestion className="w-3 h-3 text-[var(--color-text-placeholder)]" />
163
+ <span className="truncate flex-1">{path.split("/").pop()}</span>
164
+ <span className="text-[var(--color-text-tertiary)] text-[10px]">U</span>
165
+ <button
166
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded text-[var(--color-text-success)] hover:text-[var(--color-text-success)] hover:bg-[var(--color-bg-active)]"
167
+ onClick={(e) => {
168
+ e.stopPropagation();
169
+ onStage(path);
170
+ }}
171
+ title="Stage"
172
+ >
173
+ <Plus className="w-3 h-3" />
174
+ </button>
175
+ </div>
176
+ );
123
177
  });
124
178
 
125
179
  /* Commit file item (inside expanded commit) */
126
180
  interface CommitFileItemProps {
127
- path: string;
128
- status: GitFileChange["status"];
129
- isSelected: boolean;
130
- onClick: () => void;
181
+ path: string;
182
+ status: GitFileChange["status"];
183
+ isSelected: boolean;
184
+ onClick: () => void;
131
185
  }
132
186
 
133
- const CommitFileItem = memo(function CommitFileItem({ path, status, isSelected, onClick }: CommitFileItemProps) {
134
- return (
135
- <div
136
- className={`flex items-center gap-1.5 pl-7 pr-2 py-0.5 text-xs rounded cursor-pointer transition-colors ${
137
- isSelected ? "bg-[var(--color-accent)]/30 text-[var(--color-text-primary)]" : "hover:bg-[var(--color-bg-hover)] text-[var(--color-text-tertiary)]"
138
- }`}
139
- onClick={onClick}
140
- >
141
- {statusIcon(status)}
142
- <span className="truncate flex-1">{path.split("/").pop()}</span>
143
- <span className="text-gray-600 text-[10px]">{statusLabel(status)}</span>
144
- </div>
145
- );
187
+ const CommitFileItem = memo(function CommitFileItem({
188
+ path,
189
+ status,
190
+ isSelected,
191
+ onClick,
192
+ }: CommitFileItemProps) {
193
+ return (
194
+ <div
195
+ className={`flex items-center gap-1.5 pl-7 pr-2 py-0.5 text-xs rounded cursor-pointer transition-colors ${
196
+ isSelected
197
+ ? "bg-[var(--color-accent)]/30 text-[var(--color-text-primary)]"
198
+ : "hover:bg-[var(--color-bg-hover)] text-[var(--color-text-tertiary)]"
199
+ }`}
200
+ onClick={onClick}
201
+ >
202
+ {statusIcon(status)}
203
+ <span className="truncate flex-1">{path.split("/").pop()}</span>
204
+ <span className="text-[var(--color-text-tertiary)] text-[10px]">{statusLabel(status)}</span>
205
+ </div>
206
+ );
146
207
  });
147
208
 
148
209
  /* Expandable commit item */
149
210
  interface CommitItemProps {
150
- commit: GitCommit;
151
- expanded: boolean;
152
- files: GitFileChange[] | undefined;
153
- loading: boolean;
154
- selectedFilePath: string | null;
155
- onToggle: () => void;
156
- onFileClick: (filePath: string) => void;
157
- onContextMenu: (e: React.MouseEvent, commit: GitCommit) => void;
158
- t: (key: string, options?: Record<string, unknown>) => string;
211
+ commit: GitCommit;
212
+ expanded: boolean;
213
+ files: GitFileChange[] | undefined;
214
+ loading: boolean;
215
+ selectedFilePath: string | null;
216
+ onToggle: () => void;
217
+ onFileClick: (filePath: string) => void;
218
+ onContextMenu: (e: React.MouseEvent, commit: GitCommit) => void;
219
+ t: (key: string, options?: Record<string, unknown>) => string;
159
220
  }
160
221
 
161
222
  const CommitItem = memo(function CommitItem({
162
- commit, expanded, files, loading, selectedFilePath, onToggle, onFileClick, onContextMenu, t,
223
+ commit,
224
+ expanded,
225
+ files,
226
+ loading,
227
+ selectedFilePath,
228
+ onToggle,
229
+ onFileClick,
230
+ onContextMenu,
231
+ t,
163
232
  }: CommitItemProps) {
164
- return (
165
- <div>
166
- <div
167
- className="flex items-start gap-1.5 px-2 py-1 text-xs hover:bg-[var(--color-bg-hover)]/50 rounded cursor-pointer"
168
- onClick={onToggle}
169
- onContextMenu={(e) => { e.preventDefault(); onContextMenu(e, commit); }}
170
- >
171
- {expanded
172
- ? <ChevronDown className="w-3 h-3 text-[var(--color-text-placeholder)] mt-0.5 shrink-0" />
173
- : <ChevronRight className="w-3 h-3 text-[var(--color-text-placeholder)] mt-0.5 shrink-0" />}
174
- <div className="flex-1 min-w-0">
175
- <div className="text-[var(--color-text-secondary)] truncate">{commit.message}</div>
176
- <div className="text-[var(--color-text-placeholder)] text-[10px] flex items-center gap-1.5 mt-0.5">
177
- <span className="text-[var(--color-text-accent)] font-mono">{commit.shortHash}</span>
178
- <span>{commit.author}</span>
179
- <span>{relativeTime(commit.date, t)}</span>
180
- </div>
181
- </div>
182
- </div>
183
- {expanded && (
184
- <div className="ml-1">
185
- {loading ? (
186
- <div className="text-gray-600 text-[10px] pl-7 py-1">{t("git.loadingFiles")}</div>
187
- ) : files && files.length > 0 ? (
188
- files.map((f) => (
189
- <CommitFileItem
190
- key={f.path}
191
- path={f.path}
192
- status={f.status}
193
- isSelected={selectedFilePath === f.path}
194
- onClick={() => onFileClick(f.path)}
195
- />
196
- ))
197
- ) : (
198
- <div className="text-gray-600 text-[10px] pl-7 py-1">{t("git.noFiles")}</div>
199
- )}
200
- </div>
201
- )}
202
- </div>
203
- );
233
+ return (
234
+ <div>
235
+ <div
236
+ className="flex items-start gap-1.5 px-2 py-1 text-xs hover:bg-[var(--color-bg-hover)]/50 rounded cursor-pointer"
237
+ onClick={onToggle}
238
+ onContextMenu={(e) => {
239
+ e.preventDefault();
240
+ onContextMenu(e, commit);
241
+ }}
242
+ >
243
+ {expanded ? (
244
+ <ChevronDown className="w-3 h-3 text-[var(--color-text-placeholder)] mt-0.5 shrink-0" />
245
+ ) : (
246
+ <ChevronRight className="w-3 h-3 text-[var(--color-text-placeholder)] mt-0.5 shrink-0" />
247
+ )}
248
+ <div className="flex-1 min-w-0">
249
+ <div className="text-[var(--color-text-secondary)] truncate">{commit.message}</div>
250
+ <div className="text-[var(--color-text-placeholder)] text-[10px] flex items-center gap-1.5 mt-0.5">
251
+ <span className="text-[var(--color-text-accent)] font-mono">{commit.shortHash}</span>
252
+ <span>{commit.author}</span>
253
+ <span>{relativeTime(commit.date, t)}</span>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ {expanded && (
258
+ <div className="ml-1">
259
+ {loading ? (
260
+ <div className="text-[var(--color-text-tertiary)] text-[10px] pl-7 py-1">
261
+ {t("git.loadingFiles")}
262
+ </div>
263
+ ) : files && files.length > 0 ? (
264
+ files.map((f) => (
265
+ <CommitFileItem
266
+ key={f.path}
267
+ path={f.path}
268
+ status={f.status}
269
+ isSelected={selectedFilePath === f.path}
270
+ onClick={() => onFileClick(f.path)}
271
+ />
272
+ ))
273
+ ) : (
274
+ <div className="text-[var(--color-text-tertiary)] text-[10px] pl-7 py-1">
275
+ {t("git.noFiles")}
276
+ </div>
277
+ )}
278
+ </div>
279
+ )}
280
+ </div>
281
+ );
204
282
  });
205
283
 
206
284
  /* ── Main Panel ─────────────────────────────────────────── */
207
285
 
208
286
  interface GitPanelProps {
209
- hideOuterShell?: boolean;
287
+ hideOuterShell?: boolean;
210
288
  }
211
289
 
212
290
  export function GitPanel({ hideOuterShell }: GitPanelProps) {
213
- const { t } = useTranslation();
214
- const branch = useGitStore((s) => s.branch);
215
- const ahead = useGitStore((s) => s.ahead);
216
- const behind = useGitStore((s) => s.behind);
217
- const staged = useGitStore((s) => s.staged);
218
- const changed = useGitStore((s) => s.changed);
219
- const untracked = useGitStore((s) => s.untracked);
220
- const commits = useGitStore((s) => s.commits);
221
- const loadingCommits = useGitStore((s) => s.loadingCommits);
222
- const currentDiff = useGitStore((s) => s.currentDiff);
223
- const expandedCommits = useGitStore((s) => s.expandedCommits);
224
- const commitFiles = useGitStore((s) => s.commitFiles);
225
- const loadingCommitFiles = useGitStore((s) => s.loadingCommitFiles);
226
- const loadingAction = useGitStore((s) => s.loadingAction);
227
- const worktrees = useGitStore((s) => s.worktrees);
228
- const fetchStatus = useGitStore((s) => s.fetchStatus);
229
- const fetchDiff = useGitStore((s) => s.fetchDiff);
230
- const fetchLog = useGitStore((s) => s.fetchLog);
231
- const toggleCommitExpand = useGitStore((s) => s.toggleCommitExpand);
232
- const fetchCommitFileDiff = useGitStore((s) => s.fetchCommitFileDiff);
233
- const stageFiles = useGitStore((s) => s.stageFiles);
234
- const unstageFiles = useGitStore((s) => s.unstageFiles);
235
- const push = useGitStore((s) => s.push);
236
- const pull = useGitStore((s) => s.pull);
237
- const fetchWorktrees = useGitStore((s) => s.fetchWorktrees);
238
-
239
- const currentPath = useExplorerStore((s) => s.currentPath);
240
- const openFile = useExplorerStore((s) => s.openFile);
241
-
242
- const [commitsExpanded, setCommitsExpanded] = useState(false);
243
- const [showBranches, setShowBranches] = useState(false);
244
- const [showWorktrees, setShowWorktrees] = useState(false);
245
- const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; filePath: string; isStaged?: boolean } | null>(null);
246
- const [commitCtxMenu, setCommitCtxMenu] = useState<{ x: number; y: number; commit: GitCommit } | null>(null);
247
-
248
- const branchBtnRef = useRef<HTMLButtonElement>(null);
249
-
250
- /* Refresh status + worktrees on mount */
251
- const refresh = useCallback(() => {
252
- fetchStatus(currentPath);
253
- fetchWorktrees(currentPath);
254
- if (commitsExpanded) fetchLog(currentPath);
255
- }, [fetchStatus, fetchWorktrees, fetchLog, currentPath, commitsExpanded]);
256
-
257
- useEffect(() => { refresh(); }, [refresh]);
258
-
259
- /* File click handlers */
260
- const handleFileClick = useCallback((filePath: string, staged?: boolean) => {
261
- fetchDiff(currentPath, filePath, staged);
262
- }, [fetchDiff, currentPath]);
263
-
264
- const handleContextMenu = useCallback((e: React.MouseEvent, filePath: string, isStaged?: boolean) => {
265
- setCtxMenu({ x: e.clientX, y: e.clientY, filePath, isStaged });
266
- }, []);
267
-
268
- const handleOpenFile = useCallback((filePath: string) => {
269
- const fullPath = `${currentPath}/${filePath}`;
270
- openFile({ name: filePath.split("/").pop() || filePath, path: fullPath, type: "file" as const });
271
- }, [openFile, currentPath]);
272
-
273
- const handleCopyPath = useCallback(async (filePath: string) => {
274
- await navigator.clipboard.writeText(`${currentPath}/${filePath}`);
275
- }, [currentPath]);
276
-
277
- const getContextMenuItems = useCallback((filePath: string, isStaged?: boolean): MenuItem[] => [
278
- { label: t("git.openDiff"), icon: <Eye className="w-3 h-3" />, onClick: () => fetchDiff(currentPath, filePath, isStaged) },
279
- { label: t("git.openFile"), icon: <FileText className="w-3 h-3" />, onClick: () => handleOpenFile(filePath) },
280
- { label: "", onClick: () => {}, divider: true },
281
- { label: t("git.copyPath"), icon: <Copy className="w-3 h-3" />, onClick: () => handleCopyPath(filePath) },
282
- ], [fetchDiff, currentPath, handleOpenFile, handleCopyPath, t]);
283
-
284
- /* Commit context menu */
285
- const handleCommitContextMenu = useCallback((e: React.MouseEvent, commit: GitCommit) => {
286
- setCommitCtxMenu({ x: e.clientX, y: e.clientY, commit });
287
- }, []);
288
-
289
- const getCommitContextMenuItems = useCallback((commit: GitCommit): MenuItem[] => [
290
- { label: t("git.copyHash"), icon: <Copy className="w-3 h-3" />, onClick: () => navigator.clipboard.writeText(commit.hash) },
291
- { label: t("git.copyMessage"), icon: <Copy className="w-3 h-3" />, onClick: () => navigator.clipboard.writeText(commit.message) },
292
- ], [t]);
293
-
294
- /* Commit file diff */
295
- const handleCommitFileClick = useCallback((hash: string, filePath: string) => {
296
- fetchCommitFileDiff(currentPath, hash, filePath);
297
- }, [fetchCommitFileDiff, currentPath]);
298
-
299
- /* Stage / Unstage */
300
- const handleStageToggle = useCallback((filePath: string, isStaged?: boolean) => {
301
- if (isStaged) {
302
- unstageFiles(currentPath, [filePath]);
303
- } else {
304
- stageFiles(currentPath, [filePath]);
305
- }
306
- }, [stageFiles, unstageFiles, currentPath]);
307
-
308
- const handleStageAll = useCallback(() => {
309
- const paths = [...changed.map((f) => f.path), ...untracked];
310
- if (paths.length > 0) stageFiles(currentPath, paths);
311
- }, [changed, untracked, stageFiles, currentPath]);
312
-
313
- const handleUnstageAll = useCallback(() => {
314
- const paths = staged.map((f) => f.path);
315
- if (paths.length > 0) unstageFiles(currentPath, paths);
316
- }, [staged, unstageFiles, currentPath]);
317
-
318
- const handleUntrackedStage = useCallback((filePath: string) => {
319
- stageFiles(currentPath, [filePath]);
320
- }, [stageFiles, currentPath]);
321
-
322
- /* Commits toggle */
323
- const toggleCommits = useCallback(() => {
324
- const next = !commitsExpanded;
325
- setCommitsExpanded(next);
326
- if (next && commits.length === 0) fetchLog(currentPath);
327
- }, [commitsExpanded, commits.length, fetchLog, currentPath]);
328
-
329
- /* Push / Pull */
330
- const handlePush = useCallback(() => push(currentPath), [push, currentPath]);
331
- const handlePull = useCallback(() => pull(currentPath), [pull, currentPath]);
332
-
333
- const totalChanges = staged.length + changed.length + untracked.length;
334
- const selectedFilePath = currentDiff?.filePath ?? null;
335
- const hasMultipleWorktrees = worktrees.length > 1;
336
-
337
- const pinButton = <PinButton />;
338
-
339
- const panelContent = (
340
- <>
341
- {/* Header */}
342
- <div className="px-3 py-2 text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wide border-b border-[var(--color-border-primary)] flex items-center gap-1.5">
343
- <GitBranch className="w-3.5 h-3.5" />
344
- {t("git.title")}
345
- <span className="ml-auto flex items-center gap-1">
346
- {totalChanges > 0 && (
347
- <span className="bg-[var(--color-accent)] text-[var(--color-text-primary)] px-1.5 py-0.5 rounded-full text-[10px] leading-none">
348
- {totalChanges}
349
- </span>
350
- )}
351
- {pinButton}
352
- <button onClick={handlePull} className="text-[var(--color-text-placeholder)] hover:text-[var(--color-text-primary)]" disabled={loadingAction === "pull"} title={t("git.pull")}>
353
- <Download className="w-3 h-3" />
354
- </button>
355
- <button onClick={handlePush} className="text-[var(--color-text-placeholder)] hover:text-[var(--color-text-primary)]" disabled={loadingAction === "push"} title={t("git.push")}>
356
- <Upload className="w-3 h-3" />
357
- </button>
358
- <button onClick={refresh} className="text-[var(--color-text-placeholder)] hover:text-[var(--color-text-primary)]" title={t("git.refresh")}>
359
- <RefreshCw className="w-3 h-3" />
360
- </button>
361
- </span>
362
- </div>
363
-
364
- {/* Branch info + selector */}
365
- <div className="px-2 py-1.5 text-xs text-[var(--color-text-tertiary)] flex items-center gap-1.5 border-b border-[var(--color-border-primary)]/50">
366
- <button
367
- ref={branchBtnRef}
368
- className="flex items-center gap-1 hover:text-[var(--color-text-primary)] transition-colors"
369
- onClick={() => setShowBranches(!showBranches)}
370
- >
371
- <GitBranch className="w-3 h-3" />
372
- <span className="font-medium">{branch}</span>
373
- {ahead > 0 && <span className="text-green-400">↑{ahead}</span>}
374
- {behind > 0 && <span className="text-orange-400">↓{behind}</span>}
375
- <BranchChevron className="w-3 h-3 text-[var(--color-text-placeholder)]" />
376
- </button>
377
-
378
- {/* Worktree indicator */}
379
- {hasMultipleWorktrees && (
380
- <button
381
- className="ml-auto text-[var(--color-text-placeholder)] hover:text-[var(--color-text-primary)] transition-colors"
382
- onClick={() => setShowWorktrees(!showWorktrees)}
383
- title={`${worktrees.length} worktrees`}
384
- >
385
- <FolderTree className="w-3 h-3" />
386
- </button>
387
- )}
388
- </div>
389
-
390
- {/* Commit input */}
391
- <GitCommitInput />
392
-
393
- <div className="flex-1 overflow-y-auto p-1">
394
- {/* Staged */}
395
- {staged.length > 0 && (
396
- <div className="mt-1">
397
- <div className="px-2 py-1 text-[10px] uppercase tracking-wide text-[var(--color-text-placeholder)] font-semibold flex items-center">
398
- <span>{t("git.staged")} ({staged.length})</span>
399
- <button className="ml-auto text-orange-400 hover:text-orange-300" onClick={handleUnstageAll} title={t("git.unstageAll")}>
400
- <ChevronUp className="w-3 h-3" />
401
- </button>
402
- </div>
403
- {staged.map((f) => (
404
- <FileItem key={f.path} path={f.path} status={f.status} isSelected={selectedFilePath === f.path} isStaged
405
- onClick={handleFileClick} onContextMenu={handleContextMenu} onStageToggle={handleStageToggle} />
406
- ))}
407
- </div>
408
- )}
409
-
410
- {/* Changed */}
411
- {changed.length > 0 && (
412
- <div className="mt-2">
413
- <div className="px-2 py-1 text-[10px] uppercase tracking-wide text-[var(--color-text-placeholder)] font-semibold flex items-center">
414
- <span>{t("git.changes")} ({changed.length})</span>
415
- <button className="ml-auto text-[var(--color-text-success)] hover:text-green-300" onClick={handleStageAll} title={t("git.stageAll")}>
416
- <Plus className="w-3 h-3" />
417
- </button>
418
- </div>
419
- {changed.map((f) => (
420
- <FileItem key={f.path} path={f.path} status={f.status} isSelected={selectedFilePath === f.path}
421
- onClick={handleFileClick} onContextMenu={handleContextMenu} onStageToggle={handleStageToggle} />
422
- ))}
423
- </div>
424
- )}
425
-
426
- {/* Untracked */}
427
- {untracked.length > 0 && (
428
- <div className="mt-2">
429
- <div className="px-2 py-1 text-[10px] uppercase tracking-wide text-[var(--color-text-placeholder)] font-semibold">
430
- {t("git.untracked")} ({untracked.length})
431
- </div>
432
- {untracked.map((f) => (
433
- <UntrackedItem key={f} path={f} isSelected={selectedFilePath === f}
434
- onClick={handleFileClick} onContextMenu={handleContextMenu} onStage={handleUntrackedStage} />
435
- ))}
436
- </div>
437
- )}
438
-
439
- {totalChanges === 0 && !commitsExpanded && (
440
- <div className="text-[var(--color-text-placeholder)] text-xs text-center py-8">{t("git.noChanges")}</div>
441
- )}
442
-
443
- {/* Commit History */}
444
- <div className="mt-2 border-t border-[var(--color-border-primary)] pt-1">
445
- <button
446
- className="w-full px-2 py-1 text-[10px] uppercase tracking-wide text-[var(--color-text-placeholder)] font-semibold flex items-center gap-1 hover:text-[var(--color-text-secondary)] transition-colors"
447
- onClick={toggleCommits}
448
- >
449
- {commitsExpanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
450
- {t("git.commits")}
451
- {commits.length > 0 && <span className="text-gray-600 ml-auto">{commits.length}</span>}
452
- </button>
453
- {commitsExpanded && (
454
- <div className="mt-0.5">
455
- {loadingCommits ? (
456
- <div className="text-[var(--color-text-placeholder)] text-xs text-center py-4">{t("common.loading")}</div>
457
- ) : commits.length === 0 ? (
458
- <div className="text-gray-600 text-xs text-center py-4">{t("git.noCommits")}</div>
459
- ) : (
460
- commits.map((c) => (
461
- <CommitItem key={c.hash} commit={c}
462
- expanded={expandedCommits.has(c.hash)}
463
- files={commitFiles[c.hash]}
464
- loading={loadingCommitFiles.has(c.hash)}
465
- selectedFilePath={selectedFilePath}
466
- onToggle={() => toggleCommitExpand(currentPath, c.hash)}
467
- onFileClick={(fp) => handleCommitFileClick(c.hash, fp)}
468
- onContextMenu={handleCommitContextMenu}
469
- t={t}
470
- />
471
- ))
472
- )}
473
- </div>
474
- )}
475
- </div>
476
- </div>
477
-
478
- {/* Branch selector popup */}
479
- {showBranches && branchBtnRef.current && (
480
- <div className="absolute z-50" style={{
481
- top: branchBtnRef.current.getBoundingClientRect().bottom + 4,
482
- left: branchBtnRef.current.getBoundingClientRect().left,
483
- }}>
484
- <GitBranchSelector onClose={() => setShowBranches(false)} />
485
- </div>
486
- )}
487
-
488
- {/* Worktree popup */}
489
- {showWorktrees && worktrees.length > 1 && (
490
- <div className="fixed z-50 min-w-[200px] bg-[var(--color-bg-secondary)] border border-[var(--color-border-secondary)] rounded-md shadow-xl py-1"
491
- style={{ top: 80, left: 48 }}>
492
- <div className="px-3 py-1 text-[10px] uppercase tracking-wide text-[var(--color-text-placeholder)] font-semibold">{t("git.worktrees")}</div>
493
- {worktrees.map((wt) => (
494
- <div key={wt.path} className={`px-3 py-1.5 text-xs flex items-center gap-2 ${
495
- wt.path === currentPath ? "text-[var(--color-text-accent)]" : "text-[var(--color-text-secondary)]"
496
- }`}>
497
- <FolderTree className="w-3 h-3 shrink-0" />
498
- <div className="min-w-0 flex-1">
499
- <div className="truncate">{wt.branch}</div>
500
- <div className="text-[var(--color-text-placeholder)] text-[10px] truncate">{wt.path}</div>
501
- </div>
502
- {wt.isMain && <span className="text-gray-600 text-[10px]">main</span>}
503
- </div>
504
- ))}
505
- <button className="w-full text-left px-3 py-1 text-[10px] text-[var(--color-text-placeholder)] hover:text-[var(--color-text-secondary)] border-t border-[var(--color-border-primary)] mt-1 pt-1"
506
- onClick={() => setShowWorktrees(false)}>{t("git.close")}</button>
507
- </div>
508
- )}
509
-
510
- {/* Context menus */}
511
- {ctxMenu && (
512
- <ContextMenu x={ctxMenu.x} y={ctxMenu.y}
513
- items={getContextMenuItems(ctxMenu.filePath, ctxMenu.isStaged)}
514
- onClose={() => setCtxMenu(null)} />
515
- )}
516
- {commitCtxMenu && (
517
- <ContextMenu x={commitCtxMenu.x} y={commitCtxMenu.y}
518
- items={getCommitContextMenuItems(commitCtxMenu.commit)}
519
- onClose={() => setCommitCtxMenu(null)} />
520
- )}
521
- </>
522
- );
523
-
524
- if (hideOuterShell) {
525
- return <div className="flex flex-col flex-1 overflow-hidden">{panelContent}</div>;
526
- }
527
-
528
- return (
529
- <div className="w-60 bg-[var(--color-bg-primary)] flex flex-col flex-shrink-0 overflow-hidden">
530
- {panelContent}
531
- </div>
532
- );
291
+ const { t } = useTranslation();
292
+ const branch = useGitStore((s) => s.branch);
293
+ const ahead = useGitStore((s) => s.ahead);
294
+ const behind = useGitStore((s) => s.behind);
295
+ const staged = useGitStore((s) => s.staged);
296
+ const changed = useGitStore((s) => s.changed);
297
+ const untracked = useGitStore((s) => s.untracked);
298
+ const commits = useGitStore((s) => s.commits);
299
+ const loadingCommits = useGitStore((s) => s.loadingCommits);
300
+ const currentDiff = useGitStore((s) => s.currentDiff);
301
+ const expandedCommits = useGitStore((s) => s.expandedCommits);
302
+ const commitFiles = useGitStore((s) => s.commitFiles);
303
+ const loadingCommitFiles = useGitStore((s) => s.loadingCommitFiles);
304
+ const loadingAction = useGitStore((s) => s.loadingAction);
305
+ const worktrees = useGitStore((s) => s.worktrees);
306
+ const fetchStatus = useGitStore((s) => s.fetchStatus);
307
+ const fetchDiff = useGitStore((s) => s.fetchDiff);
308
+ const fetchLog = useGitStore((s) => s.fetchLog);
309
+ const toggleCommitExpand = useGitStore((s) => s.toggleCommitExpand);
310
+ const fetchCommitFileDiff = useGitStore((s) => s.fetchCommitFileDiff);
311
+ const stageFiles = useGitStore((s) => s.stageFiles);
312
+ const unstageFiles = useGitStore((s) => s.unstageFiles);
313
+ const push = useGitStore((s) => s.push);
314
+ const pull = useGitStore((s) => s.pull);
315
+ const fetchWorktrees = useGitStore((s) => s.fetchWorktrees);
316
+
317
+ const currentPath = useExplorerStore((s) => s.currentPath);
318
+ const openFile = useExplorerStore((s) => s.openFile);
319
+
320
+ const [commitsExpanded, setCommitsExpanded] = useState(false);
321
+ const [showBranches, setShowBranches] = useState(false);
322
+ const [showWorktrees, setShowWorktrees] = useState(false);
323
+ const [ctxMenu, setCtxMenu] = useState<{
324
+ x: number;
325
+ y: number;
326
+ filePath: string;
327
+ isStaged?: boolean;
328
+ } | null>(null);
329
+ const [commitCtxMenu, setCommitCtxMenu] = useState<{
330
+ x: number;
331
+ y: number;
332
+ commit: GitCommit;
333
+ } | null>(null);
334
+
335
+ const branchBtnRef = useRef<HTMLButtonElement>(null);
336
+
337
+ /* Refresh status + worktrees on mount */
338
+ const refresh = useCallback(() => {
339
+ fetchStatus(currentPath);
340
+ fetchWorktrees(currentPath);
341
+ if (commitsExpanded) fetchLog(currentPath);
342
+ }, [fetchStatus, fetchWorktrees, fetchLog, currentPath, commitsExpanded]);
343
+
344
+ useEffect(() => {
345
+ refresh();
346
+ }, [refresh]);
347
+
348
+ /* File click handlers */
349
+ const handleFileClick = useCallback(
350
+ (filePath: string, staged?: boolean) => {
351
+ fetchDiff(currentPath, filePath, staged);
352
+ },
353
+ [fetchDiff, currentPath]
354
+ );
355
+
356
+ const handleContextMenu = useCallback(
357
+ (e: React.MouseEvent, filePath: string, isStaged?: boolean) => {
358
+ setCtxMenu({ x: e.clientX, y: e.clientY, filePath, isStaged });
359
+ },
360
+ []
361
+ );
362
+
363
+ const handleOpenFile = useCallback(
364
+ (filePath: string) => {
365
+ const fullPath = `${currentPath}/${filePath}`;
366
+ openFile({
367
+ name: filePath.split("/").pop() || filePath,
368
+ path: fullPath,
369
+ type: "file" as const,
370
+ });
371
+ },
372
+ [openFile, currentPath]
373
+ );
374
+
375
+ const handleCopyPath = useCallback(
376
+ async (filePath: string) => {
377
+ await navigator.clipboard.writeText(`${currentPath}/${filePath}`);
378
+ },
379
+ [currentPath]
380
+ );
381
+
382
+ const getContextMenuItems = useCallback(
383
+ (filePath: string, isStaged?: boolean): MenuItem[] => [
384
+ {
385
+ label: t("git.openDiff"),
386
+ icon: <Eye className="w-3 h-3" />,
387
+ onClick: () => fetchDiff(currentPath, filePath, isStaged),
388
+ },
389
+ {
390
+ label: t("git.openFile"),
391
+ icon: <FileText className="w-3 h-3" />,
392
+ onClick: () => handleOpenFile(filePath),
393
+ },
394
+ { label: "", onClick: () => {}, divider: true },
395
+ {
396
+ label: t("git.copyPath"),
397
+ icon: <Copy className="w-3 h-3" />,
398
+ onClick: () => handleCopyPath(filePath),
399
+ },
400
+ ],
401
+ [fetchDiff, currentPath, handleOpenFile, handleCopyPath, t]
402
+ );
403
+
404
+ /* Commit context menu */
405
+ const handleCommitContextMenu = useCallback((e: React.MouseEvent, commit: GitCommit) => {
406
+ setCommitCtxMenu({ x: e.clientX, y: e.clientY, commit });
407
+ }, []);
408
+
409
+ const getCommitContextMenuItems = useCallback(
410
+ (commit: GitCommit): MenuItem[] => [
411
+ {
412
+ label: t("git.copyHash"),
413
+ icon: <Copy className="w-3 h-3" />,
414
+ onClick: () => navigator.clipboard.writeText(commit.hash),
415
+ },
416
+ {
417
+ label: t("git.copyMessage"),
418
+ icon: <Copy className="w-3 h-3" />,
419
+ onClick: () => navigator.clipboard.writeText(commit.message),
420
+ },
421
+ ],
422
+ [t]
423
+ );
424
+
425
+ /* Commit file diff */
426
+ const handleCommitFileClick = useCallback(
427
+ (hash: string, filePath: string) => {
428
+ fetchCommitFileDiff(currentPath, hash, filePath);
429
+ },
430
+ [fetchCommitFileDiff, currentPath]
431
+ );
432
+
433
+ /* Stage / Unstage */
434
+ const handleStageToggle = useCallback(
435
+ (filePath: string, isStaged?: boolean) => {
436
+ if (isStaged) {
437
+ unstageFiles(currentPath, [filePath]);
438
+ } else {
439
+ stageFiles(currentPath, [filePath]);
440
+ }
441
+ },
442
+ [stageFiles, unstageFiles, currentPath]
443
+ );
444
+
445
+ const handleStageAll = useCallback(() => {
446
+ const paths = [...changed.map((f) => f.path), ...untracked];
447
+ if (paths.length > 0) stageFiles(currentPath, paths);
448
+ }, [changed, untracked, stageFiles, currentPath]);
449
+
450
+ const handleUnstageAll = useCallback(() => {
451
+ const paths = staged.map((f) => f.path);
452
+ if (paths.length > 0) unstageFiles(currentPath, paths);
453
+ }, [staged, unstageFiles, currentPath]);
454
+
455
+ const handleUntrackedStage = useCallback(
456
+ (filePath: string) => {
457
+ stageFiles(currentPath, [filePath]);
458
+ },
459
+ [stageFiles, currentPath]
460
+ );
461
+
462
+ /* Commits toggle */
463
+ const toggleCommits = useCallback(() => {
464
+ const next = !commitsExpanded;
465
+ setCommitsExpanded(next);
466
+ if (next && commits.length === 0) fetchLog(currentPath);
467
+ }, [commitsExpanded, commits.length, fetchLog, currentPath]);
468
+
469
+ /* Push / Pull */
470
+ const handlePush = useCallback(() => push(currentPath), [push, currentPath]);
471
+ const handlePull = useCallback(() => pull(currentPath), [pull, currentPath]);
472
+
473
+ const totalChanges = staged.length + changed.length + untracked.length;
474
+ const selectedFilePath = currentDiff?.filePath ?? null;
475
+ const hasMultipleWorktrees = worktrees.length > 1;
476
+
477
+ const pinButton = <PinButton />;
478
+
479
+ const panelContent = (
480
+ <>
481
+ {/* Header */}
482
+ <div className="px-3 py-2 text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wide border-b border-[var(--color-border-primary)] flex items-center gap-1.5">
483
+ <GitBranch className="w-3.5 h-3.5" />
484
+ {t("git.title")}
485
+ <span className="ml-auto flex items-center gap-1">
486
+ {totalChanges > 0 && (
487
+ <span className="bg-[var(--color-accent)] text-[var(--color-text-primary)] px-1.5 py-0.5 rounded-full text-[10px] leading-none">
488
+ {totalChanges}
489
+ </span>
490
+ )}
491
+ {pinButton}
492
+ <button
493
+ onClick={handlePull}
494
+ className="text-[var(--color-text-placeholder)] hover:text-[var(--color-text-primary)]"
495
+ disabled={loadingAction === "pull"}
496
+ title={t("git.pull")}
497
+ >
498
+ <Download className="w-3 h-3" />
499
+ </button>
500
+ <button
501
+ onClick={handlePush}
502
+ className="text-[var(--color-text-placeholder)] hover:text-[var(--color-text-primary)]"
503
+ disabled={loadingAction === "push"}
504
+ title={t("git.push")}
505
+ >
506
+ <Upload className="w-3 h-3" />
507
+ </button>
508
+ <button
509
+ onClick={refresh}
510
+ className="text-[var(--color-text-placeholder)] hover:text-[var(--color-text-primary)]"
511
+ title={t("git.refresh")}
512
+ >
513
+ <RefreshCw className="w-3 h-3" />
514
+ </button>
515
+ </span>
516
+ </div>
517
+
518
+ {/* Branch info + selector */}
519
+ <div className="px-2 py-1.5 text-xs text-[var(--color-text-tertiary)] flex items-center gap-1.5 border-b border-[var(--color-border-primary)]/50">
520
+ <button
521
+ ref={branchBtnRef}
522
+ className="flex items-center gap-1 hover:text-[var(--color-text-primary)] transition-colors"
523
+ onClick={() => setShowBranches(!showBranches)}
524
+ >
525
+ <GitBranch className="w-3 h-3" />
526
+ <span className="font-medium">{branch}</span>
527
+ {ahead > 0 && <span className="text-[var(--color-text-success)]">↑{ahead}</span>}
528
+ {behind > 0 && <span className="text-[var(--color-text-warning)]">↓{behind}</span>}
529
+ <BranchChevron className="w-3 h-3 text-[var(--color-text-placeholder)]" />
530
+ </button>
531
+
532
+ {/* Worktree indicator */}
533
+ {hasMultipleWorktrees && (
534
+ <button
535
+ className="ml-auto text-[var(--color-text-placeholder)] hover:text-[var(--color-text-primary)] transition-colors"
536
+ onClick={() => setShowWorktrees(!showWorktrees)}
537
+ title={`${worktrees.length} worktrees`}
538
+ >
539
+ <FolderTree className="w-3 h-3" />
540
+ </button>
541
+ )}
542
+ </div>
543
+
544
+ {/* Commit input */}
545
+ <GitCommitInput />
546
+
547
+ <div className="flex-1 overflow-y-auto p-1">
548
+ {/* Staged */}
549
+ {staged.length > 0 && (
550
+ <div className="mt-1">
551
+ <div className="px-2 py-1 text-[10px] uppercase tracking-wide text-[var(--color-text-placeholder)] font-semibold flex items-center">
552
+ <span>
553
+ {t("git.staged")} ({staged.length})
554
+ </span>
555
+ <button
556
+ className="ml-auto text-[var(--color-text-warning)] hover:text-[var(--color-text-warning)]"
557
+ onClick={handleUnstageAll}
558
+ title={t("git.unstageAll")}
559
+ >
560
+ <ChevronUp className="w-3 h-3" />
561
+ </button>
562
+ </div>
563
+ {staged.map((f) => (
564
+ <FileItem
565
+ key={f.path}
566
+ path={f.path}
567
+ status={f.status}
568
+ isSelected={selectedFilePath === f.path}
569
+ isStaged
570
+ onClick={handleFileClick}
571
+ onContextMenu={handleContextMenu}
572
+ onStageToggle={handleStageToggle}
573
+ />
574
+ ))}
575
+ </div>
576
+ )}
577
+
578
+ {/* Changed */}
579
+ {changed.length > 0 && (
580
+ <div className="mt-2">
581
+ <div className="px-2 py-1 text-[10px] uppercase tracking-wide text-[var(--color-text-placeholder)] font-semibold flex items-center">
582
+ <span>
583
+ {t("git.changes")} ({changed.length})
584
+ </span>
585
+ <button
586
+ className="ml-auto text-[var(--color-text-success)] hover:text-[var(--color-text-success)]"
587
+ onClick={handleStageAll}
588
+ title={t("git.stageAll")}
589
+ >
590
+ <Plus className="w-3 h-3" />
591
+ </button>
592
+ </div>
593
+ {changed.map((f) => (
594
+ <FileItem
595
+ key={f.path}
596
+ path={f.path}
597
+ status={f.status}
598
+ isSelected={selectedFilePath === f.path}
599
+ onClick={handleFileClick}
600
+ onContextMenu={handleContextMenu}
601
+ onStageToggle={handleStageToggle}
602
+ />
603
+ ))}
604
+ </div>
605
+ )}
606
+
607
+ {/* Untracked */}
608
+ {untracked.length > 0 && (
609
+ <div className="mt-2">
610
+ <div className="px-2 py-1 text-[10px] uppercase tracking-wide text-[var(--color-text-placeholder)] font-semibold">
611
+ {t("git.untracked")} ({untracked.length})
612
+ </div>
613
+ {untracked.map((f) => (
614
+ <UntrackedItem
615
+ key={f}
616
+ path={f}
617
+ isSelected={selectedFilePath === f}
618
+ onClick={handleFileClick}
619
+ onContextMenu={handleContextMenu}
620
+ onStage={handleUntrackedStage}
621
+ />
622
+ ))}
623
+ </div>
624
+ )}
625
+
626
+ {totalChanges === 0 && !commitsExpanded && (
627
+ <div className="text-[var(--color-text-placeholder)] text-xs text-center py-8">
628
+ {t("git.noChanges")}
629
+ </div>
630
+ )}
631
+
632
+ {/* Commit History */}
633
+ <div className="mt-2 border-t border-[var(--color-border-primary)] pt-1">
634
+ <button
635
+ className="w-full px-2 py-1 text-[10px] uppercase tracking-wide text-[var(--color-text-placeholder)] font-semibold flex items-center gap-1 hover:text-[var(--color-text-secondary)] transition-colors"
636
+ onClick={toggleCommits}
637
+ >
638
+ {commitsExpanded ? (
639
+ <ChevronDown className="w-3 h-3" />
640
+ ) : (
641
+ <ChevronRight className="w-3 h-3" />
642
+ )}
643
+ {t("git.commits")}
644
+ {commits.length > 0 && (
645
+ <span className="text-[var(--color-text-tertiary)] ml-auto">{commits.length}</span>
646
+ )}
647
+ </button>
648
+ {commitsExpanded && (
649
+ <div className="mt-0.5">
650
+ {loadingCommits ? (
651
+ <div className="text-[var(--color-text-placeholder)] text-xs text-center py-4">
652
+ {t("common.loading")}
653
+ </div>
654
+ ) : commits.length === 0 ? (
655
+ <div className="text-[var(--color-text-tertiary)] text-xs text-center py-4">
656
+ {t("git.noCommits")}
657
+ </div>
658
+ ) : (
659
+ commits.map((c) => (
660
+ <CommitItem
661
+ key={c.hash}
662
+ commit={c}
663
+ expanded={expandedCommits.has(c.hash)}
664
+ files={commitFiles[c.hash]}
665
+ loading={loadingCommitFiles.has(c.hash)}
666
+ selectedFilePath={selectedFilePath}
667
+ onToggle={() => toggleCommitExpand(currentPath, c.hash)}
668
+ onFileClick={(fp) => handleCommitFileClick(c.hash, fp)}
669
+ onContextMenu={handleCommitContextMenu}
670
+ t={t}
671
+ />
672
+ ))
673
+ )}
674
+ </div>
675
+ )}
676
+ </div>
677
+ </div>
678
+
679
+ {/* Branch selector popup */}
680
+ {showBranches && branchBtnRef.current && (
681
+ <div
682
+ className="absolute z-50"
683
+ style={{
684
+ top: branchBtnRef.current.getBoundingClientRect().bottom + 4,
685
+ left: branchBtnRef.current.getBoundingClientRect().left,
686
+ }}
687
+ >
688
+ <GitBranchSelector onClose={() => setShowBranches(false)} />
689
+ </div>
690
+ )}
691
+
692
+ {/* Worktree popup */}
693
+ {showWorktrees && worktrees.length > 1 && (
694
+ <div
695
+ className="fixed z-50 min-w-[200px] bg-[var(--color-bg-secondary)] border border-[var(--color-border-secondary)] rounded-md shadow-xl py-1"
696
+ style={{ top: 80, left: 48 }}
697
+ >
698
+ <div className="px-3 py-1 text-[10px] uppercase tracking-wide text-[var(--color-text-placeholder)] font-semibold">
699
+ {t("git.worktrees")}
700
+ </div>
701
+ {worktrees.map((wt) => (
702
+ <div
703
+ key={wt.path}
704
+ className={`px-3 py-1.5 text-xs flex items-center gap-2 ${
705
+ wt.path === currentPath
706
+ ? "text-[var(--color-text-accent)]"
707
+ : "text-[var(--color-text-secondary)]"
708
+ }`}
709
+ >
710
+ <FolderTree className="w-3 h-3 shrink-0" />
711
+ <div className="min-w-0 flex-1">
712
+ <div className="truncate">{wt.branch}</div>
713
+ <div className="text-[var(--color-text-placeholder)] text-[10px] truncate">
714
+ {wt.path}
715
+ </div>
716
+ </div>
717
+ {wt.isMain && (
718
+ <span className="text-[var(--color-text-tertiary)] text-[10px]">main</span>
719
+ )}
720
+ </div>
721
+ ))}
722
+ <button
723
+ className="w-full text-left px-3 py-1 text-[10px] text-[var(--color-text-placeholder)] hover:text-[var(--color-text-secondary)] border-t border-[var(--color-border-primary)] mt-1 pt-1"
724
+ onClick={() => setShowWorktrees(false)}
725
+ >
726
+ {t("git.close")}
727
+ </button>
728
+ </div>
729
+ )}
730
+
731
+ {/* Context menus */}
732
+ {ctxMenu && (
733
+ <ContextMenu
734
+ x={ctxMenu.x}
735
+ y={ctxMenu.y}
736
+ items={getContextMenuItems(ctxMenu.filePath, ctxMenu.isStaged)}
737
+ onClose={() => setCtxMenu(null)}
738
+ />
739
+ )}
740
+ {commitCtxMenu && (
741
+ <ContextMenu
742
+ x={commitCtxMenu.x}
743
+ y={commitCtxMenu.y}
744
+ items={getCommitContextMenuItems(commitCtxMenu.commit)}
745
+ onClose={() => setCommitCtxMenu(null)}
746
+ />
747
+ )}
748
+ </>
749
+ );
750
+
751
+ if (hideOuterShell) {
752
+ return <div className="flex flex-col flex-1 overflow-hidden">{panelContent}</div>;
753
+ }
754
+
755
+ return (
756
+ <div className="w-60 bg-[var(--color-bg-primary)] flex flex-col flex-shrink-0 overflow-hidden">
757
+ {panelContent}
758
+ </div>
759
+ );
533
760
  }