@akiojin/gwt 2.7.4 → 2.9.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/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +17 -1
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts +2 -1
- package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/AIToolSelectorScreen.js +18 -3
- package/dist/cli/ui/components/screens/AIToolSelectorScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +5 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
- package/dist/cli/ui/hooks/useGitData.js +16 -4
- package/dist/cli/ui/hooks/useGitData.js.map +1 -1
- package/dist/cli/ui/types.d.ts +4 -0
- package/dist/cli/ui/types.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.js +47 -0
- package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
- package/dist/client/assets/{index-DxHGLTNq.js → index-CNWntAlF.js} +15 -15
- package/dist/client/index.html +1 -1
- package/dist/config/index.d.ts +17 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +66 -1
- package/dist/config/index.js.map +1 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +114 -30
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/types/api.d.ts +11 -0
- package/dist/types/api.d.ts.map +1 -1
- package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
- package/dist/web/client/src/pages/BranchDetailPage.js +55 -0
- package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
- package/dist/web/server/routes/sessions.d.ts.map +1 -1
- package/dist/web/server/routes/sessions.js +35 -0
- package/dist/web/server/routes/sessions.js.map +1 -1
- package/dist/web/server/services/branches.d.ts.map +1 -1
- package/dist/web/server/services/branches.js +4 -0
- package/dist/web/server/services/branches.js.map +1 -1
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js +3 -1
- package/dist/worktree.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +20 -0
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +53 -0
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +51 -0
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +37 -0
- package/src/cli/ui/components/App.tsx +21 -0
- package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +27 -2
- package/src/cli/ui/components/screens/BranchListScreen.tsx +6 -1
- package/src/cli/ui/hooks/useGitData.ts +15 -4
- package/src/cli/ui/types.ts +5 -0
- package/src/cli/ui/utils/branchFormatter.ts +47 -0
- package/src/config/index.ts +87 -1
- package/src/git.ts +114 -30
- package/src/index.ts +4 -1
- package/src/types/api.ts +12 -0
- package/src/web/client/src/pages/BranchDetailPage.tsx +69 -1
- package/src/web/server/routes/sessions.ts +39 -0
- package/src/web/server/services/branches.ts +5 -0
- package/src/worktree.ts +7 -1
package/src/git.ts
CHANGED
|
@@ -58,27 +58,103 @@ export async function isGitRepository(): Promise<boolean> {
|
|
|
58
58
|
* @throws {GitError} リポジトリルートの取得に失敗した場合
|
|
59
59
|
*/
|
|
60
60
|
export async function getRepositoryRoot(): Promise<string> {
|
|
61
|
+
const fs = await import("node:fs");
|
|
62
|
+
const path = await import("node:path");
|
|
63
|
+
|
|
64
|
+
const toExistingDir = (p: string): string => {
|
|
65
|
+
let current = path.resolve(p);
|
|
66
|
+
while (!fs.existsSync(current) && path.dirname(current) !== current) {
|
|
67
|
+
current = path.dirname(current);
|
|
68
|
+
}
|
|
69
|
+
if (!fs.existsSync(current)) {
|
|
70
|
+
return current;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const stat = fs.statSync(current);
|
|
74
|
+
return stat.isDirectory() ? current : path.dirname(current);
|
|
75
|
+
} catch {
|
|
76
|
+
return current;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// 1) show-toplevel を最優先
|
|
81
|
+
try {
|
|
82
|
+
const { stdout } = await execa("git", ["rev-parse", "--show-toplevel"]);
|
|
83
|
+
const top = stdout.trim();
|
|
84
|
+
if (top) {
|
|
85
|
+
const marker = `${path.sep}.worktrees${path.sep}`;
|
|
86
|
+
const idx = top.indexOf(marker);
|
|
87
|
+
if (idx >= 0) {
|
|
88
|
+
// /repo/.worktrees/<name> → repo root = /repo
|
|
89
|
+
const parent = top.slice(0, idx);
|
|
90
|
+
const upTwo = path.resolve(top, "..", "..");
|
|
91
|
+
if (fs.existsSync(parent)) return toExistingDir(parent);
|
|
92
|
+
if (fs.existsSync(upTwo)) return toExistingDir(upTwo);
|
|
93
|
+
}
|
|
94
|
+
if (fs.existsSync(top)) {
|
|
95
|
+
return toExistingDir(top);
|
|
96
|
+
}
|
|
97
|
+
return toExistingDir(path.resolve(top, ".."));
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// fallback
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 2) git-common-dir 経由
|
|
61
104
|
try {
|
|
62
|
-
// git rev-parse --git-common-dirを使用してメインリポジトリの.gitディレクトリを取得
|
|
63
105
|
const { stdout: gitCommonDir } = await execa("git", [
|
|
64
106
|
"rev-parse",
|
|
65
107
|
"--git-common-dir",
|
|
66
108
|
]);
|
|
67
|
-
const gitDir = gitCommonDir.trim();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// 相対パスが返された場合(.gitなど)は、現在のディレクトリからの相対パスとして解決
|
|
74
|
-
if (!path.isAbsolute(repoRoot)) {
|
|
75
|
-
return path.resolve(repoRoot);
|
|
109
|
+
const gitDir = path.resolve(gitCommonDir.trim());
|
|
110
|
+
const parts = gitDir.split(path.sep);
|
|
111
|
+
const idxWorktrees = parts.lastIndexOf("worktrees");
|
|
112
|
+
if (idxWorktrees > 0) {
|
|
113
|
+
const repo = parts.slice(0, idxWorktrees).join(path.sep);
|
|
114
|
+
if (fs.existsSync(repo)) return toExistingDir(repo);
|
|
76
115
|
}
|
|
116
|
+
const idxGit = parts.lastIndexOf(".git");
|
|
117
|
+
if (idxGit > 0) {
|
|
118
|
+
const repo = parts.slice(0, idxGit).join(path.sep);
|
|
119
|
+
if (fs.existsSync(repo)) return toExistingDir(repo);
|
|
120
|
+
}
|
|
121
|
+
return toExistingDir(path.dirname(gitDir));
|
|
122
|
+
} catch {
|
|
123
|
+
// fallback
|
|
124
|
+
}
|
|
77
125
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
126
|
+
// 3) .git を上に辿って探す
|
|
127
|
+
let current = process.cwd();
|
|
128
|
+
const root = path.parse(current).root;
|
|
129
|
+
while (true) {
|
|
130
|
+
const candidate = path.join(current, ".git");
|
|
131
|
+
if (fs.existsSync(candidate)) {
|
|
132
|
+
if (fs.statSync(candidate).isDirectory()) {
|
|
133
|
+
return toExistingDir(current);
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const content = fs.readFileSync(candidate, "utf-8");
|
|
137
|
+
const match = content.match(/gitdir:\s*(.+)\s*/i);
|
|
138
|
+
const gitDirMatch = match?.[1];
|
|
139
|
+
if (gitDirMatch) {
|
|
140
|
+
const gitDirPath = path.resolve(current, gitDirMatch.trim());
|
|
141
|
+
const parts = gitDirPath.split(path.sep);
|
|
142
|
+
const idxWT = parts.lastIndexOf("worktrees");
|
|
143
|
+
if (idxWT > 0) {
|
|
144
|
+
const repo = parts.slice(0, idxWT).join(path.sep);
|
|
145
|
+
return toExistingDir(repo);
|
|
146
|
+
}
|
|
147
|
+
return toExistingDir(path.dirname(gitDirPath));
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// ignore and continue
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (current === root) break;
|
|
154
|
+
current = path.dirname(current);
|
|
81
155
|
}
|
|
156
|
+
|
|
157
|
+
throw new GitError("Failed to get repository root");
|
|
82
158
|
}
|
|
83
159
|
|
|
84
160
|
/**
|
|
@@ -97,22 +173,33 @@ export async function getWorktreeRoot(): Promise<string> {
|
|
|
97
173
|
|
|
98
174
|
export async function getCurrentBranch(): Promise<string | null> {
|
|
99
175
|
try {
|
|
100
|
-
const
|
|
176
|
+
const repoRoot = await getRepositoryRoot();
|
|
177
|
+
const { stdout } = await execa(
|
|
178
|
+
"git",
|
|
179
|
+
["branch", "--show-current"],
|
|
180
|
+
{ cwd: repoRoot },
|
|
181
|
+
);
|
|
101
182
|
return stdout.trim() || null;
|
|
102
183
|
} catch {
|
|
103
|
-
|
|
184
|
+
try {
|
|
185
|
+
const { stdout } = await execa("git", ["branch", "--show-current"]);
|
|
186
|
+
return stdout.trim() || null;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
104
190
|
}
|
|
105
191
|
}
|
|
106
192
|
|
|
107
193
|
async function getBranchCommitTimestamps(
|
|
108
194
|
refs: string[],
|
|
195
|
+
cwd?: string,
|
|
109
196
|
): Promise<Map<string, number>> {
|
|
110
197
|
try {
|
|
111
|
-
const { stdout } = await execa("git", [
|
|
198
|
+
const { stdout = "" } = (await execa("git", [
|
|
112
199
|
"for-each-ref",
|
|
113
200
|
"--format=%(refname:short)%00%(committerdate:unix)",
|
|
114
201
|
...refs,
|
|
115
|
-
]);
|
|
202
|
+
], cwd ? { cwd } : undefined)) ?? { stdout: "" };
|
|
116
203
|
|
|
117
204
|
const map = new Map<string, number>();
|
|
118
205
|
|
|
@@ -135,10 +222,9 @@ async function getBranchCommitTimestamps(
|
|
|
135
222
|
export async function getLocalBranches(): Promise<BranchInfo[]> {
|
|
136
223
|
try {
|
|
137
224
|
const commitMap = await getBranchCommitTimestamps(["refs/heads"]);
|
|
138
|
-
const { stdout
|
|
139
|
-
"branch",
|
|
140
|
-
|
|
141
|
-
]);
|
|
225
|
+
const { stdout = "" } =
|
|
226
|
+
(await execa("git", ["branch", "--format=%(refname:short)"])) ??
|
|
227
|
+
{ stdout: "" };
|
|
142
228
|
return stdout
|
|
143
229
|
.split("\n")
|
|
144
230
|
.filter((line) => line.trim())
|
|
@@ -156,19 +242,17 @@ export async function getLocalBranches(): Promise<BranchInfo[]> {
|
|
|
156
242
|
: {}),
|
|
157
243
|
} satisfies BranchInfo;
|
|
158
244
|
});
|
|
159
|
-
} catch (
|
|
160
|
-
throw new GitError("Failed to get local branches",
|
|
245
|
+
} catch (primaryError) {
|
|
246
|
+
throw new GitError("Failed to get local branches", primaryError);
|
|
161
247
|
}
|
|
162
248
|
}
|
|
163
249
|
|
|
164
250
|
export async function getRemoteBranches(): Promise<BranchInfo[]> {
|
|
165
251
|
try {
|
|
166
252
|
const commitMap = await getBranchCommitTimestamps(["refs/remotes"]);
|
|
167
|
-
const { stdout
|
|
168
|
-
"branch",
|
|
169
|
-
"
|
|
170
|
-
"--format=%(refname:short)",
|
|
171
|
-
]);
|
|
253
|
+
const { stdout = "" } =
|
|
254
|
+
(await execa("git", ["branch", "-r", "--format=%(refname:short)"])) ??
|
|
255
|
+
{ stdout: "" };
|
|
172
256
|
return stdout
|
|
173
257
|
.split("\n")
|
|
174
258
|
.filter((line) => line.trim() && !line.includes("HEAD"))
|
|
@@ -187,8 +271,8 @@ export async function getRemoteBranches(): Promise<BranchInfo[]> {
|
|
|
187
271
|
: {}),
|
|
188
272
|
} satisfies BranchInfo;
|
|
189
273
|
});
|
|
190
|
-
} catch (
|
|
191
|
-
throw new GitError("Failed to get remote branches",
|
|
274
|
+
} catch (primaryError) {
|
|
275
|
+
throw new GitError("Failed to get remote branches", primaryError);
|
|
192
276
|
}
|
|
193
277
|
}
|
|
194
278
|
|
package/src/index.ts
CHANGED
|
@@ -665,11 +665,14 @@ export async function handleAIToolWorkflow(
|
|
|
665
665
|
});
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
-
// Save session with lastUsedTool
|
|
668
|
+
// Save session with lastUsedTool (tool selection is confirmed at this point)
|
|
669
669
|
await saveSession({
|
|
670
670
|
lastWorktreePath: worktreePath,
|
|
671
671
|
lastBranch: branch,
|
|
672
672
|
lastUsedTool: tool,
|
|
673
|
+
toolLabel: toolConfig.displayName ?? tool,
|
|
674
|
+
mode,
|
|
675
|
+
model: model ?? null,
|
|
673
676
|
timestamp: Date.now(),
|
|
674
677
|
repositoryRoot: repoRoot,
|
|
675
678
|
});
|
package/src/types/api.ts
CHANGED
|
@@ -30,6 +30,7 @@ export interface Branch {
|
|
|
30
30
|
state: "open" | "merged" | "closed";
|
|
31
31
|
mergedAt?: string | null;
|
|
32
32
|
} | null;
|
|
33
|
+
lastToolUsage?: LastToolUsage | null;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -46,6 +47,7 @@ export interface Worktree {
|
|
|
46
47
|
lastAccessedAt?: string | null; // ISO8601
|
|
47
48
|
divergence?: Branch["divergence"];
|
|
48
49
|
prInfo?: Branch["prInfo"];
|
|
50
|
+
lastToolUsage?: LastToolUsage | null;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
/**
|
|
@@ -102,6 +104,16 @@ export interface CustomAITool {
|
|
|
102
104
|
updatedAt?: string | null; // ISO8601
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
export interface LastToolUsage {
|
|
108
|
+
branch: string;
|
|
109
|
+
worktreePath: string | null;
|
|
110
|
+
toolId: string;
|
|
111
|
+
toolLabel: string;
|
|
112
|
+
mode?: "normal" | "continue" | "resume" | null;
|
|
113
|
+
model?: string | null;
|
|
114
|
+
timestamp: number; // epoch millis
|
|
115
|
+
}
|
|
116
|
+
|
|
105
117
|
/**
|
|
106
118
|
* REST API Response wrappers
|
|
107
119
|
*/
|
|
@@ -10,7 +10,11 @@ import {
|
|
|
10
10
|
import { useConfig } from "../hooks/useConfig";
|
|
11
11
|
import { ApiError } from "../lib/api";
|
|
12
12
|
import { Terminal } from "../components/Terminal";
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
Branch,
|
|
15
|
+
CustomAITool,
|
|
16
|
+
LastToolUsage,
|
|
17
|
+
} from "../../../../types/api.js";
|
|
14
18
|
|
|
15
19
|
type ToolType = "claude-code" | "codex-cli" | "custom";
|
|
16
20
|
type ToolMode = "normal" | "continue" | "resume";
|
|
@@ -410,6 +414,29 @@ export function BranchDetailPage() {
|
|
|
410
414
|
.sort((a, b) => (b.startedAt ?? "").localeCompare(a.startedAt ?? ""));
|
|
411
415
|
}, [sessionsData, branch?.worktreePath]);
|
|
412
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
|
+
}, [branch?.lastToolUsage, branch?.name, branch?.worktreePath, branchSessions]);
|
|
439
|
+
|
|
413
440
|
const handleSessionExit = (code: number) => {
|
|
414
441
|
setActiveSessionId(null);
|
|
415
442
|
setIsTerminalFullscreen(false);
|
|
@@ -449,6 +476,21 @@ export function BranchDetailPage() {
|
|
|
449
476
|
{branch.worktreePath ? "Worktreeあり" : "Worktree未作成"}
|
|
450
477
|
</span>
|
|
451
478
|
</div>
|
|
479
|
+
<div className="badge-group" style={{ marginTop: "0.5rem" }}>
|
|
480
|
+
{latestToolUsage ? (
|
|
481
|
+
<>
|
|
482
|
+
<span className="status-badge status-badge--muted">
|
|
483
|
+
{renderToolUsage(latestToolUsage)}
|
|
484
|
+
</span>
|
|
485
|
+
<span className="status-badge status-badge--muted">
|
|
486
|
+
{formatUsageTimestamp(latestToolUsage.timestamp)} ・ worktree:{" "}
|
|
487
|
+
{latestToolUsage.worktreePath ?? branch.worktreePath ?? "N/A"}
|
|
488
|
+
</span>
|
|
489
|
+
</>
|
|
490
|
+
) : (
|
|
491
|
+
<span className="status-badge status-badge--muted">Unknown</span>
|
|
492
|
+
)}
|
|
493
|
+
</div>
|
|
452
494
|
<div className="page-hero__actions">
|
|
453
495
|
{!canStartSession ? (
|
|
454
496
|
<button
|
|
@@ -950,6 +992,32 @@ const SESSION_STATUS_LABEL: Record<
|
|
|
950
992
|
failed: "failed",
|
|
951
993
|
};
|
|
952
994
|
|
|
995
|
+
function renderToolUsage(usage: LastToolUsage): string {
|
|
996
|
+
const modeLabel =
|
|
997
|
+
usage.mode === "normal"
|
|
998
|
+
? "New"
|
|
999
|
+
: usage.mode === "continue"
|
|
1000
|
+
? "Continue"
|
|
1001
|
+
: usage.mode === "resume"
|
|
1002
|
+
? "Resume"
|
|
1003
|
+
: null;
|
|
1004
|
+
const toolText = mapToolLabel(usage.toolId, usage.toolLabel);
|
|
1005
|
+
return [toolText, modeLabel, usage.model].filter(Boolean).join(" | ");
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function formatUsageTimestamp(value: number): string {
|
|
1009
|
+
return formatDate(new Date(value).toISOString());
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function mapToolLabel(toolId: string, toolLabel?: string | null): string {
|
|
1013
|
+
if (toolId === "claude-code") return "Claude";
|
|
1014
|
+
if (toolId === "codex-cli") return "Codex";
|
|
1015
|
+
if (toolId === "gemini-cli") return "Gemini";
|
|
1016
|
+
if (toolId === "qwen-cli") return "Qwen";
|
|
1017
|
+
if (toolLabel) return toolLabel;
|
|
1018
|
+
return "Custom";
|
|
1019
|
+
}
|
|
1020
|
+
|
|
953
1021
|
function parseExtraArgs(value: string): string[] {
|
|
954
1022
|
return value
|
|
955
1023
|
.split(/\s+/)
|
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
AIToolSession,
|
|
12
12
|
StartSessionRequest,
|
|
13
13
|
} from "../../../types/api.js";
|
|
14
|
+
import { saveSession } from "../../../config/index.js";
|
|
15
|
+
import { execa } from "execa";
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* セッション関連のルートを登録
|
|
@@ -53,6 +55,37 @@ export async function registerSessionRoutes(
|
|
|
53
55
|
toolName,
|
|
54
56
|
);
|
|
55
57
|
|
|
58
|
+
// 履歴を永続化(best-effort)
|
|
59
|
+
try {
|
|
60
|
+
const { stdout: repoRoot } = await execa("git", ["rev-parse", "--show-toplevel"], {
|
|
61
|
+
cwd: worktreePath,
|
|
62
|
+
});
|
|
63
|
+
let branchName: string | null = null;
|
|
64
|
+
try {
|
|
65
|
+
const { stdout: branchStdout } = await execa(
|
|
66
|
+
"git",
|
|
67
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
68
|
+
{ cwd: worktreePath },
|
|
69
|
+
);
|
|
70
|
+
branchName = branchStdout.trim() || null;
|
|
71
|
+
} catch {
|
|
72
|
+
branchName = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await saveSession({
|
|
76
|
+
lastWorktreePath: worktreePath,
|
|
77
|
+
lastBranch: branchName,
|
|
78
|
+
lastUsedTool: toolType === "custom" ? toolName ?? "custom" : toolType,
|
|
79
|
+
toolLabel:
|
|
80
|
+
toolType === "custom" ? toolName ?? "Custom" : toolLabelFromType(toolType),
|
|
81
|
+
mode,
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
repositoryRoot: repoRoot.trim(),
|
|
84
|
+
});
|
|
85
|
+
} catch {
|
|
86
|
+
// ignore persistence errors
|
|
87
|
+
}
|
|
88
|
+
|
|
56
89
|
reply.code(201);
|
|
57
90
|
return { success: true, data: session };
|
|
58
91
|
} catch (error) {
|
|
@@ -128,3 +161,9 @@ export async function registerSessionRoutes(
|
|
|
128
161
|
}
|
|
129
162
|
});
|
|
130
163
|
}
|
|
164
|
+
|
|
165
|
+
function toolLabelFromType(toolType: "claude-code" | "codex-cli" | "custom") {
|
|
166
|
+
if (toolType === "claude-code") return "Claude";
|
|
167
|
+
if (toolType === "codex-cli") return "Codex";
|
|
168
|
+
return "Custom";
|
|
169
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { getPullRequestByBranch } from "../../../github.js";
|
|
17
17
|
import { listAdditionalWorktrees } from "../../../worktree.js";
|
|
18
18
|
import type { Branch, BranchSyncResult } from "../../../types/api.js";
|
|
19
|
+
import { getLastToolUsageMap } from "../../../config/index.js";
|
|
19
20
|
|
|
20
21
|
type DivergenceStatus = { remoteAhead: number; localAhead: number };
|
|
21
22
|
type DivergenceValue = NonNullable<NonNullable<Branch["divergence"]>>;
|
|
@@ -38,6 +39,7 @@ function mapPullRequestState(state: string): "open" | "merged" | "closed" {
|
|
|
38
39
|
*/
|
|
39
40
|
export async function listBranches(): Promise<Branch[]> {
|
|
40
41
|
const repoRoot = await getRepositoryRoot();
|
|
42
|
+
const lastToolUsageMap = await getLastToolUsageMap(repoRoot);
|
|
41
43
|
|
|
42
44
|
// リモートブランチの最新情報を取得(失敗してもローカル情報にはフォールバック)
|
|
43
45
|
try {
|
|
@@ -103,6 +105,8 @@ export async function listBranches(): Promise<Branch[]> {
|
|
|
103
105
|
? mapDivergence(divergenceStatus)
|
|
104
106
|
: null;
|
|
105
107
|
|
|
108
|
+
const lastToolUsage = lastToolUsageMap.get(branchInfo.name) ?? null;
|
|
109
|
+
|
|
106
110
|
return {
|
|
107
111
|
name: branchInfo.name,
|
|
108
112
|
type: branchInfo.type,
|
|
@@ -116,6 +120,7 @@ export async function listBranches(): Promise<Branch[]> {
|
|
|
116
120
|
baseBranch: baseBranch ?? null,
|
|
117
121
|
divergence,
|
|
118
122
|
prInfo,
|
|
123
|
+
...(lastToolUsage ? { lastToolUsage } : {}),
|
|
119
124
|
};
|
|
120
125
|
}),
|
|
121
126
|
);
|
package/src/worktree.ts
CHANGED
|
@@ -98,7 +98,13 @@ export interface WorktreeInfo {
|
|
|
98
98
|
|
|
99
99
|
async function listWorktrees(): Promise<WorktreeInfo[]> {
|
|
100
100
|
try {
|
|
101
|
-
const {
|
|
101
|
+
const { getRepositoryRoot } = await import("./git.js");
|
|
102
|
+
const repoRoot = await getRepositoryRoot();
|
|
103
|
+
const { stdout } = await execa(
|
|
104
|
+
"git",
|
|
105
|
+
["worktree", "list", "--porcelain"],
|
|
106
|
+
{ cwd: repoRoot },
|
|
107
|
+
);
|
|
102
108
|
const worktrees: WorktreeInfo[] = [];
|
|
103
109
|
const lines = stdout.split("\n");
|
|
104
110
|
|