@akiojin/gwt 2.13.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.ja.md +41 -0
  2. package/README.md +38 -0
  3. package/dist/claude.d.ts.map +1 -1
  4. package/dist/claude.js +17 -11
  5. package/dist/claude.js.map +1 -1
  6. package/dist/cli/ui/components/App.d.ts +0 -6
  7. package/dist/cli/ui/components/App.d.ts.map +1 -1
  8. package/dist/cli/ui/components/App.js +27 -88
  9. package/dist/cli/ui/components/App.js.map +1 -1
  10. package/dist/cli/ui/components/common/Select.js +2 -2
  11. package/dist/cli/ui/components/common/Select.js.map +1 -1
  12. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  13. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  14. package/dist/cli/ui/components/screens/BranchQuickStartScreen.d.ts.map +1 -1
  15. package/dist/cli/ui/components/screens/BranchQuickStartScreen.js +3 -3
  16. package/dist/cli/ui/components/screens/BranchQuickStartScreen.js.map +1 -1
  17. package/dist/cli/ui/utils/continueSession.d.ts.map +1 -1
  18. package/dist/cli/ui/utils/continueSession.js +1 -1
  19. package/dist/cli/ui/utils/continueSession.js.map +1 -1
  20. package/dist/client/assets/index-DsDNCy5f.css +1 -0
  21. package/dist/client/assets/index-f5D2XwDh.js +72 -0
  22. package/dist/client/index.html +2 -2
  23. package/dist/codex.d.ts.map +1 -1
  24. package/dist/codex.js +21 -11
  25. package/dist/codex.js.map +1 -1
  26. package/dist/config/builtin-tools.d.ts.map +1 -1
  27. package/dist/config/builtin-tools.js +3 -2
  28. package/dist/config/builtin-tools.js.map +1 -1
  29. package/dist/config/shared-env.d.ts +41 -0
  30. package/dist/config/shared-env.d.ts.map +1 -0
  31. package/dist/config/shared-env.js +114 -0
  32. package/dist/config/shared-env.js.map +1 -0
  33. package/dist/gemini.d.ts.map +1 -1
  34. package/dist/gemini.js +20 -17
  35. package/dist/gemini.js.map +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +13 -8
  38. package/dist/index.js.map +1 -1
  39. package/dist/logging/logger.d.ts.map +1 -1
  40. package/dist/logging/logger.js +4 -1
  41. package/dist/logging/logger.js.map +1 -1
  42. package/dist/qwen.d.ts.map +1 -1
  43. package/dist/qwen.js +15 -11
  44. package/dist/qwen.js.map +1 -1
  45. package/dist/services/aiToolResolver.d.ts +41 -0
  46. package/dist/services/aiToolResolver.d.ts.map +1 -0
  47. package/dist/services/aiToolResolver.js +194 -0
  48. package/dist/services/aiToolResolver.js.map +1 -0
  49. package/dist/services/customToolResolver.d.ts +10 -0
  50. package/dist/services/customToolResolver.d.ts.map +1 -0
  51. package/dist/services/customToolResolver.js +71 -0
  52. package/dist/services/customToolResolver.js.map +1 -0
  53. package/dist/shared/aiToolConstants.d.ts +9 -0
  54. package/dist/shared/aiToolConstants.d.ts.map +1 -0
  55. package/dist/shared/aiToolConstants.js +29 -0
  56. package/dist/shared/aiToolConstants.js.map +1 -0
  57. package/dist/types/tools.d.ts +12 -0
  58. package/dist/types/tools.d.ts.map +1 -1
  59. package/dist/utils/prompt.d.ts.map +1 -1
  60. package/dist/utils/prompt.js.map +1 -1
  61. package/dist/utils/session.d.ts.map +1 -1
  62. package/dist/utils/session.js +15 -6
  63. package/dist/utils/session.js.map +1 -1
  64. package/dist/utils/terminal.d.ts +12 -3
  65. package/dist/utils/terminal.d.ts.map +1 -1
  66. package/dist/utils/terminal.js +5 -34
  67. package/dist/utils/terminal.js.map +1 -1
  68. package/dist/utils/webui.d.ts +8 -0
  69. package/dist/utils/webui.d.ts.map +1 -0
  70. package/dist/utils/webui.js +35 -0
  71. package/dist/utils/webui.js.map +1 -0
  72. package/dist/web/client/src/components/AIToolLaunchModal.d.ts +9 -0
  73. package/dist/web/client/src/components/AIToolLaunchModal.d.ts.map +1 -0
  74. package/dist/web/client/src/components/AIToolLaunchModal.js +363 -0
  75. package/dist/web/client/src/components/AIToolLaunchModal.js.map +1 -0
  76. package/dist/web/client/src/components/BranchGraph.d.ts.map +1 -1
  77. package/dist/web/client/src/components/BranchGraph.js +46 -49
  78. package/dist/web/client/src/components/BranchGraph.js.map +1 -1
  79. package/dist/web/client/src/components/CustomToolForm.d.ts +23 -0
  80. package/dist/web/client/src/components/CustomToolForm.d.ts.map +1 -0
  81. package/dist/web/client/src/components/CustomToolForm.js +209 -0
  82. package/dist/web/client/src/components/CustomToolForm.js.map +1 -0
  83. package/dist/web/client/src/components/CustomToolList.d.ts +10 -0
  84. package/dist/web/client/src/components/CustomToolList.d.ts.map +1 -0
  85. package/dist/web/client/src/components/CustomToolList.js +57 -0
  86. package/dist/web/client/src/components/CustomToolList.js.map +1 -0
  87. package/dist/web/client/src/components/EnvEditor.d.ts.map +1 -1
  88. package/dist/web/client/src/components/EnvEditor.js +33 -26
  89. package/dist/web/client/src/components/EnvEditor.js.map +1 -1
  90. package/dist/web/client/src/components/EnvironmentEditor.d.ts +17 -0
  91. package/dist/web/client/src/components/EnvironmentEditor.d.ts.map +1 -0
  92. package/dist/web/client/src/components/EnvironmentEditor.js +22 -0
  93. package/dist/web/client/src/components/EnvironmentEditor.js.map +1 -0
  94. package/dist/web/client/src/components/Terminal.d.ts.map +1 -1
  95. package/dist/web/client/src/components/Terminal.js +10 -3
  96. package/dist/web/client/src/components/Terminal.js.map +1 -1
  97. package/dist/web/client/src/components/branch-detail/BranchInfoCards.d.ts +10 -0
  98. package/dist/web/client/src/components/branch-detail/BranchInfoCards.d.ts.map +1 -0
  99. package/dist/web/client/src/components/branch-detail/BranchInfoCards.js +104 -0
  100. package/dist/web/client/src/components/branch-detail/BranchInfoCards.js.map +1 -0
  101. package/dist/web/client/src/components/branch-detail/SessionHistoryTable.d.ts +22 -0
  102. package/dist/web/client/src/components/branch-detail/SessionHistoryTable.d.ts.map +1 -0
  103. package/dist/web/client/src/components/branch-detail/SessionHistoryTable.js +79 -0
  104. package/dist/web/client/src/components/branch-detail/SessionHistoryTable.js.map +1 -0
  105. package/dist/web/client/src/components/branch-detail/TerminalPanel.d.ts +11 -0
  106. package/dist/web/client/src/components/branch-detail/TerminalPanel.d.ts.map +1 -0
  107. package/dist/web/client/src/components/branch-detail/TerminalPanel.js +32 -0
  108. package/dist/web/client/src/components/branch-detail/TerminalPanel.js.map +1 -0
  109. package/dist/web/client/src/components/branch-detail/ToolLauncher.d.ts +40 -0
  110. package/dist/web/client/src/components/branch-detail/ToolLauncher.d.ts.map +1 -0
  111. package/dist/web/client/src/components/branch-detail/ToolLauncher.js +147 -0
  112. package/dist/web/client/src/components/branch-detail/ToolLauncher.js.map +1 -0
  113. package/dist/web/client/src/components/branch-detail/index.d.ts +5 -0
  114. package/dist/web/client/src/components/branch-detail/index.d.ts.map +1 -0
  115. package/dist/web/client/src/components/branch-detail/index.js +5 -0
  116. package/dist/web/client/src/components/branch-detail/index.js.map +1 -0
  117. package/dist/web/client/src/components/common/BranchCard.d.ts +17 -0
  118. package/dist/web/client/src/components/common/BranchCard.d.ts.map +1 -0
  119. package/dist/web/client/src/components/common/BranchCard.js +36 -0
  120. package/dist/web/client/src/components/common/BranchCard.js.map +1 -0
  121. package/dist/web/client/src/components/common/MetricCard.d.ts +10 -0
  122. package/dist/web/client/src/components/common/MetricCard.d.ts.map +1 -0
  123. package/dist/web/client/src/components/common/MetricCard.js +10 -0
  124. package/dist/web/client/src/components/common/MetricCard.js.map +1 -0
  125. package/dist/web/client/src/components/common/PageHeader.d.ts +12 -0
  126. package/dist/web/client/src/components/common/PageHeader.d.ts.map +1 -0
  127. package/dist/web/client/src/components/common/PageHeader.js +14 -0
  128. package/dist/web/client/src/components/common/PageHeader.js.map +1 -0
  129. package/dist/web/client/src/components/common/SearchInput.d.ts +14 -0
  130. package/dist/web/client/src/components/common/SearchInput.d.ts.map +1 -0
  131. package/dist/web/client/src/components/common/SearchInput.js +15 -0
  132. package/dist/web/client/src/components/common/SearchInput.js.map +1 -0
  133. package/dist/web/client/src/components/common/StatusBadge.d.ts +10 -0
  134. package/dist/web/client/src/components/common/StatusBadge.d.ts.map +1 -0
  135. package/dist/web/client/src/components/common/StatusBadge.js +15 -0
  136. package/dist/web/client/src/components/common/StatusBadge.js.map +1 -0
  137. package/dist/web/client/src/components/common/index.d.ts +6 -0
  138. package/dist/web/client/src/components/common/index.d.ts.map +1 -0
  139. package/dist/web/client/src/components/common/index.js +6 -0
  140. package/dist/web/client/src/components/common/index.js.map +1 -0
  141. package/dist/web/client/src/components/ui/alert.d.ts +9 -0
  142. package/dist/web/client/src/components/ui/alert.d.ts.map +1 -0
  143. package/dist/web/client/src/components/ui/alert.js +25 -0
  144. package/dist/web/client/src/components/ui/alert.js.map +1 -0
  145. package/dist/web/client/src/components/ui/badge.d.ts +10 -0
  146. package/dist/web/client/src/components/ui/badge.d.ts.map +1 -0
  147. package/dist/web/client/src/components/ui/badge.js +25 -0
  148. package/dist/web/client/src/components/ui/badge.js.map +1 -0
  149. package/dist/web/client/src/components/ui/button.d.ts +12 -0
  150. package/dist/web/client/src/components/ui/button.d.ts.map +1 -0
  151. package/dist/web/client/src/components/ui/button.js +33 -0
  152. package/dist/web/client/src/components/ui/button.js.map +1 -0
  153. package/dist/web/client/src/components/ui/card.d.ts +9 -0
  154. package/dist/web/client/src/components/ui/card.d.ts.map +1 -0
  155. package/dist/web/client/src/components/ui/card.js +16 -0
  156. package/dist/web/client/src/components/ui/card.js.map +1 -0
  157. package/dist/web/client/src/components/ui/index.d.ts +8 -0
  158. package/dist/web/client/src/components/ui/index.d.ts.map +1 -0
  159. package/dist/web/client/src/components/ui/index.js +8 -0
  160. package/dist/web/client/src/components/ui/index.js.map +1 -0
  161. package/dist/web/client/src/components/ui/input.d.ts +4 -0
  162. package/dist/web/client/src/components/ui/input.d.ts.map +1 -0
  163. package/dist/web/client/src/components/ui/input.js +8 -0
  164. package/dist/web/client/src/components/ui/input.js.map +1 -0
  165. package/dist/web/client/src/components/ui/select.d.ts +14 -0
  166. package/dist/web/client/src/components/ui/select.d.ts.map +1 -0
  167. package/dist/web/client/src/components/ui/select.js +39 -0
  168. package/dist/web/client/src/components/ui/select.js.map +1 -0
  169. package/dist/web/client/src/components/ui/table.d.ts +11 -0
  170. package/dist/web/client/src/components/ui/table.d.ts.map +1 -0
  171. package/dist/web/client/src/components/ui/table.js +21 -0
  172. package/dist/web/client/src/components/ui/table.js.map +1 -0
  173. package/dist/web/client/src/hooks/useSessions.d.ts.map +1 -1
  174. package/dist/web/client/src/hooks/useSessions.js +6 -1
  175. package/dist/web/client/src/hooks/useSessions.js.map +1 -1
  176. package/dist/web/client/src/lib/utils.d.ts +7 -0
  177. package/dist/web/client/src/lib/utils.d.ts.map +1 -0
  178. package/dist/web/client/src/lib/utils.js +10 -0
  179. package/dist/web/client/src/lib/utils.js.map +1 -0
  180. package/dist/web/client/src/lib/websocket.d.ts +7 -0
  181. package/dist/web/client/src/lib/websocket.d.ts.map +1 -1
  182. package/dist/web/client/src/lib/websocket.js +44 -0
  183. package/dist/web/client/src/lib/websocket.js.map +1 -1
  184. package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
  185. package/dist/web/client/src/pages/BranchDetailPage.js +125 -376
  186. package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
  187. package/dist/web/client/src/pages/BranchListPage.d.ts.map +1 -1
  188. package/dist/web/client/src/pages/BranchListPage.js +89 -127
  189. package/dist/web/client/src/pages/BranchListPage.js.map +1 -1
  190. package/dist/web/client/src/pages/ConfigManagementPage.d.ts.map +1 -1
  191. package/dist/web/client/src/pages/ConfigManagementPage.js +46 -41
  192. package/dist/web/client/src/pages/ConfigManagementPage.js.map +1 -1
  193. package/dist/web/client/src/pages/ConfigPage.d.ts +3 -0
  194. package/dist/web/client/src/pages/ConfigPage.d.ts.map +1 -0
  195. package/dist/web/client/src/pages/ConfigPage.js +216 -0
  196. package/dist/web/client/src/pages/ConfigPage.js.map +1 -0
  197. package/dist/web/client/vite.config.d.ts.map +1 -1
  198. package/dist/web/client/vite.config.js +8 -1
  199. package/dist/web/client/vite.config.js.map +1 -1
  200. package/dist/web/server/index.d.ts +24 -2
  201. package/dist/web/server/index.d.ts.map +1 -1
  202. package/dist/web/server/index.js +46 -15
  203. package/dist/web/server/index.js.map +1 -1
  204. package/dist/web/server/pty/manager.d.ts +12 -10
  205. package/dist/web/server/pty/manager.d.ts.map +1 -1
  206. package/dist/web/server/pty/manager.js +76 -43
  207. package/dist/web/server/pty/manager.js.map +1 -1
  208. package/dist/web/server/routes/sessions.d.ts.map +1 -1
  209. package/dist/web/server/routes/sessions.js +35 -2
  210. package/dist/web/server/routes/sessions.js.map +1 -1
  211. package/dist/web/server/routes/worktrees.js +2 -2
  212. package/dist/web/server/routes/worktrees.js.map +1 -1
  213. package/dist/web/server/services/worktrees.d.ts.map +1 -1
  214. package/dist/web/server/services/worktrees.js +7 -1
  215. package/dist/web/server/services/worktrees.js.map +1 -1
  216. package/dist/web/server/tray.d.ts +25 -0
  217. package/dist/web/server/tray.d.ts.map +1 -0
  218. package/dist/web/server/tray.js +98 -0
  219. package/dist/web/server/tray.js.map +1 -0
  220. package/dist/web/server/websocket/handler.d.ts +18 -2
  221. package/dist/web/server/websocket/handler.d.ts.map +1 -1
  222. package/dist/web/server/websocket/handler.js +82 -9
  223. package/dist/web/server/websocket/handler.js.map +1 -1
  224. package/package.json +15 -2
  225. package/src/claude.ts +26 -15
  226. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +1 -1
  227. package/src/cli/ui/__tests__/components/common/Select.test.tsx +11 -0
  228. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +3 -1
  229. package/src/cli/ui/__tests__/components/screens/BranchQuickStartScreen.test.tsx +4 -4
  230. package/src/cli/ui/components/App.tsx +33 -133
  231. package/src/cli/ui/components/common/Select.tsx +2 -2
  232. package/src/cli/ui/components/screens/BranchListScreen.tsx +28 -21
  233. package/src/cli/ui/components/screens/BranchQuickStartScreen.tsx +41 -46
  234. package/src/cli/ui/utils/continueSession.ts +1 -7
  235. package/src/codex.ts +31 -22
  236. package/src/config/builtin-tools.ts +6 -2
  237. package/src/config/shared-env.ts +139 -0
  238. package/src/gemini.ts +35 -22
  239. package/src/index.ts +13 -8
  240. package/src/logging/logger.ts +5 -2
  241. package/src/qwen.ts +28 -19
  242. package/src/services/aiToolResolver.ts +276 -0
  243. package/src/services/customToolResolver.ts +98 -0
  244. package/src/shared/aiToolConstants.ts +30 -0
  245. package/src/trayicon.d.ts +30 -0
  246. package/src/types/tools.ts +15 -0
  247. package/src/utils/prompt.ts +15 -9
  248. package/src/utils/session.ts +80 -26
  249. package/src/utils/terminal.ts +11 -41
  250. package/src/utils/webui.ts +43 -0
  251. package/src/web/client/components.json +21 -0
  252. package/src/web/client/src/components/AIToolLaunchModal.tsx +575 -0
  253. package/src/web/client/src/components/BranchGraph.tsx +95 -75
  254. package/src/web/client/src/components/CustomToolForm.tsx +386 -0
  255. package/src/web/client/src/components/CustomToolList.tsx +119 -0
  256. package/src/web/client/src/components/EnvEditor.tsx +91 -81
  257. package/src/web/client/src/components/EnvironmentEditor.tsx +97 -0
  258. package/src/web/client/src/components/Terminal.tsx +11 -3
  259. package/src/web/client/src/components/branch-detail/BranchInfoCards.tsx +179 -0
  260. package/src/web/client/src/components/branch-detail/SessionHistoryTable.tsx +181 -0
  261. package/src/web/client/src/components/branch-detail/TerminalPanel.tsx +92 -0
  262. package/src/web/client/src/components/branch-detail/ToolLauncher.tsx +327 -0
  263. package/src/web/client/src/components/branch-detail/index.ts +4 -0
  264. package/src/web/client/src/components/common/BranchCard.tsx +117 -0
  265. package/src/web/client/src/components/common/MetricCard.tsx +22 -0
  266. package/src/web/client/src/components/common/PageHeader.tsx +44 -0
  267. package/src/web/client/src/components/common/SearchInput.tsx +40 -0
  268. package/src/web/client/src/components/common/StatusBadge.tsx +37 -0
  269. package/src/web/client/src/components/common/index.ts +5 -0
  270. package/src/web/client/src/components/ui/alert.tsx +63 -0
  271. package/src/web/client/src/components/ui/badge.tsx +44 -0
  272. package/src/web/client/src/components/ui/button.tsx +57 -0
  273. package/src/web/client/src/components/ui/card.tsx +82 -0
  274. package/src/web/client/src/components/ui/index.ts +32 -0
  275. package/src/web/client/src/components/ui/input.tsx +21 -0
  276. package/src/web/client/src/components/ui/select.tsx +156 -0
  277. package/src/web/client/src/components/ui/table.tsx +119 -0
  278. package/src/web/client/src/hooks/useSessions.ts +10 -1
  279. package/src/web/client/src/index.css +46 -816
  280. package/src/web/client/src/lib/utils.ts +10 -0
  281. package/src/web/client/src/lib/websocket.ts +48 -1
  282. package/src/web/client/src/pages/BranchDetailPage.tsx +247 -723
  283. package/src/web/client/src/pages/BranchListPage.tsx +190 -236
  284. package/src/web/client/src/pages/ConfigManagementPage.tsx +94 -76
  285. package/src/web/client/src/pages/ConfigPage.tsx +362 -0
  286. package/src/web/client/vite.config.ts +8 -1
  287. package/src/web/server/index.ts +72 -15
  288. package/src/web/server/pty/manager.ts +128 -55
  289. package/src/web/server/routes/sessions.ts +59 -7
  290. package/src/web/server/routes/worktrees.ts +3 -3
  291. package/src/web/server/services/worktrees.ts +12 -4
  292. package/src/web/server/tray.ts +120 -0
  293. package/src/web/server/websocket/handler.ts +119 -13
  294. package/dist/client/assets/index-DeNwPosA.css +0 -1
  295. package/dist/client/assets/index-Dl798X5w.js +0 -32
