@akiojin/gwt 4.1.1 → 4.3.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 (135) hide show
  1. package/README.md +28 -3
  2. package/dist/claude.d.ts +4 -0
  3. package/dist/claude.d.ts.map +1 -1
  4. package/dist/claude.js +13 -1
  5. package/dist/claude.js.map +1 -1
  6. package/dist/cli/ui/components/App.d.ts.map +1 -1
  7. package/dist/cli/ui/components/App.js +68 -68
  8. package/dist/cli/ui/components/App.js.map +1 -1
  9. package/dist/cli/ui/components/common/Select.d.ts +3 -1
  10. package/dist/cli/ui/components/common/Select.d.ts.map +1 -1
  11. package/dist/cli/ui/components/common/Select.js +13 -2
  12. package/dist/cli/ui/components/common/Select.js.map +1 -1
  13. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  14. package/dist/cli/ui/components/screens/BranchListScreen.js +6 -1
  15. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  16. package/dist/client/assets/index-ChHC-Puh.css +1 -0
  17. package/dist/client/assets/index-PqK9jkug.js +78 -0
  18. package/dist/client/index.html +2 -2
  19. package/dist/config/builtin-tools.d.ts.map +1 -1
  20. package/dist/config/builtin-tools.js +3 -0
  21. package/dist/config/builtin-tools.js.map +1 -1
  22. package/dist/config/tools.d.ts.map +1 -1
  23. package/dist/config/tools.js +10 -1
  24. package/dist/config/tools.js.map +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +37 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/launcher.d.ts.map +1 -1
  29. package/dist/launcher.js +15 -0
  30. package/dist/launcher.js.map +1 -1
  31. package/dist/services/aiToolResolver.d.ts.map +1 -1
  32. package/dist/services/aiToolResolver.js +55 -8
  33. package/dist/services/aiToolResolver.js.map +1 -1
  34. package/dist/services/customToolResolver.d.ts.map +1 -1
  35. package/dist/services/customToolResolver.js +22 -17
  36. package/dist/services/customToolResolver.js.map +1 -1
  37. package/dist/utils/prompt.d.ts +12 -0
  38. package/dist/utils/prompt.d.ts.map +1 -1
  39. package/dist/utils/prompt.js +60 -10
  40. package/dist/utils/prompt.js.map +1 -1
  41. package/dist/utils/webui.js +1 -1
  42. package/dist/web/client/src/components/BranchGraph.d.ts +5 -0
  43. package/dist/web/client/src/components/BranchGraph.d.ts.map +1 -1
  44. package/dist/web/client/src/components/BranchGraph.js +35 -108
  45. package/dist/web/client/src/components/BranchGraph.js.map +1 -1
  46. package/dist/web/client/src/components/graph/BranchDetailPanel.d.ts +15 -0
  47. package/dist/web/client/src/components/graph/BranchDetailPanel.d.ts.map +1 -0
  48. package/dist/web/client/src/components/graph/BranchDetailPanel.js +57 -0
  49. package/dist/web/client/src/components/graph/BranchDetailPanel.js.map +1 -0
  50. package/dist/web/client/src/components/graph/BranchNode.d.ts +13 -0
  51. package/dist/web/client/src/components/graph/BranchNode.d.ts.map +1 -0
  52. package/dist/web/client/src/components/graph/BranchNode.js +103 -0
  53. package/dist/web/client/src/components/graph/BranchNode.js.map +1 -0
  54. package/dist/web/client/src/components/graph/ClusterNode.d.ts +13 -0
  55. package/dist/web/client/src/components/graph/ClusterNode.d.ts.map +1 -0
  56. package/dist/web/client/src/components/graph/ClusterNode.js +109 -0
  57. package/dist/web/client/src/components/graph/ClusterNode.js.map +1 -0
  58. package/dist/web/client/src/components/graph/SynapticCanvas.d.ts +17 -0
  59. package/dist/web/client/src/components/graph/SynapticCanvas.d.ts.map +1 -0
  60. package/dist/web/client/src/components/graph/SynapticCanvas.js +94 -0
  61. package/dist/web/client/src/components/graph/SynapticCanvas.js.map +1 -0
  62. package/dist/web/client/src/components/graph/SynapticEdge.d.ts +13 -0
  63. package/dist/web/client/src/components/graph/SynapticEdge.d.ts.map +1 -0
  64. package/dist/web/client/src/components/graph/SynapticEdge.js +113 -0
  65. package/dist/web/client/src/components/graph/SynapticEdge.js.map +1 -0
  66. package/dist/web/client/src/components/graph/graphUtils.d.ts +67 -0
  67. package/dist/web/client/src/components/graph/graphUtils.d.ts.map +1 -0
  68. package/dist/web/client/src/components/graph/graphUtils.js +175 -0
  69. package/dist/web/client/src/components/graph/graphUtils.js.map +1 -0
  70. package/dist/web/client/src/components/graph/index.d.ts +10 -0
  71. package/dist/web/client/src/components/graph/index.d.ts.map +1 -0
  72. package/dist/web/client/src/components/graph/index.js +10 -0
  73. package/dist/web/client/src/components/graph/index.js.map +1 -0
  74. package/dist/web/client/src/lib/websocket.d.ts.map +1 -1
  75. package/dist/web/client/src/lib/websocket.js +2 -1
  76. package/dist/web/client/src/lib/websocket.js.map +1 -1
  77. package/dist/web/client/vite.config.js +1 -1
  78. package/dist/web/server/env/importer.d.ts.map +1 -1
  79. package/dist/web/server/env/importer.js +4 -0
  80. package/dist/web/server/env/importer.js.map +1 -1
  81. package/dist/web/server/index.d.ts.map +1 -1
  82. package/dist/web/server/index.js +9 -0
  83. package/dist/web/server/index.js.map +1 -1
  84. package/dist/web/server/pty/manager.d.ts.map +1 -1
  85. package/dist/web/server/pty/manager.js +24 -1
  86. package/dist/web/server/pty/manager.js.map +1 -1
  87. package/dist/web/server/routes/sessions.d.ts.map +1 -1
  88. package/dist/web/server/routes/sessions.js +7 -0
  89. package/dist/web/server/routes/sessions.js.map +1 -1
  90. package/dist/web/server/tray.d.ts +1 -1
  91. package/dist/web/server/tray.d.ts.map +1 -1
  92. package/dist/web/server/tray.js +52 -34
  93. package/dist/web/server/tray.js.map +1 -1
  94. package/dist/web/server/websocket/handler.d.ts.map +1 -1
  95. package/dist/web/server/websocket/handler.js +4 -0
  96. package/dist/web/server/websocket/handler.js.map +1 -1
  97. package/package.json +6 -3
  98. package/src/claude.ts +15 -1
  99. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +2 -1
  100. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +142 -8
  101. package/src/cli/ui/__tests__/components/App.test.tsx +4 -3
  102. package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +1 -0
  103. package/src/cli/ui/__tests__/components/common/Select.test.tsx +45 -0
  104. package/src/cli/ui/components/App.tsx +91 -81
  105. package/src/cli/ui/components/common/Select.tsx +14 -1
  106. package/src/cli/ui/components/screens/BranchListScreen.tsx +6 -1
  107. package/src/cli/ui/types.ts +1 -1
  108. package/src/config/builtin-tools.ts +3 -0
  109. package/src/config/tools.ts +24 -1
  110. package/src/index.ts +50 -3
  111. package/src/launcher.ts +26 -0
  112. package/src/services/aiToolResolver.ts +75 -9
  113. package/src/services/customToolResolver.ts +32 -17
  114. package/src/utils/__tests__/prompt.test.ts +72 -35
  115. package/src/utils/prompt.ts +79 -10
  116. package/src/utils/webui.ts +1 -1
  117. package/src/web/client/src/components/BranchGraph.tsx +51 -208
  118. package/src/web/client/src/components/graph/BranchDetailPanel.tsx +152 -0
  119. package/src/web/client/src/components/graph/BranchNode.tsx +200 -0
  120. package/src/web/client/src/components/graph/ClusterNode.tsx +211 -0
  121. package/src/web/client/src/components/graph/SynapticCanvas.tsx +171 -0
  122. package/src/web/client/src/components/graph/SynapticEdge.tsx +311 -0
  123. package/src/web/client/src/components/graph/graphUtils.ts +265 -0
  124. package/src/web/client/src/components/graph/index.ts +10 -0
  125. package/src/web/client/src/index.css +314 -29
  126. package/src/web/client/src/lib/websocket.ts +2 -1
  127. package/src/web/client/vite.config.ts +1 -1
  128. package/src/web/server/env/importer.ts +5 -0
  129. package/src/web/server/index.ts +10 -0
  130. package/src/web/server/pty/manager.ts +43 -1
  131. package/src/web/server/routes/sessions.ts +15 -0
  132. package/src/web/server/tray.ts +62 -46
  133. package/src/web/server/websocket/handler.ts +13 -0
  134. package/dist/client/assets/index-DsDNCy5f.css +0 -1
  135. package/dist/client/assets/index-v8smkNOL.js +0 -72
