@akiojin/gwt 2.10.0 → 2.11.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 +5 -47
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/common/Input.d.ts +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts +1 -2
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +19 -14
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/ModelSelectorScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/ModelSelectorScreen.js +6 -6
- package/dist/cli/ui/components/screens/ModelSelectorScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/PRCleanupScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/PRCleanupScreen.js +0 -3
- package/dist/cli/ui/components/screens/PRCleanupScreen.js.map +1 -1
- package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
- package/dist/cli/ui/hooks/useGitData.js +43 -2
- package/dist/cli/ui/hooks/useGitData.js.map +1 -1
- package/dist/cli/ui/types.d.ts +16 -4
- 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 +124 -15
- package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
- package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
- package/dist/cli/ui/utils/modelOptions.js.map +1 -1
- package/dist/client/assets/{index-CNWntAlF.js → index-Dl798X5w.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/git.d.ts +6 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +40 -5
- package/dist/git.js.map +1 -1
- package/dist/web/client/src/components/BranchGraph.js +1 -1
- package/dist/web/client/src/components/BranchGraph.js.map +1 -1
- package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
- package/dist/web/client/src/pages/BranchDetailPage.js +8 -3
- 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 +4 -2
- package/dist/web/server/routes/sessions.js.map +1 -1
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js +31 -44
- package/dist/worktree.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +9 -17
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +14 -20
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +14 -44
- package/src/cli/ui/__tests__/components/App.test.tsx +8 -15
- package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +12 -5
- package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/common/Input.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +15 -14
- package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +3 -3
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +1 -4
- package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/parts/Header.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +31 -41
- package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +18 -17
- package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +3 -2
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +5 -12
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +15 -10
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +3 -2
- package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +0 -4
- package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +3 -5
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +24 -22
- package/src/cli/ui/components/App.tsx +3 -74
- package/src/cli/ui/components/common/Input.tsx +1 -1
- package/src/cli/ui/components/screens/BranchListScreen.tsx +32 -22
- package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +46 -49
- package/src/cli/ui/components/screens/PRCleanupScreen.tsx +0 -3
- package/src/cli/ui/hooks/useGitData.ts +59 -1
- package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +3 -2
- package/src/cli/ui/types.ts +24 -5
- package/src/cli/ui/utils/branchFormatter.ts +123 -15
- package/src/cli/ui/utils/modelOptions.test.ts +4 -6
- package/src/cli/ui/utils/modelOptions.ts +1 -2
- package/src/config/index.ts +2 -1
- package/src/git.ts +56 -16
- package/src/web/client/src/components/BranchGraph.tsx +1 -1
- package/src/web/client/src/pages/BranchDetailPage.tsx +8 -3
- package/src/web/server/routes/sessions.ts +12 -5
- package/src/worktree.ts +31 -59
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts +0 -20
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts.map +0 -1
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.js +0 -65
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.js.map +0 -1
- package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +0 -151
- package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +0 -117
package/src/cli/ui/types.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { LastToolUsage } from "../../types/api.js";
|
|
2
|
+
export type { LastToolUsage } from "../../types/api.js";
|
|
3
|
+
|
|
1
4
|
export interface WorktreeInfo {
|
|
2
5
|
path: string;
|
|
3
6
|
locked: boolean;
|
|
@@ -5,11 +8,8 @@ export interface WorktreeInfo {
|
|
|
5
8
|
isAccessible?: boolean;
|
|
6
9
|
}
|
|
7
10
|
|
|
8
|
-
import type { LastToolUsage } from "../../types/api.js";
|
|
9
|
-
|
|
10
11
|
export type AITool = string;
|
|
11
12
|
export type InferenceLevel = "low" | "medium" | "high" | "xhigh";
|
|
12
|
-
export type { LastToolUsage } from "../../types/api.js";
|
|
13
13
|
|
|
14
14
|
export interface ModelOption {
|
|
15
15
|
id: string;
|
|
@@ -20,6 +20,12 @@ export interface ModelOption {
|
|
|
20
20
|
isDefault?: boolean;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface BranchDivergence {
|
|
24
|
+
ahead: number;
|
|
25
|
+
behind: number;
|
|
26
|
+
upToDate: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
export interface BranchInfo {
|
|
24
30
|
name: string;
|
|
25
31
|
type: "local" | "remote";
|
|
@@ -39,6 +45,9 @@ export interface BranchInfo {
|
|
|
39
45
|
mergedPR?: { number: number; mergedAt: string };
|
|
40
46
|
latestCommitTimestamp?: number;
|
|
41
47
|
lastToolUsage?: LastToolUsage | null;
|
|
48
|
+
upstream?: string | null;
|
|
49
|
+
divergence?: BranchDivergence | null;
|
|
50
|
+
hasRemoteCounterpart?: boolean;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
export interface BranchChoice {
|
|
@@ -132,7 +141,7 @@ export interface WorktreeWithPR {
|
|
|
132
141
|
pullRequest: PullRequest | null;
|
|
133
142
|
}
|
|
134
143
|
|
|
135
|
-
export type CleanupReason = "
|
|
144
|
+
export type CleanupReason = "no-diff-with-base" | "remote-synced";
|
|
136
145
|
|
|
137
146
|
export interface CleanupTarget {
|
|
138
147
|
worktreePath: string | null; // null for local branch only cleanup
|
|
@@ -173,7 +182,6 @@ export interface GitHubPRResponse {
|
|
|
173
182
|
*/
|
|
174
183
|
export type ScreenType =
|
|
175
184
|
| "branch-list"
|
|
176
|
-
| "worktree-manager"
|
|
177
185
|
| "branch-creator"
|
|
178
186
|
| "branch-action-selector"
|
|
179
187
|
| "ai-tool-selector"
|
|
@@ -201,6 +209,14 @@ export interface Screen {
|
|
|
201
209
|
*/
|
|
202
210
|
export type WorktreeStatus = "active" | "inaccessible" | undefined;
|
|
203
211
|
|
|
212
|
+
export type SyncStatus =
|
|
213
|
+
| "up-to-date"
|
|
214
|
+
| "ahead"
|
|
215
|
+
| "behind"
|
|
216
|
+
| "diverged"
|
|
217
|
+
| "no-upstream"
|
|
218
|
+
| "remote-only";
|
|
219
|
+
|
|
204
220
|
export interface BranchItem extends BranchInfo {
|
|
205
221
|
// Display properties
|
|
206
222
|
icons: string[];
|
|
@@ -209,6 +225,9 @@ export interface BranchItem extends BranchInfo {
|
|
|
209
225
|
label: string;
|
|
210
226
|
value: string;
|
|
211
227
|
lastToolUsageLabel?: string | null;
|
|
228
|
+
syncStatus?: SyncStatus;
|
|
229
|
+
syncInfo?: string | undefined;
|
|
230
|
+
remoteName?: string | undefined;
|
|
212
231
|
}
|
|
213
232
|
|
|
214
233
|
/**
|
|
@@ -25,23 +25,38 @@ const worktreeIcons: Record<Exclude<WorktreeStatus, undefined>, string> = {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
const changeIcons = {
|
|
28
|
-
current: "
|
|
29
|
-
hasChanges: "
|
|
30
|
-
unpushed: "
|
|
31
|
-
openPR: "
|
|
28
|
+
current: "👉",
|
|
29
|
+
hasChanges: "💾",
|
|
30
|
+
unpushed: "📤",
|
|
31
|
+
openPR: "🔃",
|
|
32
32
|
mergedPR: "✅",
|
|
33
33
|
warning: "⚠️",
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
const remoteIcon = "☁";
|
|
37
37
|
|
|
38
|
+
// Sync status icons
|
|
39
|
+
const syncIcons = {
|
|
40
|
+
upToDate: "✓",
|
|
41
|
+
ahead: "↑",
|
|
42
|
+
behind: "↓",
|
|
43
|
+
diverged: "↕",
|
|
44
|
+
none: "-",
|
|
45
|
+
remoteOnly: "☁",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Remote column markers
|
|
49
|
+
const remoteMarkers = {
|
|
50
|
+
tracked: "🔗", // ローカル+同名リモートあり
|
|
51
|
+
localOnly: "💻", // ローカルのみ(リモートなし)
|
|
52
|
+
remoteOnly: "☁️", // リモートのみ(ローカルなし)
|
|
53
|
+
};
|
|
54
|
+
|
|
38
55
|
// Emoji width varies by terminal. Provide explicit minimum widths so we never
|
|
39
56
|
// underestimate and accidentally push the row past the terminal columns.
|
|
40
57
|
const iconWidthOverrides: Record<string, number> = {
|
|
41
58
|
// Remote icon
|
|
42
59
|
[remoteIcon]: 1,
|
|
43
|
-
// Unpushed icon
|
|
44
|
-
"⬆": 1,
|
|
45
60
|
// Branch type icons
|
|
46
61
|
"⚡": 1,
|
|
47
62
|
"✨": 1,
|
|
@@ -53,11 +68,16 @@ const iconWidthOverrides: Record<string, number> = {
|
|
|
53
68
|
"🟢": 1,
|
|
54
69
|
"🟠": 1,
|
|
55
70
|
// Change status icons
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
71
|
+
"👉": 1,
|
|
72
|
+
"💾": 1,
|
|
73
|
+
"📤": 1,
|
|
74
|
+
"🔃": 1,
|
|
59
75
|
"✅": 1,
|
|
60
76
|
"⚠️": 1,
|
|
77
|
+
// Remote markers
|
|
78
|
+
"🔗": 1,
|
|
79
|
+
"💻": 1,
|
|
80
|
+
"☁️": 1,
|
|
61
81
|
};
|
|
62
82
|
|
|
63
83
|
const getIconWidth = (icon: string): number => {
|
|
@@ -168,17 +188,81 @@ export function formatBranchItem(
|
|
|
168
188
|
changesIcon = " ".repeat(COLUMN_WIDTH);
|
|
169
189
|
}
|
|
170
190
|
|
|
171
|
-
// Column 4: Remote
|
|
172
|
-
let
|
|
191
|
+
// Column 4: Remote status (✓ for tracked, L for local-only, R for remote-only)
|
|
192
|
+
let remoteStatusStr: string;
|
|
193
|
+
if (branch.type === "remote") {
|
|
194
|
+
// リモートのみのブランチ
|
|
195
|
+
remoteStatusStr = padIcon(remoteMarkers.remoteOnly);
|
|
196
|
+
} else if (branch.hasRemoteCounterpart) {
|
|
197
|
+
// ローカルブランチで同名リモートあり
|
|
198
|
+
remoteStatusStr = padIcon(remoteMarkers.tracked);
|
|
199
|
+
} else {
|
|
200
|
+
// ローカルブランチでリモートなし
|
|
201
|
+
remoteStatusStr = padIcon(remoteMarkers.localOnly);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Column 5: Sync status (=, ↑N, ↓N, ↕, -)
|
|
205
|
+
// 5文字固定幅(アイコン1文字 + 数字最大4桁)、9999超は「9999+」表示
|
|
206
|
+
const SYNC_COLUMN_WIDTH = 6; // アイコン1 + 数字4 + スペース1
|
|
207
|
+
|
|
208
|
+
// 数字を4桁以内にフォーマット(9999超は9999+)
|
|
209
|
+
const formatSyncNumber = (n: number): string => {
|
|
210
|
+
if (n > 9999) return "9999+";
|
|
211
|
+
return String(n);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Sync列を固定幅でパディング
|
|
215
|
+
const padSyncColumn = (content: string): string => {
|
|
216
|
+
const width = stringWidth(content);
|
|
217
|
+
const padding = Math.max(0, SYNC_COLUMN_WIDTH - width);
|
|
218
|
+
return content + " ".repeat(padding);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
let syncStatusStr: string;
|
|
222
|
+
if (branch.type === "remote") {
|
|
223
|
+
// リモートのみ → 比較不可
|
|
224
|
+
syncStatusStr = padSyncColumn(syncIcons.none);
|
|
225
|
+
} else if (branch.divergence) {
|
|
226
|
+
const { ahead, behind, upToDate } = branch.divergence;
|
|
227
|
+
if (upToDate) {
|
|
228
|
+
syncStatusStr = padSyncColumn(syncIcons.upToDate);
|
|
229
|
+
} else if (ahead > 0 && behind > 0) {
|
|
230
|
+
// diverged: ↕ のみ表示(詳細は別途表示可能)
|
|
231
|
+
syncStatusStr = padSyncColumn(syncIcons.diverged);
|
|
232
|
+
} else if (ahead > 0) {
|
|
233
|
+
// ahead: ↑N の形式
|
|
234
|
+
syncStatusStr = padSyncColumn(
|
|
235
|
+
`${syncIcons.ahead}${formatSyncNumber(ahead)}`,
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
// behind: ↓N の形式
|
|
239
|
+
syncStatusStr = padSyncColumn(
|
|
240
|
+
`${syncIcons.behind}${formatSyncNumber(behind)}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
// divergence情報なし
|
|
245
|
+
syncStatusStr = padSyncColumn(syncIcons.none);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Build Local/Remote name for display
|
|
249
|
+
// ローカルブランチ: ブランチ名を表示
|
|
250
|
+
// リモートのみ: origin/xxxをフル表示
|
|
251
|
+
let displayName: string;
|
|
252
|
+
let remoteName: string | null = null;
|
|
253
|
+
|
|
173
254
|
if (branch.type === "remote") {
|
|
174
|
-
|
|
255
|
+
// リモートのみのブランチ: フルのリモートブランチ名を表示
|
|
256
|
+
displayName = branch.name; // origin/xxx
|
|
257
|
+
remoteName = branch.name;
|
|
175
258
|
} else {
|
|
176
|
-
|
|
259
|
+
// ローカルブランチ: ブランチ名を表示
|
|
260
|
+
displayName = branch.name;
|
|
177
261
|
}
|
|
178
262
|
|
|
179
263
|
// Build label with fixed-width columns
|
|
180
|
-
// Format: [Type][Worktree][Changes][Remote]
|
|
181
|
-
const label = `${branchTypeIcon}${worktreeIcon}${changesIcon}${
|
|
264
|
+
// Format: [Type][Worktree][Changes][Remote][Sync] DisplayName
|
|
265
|
+
const label = `${branchTypeIcon}${worktreeIcon}${changesIcon}${remoteStatusStr}${syncStatusStr}${displayName}`;
|
|
182
266
|
|
|
183
267
|
// Collect icons for compatibility
|
|
184
268
|
const icons: string[] = [];
|
|
@@ -208,6 +292,27 @@ export function formatBranchItem(
|
|
|
208
292
|
icons.push(remoteIcon);
|
|
209
293
|
}
|
|
210
294
|
|
|
295
|
+
// Determine sync status for BranchItem
|
|
296
|
+
let syncStatus: BranchItem["syncStatus"];
|
|
297
|
+
if (branch.type === "remote") {
|
|
298
|
+
syncStatus = "remote-only";
|
|
299
|
+
} else if (!branch.hasRemoteCounterpart) {
|
|
300
|
+
syncStatus = "no-upstream";
|
|
301
|
+
} else if (branch.divergence) {
|
|
302
|
+
const { ahead, behind, upToDate } = branch.divergence;
|
|
303
|
+
if (upToDate) {
|
|
304
|
+
syncStatus = "up-to-date";
|
|
305
|
+
} else if (ahead > 0 && behind > 0) {
|
|
306
|
+
syncStatus = "diverged";
|
|
307
|
+
} else if (ahead > 0) {
|
|
308
|
+
syncStatus = "ahead";
|
|
309
|
+
} else {
|
|
310
|
+
syncStatus = "behind";
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
syncStatus = "no-upstream";
|
|
314
|
+
}
|
|
315
|
+
|
|
211
316
|
return {
|
|
212
317
|
// Copy all properties from BranchInfo
|
|
213
318
|
...branch,
|
|
@@ -218,6 +323,9 @@ export function formatBranchItem(
|
|
|
218
323
|
label,
|
|
219
324
|
value: branch.name,
|
|
220
325
|
lastToolUsageLabel: buildLastToolUsageLabel(branch.lastToolUsage),
|
|
326
|
+
syncStatus,
|
|
327
|
+
syncInfo: undefined,
|
|
328
|
+
remoteName: remoteName ?? undefined,
|
|
221
329
|
};
|
|
222
330
|
}
|
|
223
331
|
|
|
@@ -11,11 +11,7 @@ describe("modelOptions", () => {
|
|
|
11
11
|
it("lists Claude official aliases and sets Opus 4.5 as default", () => {
|
|
12
12
|
const options = getModelOptions("claude-code");
|
|
13
13
|
const ids = options.map((m) => m.id);
|
|
14
|
-
expect(ids).toEqual([
|
|
15
|
-
"opus",
|
|
16
|
-
"sonnet",
|
|
17
|
-
"haiku",
|
|
18
|
-
]);
|
|
14
|
+
expect(ids).toEqual(["opus", "sonnet", "haiku"]);
|
|
19
15
|
const defaultModel = getDefaultModelOption("claude-code");
|
|
20
16
|
expect(defaultModel?.id).toBe("opus");
|
|
21
17
|
expect(defaultModel?.label).toBe("Opus 4.5");
|
|
@@ -34,7 +30,9 @@ describe("modelOptions", () => {
|
|
|
34
30
|
});
|
|
35
31
|
|
|
36
32
|
it("uses medium as default reasoning for codex-max", () => {
|
|
37
|
-
const codexMax = getModelOptions("codex-cli").find(
|
|
33
|
+
const codexMax = getModelOptions("codex-cli").find(
|
|
34
|
+
(m) => m.id === "gpt-5.1-codex-max",
|
|
35
|
+
);
|
|
38
36
|
expect(getDefaultInferenceForModel(codexMax)).toBe("medium");
|
|
39
37
|
});
|
|
40
38
|
|
package/src/config/index.ts
CHANGED
|
@@ -136,7 +136,8 @@ export async function saveSession(sessionData: SessionData): Promise<void> {
|
|
|
136
136
|
branch: sessionData.lastBranch,
|
|
137
137
|
worktreePath: sessionData.lastWorktreePath,
|
|
138
138
|
toolId: sessionData.lastUsedTool ?? "unknown",
|
|
139
|
-
toolLabel:
|
|
139
|
+
toolLabel:
|
|
140
|
+
sessionData.toolLabel ?? sessionData.lastUsedTool ?? "Custom",
|
|
140
141
|
mode: sessionData.mode ?? null,
|
|
141
142
|
model: sessionData.model ?? null,
|
|
142
143
|
timestamp: sessionData.timestamp,
|
package/src/git.ts
CHANGED
|
@@ -174,11 +174,9 @@ export async function getWorktreeRoot(): Promise<string> {
|
|
|
174
174
|
export async function getCurrentBranch(): Promise<string | null> {
|
|
175
175
|
try {
|
|
176
176
|
const repoRoot = await getRepositoryRoot();
|
|
177
|
-
const { stdout } = await execa(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
{ cwd: repoRoot },
|
|
181
|
-
);
|
|
177
|
+
const { stdout } = await execa("git", ["branch", "--show-current"], {
|
|
178
|
+
cwd: repoRoot,
|
|
179
|
+
});
|
|
182
180
|
return stdout.trim() || null;
|
|
183
181
|
} catch {
|
|
184
182
|
try {
|
|
@@ -195,11 +193,15 @@ async function getBranchCommitTimestamps(
|
|
|
195
193
|
cwd?: string,
|
|
196
194
|
): Promise<Map<string, number>> {
|
|
197
195
|
try {
|
|
198
|
-
const { stdout = "" } = (await execa(
|
|
199
|
-
"
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
196
|
+
const { stdout = "" } = (await execa(
|
|
197
|
+
"git",
|
|
198
|
+
[
|
|
199
|
+
"for-each-ref",
|
|
200
|
+
"--format=%(refname:short)%00%(committerdate:unix)",
|
|
201
|
+
...refs,
|
|
202
|
+
],
|
|
203
|
+
cwd ? { cwd } : undefined,
|
|
204
|
+
)) ?? { stdout: "" };
|
|
203
205
|
|
|
204
206
|
const map = new Map<string, number>();
|
|
205
207
|
|
|
@@ -222,9 +224,10 @@ async function getBranchCommitTimestamps(
|
|
|
222
224
|
export async function getLocalBranches(): Promise<BranchInfo[]> {
|
|
223
225
|
try {
|
|
224
226
|
const commitMap = await getBranchCommitTimestamps(["refs/heads"]);
|
|
225
|
-
const { stdout = "" } =
|
|
226
|
-
|
|
227
|
-
|
|
227
|
+
const { stdout = "" } = (await execa("git", [
|
|
228
|
+
"branch",
|
|
229
|
+
"--format=%(refname:short)",
|
|
230
|
+
])) ?? { stdout: "" };
|
|
228
231
|
return stdout
|
|
229
232
|
.split("\n")
|
|
230
233
|
.filter((line) => line.trim())
|
|
@@ -250,9 +253,11 @@ export async function getLocalBranches(): Promise<BranchInfo[]> {
|
|
|
250
253
|
export async function getRemoteBranches(): Promise<BranchInfo[]> {
|
|
251
254
|
try {
|
|
252
255
|
const commitMap = await getBranchCommitTimestamps(["refs/remotes"]);
|
|
253
|
-
const { stdout = "" } =
|
|
254
|
-
|
|
255
|
-
|
|
256
|
+
const { stdout = "" } = (await execa("git", [
|
|
257
|
+
"branch",
|
|
258
|
+
"-r",
|
|
259
|
+
"--format=%(refname:short)",
|
|
260
|
+
])) ?? { stdout: "" };
|
|
256
261
|
return stdout
|
|
257
262
|
.split("\n")
|
|
258
263
|
.filter((line) => line.trim() && !line.includes("HEAD"))
|
|
@@ -299,6 +304,41 @@ export async function getAllBranches(): Promise<BranchInfo[]> {
|
|
|
299
304
|
return [...localBranches, ...remoteBranches];
|
|
300
305
|
}
|
|
301
306
|
|
|
307
|
+
/**
|
|
308
|
+
* ローカルブランチのupstream(追跡ブランチ)情報を取得
|
|
309
|
+
* @param cwd - 作業ディレクトリ(省略時はリポジトリルート)
|
|
310
|
+
* @returns Map<ローカルブランチ名, upstreamブランチ名>
|
|
311
|
+
*/
|
|
312
|
+
export async function collectUpstreamMap(
|
|
313
|
+
cwd?: string,
|
|
314
|
+
): Promise<Map<string, string>> {
|
|
315
|
+
const workDir = cwd ?? (await getRepositoryRoot());
|
|
316
|
+
try {
|
|
317
|
+
const { stdout } = await execa(
|
|
318
|
+
"git",
|
|
319
|
+
[
|
|
320
|
+
"for-each-ref",
|
|
321
|
+
"--format=%(refname:short)|%(upstream:short)",
|
|
322
|
+
"refs/heads",
|
|
323
|
+
],
|
|
324
|
+
{ cwd: workDir },
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
return stdout
|
|
328
|
+
.split("\n")
|
|
329
|
+
.filter((line) => line.includes("|"))
|
|
330
|
+
.reduce((map, line) => {
|
|
331
|
+
const [branch, upstream] = line.split("|");
|
|
332
|
+
if (branch?.trim() && upstream?.trim()) {
|
|
333
|
+
map.set(branch.trim(), upstream.trim());
|
|
334
|
+
}
|
|
335
|
+
return map;
|
|
336
|
+
}, new Map<string, string>());
|
|
337
|
+
} catch {
|
|
338
|
+
return new Map();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
302
342
|
export async function createBranch(
|
|
303
343
|
branchName: string,
|
|
304
344
|
baseBranch = "main",
|
|
@@ -425,17 +425,22 @@ export function BranchDetailPage() {
|
|
|
425
425
|
worktreePath: branch.worktreePath ?? null,
|
|
426
426
|
toolId:
|
|
427
427
|
first.toolType === "custom"
|
|
428
|
-
? first.toolName ?? "custom"
|
|
428
|
+
? (first.toolName ?? "custom")
|
|
429
429
|
: (first.toolType as LastToolUsage["toolId"]),
|
|
430
430
|
toolLabel:
|
|
431
431
|
first.toolType === "custom"
|
|
432
|
-
? first.toolName ?? "Custom"
|
|
432
|
+
? (first.toolName ?? "Custom")
|
|
433
433
|
: toolLabel(first.toolType),
|
|
434
434
|
mode: first.mode ?? "normal",
|
|
435
435
|
model: null,
|
|
436
436
|
timestamp: first.startedAt ? Date.parse(first.startedAt) : Date.now(),
|
|
437
437
|
};
|
|
438
|
-
}, [
|
|
438
|
+
}, [
|
|
439
|
+
branch?.lastToolUsage,
|
|
440
|
+
branch?.name,
|
|
441
|
+
branch?.worktreePath,
|
|
442
|
+
branchSessions,
|
|
443
|
+
]);
|
|
439
444
|
|
|
440
445
|
const handleSessionExit = (code: number) => {
|
|
441
446
|
setActiveSessionId(null);
|
|
@@ -57,9 +57,13 @@ export async function registerSessionRoutes(
|
|
|
57
57
|
|
|
58
58
|
// 履歴を永続化(best-effort)
|
|
59
59
|
try {
|
|
60
|
-
const { stdout: repoRoot } = await execa(
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
const { stdout: repoRoot } = await execa(
|
|
61
|
+
"git",
|
|
62
|
+
["rev-parse", "--show-toplevel"],
|
|
63
|
+
{
|
|
64
|
+
cwd: worktreePath,
|
|
65
|
+
},
|
|
66
|
+
);
|
|
63
67
|
let branchName: string | null = null;
|
|
64
68
|
try {
|
|
65
69
|
const { stdout: branchStdout } = await execa(
|
|
@@ -75,9 +79,12 @@ export async function registerSessionRoutes(
|
|
|
75
79
|
await saveSession({
|
|
76
80
|
lastWorktreePath: worktreePath,
|
|
77
81
|
lastBranch: branchName,
|
|
78
|
-
lastUsedTool:
|
|
82
|
+
lastUsedTool:
|
|
83
|
+
toolType === "custom" ? (toolName ?? "custom") : toolType,
|
|
79
84
|
toolLabel:
|
|
80
|
-
toolType === "custom"
|
|
85
|
+
toolType === "custom"
|
|
86
|
+
? (toolName ?? "Custom")
|
|
87
|
+
: toolLabelFromType(toolType),
|
|
81
88
|
mode,
|
|
82
89
|
timestamp: Date.now(),
|
|
83
90
|
repositoryRoot: repoRoot.trim(),
|
package/src/worktree.ts
CHANGED
|
@@ -6,10 +6,9 @@ import {
|
|
|
6
6
|
WorktreeConfig,
|
|
7
7
|
WorktreeWithPR,
|
|
8
8
|
CleanupTarget,
|
|
9
|
-
MergedPullRequest,
|
|
10
9
|
CleanupReason,
|
|
11
10
|
} from "./cli/ui/types.js";
|
|
12
|
-
import { getPullRequestByBranch
|
|
11
|
+
import { getPullRequestByBranch } from "./github.js";
|
|
13
12
|
import {
|
|
14
13
|
hasUncommittedChanges,
|
|
15
14
|
hasUnpushedCommits,
|
|
@@ -27,6 +26,23 @@ import { getConfig } from "./config/index.js";
|
|
|
27
26
|
import { GIT_CONFIG } from "./config/constants.js";
|
|
28
27
|
import { startSpinner } from "./utils/spinner.js";
|
|
29
28
|
|
|
29
|
+
async function getUpstreamBranch(branch: string): Promise<string | null> {
|
|
30
|
+
try {
|
|
31
|
+
const result = await execa("git", [
|
|
32
|
+
"rev-parse",
|
|
33
|
+
"--abbrev-ref",
|
|
34
|
+
`${branch}@{upstream}`,
|
|
35
|
+
]);
|
|
36
|
+
const stdout =
|
|
37
|
+
typeof (result as { stdout?: unknown })?.stdout === "string"
|
|
38
|
+
? (result as { stdout: string }).stdout.trim()
|
|
39
|
+
: "";
|
|
40
|
+
return stdout.length ? stdout : null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
// Re-export WorktreeConfig for external use
|
|
31
47
|
export type { WorktreeConfig };
|
|
32
48
|
|
|
@@ -414,15 +430,13 @@ async function getWorktreesWithPRStatus(): Promise<WorktreeWithPR[]> {
|
|
|
414
430
|
}
|
|
415
431
|
|
|
416
432
|
/**
|
|
417
|
-
* worktree
|
|
433
|
+
* worktreeに存在しないローカルブランチのクリーンアップ候補を取得
|
|
418
434
|
* @returns {Promise<CleanupTarget[]>} クリーンアップ候補の配列
|
|
419
435
|
*/
|
|
420
436
|
async function getOrphanedLocalBranches({
|
|
421
|
-
mergedPRs,
|
|
422
437
|
baseBranch,
|
|
423
438
|
repoRoot,
|
|
424
439
|
}: {
|
|
425
|
-
mergedPRs: MergedPullRequest[];
|
|
426
440
|
baseBranch: string;
|
|
427
441
|
repoRoot: string;
|
|
428
442
|
}): Promise<CleanupTarget[]> {
|
|
@@ -467,7 +481,6 @@ async function getOrphanedLocalBranches({
|
|
|
467
481
|
|
|
468
482
|
// worktreeに存在しないローカルブランチのみ対象
|
|
469
483
|
if (!worktreeBranches.has(localBranch.name)) {
|
|
470
|
-
const mergedPR = findMatchingPR(localBranch.name, mergedPRs);
|
|
471
484
|
let hasUnpushed = false;
|
|
472
485
|
try {
|
|
473
486
|
hasUnpushed = await hasUnpushedCommitsInRepo(
|
|
@@ -480,13 +493,12 @@ async function getOrphanedLocalBranches({
|
|
|
480
493
|
|
|
481
494
|
const reasons: CleanupReason[] = [];
|
|
482
495
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
496
|
+
const upstreamBranch = await getUpstreamBranch(localBranch.name);
|
|
497
|
+
const comparisonBase = upstreamBranch ?? baseBranch;
|
|
486
498
|
|
|
487
499
|
const hasUniqueCommits = await branchHasUniqueCommitsComparedToBase(
|
|
488
500
|
localBranch.name,
|
|
489
|
-
|
|
501
|
+
comparisonBase,
|
|
490
502
|
repoRoot,
|
|
491
503
|
);
|
|
492
504
|
|
|
@@ -497,7 +509,7 @@ async function getOrphanedLocalBranches({
|
|
|
497
509
|
if (process.env.DEBUG_CLEANUP) {
|
|
498
510
|
console.log(
|
|
499
511
|
chalk.gray(
|
|
500
|
-
`Debug: Checking orphaned branch ${localBranch.name} ->
|
|
512
|
+
`Debug: Checking orphaned branch ${localBranch.name} -> reasons: ${reasons.join(", ")}`,
|
|
501
513
|
),
|
|
502
514
|
);
|
|
503
515
|
}
|
|
@@ -517,7 +529,7 @@ async function getOrphanedLocalBranches({
|
|
|
517
529
|
cleanupTargets.push({
|
|
518
530
|
worktreePath: null, // worktreeは存在しない
|
|
519
531
|
branch: localBranch.name,
|
|
520
|
-
pullRequest:
|
|
532
|
+
pullRequest: null,
|
|
521
533
|
hasUncommittedChanges: false, // worktreeが存在しないため常にfalse
|
|
522
534
|
hasUnpushedCommits: hasUnpushed,
|
|
523
535
|
cleanupType: "branch-only",
|
|
@@ -546,49 +558,19 @@ async function getOrphanedLocalBranches({
|
|
|
546
558
|
}
|
|
547
559
|
}
|
|
548
560
|
|
|
549
|
-
function normalizeBranchName(branchName: string): string {
|
|
550
|
-
return branchName
|
|
551
|
-
.replace(/^origin\//, "")
|
|
552
|
-
.replace(/^refs\/heads\//, "")
|
|
553
|
-
.replace(/^refs\/remotes\/origin\//, "")
|
|
554
|
-
.trim();
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function findMatchingPR(
|
|
558
|
-
worktreeBranch: string,
|
|
559
|
-
mergedPRs: MergedPullRequest[],
|
|
560
|
-
): MergedPullRequest | null {
|
|
561
|
-
const normalizedWorktreeBranch = normalizeBranchName(worktreeBranch);
|
|
562
|
-
|
|
563
|
-
for (const pr of mergedPRs) {
|
|
564
|
-
const normalizedPRBranch = normalizeBranchName(pr.branch);
|
|
565
|
-
|
|
566
|
-
if (normalizedWorktreeBranch === normalizedPRBranch) {
|
|
567
|
-
return pr;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
561
|
/**
|
|
575
562
|
* マージ済みPRに関連するworktreeおよびローカルブランチのクリーンアップ候補を取得
|
|
576
563
|
* @returns {Promise<CleanupTarget[]>} クリーンアップ候補の配列
|
|
577
564
|
*/
|
|
578
565
|
export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
|
|
579
|
-
const [config, repoRoot] = await Promise.all([
|
|
566
|
+
const [config, repoRoot, worktreesWithPR] = await Promise.all([
|
|
580
567
|
getConfig(),
|
|
581
568
|
getRepositoryRoot(),
|
|
569
|
+
getWorktreesWithPRStatus(),
|
|
582
570
|
]);
|
|
583
571
|
const baseBranch = config.defaultBaseBranch || GIT_CONFIG.DEFAULT_BASE_BRANCH;
|
|
584
572
|
|
|
585
|
-
// 並列実行で高速化 - worktreeとマージ済みPRの両方を取得
|
|
586
|
-
const [mergedPRs, worktreesWithPR] = await Promise.all([
|
|
587
|
-
getMergedPullRequests(),
|
|
588
|
-
getWorktreesWithPRStatus(),
|
|
589
|
-
]);
|
|
590
573
|
const orphanedBranches = await getOrphanedLocalBranches({
|
|
591
|
-
mergedPRs,
|
|
592
574
|
baseBranch,
|
|
593
575
|
repoRoot,
|
|
594
576
|
});
|
|
@@ -599,8 +581,6 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
|
|
|
599
581
|
worktreesWithPR.forEach((w) =>
|
|
600
582
|
console.log(` ${w.branch} -> ${w.worktreePath}`),
|
|
601
583
|
);
|
|
602
|
-
console.log(chalk.cyan("Debug: Merged PRs:"));
|
|
603
|
-
mergedPRs.forEach((pr) => console.log(` ${pr.branch} (PR #${pr.number})`));
|
|
604
584
|
}
|
|
605
585
|
|
|
606
586
|
for (const worktree of worktreesWithPR) {
|
|
@@ -614,15 +594,8 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
|
|
|
614
594
|
continue;
|
|
615
595
|
}
|
|
616
596
|
|
|
617
|
-
const mergedPR = findMatchingPR(worktree.branch, mergedPRs);
|
|
618
|
-
|
|
619
597
|
if (process.env.DEBUG_CLEANUP) {
|
|
620
|
-
|
|
621
|
-
console.log(
|
|
622
|
-
chalk.gray(
|
|
623
|
-
`Debug: Checking worktree ${worktree.branch} (normalized: ${normalizedWorktree}) -> ${mergedPR ? "MATCH" : "NO MATCH"}`,
|
|
624
|
-
),
|
|
625
|
-
);
|
|
598
|
+
console.log(chalk.gray(`Debug: Checking worktree ${worktree.branch}`));
|
|
626
599
|
}
|
|
627
600
|
|
|
628
601
|
const cleanupReasons: CleanupReason[] = [];
|
|
@@ -670,13 +643,12 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
|
|
|
670
643
|
hasRemoteBranch = false;
|
|
671
644
|
}
|
|
672
645
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
646
|
+
const upstreamBranch = await getUpstreamBranch(worktree.branch);
|
|
647
|
+
const comparisonBase = upstreamBranch ?? baseBranch;
|
|
676
648
|
|
|
677
649
|
const hasUniqueCommits = await branchHasUniqueCommitsComparedToBase(
|
|
678
650
|
worktree.branch,
|
|
679
|
-
|
|
651
|
+
comparisonBase,
|
|
680
652
|
repoRoot,
|
|
681
653
|
);
|
|
682
654
|
|
|
@@ -703,7 +675,7 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
|
|
|
703
675
|
const target: CleanupTarget = {
|
|
704
676
|
worktreePath: worktree.worktreePath,
|
|
705
677
|
branch: worktree.branch,
|
|
706
|
-
pullRequest:
|
|
678
|
+
pullRequest: null,
|
|
707
679
|
hasUncommittedChanges: hasUncommitted,
|
|
708
680
|
hasUnpushedCommits: hasUnpushed,
|
|
709
681
|
cleanupType: "worktree-and-branch",
|