@@ -9,7 +9,17 @@ import {
9
9
  } from "../hooks/useSessions";
10
10
  import { useConfig } from "../hooks/useConfig";
11
11
  import { ApiError } from "../lib/api";
12
- import { Terminal } from "../components/Terminal";
12
+ import { PageHeader } from "@/components/common/PageHeader";
13
+ import { Button } from "@/components/ui/button";
14
+ import { Badge } from "@/components/ui/badge";
15
+ import { Alert, AlertDescription } from "@/components/ui/alert";
16
+ import {
17
+ SessionHistoryTable,
18
+ ToolLauncher,
19
+ BranchInfoCards,
20
+ TerminalPanel,
21
+ type SelectableTool,
22
+ } from "@/components/branch-detail";
13
23
  import type {
14
24
  Branch,
15
25
  CustomAITool,
@@ -19,44 +29,6 @@ import type {
19
29
  type ToolType = "claude-code" | "codex-cli" | "custom";
20
30
  type ToolMode = "normal" | "continue" | "resume";
21
31
 
22
- type SelectableTool =
23
- | { id: "claude-code"; label: string; target: "claude" }
24
- | { id: "codex-cli"; label: string; target: "codex" }
25
- | { id: string; label: string; target: "custom"; definition: CustomAITool };
26
-
27
- interface ToolSummary {
28
- command: string;
29
- defaultArgs?: string[] | null;
30
- modeArgs?: {
31
- normal?: string[];
32
- continue?: string[];
33
- resume?: string[];
34
- };
35
- permissionSkipArgs?: string[] | null;
36
- }
37
-
38
- const BUILTIN_TOOL_SUMMARIES: Record<string, ToolSummary> = {
39
- "claude-code": {
40
- command: "claude",
41
- defaultArgs: [],
42
- modeArgs: {
43
- normal: [],
44
- continue: ["-c"],
45
- resume: ["-r"],
46
- },
47
- permissionSkipArgs: ["--dangerously-skip-permissions"],
48
- },
49
- "codex-cli": {
50
- command: "codex",
51
- defaultArgs: ["--auto-approve", "--verbose"],
52
- modeArgs: {
53
- normal: [],
54
- continue: ["resume", "--last"],
55
- resume: ["resume"],
56
- },
57
- },
58
- };
59
-
60
32
  interface BannerState {
61
33
  type: "success" | "error" | "info";
62
34
  message: string;
@@ -73,15 +45,6 @@ const MERGE_STATUS_LABEL: Record<Branch["mergeStatus"], string> = {
73
45
  unknown: "状態不明",
74
46
  };
75
47
 
76
- const MERGE_STATUS_TONE: Record<
77
- Branch["mergeStatus"],
78
- "success" | "warning" | "muted"
79
- > = {
80
- merged: "success",
81
- unmerged: "warning",
82
- unknown: "muted",
83
- };
84
-
85
48
  export function BranchDetailPage() {
86
49
  const { branchName } = useParams<{ branchName: string }>();
87
50
  const decodedBranchName = branchName ? decodeURIComponent(branchName) : "";
@@ -115,58 +78,136 @@ export function BranchDetailPage() {
115
78
  [branch?.commitDate],
116
79
  );
117
80
 
81
+ // Handle fullscreen body overflow
118
82
  useEffect(() => {
119
- if (!isTerminalFullscreen) {
120
- return undefined;
121
- }
122
-
83
+ if (!isTerminalFullscreen) return undefined;
123
84
  const previousOverflow = document.body.style.overflow;
124
85
  document.body.style.overflow = "hidden";
125
-
126
86
  return () => {
127
87
  document.body.style.overflow = previousOverflow;
128
88
  };
129
89
  }, [isTerminalFullscreen]);
130
90
 
91
+ // Available tools - must be before conditional returns
92
+ const customTools: CustomAITool[] = config?.tools ?? [];
93
+ const availableTools: SelectableTool[] = useMemo(
94
+ () => [
95
+ { id: "claude-code", label: "Claude Code", target: "claude" },
96
+ { id: "codex-cli", label: "Codex CLI", target: "codex" },
97
+ ...customTools.map(
98
+ (tool): SelectableTool => ({
99
+ id: tool.id,
100
+ label: tool.displayName,
101
+ target: "custom" as const,
102
+ definition: tool,
103
+ }),
104
+ ),
105
+ ],
106
+ [customTools],
107
+ );
108
+
109
+ // Ensure selected tool is valid - must be before conditional returns
110
+ useEffect(() => {
111
+ if (!availableTools.length) {
112
+ setSelectedToolId("claude-code");
113
+ return;
114
+ }
115
+ if (!availableTools.find((tool) => tool.id === selectedToolId)) {
116
+ const first = availableTools[0];
117
+ if (first) setSelectedToolId(first.id);
118
+ }
119
+ }, [availableTools, selectedToolId]);
120
+
121
+ // Branch sessions - must be before conditional returns
122
+ const branchSessions = useMemo(() => {
123
+ return (sessionsData ?? [])
124
+ .filter((session) => session.worktreePath === branch?.worktreePath)
125
+ .sort((a, b) => (b.startedAt ?? "").localeCompare(a.startedAt ?? ""));
126
+ }, [sessionsData, branch?.worktreePath]);
127
+
128
+ // Latest tool usage - must be before conditional returns
129
+ const latestToolUsage: LastToolUsage | null = useMemo(() => {
130
+ if (!branch) return null;
131
+ if (branch.lastToolUsage) return branch.lastToolUsage;
132
+ const first = branchSessions[0];
133
+ if (!first) return null;
134
+ return {
135
+ branch: branch.name,
136
+ worktreePath: branch.worktreePath ?? null,
137
+ toolId:
138
+ first.toolType === "custom"
139
+ ? (first.toolName ?? "custom")
140
+ : (first.toolType as LastToolUsage["toolId"]),
141
+ toolLabel:
142
+ first.toolType === "custom"
143
+ ? (first.toolName ?? "Custom")
144
+ : toolLabel(first.toolType),
145
+ mode: first.mode ?? "normal",
146
+ model: null,
147
+ timestamp: first.startedAt ? Date.parse(first.startedAt) : Date.now(),
148
+ };
149
+ }, [branch, branchSessions]);
150
+
151
+ // Loading state
131
152
  if (isLoading) {
132
153
  return (
133
- <div className="app-shell">
134
- <div className="page-state page-state--centered">
135
- <h1>読み込み中</h1>
136
- <p>ブランチ情報を取得しています...</p>
137
- </div>
154
+ <div className="min-h-screen bg-background">
155
+ <PageHeader
156
+ eyebrow="BRANCH DETAIL"
157
+ title="読み込み中..."
158
+ subtitle="ブランチ情報を取得しています"
159
+ />
160
+ <main className="mx-auto max-w-7xl px-6 py-8">
161
+ <div className="flex items-center justify-center py-20">
162
+ <div className="text-center">
163
+ <div className="mb-4 text-4xl">⏳</div>
164
+ <p className="text-muted-foreground">Loading branch...</p>
165
+ </div>
166
+ </div>
167
+ </main>
138
168
  </div>
139
169
  );
140
170
  }
141
171
 
172
+ // Error state
142
173
  if (error) {
143
174
  return (
144
- <div className="app-shell">
145
- <div className="page-state page-state--centered">
146
- <h1>ブランチの取得に失敗しました</h1>
147
- <p>{error instanceof Error ? error.message : "未知のエラーです"}</p>
148
- <Link to="/" className="button button--ghost">
149
- ブランチ一覧に戻る
150
- </Link>
151
- </div>
175
+ <div className="min-h-screen bg-background">
176
+ <PageHeader eyebrow="BRANCH DETAIL" title="エラー" />
177
+ <main className="mx-auto max-w-7xl px-6 py-8">
178
+ <Alert variant="destructive">
179
+ <AlertDescription>
180
+ {error instanceof Error ? error.message : "未知のエラーです"}
181
+ </AlertDescription>
182
+ </Alert>
183
+ <div className="mt-4">
184
+ <Button variant="ghost" asChild>
185
+ <Link to="/">← ブランチ一覧に戻る</Link>
186
+ </Button>
187
+ </div>
188
+ </main>
152
189
  </div>
153
190
  );
154
191
  }
155
192
 
193
+ // Not found state
156
194
  if (!branch) {
157
195
  return (
158
- <div className="app-shell">
159
- <div className="page-state page-state--centered">
160
- <h1>Branch not found</h1>
161
- <p>指定されたブランチは存在しません。</p>
162
- <Link to="/" className="button button--ghost">
163
- ブランチ一覧に戻る
164
- </Link>
165
- </div>
196
+ <div className="min-h-screen bg-background">
197
+ <PageHeader eyebrow="BRANCH DETAIL" title="Branch not found" />
198
+ <main className="mx-auto max-w-7xl px-6 py-8">
199
+ <p className="mb-4 text-muted-foreground">
200
+ 指定されたブランチは存在しません。
201
+ </p>
202
+ <Button variant="ghost" asChild>
203
+ <Link to="/">← ブランチ一覧に戻る</Link>
204
+ </Button>
205
+ </main>
166
206
  </div>
167
207
  );
168
208
  }
169
209
 
210
+ // Computed values
170
211
  const canStartSession = Boolean(branch.worktreePath);
171
212
  const divergenceInfo = branch.divergence ?? null;
172
213
  const hasBlockingDivergence = Boolean(
@@ -174,84 +215,18 @@ export function BranchDetailPage() {
174
215
  );
175
216
  const needsRemoteSync = Boolean(
176
217
  branch.worktreePath &&
177
- divergenceInfo &&
178
- divergenceInfo.behind > 0 &&
179
- divergenceInfo.ahead === 0 &&
180
- !hasBlockingDivergence,
218
+ divergenceInfo &&
219
+ divergenceInfo.behind > 0 &&
220
+ divergenceInfo.ahead === 0 &&
221
+ !hasBlockingDivergence,
181
222
  );
182
223
  const isSyncingBranch = syncBranch.isPending;
183
224
 
184
- const customTools: CustomAITool[] = config?.tools ?? [];
185
- const availableTools: SelectableTool[] = useMemo(
186
- () => [
187
- { id: "claude-code", label: "Claude Code", target: "claude" },
188
- { id: "codex-cli", label: "Codex CLI", target: "codex" },
189
- ...customTools.map(
190
- (tool): SelectableTool => ({
191
- id: tool.id,
192
- label: tool.displayName,
193
- target: "custom" as const,
194
- definition: tool,
195
- }),
196
- ),
197
- ],
198
- [customTools],
199
- );
200
-
201
- useEffect(() => {
202
- if (!availableTools.length) {
203
- setSelectedToolId("claude-code");
204
- return;
205
- }
206
- if (!availableTools.find((tool) => tool.id === selectedToolId)) {
207
- const first = availableTools[0];
208
- if (first) {
209
- setSelectedToolId(first.id);
210
- }
211
- }
212
- }, [availableTools, selectedToolId]);
213
-
214
225
  const selectedTool = availableTools.find(
215
226
  (tool) => tool.id === selectedToolId,
216
227
  );
217
228
 
218
- const selectedToolSummary: ToolSummary | null = useMemo(() => {
219
- if (!selectedTool) {
220
- return null;
221
- }
222
- if (selectedTool.target === "custom") {
223
- return {
224
- command: selectedTool.definition.command,
225
- defaultArgs: selectedTool.definition.defaultArgs ?? null,
226
- modeArgs: selectedTool.definition.modeArgs,
227
- permissionSkipArgs: selectedTool.definition.permissionSkipArgs ?? null,
228
- };
229
- }
230
- return BUILTIN_TOOL_SUMMARIES[selectedTool.id] ?? null;
231
- }, [selectedTool]);
232
-
233
- const argsPreview = useMemo(() => {
234
- if (!selectedToolSummary) {
235
- return null;
236
- }
237
- const args: string[] = [];
238
- if (selectedToolSummary.defaultArgs?.length) {
239
- args.push(...selectedToolSummary.defaultArgs);
240
- }
241
- const mode = selectedToolSummary.modeArgs?.[selectedMode];
242
- if (mode?.length) {
243
- args.push(...mode);
244
- }
245
- if (skipPermissions && selectedToolSummary.permissionSkipArgs?.length) {
246
- args.push(...selectedToolSummary.permissionSkipArgs);
247
- }
248
- const extraArgs = parseExtraArgs(extraArgsText);
249
- if (extraArgs.length) {
250
- args.push(...extraArgs);
251
- }
252
- return { command: selectedToolSummary.command, args };
253
- }, [selectedToolSummary, selectedMode, skipPermissions, extraArgsText]);
254
-
229
+ // Handlers
255
230
  const handleCreateWorktree = async () => {
256
231
  try {
257
232
  await createWorktree.mutateAsync({
@@ -290,8 +265,7 @@ export function BranchDetailPage() {
290
265
  if (needsRemoteSync) {
291
266
  setBanner({
292
267
  type: "error",
293
- message:
294
- "リモートの更新を取り込むまでAIツールは起動できません。『最新の変更を同期』を実行してください。",
268
+ message: "リモートの更新を取り込むまでAIツールは起動できません。",
295
269
  });
296
270
  return;
297
271
  }
@@ -299,17 +273,14 @@ export function BranchDetailPage() {
299
273
  if (hasBlockingDivergence) {
300
274
  setBanner({
301
275
  type: "error",
302
- message:
303
- "リモートとローカルの双方で進捗が発生しているため、CLIと同様にAIツールの起動をブロックしました。先に rebase/merge 等で差分を解消してください。",
276
+ message: "差分を解消してから起動してください。",
304
277
  });
305
278
  return;
306
279
  }
307
280
 
308
281
  if (
309
282
  skipPermissions &&
310
- !window.confirm(
311
- "権限チェックをスキップして起動します。自己責任で実行してください。続行しますか?",
312
- )
283
+ !window.confirm("権限チェックをスキップして起動します。続行しますか?")
313
284
  ) {
314
285
  return;
315
286
  }
@@ -322,7 +293,10 @@ export function BranchDetailPage() {
322
293
  : selectedTool.target === "custom"
323
294
  ? "custom"
324
295
  : "claude-code";
325
- const extraArgs = parseExtraArgs(extraArgsText);
296
+ const extraArgs = extraArgsText
297
+ .split(/\s+/)
298
+ .map((c) => c.trim())
299
+ .filter(Boolean);
326
300
  const sessionRequest = {
327
301
  toolType,
328
302
  toolName: selectedTool.target === "custom" ? selectedTool.id : null,
@@ -360,9 +334,7 @@ export function BranchDetailPage() {
360
334
  try {
361
335
  await deleteSession.mutateAsync(sessionId);
362
336
  setBanner({ type: "success", message: "セッションを終了しました" });
363
- if (activeSessionId === sessionId) {
364
- setActiveSessionId(null);
365
- }
337
+ if (activeSessionId === sessionId) setActiveSessionId(null);
366
338
  } catch (err) {
367
339
  setBanner({
368
340
  type: "error",
@@ -408,40 +380,6 @@ export function BranchDetailPage() {
408
380
  }
409
381
  };
410
382
 
411
- const branchSessions = useMemo(() => {
412
- return (sessionsData ?? [])
413
- .filter((session) => session.worktreePath === branch?.worktreePath)
414
- .sort((a, b) => (b.startedAt ?? "").localeCompare(a.startedAt ?? ""));
415
- }, [sessionsData, branch?.worktreePath]);
416
-
417
- const latestToolUsage: LastToolUsage | null = useMemo(() => {
418
- if (branch?.lastToolUsage) {
419
- return branch.lastToolUsage;
420
- }
421
- const first = branchSessions[0];
422
- if (!first) return null;
423
- return {
424
- branch: branch.name,
425
- worktreePath: branch.worktreePath ?? null,
426
- toolId:
427
- first.toolType === "custom"
428
- ? (first.toolName ?? "custom")
429
- : (first.toolType as LastToolUsage["toolId"]),
430
- toolLabel:
431
- first.toolType === "custom"
432
- ? (first.toolName ?? "Custom")
433
- : toolLabel(first.toolType),
434
- mode: first.mode ?? "normal",
435
- model: null,
436
- timestamp: first.startedAt ? Date.parse(first.startedAt) : Date.now(),
437
- };
438
- }, [
439
- branch?.lastToolUsage,
440
- branch?.name,
441
- branch?.worktreePath,
442
- branchSessions,
443
- ]);
444
-
445
383
  const handleSessionExit = (code: number) => {
446
384
  setActiveSessionId(null);
447
385
  setIsTerminalFullscreen(false);
@@ -452,488 +390,132 @@ export function BranchDetailPage() {
452
390
  };
453
391
 
454
392
  return (
455
- <div className="app-shell">
456
- <header className="page-hero page-hero--compact">
457
- <Link to="/" className="link-back">
458
- ← ブランチ一覧に戻る
459
- </Link>
460
- <p className="page-hero__eyebrow">BRANCH DETAIL</p>
461
- <h1>{branch.name}</h1>
462
- <p className="page-hero__subtitle">
463
- 最新コミット {branch.commitHash.slice(0, 7)} ・ {formattedCommitDate}
464
- </p>
465
- <div className="badge-group">
466
- <span className={`status-badge status-badge--${branch.type}`}>
393
+ <div className="min-h-screen bg-background">
394
+ {/* Fullscreen backdrop */}
395
+ {isTerminalFullscreen && (
396
+ <div
397
+ className="fixed inset-0 z-40 bg-black/80"
398
+ aria-hidden="true"
399
+ onClick={() => setIsTerminalFullscreen(false)}
400
+ />
401
+ )}
402
+
403
+ {/* Header */}
404
+ <PageHeader
405
+ eyebrow="BRANCH DETAIL"
406
+ title={branch.name}
407
+ subtitle={`最新コミット ${branch.commitHash.slice(0, 7)} · ${formattedCommitDate}`}
408
+ >
409
+ <div className="mt-4 flex flex-wrap gap-2">
410
+ <Badge variant={branch.type === "local" ? "local" : "remote"}>
467
411
  {BRANCH_TYPE_LABEL[branch.type]}
468
- </span>
469
- <span
470
- className={`status-badge status-badge--${MERGE_STATUS_TONE[branch.mergeStatus]}`}
412
+ </Badge>
413
+ <Badge
414
+ variant={
415
+ branch.mergeStatus === "merged"
416
+ ? "success"
417
+ : branch.mergeStatus === "unmerged"
418
+ ? "warning"
419
+ : "outline"
420
+ }
471
421
  >
472
422
  {MERGE_STATUS_LABEL[branch.mergeStatus]}
473
- </span>
474
- <span
475
- className={`status-badge ${
476
- branch.worktreePath
477
- ? "status-badge--success"
478
- : "status-badge--muted"
479
- }`}
480
- >
423
+ </Badge>
424
+ <Badge variant={branch.worktreePath ? "success" : "outline"}>
481
425
  {branch.worktreePath ? "Worktreeあり" : "Worktree未作成"}
482
- </span>
426
+ </Badge>
483
427
  </div>
484
- <div className="badge-group" style={{ marginTop: "0.5rem" }}>
485
- {latestToolUsage ? (
486
- <>
487
- <span className="status-badge status-badge--muted">
488
- {renderToolUsage(latestToolUsage)}
489
- </span>
490
- <span className="status-badge status-badge--muted">
491
- {formatUsageTimestamp(latestToolUsage.timestamp)} ・ worktree:{" "}
492
- {latestToolUsage.worktreePath ?? branch.worktreePath ?? "N/A"}
493
- </span>
494
- </>
495
- ) : (
496
- <span className="status-badge status-badge--muted">Unknown</span>
497
- )}
498
- </div>
499
- <div className="page-hero__actions">
428
+ <div className="mt-4 flex flex-wrap gap-2">
429
+ <Button variant="ghost" size="sm" asChild>
430
+ <Link to="/">← ブランチ一覧</Link>
431
+ </Button>
500
432
  {!canStartSession ? (
501
- <button
502
- type="button"
503
- className="button button--primary"
433
+ <Button
504
434
  onClick={handleCreateWorktree}
505
435
  disabled={createWorktree.isPending}
506
436
  >
507
437
  {createWorktree.isPending ? "作成中..." : "Worktreeを作成"}
508
- </button>
438
+ </Button>
509
439
  ) : (
510
- <Link to="/config" className="button button--secondary">
511
- カスタムツール設定を開く
512
- </Link>
440
+ <Button variant="secondary" asChild>
441
+ <Link to="/config">カスタムツール設定</Link>
442
+ </Button>
513
443
  )}
514
444
  </div>
515
-
516
- {banner && (
517
- <div className={`inline-banner inline-banner--${banner.type}`}>
518
- {banner.message}
519
- </div>
520
- )}
521
- </header>
522
-
523
- {isTerminalFullscreen && (
524
- <div
525
- className="terminal-overlay-backdrop"
526
- aria-hidden="true"
527
- onClick={() => setIsTerminalFullscreen(false)}
528
- />
445
+ </PageHeader>
446
+
447
+ {/* Banner */}
448
+ {banner && (
449
+ <div className="mx-auto max-w-7xl px-6 pt-4">
450
+ <Alert
451
+ variant={
452
+ banner.type === "error"
453
+ ? "destructive"
454
+ : banner.type === "success"
455
+ ? "success"
456
+ : "info"
457
+ }
458
+ >
459
+ <AlertDescription>{banner.message}</AlertDescription>
460
+ </Alert>
461
+ </div>
529
462
  )}
530
- <main className="page-content page-content--wide">
531
- <div className="page-layout page-layout--split">
532
- <div className="info-stack">
533
- <section className="section-card">
534
- <header className="terminal-section__header">
535
- <div>
536
- <h2>AIツール起動</h2>
537
- <p className="section-card__body">
538
- Web UI
539
- から直接AIツールを起動できます。設定したカスタムツールも一覧に表示されます。
540
- </p>
541
- </div>
542
- {configError && (
543
- <span className="pill pill--warning">
544
- 設定の取得に失敗しました
545
- </span>
546
- )}
547
- </header>
548
-
549
- {!canStartSession ? (
550
- <p className="section-card__body">
551
- Worktreeが未作成のため、先にWorktreeを作成してください。
552
- </p>
553
- ) : (
554
- <div className="tool-form">
555
- <div className="form-grid">
556
- <label className="form-field">
557
- <span>AIツール</span>
558
- <select
559
- value={selectedToolId}
560
- onChange={(event) =>
561
- setSelectedToolId(event.target.value)
562
- }
563
- disabled={isConfigLoading}
564
- >
565
- {availableTools.map((tool) => (
566
- <option key={tool.id} value={tool.id}>
567
- {tool.label}
568
- </option>
569
- ))}
570
- </select>
571
- </label>
572
-
573
- <label className="form-field">
574
- <span>起動モード</span>
575
- <select
576
- value={selectedMode}
577
- onChange={(event) =>
578
- setSelectedMode(event.target.value as ToolMode)
579
- }
580
- >
581
- <option value="normal">normal</option>
582
- <option value="continue">continue</option>
583
- <option value="resume">resume</option>
584
- </select>
585
- </label>
586
-
587
- <label className="form-field">
588
- <span>追加引数 (スペース区切り)</span>
589
- <input
590
- type="text"
591
- value={extraArgsText}
592
- onChange={(event) =>
593
- setExtraArgsText(event.target.value)
594
- }
595
- placeholder="--flag value"
596
- />
597
- </label>
598
- </div>
599
-
600
- <label className="form-field">
601
- <span>
602
- <input
603
- type="checkbox"
604
- checked={skipPermissions}
605
- onChange={(event) =>
606
- setSkipPermissions(event.target.checked)
607
- }
608
- />
609
- <span style={{ marginLeft: "0.5rem" }}>
610
- 権限チェックをスキップ (自己責任)
611
- </span>
612
- </span>
613
- </label>
614
- {skipPermissions && (
615
- <div className="inline-banner inline-banner--warning">
616
- <p>
617
- 権限チェックをスキップすることで、CLI での
618
- `--dangerously-skip-permissions`
619
- 指定と同様のリスクを負います。
620
- </p>
621
- </div>
622
- )}
623
- {needsRemoteSync && (
624
- <div
625
- className="inline-banner inline-banner--info"
626
- data-testid="sync-required"
627
- >
628
- <p>
629
- リモートに未取得の更新 ({branch.divergence?.behind ?? 0}{" "}
630
- commits)
631
- があるため、AIツールを起動する前に同期してください。
632
- </p>
633
- <p className="section-card__body">
634
- CLI の `git fetch --all` と `git pull --ff-only`
635
- と同じ処理を Web UI から実行できます。
636
- </p>
637
- </div>
638
- )}
639
- {hasBlockingDivergence && (
640
- <div
641
- className="inline-banner inline-banner--warning"
642
- data-testid="divergence-warning"
643
- >
644
- <p>
645
- リモートとローカルの両方に未解決の差分があるため、Web UI
646
- でも CLI と同様に起動をブロックしています。
647
- </p>
648
- <ul className="list-muted">
649
- <li>
650
- git fetch && git pull --ff-only origin {branch.name}
651
- </li>
652
- <li>
653
- 必要に応じて git push origin {branch.name}{" "}
654
- でローカル進捗を共有
655
- </li>
656
- </ul>
657
- <p className="section-card__body">
658
- rebase / merge
659
- などで差分を解消した後にページを更新してください。
660
- </p>
661
- </div>
662
- )}
663
463
 
664
- <div className="tool-card__actions">
665
- <button
666
- type="button"
667
- className="button button--primary"
668
- onClick={handleStartSession}
669
- disabled={
670
- isStartingSession ||
671
- !selectedTool ||
672
- hasBlockingDivergence ||
673
- needsRemoteSync ||
674
- isSyncingBranch
675
- }
676
- >
677
- {isStartingSession ? "起動中..." : "セッションを起動"}
678
- </button>
679
- <button
680
- type="button"
681
- className="button button--secondary"
682
- onClick={handleSyncBranch}
683
- disabled={!branch.worktreePath || isSyncingBranch}
684
- >
685
- {isSyncingBranch ? "同期中..." : "最新の変更を同期"}
686
- </button>
687
- <Link to="/config" className="button button--ghost">
688
- 設定を編集
689
- </Link>
690
- </div>
691
-
692
- {selectedToolSummary && (
693
- <dl className="metadata-grid metadata-grid--compact">
694
- <div>
695
- <dt>コマンド</dt>
696
- <dd className="tool-card__command">
697
- {selectedToolSummary.command}
698
- </dd>
699
- </div>
700
- <div>
701
- <dt>defaultArgs</dt>
702
- <dd>{renderArgs(selectedToolSummary.defaultArgs)}</dd>
703
- </div>
704
- <div>
705
- <dt>permissionSkipArgs</dt>
706
- <dd>
707
- {renderArgs(selectedToolSummary.permissionSkipArgs)}
708
- </dd>
709
- </div>
710
- {argsPreview && (
711
- <div className="metadata-grid__full">
712
- <dt>最終的に実行されるコマンド</dt>
713
- <dd className="tool-card__command">
714
- {argsPreview.command} {argsPreview.args.join(" ")}
715
- </dd>
716
- </div>
717
- )}
718
- </dl>
719
- )}
720
- </div>
721
- )}
722
- </section>
723
- <section className="section-card">
724
- <header className="terminal-section__header">
725
- <div>
726
- <h2>セッション履歴</h2>
727
- <p className="section-card__body">
728
- この Worktree に紐づいた最新の AI
729
- セッションが表示されます。CLI からの起動分も共有されます。
730
- </p>
731
- </div>
732
- {isSessionsLoading && (
733
- <span className="pill">読み込み中...</span>
734
- )}
735
- </header>
736
- {branchSessions.length === 0 ? (
737
- <p className="section-card__body">
738
- セッション履歴はまだありません。
739
- </p>
740
- ) : (
741
- <div className="session-table-wrapper">
742
- <table className="session-table">
743
- <thead>
744
- <tr>
745
- <th>状態</th>
746
- <th>ツール</th>
747
- <th>モード</th>
748
- <th>開始時刻</th>
749
- <th>終了時刻</th>
750
- <th>操作</th>
751
- </tr>
752
- </thead>
753
- <tbody>
754
- {branchSessions.slice(0, 5).map((session) => (
755
- <tr key={session.sessionId}>
756
- <td>
757
- <span
758
- className={`status-pill status-pill--${session.status}`}
759
- >
760
- {SESSION_STATUS_LABEL[session.status]}
761
- </span>
762
- </td>
763
- <td>
764
- {session.toolType === "custom"
765
- ? (session.toolName ?? "custom")
766
- : toolLabel(session.toolType)}
767
- </td>
768
- <td>{session.mode}</td>
769
- <td>{formatDate(session.startedAt)}</td>
770
- <td>
771
- {session.endedAt
772
- ? formatDate(session.endedAt)
773
- : "--"}
774
- </td>
775
- <td>
776
- {session.status === "running" ? (
777
- <button
778
- type="button"
779
- className="button button--ghost"
780
- onClick={() =>
781
- handleTerminateSession(session.sessionId)
782
- }
783
- disabled={
784
- terminatingSessionId === session.sessionId ||
785
- deleteSession.isPending
786
- }
787
- >
788
- {terminatingSessionId === session.sessionId
789
- ? "終了中..."
790
- : "終了"}
791
- </button>
792
- ) : (
793
- <span className="session-table__muted">--</span>
794
- )}
795
- </td>
796
- </tr>
797
- ))}
798
- </tbody>
799
- </table>
800
- </div>
801
- )}
802
- </section>
803
- <section className="section-card">
804
- <header>
805
- <h2>ブランチインサイト</h2>
806
- </header>
807
- <dl className="metadata-grid">
808
- <div>
809
- <dt>コミット</dt>
810
- <dd>{branch.commitHash}</dd>
811
- </div>
812
- <div>
813
- <dt>Author</dt>
814
- <dd>{branch.author ?? "N/A"}</dd>
815
- </div>
816
- <div>
817
- <dt>更新日</dt>
818
- <dd>{formattedCommitDate}</dd>
819
- </div>
820
- <div>
821
- <dt>Worktree</dt>
822
- <dd>{branch.worktreePath ?? "未作成"}</dd>
823
- </div>
824
- </dl>
825
- </section>
826
-
827
- <section className="section-card">
828
- <header>
829
- <h2>コミット情報</h2>
830
- </header>
831
- <p className="section-card__body">
832
- {branch.commitMessage ?? "コミットメッセージがありません。"}
833
- </p>
834
- </section>
835
-
836
- {branch.divergence && (
837
- <section className="section-card">
838
- <header>
839
- <h2>差分状況</h2>
840
- </header>
841
- <div className="pill-group">
842
- <span className="pill">Ahead {branch.divergence.ahead}</span>
843
- <span className="pill">
844
- Behind {branch.divergence.behind}
845
- </span>
846
- <span
847
- className={`pill ${
848
- branch.divergence.upToDate
849
- ? "pill--success"
850
- : "pill--warning"
851
- }`}
852
- >
853
- {branch.divergence.upToDate ? "最新" : "更新あり"}
854
- </span>
855
- </div>
856
- </section>
857
- )}
858
-
859
- <section className="section-card">
860
- <header>
861
- <h2>Worktree情報</h2>
862
- </header>
863
- <ul className="list-muted">
864
- <li>
865
- パス: <strong>{branch.worktreePath ?? "未作成"}</strong>
866
- </li>
867
- <li>
868
- AIツールの起動にはクリーンなワークツリーであることを推奨します。
869
- </li>
870
- <li>
871
- Worktreeを再作成すると既存のローカル変更が失われる可能性があります。
872
- </li>
873
- </ul>
874
- </section>
464
+ {/* Main Content */}
465
+ <main className="mx-auto max-w-7xl space-y-6 px-6 py-8">
466
+ <div className="grid gap-6 lg:grid-cols-[1fr_400px]">
467
+ {/* Left Column - Tool Launcher & Session History */}
468
+ <div className="space-y-6">
469
+ <ToolLauncher
470
+ branch={branch}
471
+ availableTools={availableTools}
472
+ selectedToolId={selectedToolId}
473
+ selectedMode={selectedMode}
474
+ skipPermissions={skipPermissions}
475
+ extraArgsText={extraArgsText}
476
+ isConfigLoading={isConfigLoading}
477
+ configError={configError ?? null}
478
+ isStartingSession={isStartingSession}
479
+ isSyncingBranch={isSyncingBranch}
480
+ needsRemoteSync={needsRemoteSync}
481
+ hasBlockingDivergence={hasBlockingDivergence}
482
+ onToolChange={setSelectedToolId}
483
+ onModeChange={setSelectedMode}
484
+ onSkipPermissionsChange={setSkipPermissions}
485
+ onExtraArgsChange={setExtraArgsText}
486
+ onStartSession={handleStartSession}
487
+ onSyncBranch={handleSyncBranch}
488
+ />
489
+
490
+ <SessionHistoryTable
491
+ sessions={branchSessions}
492
+ isLoading={isSessionsLoading}
493
+ terminatingSessionId={terminatingSessionId}
494
+ isDeleting={deleteSession.isPending}
495
+ onTerminate={handleTerminateSession}
496
+ onSelectSession={setActiveSessionId}
497
+ />
498
+
499
+ <BranchInfoCards
500
+ branch={branch}
501
+ formattedCommitDate={formattedCommitDate}
502
+ latestToolUsage={latestToolUsage}
503
+ />
875
504
  </div>
876
505
 
877
- <div className="terminal-column">
878
- {activeSessionId ? (
879
- <section
880
- className={`section-card terminal-section ${
881
- isTerminalFullscreen ? "terminal-section--fullscreen" : ""
882
- }`}
883
- data-testid="active-terminal"
884
- >
885
- <div className="terminal-section__header">
886
- <div>
887
- <h2>ターミナルセッション</h2>
888
- <p className="section-card__body">
889
- 出力はリアルタイムにストリームされます。終了するとこのパネルは自動で閉じます。
890
- </p>
891
- </div>
892
- <div className="terminal-section__controls">
893
- <button
894
- type="button"
895
- className="button button--ghost"
896
- onClick={() => setIsTerminalFullscreen((prev) => !prev)}
897
- >
898
- {isTerminalFullscreen
899
- ? "通常表示に戻す"
900
- : "ターミナルを最大化"}
901
- </button>
902
- </div>
903
- </div>
904
- <div className="terminal-surface">
905
- <Terminal
906
- sessionId={activeSessionId}
907
- onExit={handleSessionExit}
908
- onError={(message) =>
909
- setBanner({
910
- type: "error",
911
- message: message ?? "不明なエラー",
912
- })
913
- }
914
- />
915
- </div>
916
- {isTerminalFullscreen && (
917
- <button
918
- type="button"
919
- className="terminal-section__close"
920
- aria-label="ターミナルを閉じる"
921
- onClick={() => setIsTerminalFullscreen(false)}
922
- >
923
- ×
924
- </button>
925
- )}
926
- </section>
927
- ) : (
928
- <section className="section-card session-hint">
929
- <header>
930
- <h2>セッションは未起動</h2>
931
- </header>
932
- <p className="section-card__body">
933
- 上部のアクションからAIツールを起動すると、このエリアにターミナルが表示されます。
934
- </p>
935
- </section>
936
- )}
506
+ {/* Right Column - Terminal */}
507
+ <div className="lg:sticky lg:top-6 lg:self-start">
508
+ <TerminalPanel
509
+ sessionId={activeSessionId}
510
+ isFullscreen={isTerminalFullscreen}
511
+ onToggleFullscreen={() =>
512
+ setIsTerminalFullscreen((prev) => !prev)
513
+ }
514
+ onExit={handleSessionExit}
515
+ onError={(message) =>
516
+ setBanner({ type: "error", message: message ?? "不明なエラー" })
517
+ }
518
+ />
937
519
  </div>
938
520
  </div>
939
521
  </main>
@@ -941,91 +523,33 @@ export function BranchDetailPage() {
941
523
  );
942
524
  }
943
525
 
944
- function formatDate(value?: string | null) {
945
- if (!value) {
946
- return "日時不明";
947
- }
948
-
526
+ // Helper functions
527
+ function formatDate(value?: string | null): string {
528
+ if (!value) return "日時不明";
949
529
  try {
950
- const date = new Date(value);
951
530
  return new Intl.DateTimeFormat("ja-JP", {
952
531
  year: "numeric",
953
532
  month: "short",
954
533
  day: "numeric",
955
534
  hour: "2-digit",
956
535
  minute: "2-digit",
957
- }).format(date);
958
- } catch (_err) {
536
+ }).format(new Date(value));
537
+ } catch {
959
538
  return value;
960
539
  }
961
540
  }
962
541
 
963
- function formatError(error: unknown, fallback: string) {
542
+ function formatError(error: unknown, fallback: string): string {
964
543
  if (error instanceof ApiError) {
965
544
  return `${error.message}${error.details ? `\n${error.details}` : ""}`;
966
545
  }
967
- if (error instanceof Error) {
968
- return error.message;
969
- }
546
+ if (error instanceof Error) return error.message;
970
547
  return fallback;
971
548
  }
972
549
 
973
- function toolLabel(tool: ToolType, selectedTool?: SelectableTool) {
974
- if (tool === "custom" && selectedTool?.target === "custom") {
550
+ function toolLabel(tool: string, selectedTool?: SelectableTool): string {
551
+ if (tool === "custom" && selectedTool?.target === "custom")
975
552
  return selectedTool.label;
976
- }
977
- if (tool === "codex-cli") {
978
- return "Codex CLI";
979
- }
553
+ if (tool === "codex-cli") return "Codex CLI";
980
554
  return "Claude Code";
981
555
  }
982
-
983
- function renderArgs(args?: string[] | null) {
984
- if (!args || args.length === 0) {
985
- return <span className="tool-card__muted">未設定</span>;
986
- }
987
- return args.join(" ");
988
- }
989
-
990
- const SESSION_STATUS_LABEL: Record<
991
- "pending" | "running" | "completed" | "failed",
992
- string
993
- > = {
994
- pending: "pending",
995
- running: "running",
996
- completed: "completed",
997
- failed: "failed",
998
- };
999
-
1000
- function renderToolUsage(usage: LastToolUsage): string {
1001
- const modeLabel =
1002
- usage.mode === "normal"
1003
- ? "New"
1004
- : usage.mode === "continue"
1005
- ? "Continue"
1006
- : usage.mode === "resume"
1007
- ? "Resume"
1008
- : null;
1009
- const toolText = mapToolLabel(usage.toolId, usage.toolLabel);
1010
- return [toolText, modeLabel, usage.model].filter(Boolean).join(" | ");
1011
- }
1012
-
1013
- function formatUsageTimestamp(value: number): string {
1014
- return formatDate(new Date(value).toISOString());
1015
- }
1016
-
1017
- function mapToolLabel(toolId: string, toolLabel?: string | null): string {
1018
- if (toolId === "claude-code") return "Claude";
1019
- if (toolId === "codex-cli") return "Codex";
1020
- if (toolId === "gemini-cli") return "Gemini";
1021
- if (toolId === "qwen-cli") return "Qwen";
1022
- if (toolLabel) return toolLabel;
1023
- return "Custom";
1024
- }
1025
-
1026
- function parseExtraArgs(value: string): string[] {
1027
- return value
1028
- .split(/\s+/)
1029
- .map((chunk) => chunk.trim())
1030
- .filter(Boolean);
1031
- }