@@ -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
- }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * ブランチ詳細パネル
3
+ *
4
+ * ノードクリック時に右側に表示されるサイドパネル
5
+ */
6
+
7
+ import React from "react";
8
+ import { Link } from "react-router-dom";
9
+ import { X } from "lucide-react";
10
+ import { Badge } from "@/components/ui/badge";
11
+ import { Button } from "@/components/ui/button";
12
+ import {
13
+ Card,
14
+ CardHeader,
15
+ CardContent,
16
+ CardFooter,
17
+ } from "@/components/ui/card";
18
+ import { cn } from "@/lib/utils";
19
+ import type { Branch } from "../../../../../types/api.js";
20
+
21
+ interface BranchDetailPanelProps {
22
+ branch: Branch | null;
23
+ onClose: () => void;
24
+ className?: string;
25
+ }
26
+
27
+ export function BranchDetailPanel({
28
+ branch,
29
+ onClose,
30
+ className,
31
+ }: BranchDetailPanelProps) {
32
+ if (!branch) return null;
33
+
34
+ const hasWorktree = Boolean(branch.worktreePath);
35
+
36
+ return (
37
+ <div
38
+ className={cn(
39
+ "absolute right-0 top-0 h-full w-80 border-l bg-card/95 backdrop-blur-sm",
40
+ "animate-in slide-in-from-right duration-300",
41
+ className,
42
+ )}
43
+ >
44
+ <Card className="h-full rounded-none border-0">
45
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
46
+ <div className="flex items-center gap-2">
47
+ <Badge variant={branch.type === "local" ? "local" : "remote"}>
48
+ {branch.type === "local" ? "Local" : "Remote"}
49
+ </Badge>
50
+ {hasWorktree && <Badge variant="success">Worktree</Badge>}
51
+ </div>
52
+ <Button
53
+ variant="ghost"
54
+ size="icon"
55
+ onClick={onClose}
56
+ className="h-8 w-8"
57
+ >
58
+ <X className="h-4 w-4" />
59
+ </Button>
60
+ </CardHeader>
61
+
62
+ <CardContent className="space-y-4">
63
+ {/* ブランチ名 */}
64
+ <div>
65
+ <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
66
+ Branch Name
67
+ </p>
68
+ <p className="mt-1 break-all font-mono text-sm">{branch.name}</p>
69
+ </div>
70
+
71
+ {/* ベースブランチ */}
72
+ <div>
73
+ <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
74
+ Base Branch
75
+ </p>
76
+ <p className="mt-1 font-mono text-sm">
77
+ {branch.baseBranch ?? "Unknown"}
78
+ </p>
79
+ </div>
80
+
81
+ {/* コミットメッセージ */}
82
+ <div>
83
+ <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
84
+ Last Commit
85
+ </p>
86
+ <p className="mt-1 text-sm text-muted-foreground">
87
+ {branch.commitMessage ?? "No commit message"}
88
+ </p>
89
+ </div>
90
+
91
+ {/* Divergence */}
92
+ {branch.divergence && (
93
+ <div>
94
+ <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
95
+ Divergence
96
+ </p>
97
+ <div className="mt-2 flex flex-wrap gap-2">
98
+ <Badge variant="outline" className="text-xs">
99
+ ↑ {branch.divergence.ahead} ahead
100
+ </Badge>
101
+ <Badge variant="outline" className="text-xs">
102
+ ↓ {branch.divergence.behind} behind
103
+ </Badge>
104
+ <Badge
105
+ variant={branch.divergence.upToDate ? "success" : "warning"}
106
+ className="text-xs"
107
+ >
108
+ {branch.divergence.upToDate ? "Up to date" : "Needs sync"}
109
+ </Badge>
110
+ </div>
111
+ </div>
112
+ )}
113
+
114
+ {/* Worktree パス */}
115
+ {hasWorktree && (
116
+ <div>
117
+ <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
118
+ Worktree Path
119
+ </p>
120
+ <p className="mt-1 break-all font-mono text-xs text-muted-foreground">
121
+ {branch.worktreePath}
122
+ </p>
123
+ </div>
124
+ )}
125
+
126
+ {/* マージステータス */}
127
+ {branch.mergeStatus && (
128
+ <div>
129
+ <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
130
+ Merge Status
131
+ </p>
132
+ <Badge
133
+ variant={
134
+ branch.mergeStatus === "merged" ? "success" : "outline"
135
+ }
136
+ className="mt-1"
137
+ >
138
+ {branch.mergeStatus}
139
+ </Badge>
140
+ </div>
141
+ )}
142
+ </CardContent>
143
+
144
+ <CardFooter className="flex flex-col gap-2">
145
+ <Button asChild className="w-full">
146
+ <Link to={`/${encodeURIComponent(branch.name)}`}>View Details</Link>
147
+ </Button>
148
+ </CardFooter>
149
+ </Card>
150
+ </div>
151
+ );
152
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * ブランチノードコンポーネント
3
+ *
4
+ * ソーマ(神経細胞体)をイメージした生物発光ノード
5
+ * 多層グロー効果と有機的なアニメーションで生命感を表現
6
+ */
7
+
8
+ import React, { memo, useMemo } from "react";
9
+ import { Handle, Position, type NodeProps } from "@xyflow/react";
10
+ import { cn } from "@/lib/utils";
11
+ import { getNodeColor, getNodeSize, type GraphNode } from "./graphUtils";
12
+
13
+ type BranchNodeProps = NodeProps<GraphNode>;
14
+
15
+ export const BranchNode = memo(function BranchNode({
16
+ data,
17
+ selected,
18
+ }: BranchNodeProps) {
19
+ const { branch } = data;
20
+
21
+ if (!branch) return null;
22
+
23
+ const size = getNodeSize(branch);
24
+ const _baseColor = getNodeColor(branch);
25
+ const hasWorktree = Boolean(branch.worktreePath);
26
+ const isMerged = branch.mergeStatus === "merged";
27
+
28
+ // ブランチ名を短縮
29
+ const displayName = useMemo(() => {
30
+ if (branch.name.length > 18) {
31
+ return `${branch.name.slice(0, 15)}...`;
32
+ }
33
+ return branch.name;
34
+ }, [branch.name]);
35
+
36
+ // ノードカラーをHSL値として抽出(動的スタイル用)
37
+ const colorStyle = useMemo(() => {
38
+ if (hasWorktree) {
39
+ return {
40
+ primary: "hsl(160 90% 45%)",
41
+ glow: "hsl(160 90% 45% / 0.4)",
42
+ inner: "hsl(160 90% 50% / 0.2)",
43
+ };
44
+ }
45
+ if (branch.type === "local") {
46
+ return {
47
+ primary: "hsl(280 85% 65%)",
48
+ glow: "hsl(280 85% 65% / 0.4)",
49
+ inner: "hsl(280 85% 70% / 0.2)",
50
+ };
51
+ }
52
+ return {
53
+ primary: "hsl(200 90% 55%)",
54
+ glow: "hsl(200 90% 55% / 0.4)",
55
+ inner: "hsl(200 90% 60% / 0.2)",
56
+ };
57
+ }, [hasWorktree, branch.type]);
58
+
59
+ return (
60
+ <>
61
+ {/* 入力ハンドル - デンドライト接続点 */}
62
+ <Handle
63
+ type="target"
64
+ position={Position.Top}
65
+ className="!h-3 !w-3 !rounded-full !border-2 !bg-transparent transition-all duration-300"
66
+ style={{
67
+ borderColor: colorStyle.primary,
68
+ boxShadow: `0 0 8px ${colorStyle.glow}`,
69
+ }}
70
+ />
71
+
72
+ {/* ソーマ本体 */}
73
+ <div
74
+ className={cn(
75
+ "group relative flex cursor-pointer items-center justify-center rounded-full",
76
+ "transition-all duration-500 ease-out",
77
+ hasWorktree && "animate-active-synapse",
78
+ !hasWorktree && "animate-soma-pulse",
79
+ selected && "ring-2 ring-offset-4 ring-offset-background",
80
+ isMerged && "opacity-50",
81
+ )}
82
+ style={{
83
+ width: size,
84
+ height: size,
85
+ background: `radial-gradient(circle at 30% 30%, ${colorStyle.inner}, transparent 60%)`,
86
+ border: `2px solid ${colorStyle.primary}`,
87
+ boxShadow: selected
88
+ ? `0 0 30px ${colorStyle.glow}, inset 0 0 20px ${colorStyle.inner}, 0 0 0 4px ${colorStyle.primary}`
89
+ : undefined,
90
+ }}
91
+ >
92
+ {/* 外殻グロー */}
93
+ <div
94
+ className="absolute -inset-2 rounded-full opacity-30 blur-md"
95
+ style={{ backgroundColor: colorStyle.primary }}
96
+ />
97
+
98
+ {/* 核膜 - 内側のリング */}
99
+ <div
100
+ className="absolute inset-2 rounded-full border opacity-40"
101
+ style={{ borderColor: colorStyle.primary }}
102
+ />
103
+
104
+ {/* 核 - 中心部 */}
105
+ <div
106
+ className="relative z-10 flex items-center justify-center rounded-full shadow-lg"
107
+ style={{
108
+ width: size * 0.45,
109
+ height: size * 0.45,
110
+ background: `linear-gradient(135deg, ${colorStyle.primary}, ${colorStyle.inner})`,
111
+ boxShadow: `0 0 15px ${colorStyle.glow}`,
112
+ }}
113
+ >
114
+ {/* アイコン/タイプ表示 */}
115
+ <span className="text-xs font-bold text-background">
116
+ {branch.type === "local" ? "L" : "R"}
117
+ </span>
118
+ </div>
119
+
120
+ {/* 細胞小器官 - 装飾ドット */}
121
+ {!isMerged && (
122
+ <>
123
+ <div
124
+ className="absolute h-1.5 w-1.5 rounded-full animate-vesicle-release"
125
+ style={{
126
+ backgroundColor: colorStyle.primary,
127
+ top: "20%",
128
+ right: "25%",
129
+ animationDelay: "0s",
130
+ }}
131
+ />
132
+ <div
133
+ className="absolute h-1 w-1 rounded-full animate-vesicle-release"
134
+ style={{
135
+ backgroundColor: colorStyle.primary,
136
+ bottom: "25%",
137
+ left: "20%",
138
+ animationDelay: "0.7s",
139
+ }}
140
+ />
141
+ </>
142
+ )}
143
+
144
+ {/* Worktree活性インジケータ */}
145
+ {hasWorktree && (
146
+ <div
147
+ className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full animate-synapse-spark"
148
+ style={{
149
+ backgroundColor: "hsl(160 90% 45%)",
150
+ boxShadow: "0 0 10px hsl(160 90% 45% / 0.8)",
151
+ }}
152
+ >
153
+ <span className="text-[8px] font-bold text-background">W</span>
154
+ </div>
155
+ )}
156
+
157
+ {/* マージ済みオーバーレイ */}
158
+ {isMerged && (
159
+ <div className="absolute inset-0 flex items-center justify-center rounded-full bg-background/60 backdrop-blur-sm">
160
+ <span className="text-[10px] font-medium text-muted-foreground">
161
+ merged
162
+ </span>
163
+ </div>
164
+ )}
165
+
166
+ {/* ホバーツールチップ */}
167
+ <div
168
+ className={cn(
169
+ "absolute -bottom-12 left-1/2 z-50 -translate-x-1/2",
170
+ "rounded-lg border border-border/50 bg-card/95 px-3 py-1.5 backdrop-blur-md",
171
+ "opacity-0 shadow-xl transition-all duration-300",
172
+ "group-hover:opacity-100 group-hover:-translate-y-1",
173
+ )}
174
+ style={{
175
+ boxShadow: `0 4px 20px hsl(220 30% 0% / 0.5), 0 0 20px ${colorStyle.glow}`,
176
+ }}
177
+ >
178
+ <p className="whitespace-nowrap text-xs font-medium text-foreground">
179
+ {displayName}
180
+ </p>
181
+ <p className="text-[10px] text-muted-foreground">
182
+ {branch.type === "local" ? "Local" : "Remote"}
183
+ {hasWorktree && " • Active"}
184
+ </p>
185
+ </div>
186
+ </div>
187
+
188
+ {/* 出力ハンドル - アクソン接続点 */}
189
+ <Handle
190
+ type="source"
191
+ position={Position.Bottom}
192
+ className="!h-3 !w-3 !rounded-full !border-2 !bg-transparent transition-all duration-300"
193
+ style={{
194
+ borderColor: colorStyle.primary,
195
+ boxShadow: `0 0 8px ${colorStyle.glow}`,
196
+ }}
197
+ />
198
+ </>
199
+ );
200
+ });