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