@akiojin/gwt 4.2.0 → 4.3.1

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 (119) hide show
  1. package/dist/claude.d.ts +2 -0
  2. package/dist/claude.d.ts.map +1 -1
  3. package/dist/claude.js +49 -2
  4. package/dist/claude.js.map +1 -1
  5. package/dist/cli/ui/components/App.d.ts.map +1 -1
  6. package/dist/cli/ui/components/App.js +68 -68
  7. package/dist/cli/ui/components/App.js.map +1 -1
  8. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  9. package/dist/cli/ui/components/screens/BranchListScreen.js +6 -1
  10. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  11. package/dist/client/assets/index-ChHC-Puh.css +1 -0
  12. package/dist/client/assets/index-PqK9jkug.js +78 -0
  13. package/dist/client/index.html +2 -2
  14. package/dist/config/builtin-tools.d.ts.map +1 -1
  15. package/dist/config/builtin-tools.js +3 -0
  16. package/dist/config/builtin-tools.js.map +1 -1
  17. package/dist/config/tools.d.ts.map +1 -1
  18. package/dist/config/tools.js +10 -1
  19. package/dist/config/tools.js.map +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/launcher.d.ts.map +1 -1
  24. package/dist/launcher.js +15 -0
  25. package/dist/launcher.js.map +1 -1
  26. package/dist/services/aiToolResolver.d.ts.map +1 -1
  27. package/dist/services/aiToolResolver.js +55 -8
  28. package/dist/services/aiToolResolver.js.map +1 -1
  29. package/dist/services/customToolResolver.d.ts.map +1 -1
  30. package/dist/services/customToolResolver.js +22 -17
  31. package/dist/services/customToolResolver.js.map +1 -1
  32. package/dist/utils/webui.js +1 -1
  33. package/dist/web/client/src/components/BranchGraph.d.ts +5 -0
  34. package/dist/web/client/src/components/BranchGraph.d.ts.map +1 -1
  35. package/dist/web/client/src/components/BranchGraph.js +35 -108
  36. package/dist/web/client/src/components/BranchGraph.js.map +1 -1
  37. package/dist/web/client/src/components/graph/BranchDetailPanel.d.ts +15 -0
  38. package/dist/web/client/src/components/graph/BranchDetailPanel.d.ts.map +1 -0
  39. package/dist/web/client/src/components/graph/BranchDetailPanel.js +57 -0
  40. package/dist/web/client/src/components/graph/BranchDetailPanel.js.map +1 -0
  41. package/dist/web/client/src/components/graph/BranchNode.d.ts +13 -0
  42. package/dist/web/client/src/components/graph/BranchNode.d.ts.map +1 -0
  43. package/dist/web/client/src/components/graph/BranchNode.js +103 -0
  44. package/dist/web/client/src/components/graph/BranchNode.js.map +1 -0
  45. package/dist/web/client/src/components/graph/ClusterNode.d.ts +13 -0
  46. package/dist/web/client/src/components/graph/ClusterNode.d.ts.map +1 -0
  47. package/dist/web/client/src/components/graph/ClusterNode.js +109 -0
  48. package/dist/web/client/src/components/graph/ClusterNode.js.map +1 -0
  49. package/dist/web/client/src/components/graph/SynapticCanvas.d.ts +17 -0
  50. package/dist/web/client/src/components/graph/SynapticCanvas.d.ts.map +1 -0
  51. package/dist/web/client/src/components/graph/SynapticCanvas.js +94 -0
  52. package/dist/web/client/src/components/graph/SynapticCanvas.js.map +1 -0
  53. package/dist/web/client/src/components/graph/SynapticEdge.d.ts +13 -0
  54. package/dist/web/client/src/components/graph/SynapticEdge.d.ts.map +1 -0
  55. package/dist/web/client/src/components/graph/SynapticEdge.js +113 -0
  56. package/dist/web/client/src/components/graph/SynapticEdge.js.map +1 -0
  57. package/dist/web/client/src/components/graph/graphUtils.d.ts +67 -0
  58. package/dist/web/client/src/components/graph/graphUtils.d.ts.map +1 -0
  59. package/dist/web/client/src/components/graph/graphUtils.js +175 -0
  60. package/dist/web/client/src/components/graph/graphUtils.js.map +1 -0
  61. package/dist/web/client/src/components/graph/index.d.ts +10 -0
  62. package/dist/web/client/src/components/graph/index.d.ts.map +1 -0
  63. package/dist/web/client/src/components/graph/index.js +10 -0
  64. package/dist/web/client/src/components/graph/index.js.map +1 -0
  65. package/dist/web/client/src/lib/websocket.d.ts.map +1 -1
  66. package/dist/web/client/src/lib/websocket.js +2 -1
  67. package/dist/web/client/src/lib/websocket.js.map +1 -1
  68. package/dist/web/client/vite.config.js +1 -1
  69. package/dist/web/server/env/importer.d.ts.map +1 -1
  70. package/dist/web/server/env/importer.js +4 -0
  71. package/dist/web/server/env/importer.js.map +1 -1
  72. package/dist/web/server/index.d.ts.map +1 -1
  73. package/dist/web/server/index.js +9 -0
  74. package/dist/web/server/index.js.map +1 -1
  75. package/dist/web/server/pty/manager.d.ts.map +1 -1
  76. package/dist/web/server/pty/manager.js +24 -1
  77. package/dist/web/server/pty/manager.js.map +1 -1
  78. package/dist/web/server/routes/sessions.d.ts.map +1 -1
  79. package/dist/web/server/routes/sessions.js +7 -0
  80. package/dist/web/server/routes/sessions.js.map +1 -1
  81. package/dist/web/server/tray.d.ts +1 -1
  82. package/dist/web/server/tray.d.ts.map +1 -1
  83. package/dist/web/server/tray.js +52 -34
  84. package/dist/web/server/tray.js.map +1 -1
  85. package/dist/web/server/websocket/handler.d.ts.map +1 -1
  86. package/dist/web/server/websocket/handler.js +4 -0
  87. package/dist/web/server/websocket/handler.js.map +1 -1
  88. package/package.json +5 -2
  89. package/src/claude.ts +57 -2
  90. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +104 -0
  91. package/src/cli/ui/components/App.tsx +91 -81
  92. package/src/cli/ui/components/screens/BranchListScreen.tsx +6 -1
  93. package/src/cli/ui/types.ts +1 -1
  94. package/src/config/builtin-tools.ts +3 -0
  95. package/src/config/tools.ts +24 -1
  96. package/src/index.ts +3 -1
  97. package/src/launcher.ts +26 -0
  98. package/src/services/aiToolResolver.ts +75 -9
  99. package/src/services/customToolResolver.ts +32 -17
  100. package/src/utils/webui.ts +1 -1
  101. package/src/web/client/src/components/BranchGraph.tsx +51 -208
  102. package/src/web/client/src/components/graph/BranchDetailPanel.tsx +152 -0
  103. package/src/web/client/src/components/graph/BranchNode.tsx +200 -0
  104. package/src/web/client/src/components/graph/ClusterNode.tsx +211 -0
  105. package/src/web/client/src/components/graph/SynapticCanvas.tsx +171 -0
  106. package/src/web/client/src/components/graph/SynapticEdge.tsx +311 -0
  107. package/src/web/client/src/components/graph/graphUtils.ts +265 -0
  108. package/src/web/client/src/components/graph/index.ts +10 -0
  109. package/src/web/client/src/index.css +314 -29
  110. package/src/web/client/src/lib/websocket.ts +2 -1
  111. package/src/web/client/vite.config.ts +1 -1
  112. package/src/web/server/env/importer.ts +5 -0
  113. package/src/web/server/index.ts +10 -0
  114. package/src/web/server/pty/manager.ts +43 -1
  115. package/src/web/server/routes/sessions.ts +15 -0
  116. package/src/web/server/tray.ts +62 -46
  117. package/src/web/server/websocket/handler.ts +13 -0
  118. package/dist/client/assets/index-DsDNCy5f.css +0 -1
  119. package/dist/client/assets/index-v8smkNOL.js +0 -72
