@akiojin/gwt 2.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.
- package/README.ja.md +323 -0
- package/README.md +347 -0
- package/bin/gwt.js +5 -0
- package/package.json +125 -0
- package/src/claude-history.ts +717 -0
- package/src/claude.ts +292 -0
- package/src/cli/ui/__tests__/SKIPPED_TESTS.md +119 -0
- package/src/cli/ui/__tests__/acceptance/branchList.acceptance.test.tsx.skip +239 -0
- package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +214 -0
- package/src/cli/ui/__tests__/acceptance/realtimeUpdate.acceptance.test.tsx.skip +219 -0
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +183 -0
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +313 -0
- package/src/cli/ui/__tests__/components/App.test.tsx +270 -0
- package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +66 -0
- package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +103 -0
- package/src/cli/ui/__tests__/components/common/Input.test.tsx +92 -0
- package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +127 -0
- package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +264 -0
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +246 -0
- package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +62 -0
- package/src/cli/ui/__tests__/components/parts/Header.test.tsx +54 -0
- package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +68 -0
- package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +135 -0
- package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +153 -0
- package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +215 -0
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +293 -0
- package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +161 -0
- package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +215 -0
- package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +99 -0
- package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +127 -0
- package/src/cli/ui/__tests__/hooks/useGitData.test.ts.skip +228 -0
- package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +146 -0
- package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +98 -0
- package/src/cli/ui/__tests__/integration/branchList.test.tsx.skip +253 -0
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +306 -0
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +405 -0
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +505 -0
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx.skip +216 -0
- package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +180 -0
- package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +237 -0
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +775 -0
- package/src/cli/ui/__tests__/utils/statisticsCalculator.test.ts +243 -0
- package/src/cli/ui/components/App.tsx +793 -0
- package/src/cli/ui/components/common/Confirm.tsx +40 -0
- package/src/cli/ui/components/common/ErrorBoundary.tsx +57 -0
- package/src/cli/ui/components/common/Input.tsx +36 -0
- package/src/cli/ui/components/common/LoadingIndicator.tsx +95 -0
- package/src/cli/ui/components/common/Select.tsx +216 -0
- package/src/cli/ui/components/parts/Footer.tsx +41 -0
- package/src/cli/ui/components/parts/Header.test.tsx +85 -0
- package/src/cli/ui/components/parts/Header.tsx +63 -0
- package/src/cli/ui/components/parts/MergeStatusList.tsx +75 -0
- package/src/cli/ui/components/parts/ProgressBar.tsx +73 -0
- package/src/cli/ui/components/parts/ScrollableList.tsx +24 -0
- package/src/cli/ui/components/parts/Stats.tsx +67 -0
- package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +116 -0
- package/src/cli/ui/components/screens/BatchMergeProgressScreen.tsx +70 -0
- package/src/cli/ui/components/screens/BatchMergeResultScreen.tsx +104 -0
- package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +213 -0
- package/src/cli/ui/components/screens/BranchListScreen.tsx +299 -0
- package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +149 -0
- package/src/cli/ui/components/screens/PRCleanupScreen.tsx +167 -0
- package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +100 -0
- package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +117 -0
- package/src/cli/ui/hooks/useBatchMerge.ts +96 -0
- package/src/cli/ui/hooks/useGitData.ts +157 -0
- package/src/cli/ui/hooks/useScreenState.ts +44 -0
- package/src/cli/ui/hooks/useTerminalSize.ts +33 -0
- package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +102 -0
- package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +151 -0
- package/src/cli/ui/types.ts +295 -0
- package/src/cli/ui/utils/baseBranch.ts +34 -0
- package/src/cli/ui/utils/branchFormatter.ts +222 -0
- package/src/cli/ui/utils/statisticsCalculator.ts +44 -0
- package/src/codex.ts +139 -0
- package/src/config/builtin-tools.ts +44 -0
- package/src/config/constants.ts +100 -0
- package/src/config/env-history.ts +45 -0
- package/src/config/index.ts +204 -0
- package/src/config/tools.ts +293 -0
- package/src/git.ts +1102 -0
- package/src/github.ts +158 -0
- package/src/index.test.ts +87 -0
- package/src/index.ts +684 -0
- package/src/index.ts.backup +1543 -0
- package/src/launcher.ts +142 -0
- package/src/repositories/git.repository.ts +129 -0
- package/src/repositories/github.repository.ts +83 -0
- package/src/repositories/worktree.repository.ts +69 -0
- package/src/services/BatchMergeService.ts +251 -0
- package/src/services/WorktreeOrchestrator.ts +115 -0
- package/src/services/__tests__/BatchMergeService.test.ts +518 -0
- package/src/services/__tests__/WorktreeOrchestrator.test.ts +258 -0
- package/src/services/dependency-installer.ts +199 -0
- package/src/services/git.service.ts +113 -0
- package/src/services/github.service.ts +61 -0
- package/src/services/worktree.service.ts +66 -0
- package/src/types/api.ts +241 -0
- package/src/types/tools.ts +235 -0
- package/src/utils/spinner.ts +54 -0
- package/src/utils/terminal.ts +272 -0
- package/src/utils.test.ts +43 -0
- package/src/utils.ts +60 -0
- package/src/web/client/index.html +12 -0
- package/src/web/client/src/components/BranchGraph.tsx +231 -0
- package/src/web/client/src/components/EnvEditor.tsx +145 -0
- package/src/web/client/src/components/Terminal.tsx +137 -0
- package/src/web/client/src/hooks/useBranches.ts +41 -0
- package/src/web/client/src/hooks/useConfig.ts +31 -0
- package/src/web/client/src/hooks/useSessions.ts +59 -0
- package/src/web/client/src/hooks/useWorktrees.ts +47 -0
- package/src/web/client/src/index.css +834 -0
- package/src/web/client/src/lib/api.ts +184 -0
- package/src/web/client/src/lib/websocket.ts +174 -0
- package/src/web/client/src/main.tsx +29 -0
- package/src/web/client/src/pages/BranchDetailPage.tsx +847 -0
- package/src/web/client/src/pages/BranchListPage.tsx +264 -0
- package/src/web/client/src/pages/ConfigManagementPage.tsx +203 -0
- package/src/web/client/src/router.tsx +27 -0
- package/src/web/client/vite.config.ts +21 -0
- package/src/web/server/env/importer.ts +54 -0
- package/src/web/server/index.ts +74 -0
- package/src/web/server/pty/manager.ts +189 -0
- package/src/web/server/routes/branches.ts +126 -0
- package/src/web/server/routes/config.ts +220 -0
- package/src/web/server/routes/index.ts +37 -0
- package/src/web/server/routes/sessions.ts +130 -0
- package/src/web/server/routes/worktrees.ts +108 -0
- package/src/web/server/services/branches.ts +368 -0
- package/src/web/server/services/worktrees.ts +85 -0
- package/src/web/server/websocket/handler.ts +180 -0
- package/src/worktree.ts +703 -0
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { Link, useParams } from "react-router-dom";
|
|
3
|
+
import { useBranch, useSyncBranch } from "../hooks/useBranches";
|
|
4
|
+
import { useCreateWorktree } from "../hooks/useWorktrees";
|
|
5
|
+
import {
|
|
6
|
+
useStartSession,
|
|
7
|
+
useSessions,
|
|
8
|
+
useDeleteSession,
|
|
9
|
+
} from "../hooks/useSessions";
|
|
10
|
+
import { useConfig } from "../hooks/useConfig";
|
|
11
|
+
import { ApiError } from "../lib/api";
|
|
12
|
+
import { Terminal } from "../components/Terminal";
|
|
13
|
+
import type { Branch, CustomAITool } from "../../../../types/api.js";
|
|
14
|
+
|
|
15
|
+
type ToolType = "claude-code" | "codex-cli" | "custom";
|
|
16
|
+
type ToolMode = "normal" | "continue" | "resume";
|
|
17
|
+
|
|
18
|
+
type SelectableTool =
|
|
19
|
+
| { id: "claude-code"; label: string; target: "claude" }
|
|
20
|
+
| { id: "codex-cli"; label: string; target: "codex" }
|
|
21
|
+
| { id: string; label: string; target: "custom"; definition: CustomAITool };
|
|
22
|
+
|
|
23
|
+
interface ToolSummary {
|
|
24
|
+
command: string;
|
|
25
|
+
defaultArgs?: string[] | null;
|
|
26
|
+
modeArgs?: {
|
|
27
|
+
normal?: string[];
|
|
28
|
+
continue?: string[];
|
|
29
|
+
resume?: string[];
|
|
30
|
+
};
|
|
31
|
+
permissionSkipArgs?: string[] | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const BUILTIN_TOOL_SUMMARIES: Record<string, ToolSummary> = {
|
|
35
|
+
"claude-code": {
|
|
36
|
+
command: "claude",
|
|
37
|
+
defaultArgs: [],
|
|
38
|
+
modeArgs: {
|
|
39
|
+
normal: [],
|
|
40
|
+
continue: ["-c"],
|
|
41
|
+
resume: ["-r"],
|
|
42
|
+
},
|
|
43
|
+
permissionSkipArgs: ["--dangerously-skip-permissions"],
|
|
44
|
+
},
|
|
45
|
+
"codex-cli": {
|
|
46
|
+
command: "codex",
|
|
47
|
+
defaultArgs: ["--auto-approve", "--verbose"],
|
|
48
|
+
modeArgs: {
|
|
49
|
+
normal: [],
|
|
50
|
+
continue: ["resume", "--last"],
|
|
51
|
+
resume: ["resume"],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
interface BannerState {
|
|
57
|
+
type: "success" | "error" | "info";
|
|
58
|
+
message: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const BRANCH_TYPE_LABEL: Record<Branch["type"], string> = {
|
|
62
|
+
local: "ローカル",
|
|
63
|
+
remote: "リモート",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const MERGE_STATUS_LABEL: Record<Branch["mergeStatus"], string> = {
|
|
67
|
+
merged: "マージ済み",
|
|
68
|
+
unmerged: "未マージ",
|
|
69
|
+
unknown: "状態不明",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const MERGE_STATUS_TONE: Record<Branch["mergeStatus"], "success" | "warning" | "muted"> = {
|
|
73
|
+
merged: "success",
|
|
74
|
+
unmerged: "warning",
|
|
75
|
+
unknown: "muted",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function BranchDetailPage() {
|
|
79
|
+
const { branchName } = useParams<{ branchName: string }>();
|
|
80
|
+
const decodedBranchName = branchName ? decodeURIComponent(branchName) : "";
|
|
81
|
+
|
|
82
|
+
const { data: branch, isLoading, error } = useBranch(decodedBranchName);
|
|
83
|
+
const syncBranch = useSyncBranch(decodedBranchName);
|
|
84
|
+
const createWorktree = useCreateWorktree();
|
|
85
|
+
const startSession = useStartSession();
|
|
86
|
+
const { data: sessionsData, isLoading: isSessionsLoading } = useSessions();
|
|
87
|
+
const deleteSession = useDeleteSession();
|
|
88
|
+
const {
|
|
89
|
+
data: config,
|
|
90
|
+
isLoading: isConfigLoading,
|
|
91
|
+
error: configError,
|
|
92
|
+
} = useConfig();
|
|
93
|
+
|
|
94
|
+
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
|
95
|
+
const [isStartingSession, setIsStartingSession] = useState(false);
|
|
96
|
+
const [banner, setBanner] = useState<BannerState | null>(null);
|
|
97
|
+
const [isTerminalFullscreen, setIsTerminalFullscreen] = useState(false);
|
|
98
|
+
const [selectedToolId, setSelectedToolId] = useState<string>("claude-code");
|
|
99
|
+
const [selectedMode, setSelectedMode] = useState<ToolMode>("normal");
|
|
100
|
+
const [skipPermissions, setSkipPermissions] = useState(false);
|
|
101
|
+
const [extraArgsText, setExtraArgsText] = useState("");
|
|
102
|
+
const [terminatingSessionId, setTerminatingSessionId] = useState<string | null>(null);
|
|
103
|
+
|
|
104
|
+
const formattedCommitDate = useMemo(
|
|
105
|
+
() => formatDate(branch?.commitDate),
|
|
106
|
+
[branch?.commitDate],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!isTerminalFullscreen) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const previousOverflow = document.body.style.overflow;
|
|
115
|
+
document.body.style.overflow = "hidden";
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
document.body.style.overflow = previousOverflow;
|
|
119
|
+
};
|
|
120
|
+
}, [isTerminalFullscreen]);
|
|
121
|
+
|
|
122
|
+
if (isLoading) {
|
|
123
|
+
return (
|
|
124
|
+
<div className="app-shell">
|
|
125
|
+
<div className="page-state page-state--centered">
|
|
126
|
+
<h1>読み込み中</h1>
|
|
127
|
+
<p>ブランチ情報を取得しています...</p>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (error) {
|
|
134
|
+
return (
|
|
135
|
+
<div className="app-shell">
|
|
136
|
+
<div className="page-state page-state--centered">
|
|
137
|
+
<h1>ブランチの取得に失敗しました</h1>
|
|
138
|
+
<p>{error instanceof Error ? error.message : "未知のエラーです"}</p>
|
|
139
|
+
<Link to="/" className="button button--ghost">
|
|
140
|
+
ブランチ一覧に戻る
|
|
141
|
+
</Link>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!branch) {
|
|
148
|
+
return (
|
|
149
|
+
<div className="app-shell">
|
|
150
|
+
<div className="page-state page-state--centered">
|
|
151
|
+
<h1>Branch not found</h1>
|
|
152
|
+
<p>指定されたブランチは存在しません。</p>
|
|
153
|
+
<Link to="/" className="button button--ghost">
|
|
154
|
+
ブランチ一覧に戻る
|
|
155
|
+
</Link>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const canStartSession = Boolean(branch.worktreePath);
|
|
162
|
+
const divergenceInfo = branch.divergence ?? null;
|
|
163
|
+
const hasBlockingDivergence = Boolean(
|
|
164
|
+
divergenceInfo && divergenceInfo.ahead > 0 && divergenceInfo.behind > 0,
|
|
165
|
+
);
|
|
166
|
+
const needsRemoteSync = Boolean(
|
|
167
|
+
branch.worktreePath &&
|
|
168
|
+
divergenceInfo &&
|
|
169
|
+
divergenceInfo.behind > 0 &&
|
|
170
|
+
divergenceInfo.ahead === 0 &&
|
|
171
|
+
!hasBlockingDivergence,
|
|
172
|
+
);
|
|
173
|
+
const isSyncingBranch = syncBranch.isPending;
|
|
174
|
+
|
|
175
|
+
const customTools: CustomAITool[] = config?.tools ?? [];
|
|
176
|
+
const availableTools: SelectableTool[] = useMemo(
|
|
177
|
+
() => [
|
|
178
|
+
{ id: "claude-code", label: "Claude Code", target: "claude" },
|
|
179
|
+
{ id: "codex-cli", label: "Codex CLI", target: "codex" },
|
|
180
|
+
...customTools.map((tool): SelectableTool => ({
|
|
181
|
+
id: tool.id,
|
|
182
|
+
label: tool.displayName,
|
|
183
|
+
target: "custom" as const,
|
|
184
|
+
definition: tool,
|
|
185
|
+
})),
|
|
186
|
+
],
|
|
187
|
+
[customTools],
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!availableTools.length) {
|
|
192
|
+
setSelectedToolId("claude-code");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!availableTools.find((tool) => tool.id === selectedToolId)) {
|
|
196
|
+
const first = availableTools[0];
|
|
197
|
+
if (first) {
|
|
198
|
+
setSelectedToolId(first.id);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}, [availableTools, selectedToolId]);
|
|
202
|
+
|
|
203
|
+
const selectedTool = availableTools.find((tool) => tool.id === selectedToolId);
|
|
204
|
+
|
|
205
|
+
const selectedToolSummary: ToolSummary | null = useMemo(() => {
|
|
206
|
+
if (!selectedTool) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
if (selectedTool.target === "custom") {
|
|
210
|
+
return {
|
|
211
|
+
command: selectedTool.definition.command,
|
|
212
|
+
defaultArgs: selectedTool.definition.defaultArgs ?? null,
|
|
213
|
+
modeArgs: selectedTool.definition.modeArgs,
|
|
214
|
+
permissionSkipArgs: selectedTool.definition.permissionSkipArgs ?? null,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return BUILTIN_TOOL_SUMMARIES[selectedTool.id] ?? null;
|
|
218
|
+
}, [selectedTool]);
|
|
219
|
+
|
|
220
|
+
const argsPreview = useMemo(() => {
|
|
221
|
+
if (!selectedToolSummary) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const args: string[] = [];
|
|
225
|
+
if (selectedToolSummary.defaultArgs?.length) {
|
|
226
|
+
args.push(...selectedToolSummary.defaultArgs);
|
|
227
|
+
}
|
|
228
|
+
const mode = selectedToolSummary.modeArgs?.[selectedMode];
|
|
229
|
+
if (mode?.length) {
|
|
230
|
+
args.push(...mode);
|
|
231
|
+
}
|
|
232
|
+
if (skipPermissions && selectedToolSummary.permissionSkipArgs?.length) {
|
|
233
|
+
args.push(...selectedToolSummary.permissionSkipArgs);
|
|
234
|
+
}
|
|
235
|
+
const extraArgs = parseExtraArgs(extraArgsText);
|
|
236
|
+
if (extraArgs.length) {
|
|
237
|
+
args.push(...extraArgs);
|
|
238
|
+
}
|
|
239
|
+
return { command: selectedToolSummary.command, args };
|
|
240
|
+
}, [selectedToolSummary, selectedMode, skipPermissions, extraArgsText]);
|
|
241
|
+
|
|
242
|
+
const handleCreateWorktree = async () => {
|
|
243
|
+
try {
|
|
244
|
+
await createWorktree.mutateAsync({
|
|
245
|
+
branchName: branch.name,
|
|
246
|
+
createBranch: false,
|
|
247
|
+
});
|
|
248
|
+
setBanner({
|
|
249
|
+
type: "success",
|
|
250
|
+
message: `${branch.name} のWorktreeを作成しました。`,
|
|
251
|
+
});
|
|
252
|
+
} catch (err) {
|
|
253
|
+
setBanner({
|
|
254
|
+
type: "error",
|
|
255
|
+
message: formatError(err, "Worktreeの作成に失敗しました"),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const handleStartSession = async () => {
|
|
261
|
+
if (!branch.worktreePath) {
|
|
262
|
+
setBanner({
|
|
263
|
+
type: "error",
|
|
264
|
+
message: "Worktreeが存在しないため、先に作成してください。",
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!selectedTool) {
|
|
270
|
+
setBanner({ type: "error", message: "起動するAIツールを選択してください" });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (needsRemoteSync) {
|
|
275
|
+
setBanner({
|
|
276
|
+
type: "error",
|
|
277
|
+
message: "リモートの更新を取り込むまでAIツールは起動できません。『最新の変更を同期』を実行してください。",
|
|
278
|
+
});
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (hasBlockingDivergence) {
|
|
283
|
+
setBanner({
|
|
284
|
+
type: "error",
|
|
285
|
+
message:
|
|
286
|
+
"リモートとローカルの双方で進捗が発生しているため、CLIと同様にAIツールの起動をブロックしました。先に rebase/merge 等で差分を解消してください。",
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (skipPermissions && !window.confirm("権限チェックをスキップして起動します。自己責任で実行してください。続行しますか?")) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
setIsStartingSession(true);
|
|
296
|
+
try {
|
|
297
|
+
const toolType: ToolType =
|
|
298
|
+
selectedTool.target === "codex"
|
|
299
|
+
? "codex-cli"
|
|
300
|
+
: selectedTool.target === "custom"
|
|
301
|
+
? "custom"
|
|
302
|
+
: "claude-code";
|
|
303
|
+
const extraArgs = parseExtraArgs(extraArgsText);
|
|
304
|
+
const sessionRequest = {
|
|
305
|
+
toolType,
|
|
306
|
+
toolName: selectedTool.target === "custom" ? selectedTool.id : null,
|
|
307
|
+
...(selectedTool.target === "custom"
|
|
308
|
+
? { customToolId: selectedTool.id }
|
|
309
|
+
: {}),
|
|
310
|
+
mode: selectedMode,
|
|
311
|
+
worktreePath: branch.worktreePath,
|
|
312
|
+
skipPermissions,
|
|
313
|
+
...(selectedTool.target === "codex"
|
|
314
|
+
? { bypassApprovals: skipPermissions }
|
|
315
|
+
: {}),
|
|
316
|
+
...(extraArgs.length ? { extraArgs } : {}),
|
|
317
|
+
} as const;
|
|
318
|
+
|
|
319
|
+
const session = await startSession.mutateAsync(sessionRequest);
|
|
320
|
+
setActiveSessionId(session.sessionId);
|
|
321
|
+
setIsTerminalFullscreen(false);
|
|
322
|
+
setBanner({
|
|
323
|
+
type: "info",
|
|
324
|
+
message: `${toolLabel(toolType, selectedTool)} を起動しました。`,
|
|
325
|
+
});
|
|
326
|
+
} catch (err) {
|
|
327
|
+
setBanner({
|
|
328
|
+
type: "error",
|
|
329
|
+
message: formatError(err, "セッションの起動に失敗しました"),
|
|
330
|
+
});
|
|
331
|
+
} finally {
|
|
332
|
+
setIsStartingSession(false);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const handleTerminateSession = async (sessionId: string) => {
|
|
337
|
+
setTerminatingSessionId(sessionId);
|
|
338
|
+
try {
|
|
339
|
+
await deleteSession.mutateAsync(sessionId);
|
|
340
|
+
setBanner({ type: "success", message: "セッションを終了しました" });
|
|
341
|
+
if (activeSessionId === sessionId) {
|
|
342
|
+
setActiveSessionId(null);
|
|
343
|
+
}
|
|
344
|
+
} catch (err) {
|
|
345
|
+
setBanner({ type: "error", message: formatError(err, "セッションの終了に失敗しました") });
|
|
346
|
+
} finally {
|
|
347
|
+
setTerminatingSessionId(null);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const handleSyncBranch = async () => {
|
|
352
|
+
if (!branch.worktreePath) {
|
|
353
|
+
setBanner({ type: "error", message: "Worktreeが存在しないため同期できません。" });
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const result = await syncBranch.mutateAsync({ worktreePath: branch.worktreePath });
|
|
359
|
+
if (result.pullStatus === "success") {
|
|
360
|
+
setBanner({ type: "success", message: "リモートの最新変更を取り込みました。" });
|
|
361
|
+
} else {
|
|
362
|
+
const warning = result.warnings?.join("\n") ?? "fast-forward pull が完了しませんでした";
|
|
363
|
+
setBanner({
|
|
364
|
+
type: "error",
|
|
365
|
+
message: `git pull --ff-only が失敗しました。\n${warning}`,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
} catch (err) {
|
|
369
|
+
setBanner({ type: "error", message: formatError(err, "Git同期に失敗しました") });
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const branchSessions = useMemo(() => {
|
|
374
|
+
return (sessionsData ?? [])
|
|
375
|
+
.filter((session) => session.worktreePath === branch?.worktreePath)
|
|
376
|
+
.sort((a, b) => (b.startedAt ?? "").localeCompare(a.startedAt ?? ""));
|
|
377
|
+
}, [sessionsData, branch?.worktreePath]);
|
|
378
|
+
|
|
379
|
+
const handleSessionExit = (code: number) => {
|
|
380
|
+
setActiveSessionId(null);
|
|
381
|
+
setIsTerminalFullscreen(false);
|
|
382
|
+
setBanner({
|
|
383
|
+
type: code === 0 ? "success" : "error",
|
|
384
|
+
message: `セッションがコード ${code} で終了しました。`,
|
|
385
|
+
});
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<div className="app-shell">
|
|
390
|
+
<header className="page-hero page-hero--compact">
|
|
391
|
+
<Link to="/" className="link-back">
|
|
392
|
+
← ブランチ一覧に戻る
|
|
393
|
+
</Link>
|
|
394
|
+
<p className="page-hero__eyebrow">BRANCH DETAIL</p>
|
|
395
|
+
<h1>{branch.name}</h1>
|
|
396
|
+
<p className="page-hero__subtitle">
|
|
397
|
+
最新コミット {branch.commitHash.slice(0, 7)} ・ {formattedCommitDate}
|
|
398
|
+
</p>
|
|
399
|
+
<div className="badge-group">
|
|
400
|
+
<span className={`status-badge status-badge--${branch.type}`}>
|
|
401
|
+
{BRANCH_TYPE_LABEL[branch.type]}
|
|
402
|
+
</span>
|
|
403
|
+
<span className={`status-badge status-badge--${MERGE_STATUS_TONE[branch.mergeStatus]}`}>
|
|
404
|
+
{MERGE_STATUS_LABEL[branch.mergeStatus]}
|
|
405
|
+
</span>
|
|
406
|
+
<span
|
|
407
|
+
className={`status-badge ${
|
|
408
|
+
branch.worktreePath ? "status-badge--success" : "status-badge--muted"
|
|
409
|
+
}`}
|
|
410
|
+
>
|
|
411
|
+
{branch.worktreePath ? "Worktreeあり" : "Worktree未作成"}
|
|
412
|
+
</span>
|
|
413
|
+
</div>
|
|
414
|
+
<div className="page-hero__actions">
|
|
415
|
+
{!canStartSession ? (
|
|
416
|
+
<button
|
|
417
|
+
type="button"
|
|
418
|
+
className="button button--primary"
|
|
419
|
+
onClick={handleCreateWorktree}
|
|
420
|
+
disabled={createWorktree.isPending}
|
|
421
|
+
>
|
|
422
|
+
{createWorktree.isPending ? "作成中..." : "Worktreeを作成"}
|
|
423
|
+
</button>
|
|
424
|
+
) : (
|
|
425
|
+
<Link to="/config" className="button button--secondary">
|
|
426
|
+
カスタムツール設定を開く
|
|
427
|
+
</Link>
|
|
428
|
+
)}
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
{banner && (
|
|
432
|
+
<div className={`inline-banner inline-banner--${banner.type}`}>
|
|
433
|
+
{banner.message}
|
|
434
|
+
</div>
|
|
435
|
+
)}
|
|
436
|
+
</header>
|
|
437
|
+
|
|
438
|
+
{isTerminalFullscreen && (
|
|
439
|
+
<div
|
|
440
|
+
className="terminal-overlay-backdrop"
|
|
441
|
+
aria-hidden="true"
|
|
442
|
+
onClick={() => setIsTerminalFullscreen(false)}
|
|
443
|
+
/>
|
|
444
|
+
)}
|
|
445
|
+
<main className="page-content page-content--wide">
|
|
446
|
+
<div className="page-layout page-layout--split">
|
|
447
|
+
<div className="info-stack">
|
|
448
|
+
<section className="section-card">
|
|
449
|
+
<header className="terminal-section__header">
|
|
450
|
+
<div>
|
|
451
|
+
<h2>AIツール起動</h2>
|
|
452
|
+
<p className="section-card__body">
|
|
453
|
+
Web UI から直接AIツールを起動できます。設定したカスタムツールも一覧に表示されます。
|
|
454
|
+
</p>
|
|
455
|
+
</div>
|
|
456
|
+
{configError && (
|
|
457
|
+
<span className="pill pill--warning">設定の取得に失敗しました</span>
|
|
458
|
+
)}
|
|
459
|
+
</header>
|
|
460
|
+
|
|
461
|
+
{!canStartSession ? (
|
|
462
|
+
<p className="section-card__body">
|
|
463
|
+
Worktreeが未作成のため、先にWorktreeを作成してください。
|
|
464
|
+
</p>
|
|
465
|
+
) : (
|
|
466
|
+
<div className="tool-form">
|
|
467
|
+
<div className="form-grid">
|
|
468
|
+
<label className="form-field">
|
|
469
|
+
<span>AIツール</span>
|
|
470
|
+
<select
|
|
471
|
+
value={selectedToolId}
|
|
472
|
+
onChange={(event) => setSelectedToolId(event.target.value)}
|
|
473
|
+
disabled={isConfigLoading}
|
|
474
|
+
>
|
|
475
|
+
{availableTools.map((tool) => (
|
|
476
|
+
<option key={tool.id} value={tool.id}>
|
|
477
|
+
{tool.label}
|
|
478
|
+
</option>
|
|
479
|
+
))}
|
|
480
|
+
</select>
|
|
481
|
+
</label>
|
|
482
|
+
|
|
483
|
+
<label className="form-field">
|
|
484
|
+
<span>起動モード</span>
|
|
485
|
+
<select
|
|
486
|
+
value={selectedMode}
|
|
487
|
+
onChange={(event) => setSelectedMode(event.target.value as ToolMode)}
|
|
488
|
+
>
|
|
489
|
+
<option value="normal">normal</option>
|
|
490
|
+
<option value="continue">continue</option>
|
|
491
|
+
<option value="resume">resume</option>
|
|
492
|
+
</select>
|
|
493
|
+
</label>
|
|
494
|
+
|
|
495
|
+
<label className="form-field">
|
|
496
|
+
<span>追加引数 (スペース区切り)</span>
|
|
497
|
+
<input
|
|
498
|
+
type="text"
|
|
499
|
+
value={extraArgsText}
|
|
500
|
+
onChange={(event) => setExtraArgsText(event.target.value)}
|
|
501
|
+
placeholder="--flag value"
|
|
502
|
+
/>
|
|
503
|
+
</label>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<label className="form-field">
|
|
507
|
+
<span>
|
|
508
|
+
<input
|
|
509
|
+
type="checkbox"
|
|
510
|
+
checked={skipPermissions}
|
|
511
|
+
onChange={(event) => setSkipPermissions(event.target.checked)}
|
|
512
|
+
/>
|
|
513
|
+
<span style={{ marginLeft: "0.5rem" }}>権限チェックをスキップ (自己責任)</span>
|
|
514
|
+
</span>
|
|
515
|
+
</label>
|
|
516
|
+
{skipPermissions && (
|
|
517
|
+
<div className="inline-banner inline-banner--warning">
|
|
518
|
+
<p>
|
|
519
|
+
権限チェックをスキップすることで、CLI での `--dangerously-skip-permissions` 指定と同様のリスクを負います。
|
|
520
|
+
</p>
|
|
521
|
+
</div>
|
|
522
|
+
)}
|
|
523
|
+
{needsRemoteSync && (
|
|
524
|
+
<div className="inline-banner inline-banner--info" data-testid="sync-required">
|
|
525
|
+
<p>
|
|
526
|
+
リモートに未取得の更新 ({branch.divergence?.behind ?? 0} commits) があるため、AIツールを起動する前に同期してください。
|
|
527
|
+
</p>
|
|
528
|
+
<p className="section-card__body">
|
|
529
|
+
CLI の `git fetch --all` と `git pull --ff-only` と同じ処理を Web UI から実行できます。
|
|
530
|
+
</p>
|
|
531
|
+
</div>
|
|
532
|
+
)}
|
|
533
|
+
{hasBlockingDivergence && (
|
|
534
|
+
<div className="inline-banner inline-banner--warning" data-testid="divergence-warning">
|
|
535
|
+
<p>
|
|
536
|
+
リモートとローカルの両方に未解決の差分があるため、Web UI でも CLI と同様に起動をブロックしています。
|
|
537
|
+
</p>
|
|
538
|
+
<ul className="list-muted">
|
|
539
|
+
<li>git fetch && git pull --ff-only origin {branch.name}</li>
|
|
540
|
+
<li>必要に応じて git push origin {branch.name} でローカル進捗を共有</li>
|
|
541
|
+
</ul>
|
|
542
|
+
<p className="section-card__body">
|
|
543
|
+
rebase / merge などで差分を解消した後にページを更新してください。
|
|
544
|
+
</p>
|
|
545
|
+
</div>
|
|
546
|
+
)}
|
|
547
|
+
|
|
548
|
+
<div className="tool-card__actions">
|
|
549
|
+
<button
|
|
550
|
+
type="button"
|
|
551
|
+
className="button button--primary"
|
|
552
|
+
onClick={handleStartSession}
|
|
553
|
+
disabled={
|
|
554
|
+
isStartingSession ||
|
|
555
|
+
!selectedTool ||
|
|
556
|
+
hasBlockingDivergence ||
|
|
557
|
+
needsRemoteSync ||
|
|
558
|
+
isSyncingBranch
|
|
559
|
+
}
|
|
560
|
+
>
|
|
561
|
+
{isStartingSession ? "起動中..." : "セッションを起動"}
|
|
562
|
+
</button>
|
|
563
|
+
<button
|
|
564
|
+
type="button"
|
|
565
|
+
className="button button--secondary"
|
|
566
|
+
onClick={handleSyncBranch}
|
|
567
|
+
disabled={!branch.worktreePath || isSyncingBranch}
|
|
568
|
+
>
|
|
569
|
+
{isSyncingBranch ? "同期中..." : "最新の変更を同期"}
|
|
570
|
+
</button>
|
|
571
|
+
<Link to="/config" className="button button--ghost">
|
|
572
|
+
設定を編集
|
|
573
|
+
</Link>
|
|
574
|
+
</div>
|
|
575
|
+
|
|
576
|
+
{selectedToolSummary && (
|
|
577
|
+
<dl className="metadata-grid metadata-grid--compact">
|
|
578
|
+
<div>
|
|
579
|
+
<dt>コマンド</dt>
|
|
580
|
+
<dd className="tool-card__command">{selectedToolSummary.command}</dd>
|
|
581
|
+
</div>
|
|
582
|
+
<div>
|
|
583
|
+
<dt>defaultArgs</dt>
|
|
584
|
+
<dd>{renderArgs(selectedToolSummary.defaultArgs)}</dd>
|
|
585
|
+
</div>
|
|
586
|
+
<div>
|
|
587
|
+
<dt>permissionSkipArgs</dt>
|
|
588
|
+
<dd>{renderArgs(selectedToolSummary.permissionSkipArgs)}</dd>
|
|
589
|
+
</div>
|
|
590
|
+
{argsPreview && (
|
|
591
|
+
<div className="metadata-grid__full">
|
|
592
|
+
<dt>最終的に実行されるコマンド</dt>
|
|
593
|
+
<dd className="tool-card__command">
|
|
594
|
+
{argsPreview.command} {argsPreview.args.join(" ")}
|
|
595
|
+
</dd>
|
|
596
|
+
</div>
|
|
597
|
+
)}
|
|
598
|
+
</dl>
|
|
599
|
+
)}
|
|
600
|
+
</div>
|
|
601
|
+
)}
|
|
602
|
+
</section>
|
|
603
|
+
<section className="section-card">
|
|
604
|
+
<header className="terminal-section__header">
|
|
605
|
+
<div>
|
|
606
|
+
<h2>セッション履歴</h2>
|
|
607
|
+
<p className="section-card__body">
|
|
608
|
+
この Worktree に紐づいた最新の AI セッションが表示されます。CLI からの起動分も共有されます。
|
|
609
|
+
</p>
|
|
610
|
+
</div>
|
|
611
|
+
{isSessionsLoading && <span className="pill">読み込み中...</span>}
|
|
612
|
+
</header>
|
|
613
|
+
{branchSessions.length === 0 ? (
|
|
614
|
+
<p className="section-card__body">セッション履歴はまだありません。</p>
|
|
615
|
+
) : (
|
|
616
|
+
<div className="session-table-wrapper">
|
|
617
|
+
<table className="session-table">
|
|
618
|
+
<thead>
|
|
619
|
+
<tr>
|
|
620
|
+
<th>状態</th>
|
|
621
|
+
<th>ツール</th>
|
|
622
|
+
<th>モード</th>
|
|
623
|
+
<th>開始時刻</th>
|
|
624
|
+
<th>終了時刻</th>
|
|
625
|
+
<th>操作</th>
|
|
626
|
+
</tr>
|
|
627
|
+
</thead>
|
|
628
|
+
<tbody>
|
|
629
|
+
{branchSessions.slice(0, 5).map((session) => (
|
|
630
|
+
<tr key={session.sessionId}>
|
|
631
|
+
<td>
|
|
632
|
+
<span className={`status-pill status-pill--${session.status}`}>
|
|
633
|
+
{SESSION_STATUS_LABEL[session.status]}
|
|
634
|
+
</span>
|
|
635
|
+
</td>
|
|
636
|
+
<td>{session.toolType === "custom" ? session.toolName ?? "custom" : toolLabel(session.toolType)}</td>
|
|
637
|
+
<td>{session.mode}</td>
|
|
638
|
+
<td>{formatDate(session.startedAt)}</td>
|
|
639
|
+
<td>{session.endedAt ? formatDate(session.endedAt) : "--"}</td>
|
|
640
|
+
<td>
|
|
641
|
+
{session.status === "running" ? (
|
|
642
|
+
<button
|
|
643
|
+
type="button"
|
|
644
|
+
className="button button--ghost"
|
|
645
|
+
onClick={() => handleTerminateSession(session.sessionId)}
|
|
646
|
+
disabled={terminatingSessionId === session.sessionId || deleteSession.isPending}
|
|
647
|
+
>
|
|
648
|
+
{terminatingSessionId === session.sessionId ? "終了中..." : "終了"}
|
|
649
|
+
</button>
|
|
650
|
+
) : (
|
|
651
|
+
<span className="session-table__muted">--</span>
|
|
652
|
+
)}
|
|
653
|
+
</td>
|
|
654
|
+
</tr>
|
|
655
|
+
))}
|
|
656
|
+
</tbody>
|
|
657
|
+
</table>
|
|
658
|
+
</div>
|
|
659
|
+
)}
|
|
660
|
+
</section>
|
|
661
|
+
<section className="section-card">
|
|
662
|
+
<header>
|
|
663
|
+
<h2>ブランチインサイト</h2>
|
|
664
|
+
</header>
|
|
665
|
+
<dl className="metadata-grid">
|
|
666
|
+
<div>
|
|
667
|
+
<dt>コミット</dt>
|
|
668
|
+
<dd>{branch.commitHash}</dd>
|
|
669
|
+
</div>
|
|
670
|
+
<div>
|
|
671
|
+
<dt>Author</dt>
|
|
672
|
+
<dd>{branch.author ?? "N/A"}</dd>
|
|
673
|
+
</div>
|
|
674
|
+
<div>
|
|
675
|
+
<dt>更新日</dt>
|
|
676
|
+
<dd>{formattedCommitDate}</dd>
|
|
677
|
+
</div>
|
|
678
|
+
<div>
|
|
679
|
+
<dt>Worktree</dt>
|
|
680
|
+
<dd>{branch.worktreePath ?? "未作成"}</dd>
|
|
681
|
+
</div>
|
|
682
|
+
</dl>
|
|
683
|
+
</section>
|
|
684
|
+
|
|
685
|
+
<section className="section-card">
|
|
686
|
+
<header>
|
|
687
|
+
<h2>コミット情報</h2>
|
|
688
|
+
</header>
|
|
689
|
+
<p className="section-card__body">
|
|
690
|
+
{branch.commitMessage ?? "コミットメッセージがありません。"}
|
|
691
|
+
</p>
|
|
692
|
+
</section>
|
|
693
|
+
|
|
694
|
+
{branch.divergence && (
|
|
695
|
+
<section className="section-card">
|
|
696
|
+
<header>
|
|
697
|
+
<h2>差分状況</h2>
|
|
698
|
+
</header>
|
|
699
|
+
<div className="pill-group">
|
|
700
|
+
<span className="pill">Ahead {branch.divergence.ahead}</span>
|
|
701
|
+
<span className="pill">Behind {branch.divergence.behind}</span>
|
|
702
|
+
<span
|
|
703
|
+
className={`pill ${
|
|
704
|
+
branch.divergence.upToDate ? "pill--success" : "pill--warning"
|
|
705
|
+
}`}
|
|
706
|
+
>
|
|
707
|
+
{branch.divergence.upToDate ? "最新" : "更新あり"}
|
|
708
|
+
</span>
|
|
709
|
+
</div>
|
|
710
|
+
</section>
|
|
711
|
+
)}
|
|
712
|
+
|
|
713
|
+
<section className="section-card">
|
|
714
|
+
<header>
|
|
715
|
+
<h2>Worktree情報</h2>
|
|
716
|
+
</header>
|
|
717
|
+
<ul className="list-muted">
|
|
718
|
+
<li>
|
|
719
|
+
パス: <strong>{branch.worktreePath ?? "未作成"}</strong>
|
|
720
|
+
</li>
|
|
721
|
+
<li>AIツールの起動にはクリーンなワークツリーであることを推奨します。</li>
|
|
722
|
+
<li>Worktreeを再作成すると既存のローカル変更が失われる可能性があります。</li>
|
|
723
|
+
</ul>
|
|
724
|
+
</section>
|
|
725
|
+
</div>
|
|
726
|
+
|
|
727
|
+
<div className="terminal-column">
|
|
728
|
+
{activeSessionId ? (
|
|
729
|
+
<section
|
|
730
|
+
className={`section-card terminal-section ${
|
|
731
|
+
isTerminalFullscreen ? "terminal-section--fullscreen" : ""
|
|
732
|
+
}`}
|
|
733
|
+
data-testid="active-terminal"
|
|
734
|
+
>
|
|
735
|
+
<div className="terminal-section__header">
|
|
736
|
+
<div>
|
|
737
|
+
<h2>ターミナルセッション</h2>
|
|
738
|
+
<p className="section-card__body">
|
|
739
|
+
出力はリアルタイムにストリームされます。終了するとこのパネルは自動で閉じます。
|
|
740
|
+
</p>
|
|
741
|
+
</div>
|
|
742
|
+
<div className="terminal-section__controls">
|
|
743
|
+
<button
|
|
744
|
+
type="button"
|
|
745
|
+
className="button button--ghost"
|
|
746
|
+
onClick={() => setIsTerminalFullscreen((prev) => !prev)}
|
|
747
|
+
>
|
|
748
|
+
{isTerminalFullscreen ? "通常表示に戻す" : "ターミナルを最大化"}
|
|
749
|
+
</button>
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
<div className="terminal-surface">
|
|
753
|
+
<Terminal
|
|
754
|
+
sessionId={activeSessionId}
|
|
755
|
+
onExit={handleSessionExit}
|
|
756
|
+
onError={(message) =>
|
|
757
|
+
setBanner({ type: "error", message: message ?? "不明なエラー" })
|
|
758
|
+
}
|
|
759
|
+
/>
|
|
760
|
+
</div>
|
|
761
|
+
{isTerminalFullscreen && (
|
|
762
|
+
<button
|
|
763
|
+
type="button"
|
|
764
|
+
className="terminal-section__close"
|
|
765
|
+
aria-label="ターミナルを閉じる"
|
|
766
|
+
onClick={() => setIsTerminalFullscreen(false)}
|
|
767
|
+
>
|
|
768
|
+
×
|
|
769
|
+
</button>
|
|
770
|
+
)}
|
|
771
|
+
</section>
|
|
772
|
+
) : (
|
|
773
|
+
<section className="section-card session-hint">
|
|
774
|
+
<header>
|
|
775
|
+
<h2>セッションは未起動</h2>
|
|
776
|
+
</header>
|
|
777
|
+
<p className="section-card__body">
|
|
778
|
+
上部のアクションからAIツールを起動すると、このエリアにターミナルが表示されます。
|
|
779
|
+
</p>
|
|
780
|
+
</section>
|
|
781
|
+
)}
|
|
782
|
+
</div>
|
|
783
|
+
</div>
|
|
784
|
+
</main>
|
|
785
|
+
</div>
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function formatDate(value?: string | null) {
|
|
790
|
+
if (!value) {
|
|
791
|
+
return "日時不明";
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
const date = new Date(value);
|
|
796
|
+
return new Intl.DateTimeFormat("ja-JP", {
|
|
797
|
+
year: "numeric",
|
|
798
|
+
month: "short",
|
|
799
|
+
day: "numeric",
|
|
800
|
+
hour: "2-digit",
|
|
801
|
+
minute: "2-digit",
|
|
802
|
+
}).format(date);
|
|
803
|
+
} catch (_err) {
|
|
804
|
+
return value;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function formatError(error: unknown, fallback: string) {
|
|
809
|
+
if (error instanceof ApiError) {
|
|
810
|
+
return `${error.message}${error.details ? `\n${error.details}` : ""}`;
|
|
811
|
+
}
|
|
812
|
+
if (error instanceof Error) {
|
|
813
|
+
return error.message;
|
|
814
|
+
}
|
|
815
|
+
return fallback;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function toolLabel(tool: ToolType, selectedTool?: SelectableTool) {
|
|
819
|
+
if (tool === "custom" && selectedTool?.target === "custom") {
|
|
820
|
+
return selectedTool.label;
|
|
821
|
+
}
|
|
822
|
+
if (tool === "codex-cli") {
|
|
823
|
+
return "Codex CLI";
|
|
824
|
+
}
|
|
825
|
+
return "Claude Code";
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function renderArgs(args?: string[] | null) {
|
|
829
|
+
if (!args || args.length === 0) {
|
|
830
|
+
return <span className="tool-card__muted">未設定</span>;
|
|
831
|
+
}
|
|
832
|
+
return args.join(" ");
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const SESSION_STATUS_LABEL: Record<"pending" | "running" | "completed" | "failed", string> = {
|
|
836
|
+
pending: "pending",
|
|
837
|
+
running: "running",
|
|
838
|
+
completed: "completed",
|
|
839
|
+
failed: "failed",
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
function parseExtraArgs(value: string): string[] {
|
|
843
|
+
return value
|
|
844
|
+
.split(/\s+/)
|
|
845
|
+
.map((chunk) => chunk.trim())
|
|
846
|
+
.filter(Boolean);
|
|
847
|
+
}
|