@@ -1,12 +1,16 @@
1
1
  import { execa } from "execa";
2
2
  import { platform } from "os";
3
3
  import { getToolById } from "../config/tools.js";
4
+ import { CLAUDE_CODE_TOOL } from "../config/builtin-tools.js";
4
5
  import {
5
6
  CODEX_DEFAULT_ARGS,
6
7
  CLAUDE_PERMISSION_SKIP_ARGS,
7
8
  } from "../shared/aiToolConstants.js";
8
9
  import { prepareCustomToolExecution } from "./customToolResolver.js";
9
10
  import type { LaunchOptions } from "../types/tools.js";
11
+ import { createLogger } from "../logging/logger.js";
12
+
13
+ const logger = createLogger({ category: "resolver" });
10
14
 
11
15
  const DETECTION_COMMAND = platform() === "win32" ? "where" : "which";
12
16
  const MIN_BUN_MAJOR = 1;
@@ -41,12 +45,32 @@ export class AIToolResolutionError extends Error {
41
45
  async function commandExists(command: string): Promise<boolean> {
42
46
  try {
43
47
  await execa(DETECTION_COMMAND, [command], { shell: true });
48
+ logger.debug({ command, exists: true }, "Command check");
44
49
  return true;
45
50
  } catch {
51
+ logger.debug({ command, exists: false }, "Command check");
46
52
  return false;
47
53
  }
48
54
  }
49
55
 
56
+ /**
57
+ * コマンドのフルパスを取得
58
+ * node-ptyはシェルを経由しないため、フルパスが必要
59
+ */
60
+ async function resolveCommandPath(command: string): Promise<string | null> {
61
+ try {
62
+ const { stdout } = await execa(DETECTION_COMMAND, [command], {
63
+ shell: true,
64
+ });
65
+ const fullPath = stdout.trim().split("\n")[0];
66
+ logger.debug({ command, fullPath }, "Command path resolved");
67
+ return fullPath || null;
68
+ } catch {
69
+ logger.debug({ command, fullPath: null }, "Command path resolution failed");
70
+ return null;
71
+ }
72
+ }
73
+
50
74
  let bunxCheckPromise: Promise<void> | null = null;
51
75
 
52
76
  async function ensureBunxAvailable(): Promise<void> {
@@ -67,6 +91,7 @@ async function ensureBunxAvailable(): Promise<void> {
67
91
  try {
68
92
  const { stdout } = await execa("bun", ["--version"]);
69
93
  const version = stdout.trim();
94
+ logger.debug({ bunVersion: version }, "Bun version detected");
70
95
  const major = parseInt(version.split(".")[0] ?? "0", 10);
71
96
  if (!Number.isFinite(major) || major < MIN_BUN_MAJOR) {
72
97
  throw new AIToolResolutionError(
@@ -144,20 +169,40 @@ export async function resolveClaudeCommand(
144
169
  options: ClaudeCommandOptions = {},
145
170
  ): Promise<ResolvedCommand> {
146
171
  const args = buildClaudeArgs(options);
147
-
148
- if (await commandExists("claude")) {
172
+ const envOverrides = CLAUDE_CODE_TOOL.env
173
+ ? { env: { ...CLAUDE_CODE_TOOL.env } as NodeJS.ProcessEnv }
174
+ : {};
175
+
176
+ // フルパスを取得(node-ptyはシェルを経由しないため必要)
177
+ const claudePath = await resolveCommandPath("claude");
178
+ if (claudePath) {
179
+ logger.info(
180
+ { command: claudePath, usesFallback: false },
181
+ "Claude command resolved",
182
+ );
149
183
  return {
150
- command: "claude",
184
+ command: claudePath,
151
185
  args,
152
186
  usesFallback: false,
187
+ ...envOverrides,
153
188
  };
154
189
  }
155
190
 
156
- await ensureBunxAvailable();
191
+ // bunxへフォールバック
192
+ const bunxPath = await resolveCommandPath("bunx");
193
+ if (!bunxPath) {
194
+ await ensureBunxAvailable(); // エラーをスローする
195
+ }
196
+
197
+ logger.info(
198
+ { command: bunxPath ?? "bunx", usesFallback: true },
199
+ "Claude command resolved (fallback)",
200
+ );
157
201
  return {
158
- command: "bunx",
202
+ command: bunxPath ?? "bunx",
159
203
  args: [CLAUDE_CLI_PACKAGE, ...args],
160
204
  usesFallback: true,
205
+ ...envOverrides,
161
206
  };
162
207
  }
163
208
 
@@ -198,17 +243,32 @@ export async function resolveCodexCommand(
198
243
  ): Promise<ResolvedCommand> {
199
244
  const args = buildCodexArgs(options);
200
245
 
201
- if (await commandExists("codex")) {
246
+ // フルパスを取得(node-ptyはシェルを経由しないため必要)
247
+ const codexPath = await resolveCommandPath("codex");
248
+ if (codexPath) {
249
+ logger.info(
250
+ { command: codexPath, usesFallback: false },
251
+ "Codex command resolved",
252
+ );
202
253
  return {
203
- command: "codex",
254
+ command: codexPath,
204
255
  args,
205
256
  usesFallback: false,
206
257
  };
207
258
  }
208
259
 
209
- await ensureBunxAvailable();
260
+ // bunxへフォールバック
261
+ const bunxPath = await resolveCommandPath("bunx");
262
+ if (!bunxPath) {
263
+ await ensureBunxAvailable(); // エラーをスローする
264
+ }
265
+
266
+ logger.info(
267
+ { command: bunxPath ?? "bunx", usesFallback: true },
268
+ "Codex command resolved (fallback)",
269
+ );
210
270
  return {
211
- command: "bunx",
271
+ command: bunxPath ?? "bunx",
212
272
  args: [CODEX_CLI_PACKAGE, ...args],
213
273
  usesFallback: true,
214
274
  };
@@ -223,6 +283,7 @@ export async function resolveCustomToolCommand(
223
283
  ): Promise<ResolvedCommand> {
224
284
  const tool = await getToolById(options.toolId);
225
285
  if (!tool) {
286
+ logger.error({ toolId: options.toolId }, "Custom tool not found");
226
287
  throw new AIToolResolutionError(
227
288
  "CUSTOM_TOOL_NOT_FOUND",
228
289
  `Custom tool not found: ${options.toolId}`,
@@ -235,6 +296,11 @@ export async function resolveCustomToolCommand(
235
296
 
236
297
  const execution = await prepareCustomToolExecution(tool, options);
237
298
 
299
+ logger.info(
300
+ { toolId: options.toolId, command: execution.command },
301
+ "Custom tool command resolved",
302
+ );
303
+
238
304
  return {
239
305
  command: execution.command,
240
306
  args: execution.args,
@@ -1,5 +1,8 @@
1
1
  import { execa } from "execa";
2
2
  import type { CustomAITool, LaunchOptions } from "../types/tools.js";
3
+ import { createLogger } from "../logging/logger.js";
4
+
5
+ const logger = createLogger({ category: "custom-resolver" });
3
6
 
4
7
  export interface CustomToolExecutionPlan {
5
8
  command: string;
@@ -21,6 +24,7 @@ export async function resolveCommandPath(commandName: string): Promise<string> {
21
24
  );
22
25
  }
23
26
 
27
+ logger.debug({ commandName, resolvedPath }, "Command path resolved");
24
28
  return resolvedPath;
25
29
  } catch (error) {
26
30
  const reason = error instanceof Error ? error.message : String(error);
@@ -55,6 +59,10 @@ export function buildCustomToolArgs(
55
59
  args.push(...options.extraArgs);
56
60
  }
57
61
 
62
+ logger.debug(
63
+ { toolId: tool.id, argsCount: args.length },
64
+ "Custom tool args built",
65
+ );
58
66
  return args;
59
67
  }
60
68
 
@@ -62,37 +70,44 @@ export async function prepareCustomToolExecution(
62
70
  tool: CustomAITool,
63
71
  options: LaunchOptions = {},
64
72
  ): Promise<CustomToolExecutionPlan> {
65
- const args = buildCustomToolArgs(tool, options);
73
+ const baseArgs = buildCustomToolArgs(tool, options);
66
74
  const envOverrides: NodeJS.ProcessEnv | undefined = tool.env
67
75
  ? ({ ...tool.env } as NodeJS.ProcessEnv)
68
76
  : undefined;
69
77
 
78
+ let command: string;
79
+ let args: string[];
80
+
70
81
  switch (tool.type) {
71
82
  case "path": {
72
- return {
73
- command: tool.command,
74
- args,
75
- ...(envOverrides ? { env: envOverrides } : {}),
76
- };
83
+ command = tool.command;
84
+ args = baseArgs;
85
+ break;
77
86
  }
78
87
  case "bunx": {
79
- return {
80
- command: "bunx",
81
- args: [tool.command, ...args],
82
- ...(envOverrides ? { env: envOverrides } : {}),
83
- };
88
+ command = "bunx";
89
+ args = [tool.command, ...baseArgs];
90
+ break;
84
91
  }
85
92
  case "command": {
86
- const resolved = await resolveCommandPath(tool.command);
87
- return {
88
- command: resolved,
89
- args,
90
- ...(envOverrides ? { env: envOverrides } : {}),
91
- };
93
+ command = await resolveCommandPath(tool.command);
94
+ args = baseArgs;
95
+ break;
92
96
  }
93
97
  default: {
94
98
  const exhaustive: never = tool.type;
95
99
  throw new Error(`Unknown custom tool type: ${exhaustive as string}`);
96
100
  }
97
101
  }
102
+
103
+ logger.debug(
104
+ { toolId: tool.id, toolType: tool.type, command, hasEnv: !!envOverrides },
105
+ "Custom tool execution prepared",
106
+ );
107
+
108
+ return {
109
+ command,
110
+ args,
111
+ ...(envOverrides ? { env: envOverrides } : {}),
112
+ };
98
113
  }
@@ -2,7 +2,7 @@ import * as net from "node:net";
2
2
 
3
3
  export function resolveWebUiPort(
4
4
  portEnv: string | undefined = process.env.PORT,
5
- defaultPort = 3000,
5
+ defaultPort = 3001,
6
6
  ): number {
7
7
  if (!portEnv) {
8
8
  return defaultPort;
@@ -1,89 +1,31 @@
1
- import React, { useMemo } from "react";
2
- import { Link } from "react-router-dom";
1
+ /**
2
+ * ブランチグラフ - シナプス風ビジュアライゼーション
3
+ *
4
+ * React Flow + D3-forceによるインタラクティブなブランチ関係図
5
+ */
6
+
7
+ import React, { useState, useCallback } from "react";
3
8
  import { Card, CardHeader, CardContent } from "@/components/ui/card";
4
9
  import { Badge } from "@/components/ui/badge";
5
- import { cn } from "@/lib/utils";
6
10
  import type { Branch } from "../../../../types/api.js";
7
-
8
- const UNKNOWN_BASE = "__unknown__";
9
-
10
- interface Lane {
11
- id: string;
12
- baseLabel: string;
13
- baseNode: Branch | null;
14
- nodes: Branch[];
15
- isSyntheticBase: boolean;
16
- }
11
+ import { SynapticCanvas, BranchDetailPanel } from "./graph";
17
12
 
18
13
  interface BranchGraphProps {
19
14
  branches: Branch[];
20
15
  }
21
16
 
22
- function formatBranchLabel(branch: Branch): string {
23
- return branch.name.length > 32
24
- ? `${branch.name.slice(0, 29)}...`
25
- : branch.name;
26
- }
27
-
28
- function getDivergenceLabel(branch: Branch): string {
29
- if (!branch.divergence) {
30
- return "divergence: n/a";
31
- }
32
- const { ahead, behind, upToDate } = branch.divergence;
33
- if (upToDate) {
34
- return "divergence: up-to-date";
35
- }
36
- return `divergence: +${ahead} / -${behind}`;
37
- }
38
-
39
17
  export function BranchGraph({ branches }: BranchGraphProps) {
40
- const branchMap = useMemo(() => {
41
- return new Map(branches.map((branch) => [branch.name, branch]));
42
- }, [branches]);
43
-
44
- const referencedBases = useMemo(() => {
45
- const baseSet = new Set<string>();
46
- branches.forEach((branch) => {
47
- if (branch.baseBranch) {
48
- baseSet.add(branch.baseBranch);
49
- }
50
- });
51
- return baseSet;
52
- }, [branches]);
53
-
54
- const lanes = useMemo<Lane[]>(() => {
55
- const laneMap = new Map<string, Lane>();
56
-
57
- branches.forEach((branch) => {
58
- const base = branch.baseBranch ?? UNKNOWN_BASE;
18
+ const [selectedBranch, setSelectedBranch] = useState<Branch | null>(null);
59
19
 
60
- if (!branch.baseBranch && referencedBases.has(branch.name)) {
61
- return;
62
- }
20
+ const handleNodeClick = useCallback((branch: Branch | null) => {
21
+ setSelectedBranch(branch);
22
+ }, []);
63
23
 
64
- if (!laneMap.has(base)) {
65
- const baseNode =
66
- base !== UNKNOWN_BASE ? (branchMap.get(base) ?? null) : null;
67
- laneMap.set(base, {
68
- id: base,
69
- baseLabel: base === UNKNOWN_BASE ? "ベース不明" : base,
70
- baseNode,
71
- nodes: [],
72
- isSyntheticBase: baseNode === null,
73
- });
74
- }
24
+ const handlePanelClose = useCallback(() => {
25
+ setSelectedBranch(null);
26
+ }, []);
75
27
 
76
- laneMap.get(base)?.nodes.push(branch);
77
- });
78
-
79
- return Array.from(laneMap.values()).sort((a, b) => {
80
- if (a.id === UNKNOWN_BASE) return 1;
81
- if (b.id === UNKNOWN_BASE) return -1;
82
- return a.baseLabel.localeCompare(b.baseLabel, "ja");
83
- });
84
- }, [branches, branchMap, referencedBases]);
85
-
86
- if (!lanes.length) {
28
+ if (!branches.length) {
87
29
  return (
88
30
  <Card className="border-dashed">
89
31
  <CardContent className="flex flex-col items-center justify-center py-12 text-center">
@@ -99,154 +41,55 @@ export function BranchGraph({ branches }: BranchGraphProps) {
99
41
  }
100
42
 
101
43
  return (
102
- <Card>
44
+ <Card className="relative overflow-hidden">
103
45
  <CardHeader className="pb-4">
104
46
  <div className="flex flex-wrap items-start justify-between gap-4">
105
47
  <div>
106
48
  <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
107
- BRANCH GRAPH
49
+ SYNAPTIC GRAPH
108
50
  </p>
109
- <h2 className="mt-1 text-lg font-semibold">
110
- ベースブランチの関係をグラフィカルに把握
111
- </h2>
51
+ <h2 className="mt-1 text-lg font-semibold">ブランチネットワーク</h2>
112
52
  <p className="mt-1 text-sm text-muted-foreground">
113
- baseRef、Git
114
- upstream、merge-baseヒューリスティクスを用いて推定したベースブランチ単位で派生ノードをレーン表示します。
53
+ クリックでノードを展開・詳細表示。ドラッグで移動、スクロールでズーム。
115
54
  </p>
116
55
  </div>
117
56
  <div className="flex flex-wrap gap-2">
118
- <Badge variant="outline">Base</Badge>
119
- <Badge variant="local">Local</Badge>
120
- <Badge variant="remote">Remote</Badge>
121
- <Badge variant="success">Worktree</Badge>
57
+ <Badge variant="outline" className="flex items-center gap-1">
58
+ <span className="h-2 w-2 rounded-full bg-muted-foreground/30" />
59
+ Cluster
60
+ </Badge>
61
+ <Badge variant="local" className="flex items-center gap-1">
62
+ <span className="h-2 w-2 rounded-full bg-local" />
63
+ Local
64
+ </Badge>
65
+ <Badge variant="remote" className="flex items-center gap-1">
66
+ <span className="h-2 w-2 rounded-full bg-remote" />
67
+ Remote
68
+ </Badge>
69
+ <Badge variant="success" className="flex items-center gap-1">
70
+ <span className="h-2 w-2 rounded-full bg-success" />
71
+ Worktree
72
+ </Badge>
122
73
  </div>
123
74
  </div>
124
75
  </CardHeader>
125
76
 
126
- <CardContent className="space-y-4">
127
- {lanes.map((lane) => (
128
- <article key={lane.id} className="rounded-lg border bg-muted/30 p-4">
129
- <div className="mb-3 flex items-center justify-between">
130
- <div className="flex items-center gap-2">
131
- <span className="font-semibold">{lane.baseLabel}</span>
132
- {lane.baseNode && (
133
- <Badge
134
- variant={
135
- lane.baseNode.type === "local" ? "local" : "remote"
136
- }
137
- className="text-xs"
138
- >
139
- {lane.baseNode.type === "local" ? "LOCAL" : "REMOTE"}
140
- </Badge>
141
- )}
142
- {lane.isSyntheticBase && (
143
- <Badge
144
- variant="outline"
145
- className="text-xs text-muted-foreground"
146
- >
147
- 推定のみ
148
- </Badge>
149
- )}
150
- </div>
151
- <span className="text-sm text-muted-foreground">
152
- {lane.nodes.length} branch{lane.nodes.length > 1 ? "es" : ""}
153
- </span>
154
- </div>
155
-
156
- <div className="flex flex-wrap gap-2">
157
- {renderBaseNode(lane)}
158
- {lane.nodes.map((branch) => (
159
- <BranchNode key={branch.name} branch={branch} />
160
- ))}
161
- </div>
162
- </article>
163
- ))}
77
+ <CardContent className="relative p-0">
78
+ {/* キャンバスコンテナ */}
79
+ <div className="relative h-[500px] w-full">
80
+ <SynapticCanvas
81
+ branches={branches}
82
+ onNodeClick={handleNodeClick}
83
+ className="h-full w-full"
84
+ />
85
+
86
+ {/* 詳細パネル */}
87
+ <BranchDetailPanel
88
+ branch={selectedBranch}
89
+ onClose={handlePanelClose}
90
+ />
91
+ </div>
164
92
  </CardContent>
165
93
  </Card>
166
94
  );
167
95
  }
168
-
169
- function renderBaseNode(lane: Lane) {
170
- const label =
171
- lane.baseLabel === "ベース不明" ? "Unknown base" : lane.baseLabel;
172
-
173
- const content = (
174
- <div
175
- className={cn(
176
- "group relative rounded-md border bg-card px-3 py-2 transition-colors hover:border-muted-foreground/50",
177
- lane.baseNode?.type === "local" && "border-l-2 border-l-local",
178
- lane.baseNode?.type === "remote" && "border-l-2 border-l-remote",
179
- )}
180
- >
181
- <span className="block truncate text-sm font-medium">{label}</span>
182
- <span className="text-xs text-muted-foreground">BASE</span>
183
-
184
- {/* Tooltip */}
185
- <div className="invisible absolute bottom-full left-0 z-10 mb-2 w-48 rounded-md border bg-popover p-2 text-xs shadow-md group-hover:visible">
186
- <p className="font-medium">{label}</p>
187
- <p className="text-muted-foreground">
188
- {lane.baseNode
189
- ? `type: ${lane.baseNode.type}`
190
- : "推定されたベースブランチ"}
191
- </p>
192
- </div>
193
- </div>
194
- );
195
-
196
- if (lane.baseNode) {
197
- return (
198
- <Link
199
- key={`base-${lane.id}`}
200
- to={`/${encodeURIComponent(lane.baseNode.name)}`}
201
- className="block"
202
- aria-label={`ベースブランチ ${lane.baseNode.name} を開く`}
203
- >
204
- {content}
205
- </Link>
206
- );
207
- }
208
-
209
- return <div key={`base-${lane.id}`}>{content}</div>;
210
- }
211
-
212
- function BranchNode({ branch }: { branch: Branch }) {
213
- const node = (
214
- <div
215
- className={cn(
216
- "group relative rounded-md border bg-card px-3 py-2 transition-colors hover:border-muted-foreground/50",
217
- branch.type === "local" && "border-l-2 border-l-local",
218
- branch.type === "remote" && "border-l-2 border-l-remote",
219
- branch.mergeStatus === "merged" && "opacity-60",
220
- )}
221
- >
222
- <span className="block truncate text-sm font-medium">
223
- {formatBranchLabel(branch)}
224
- </span>
225
- <span className="text-xs text-muted-foreground">
226
- {branch.worktreePath ? "Worktree" : "No Worktree"}
227
- </span>
228
-
229
- {/* Tooltip */}
230
- <div className="invisible absolute bottom-full left-0 z-10 mb-2 w-56 rounded-md border bg-popover p-2 text-xs shadow-md group-hover:visible">
231
- <p className="font-medium">{branch.name}</p>
232
- <p className="text-muted-foreground">
233
- base: {branch.baseBranch ?? "unknown"}
234
- </p>
235
- <p className="text-muted-foreground">{getDivergenceLabel(branch)}</p>
236
- <p className="text-muted-foreground">
237
- {branch.worktreePath ?? "Worktree未作成"}
238
- </p>
239
- </div>
240
- </div>
241
- );
242
-
243
- return (
244
- <Link
245
- to={`/${encodeURIComponent(branch.name)}`}
246
- className="block"
247
- aria-label={`${branch.name} の詳細を開く`}
248
- >
249
- {node}
250
- </Link>
251
- );
252
- }