@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.
Files changed (63) hide show
  1. package/dist/cli/ui/components/App.d.ts.map +1 -1
  2. package/dist/cli/ui/components/App.js +17 -1
  3. package/dist/cli/ui/components/App.js.map +1 -1
  4. package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts +2 -1
  5. package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts.map +1 -1
  6. package/dist/cli/ui/components/screens/AIToolSelectorScreen.js +18 -3
  7. package/dist/cli/ui/components/screens/AIToolSelectorScreen.js.map +1 -1
  8. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  9. package/dist/cli/ui/components/screens/BranchListScreen.js +5 -1
  10. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  11. package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
  12. package/dist/cli/ui/hooks/useGitData.js +16 -4
  13. package/dist/cli/ui/hooks/useGitData.js.map +1 -1
  14. package/dist/cli/ui/types.d.ts +4 -0
  15. package/dist/cli/ui/types.d.ts.map +1 -1
  16. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  17. package/dist/cli/ui/utils/branchFormatter.js +47 -0
  18. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  19. package/dist/client/assets/{index-DxHGLTNq.js → index-CNWntAlF.js} +15 -15
  20. package/dist/client/index.html +1 -1
  21. package/dist/config/index.d.ts +17 -0
  22. package/dist/config/index.d.ts.map +1 -1
  23. package/dist/config/index.js +66 -1
  24. package/dist/config/index.js.map +1 -1
  25. package/dist/git.d.ts.map +1 -1
  26. package/dist/git.js +114 -30
  27. package/dist/git.js.map +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/types/api.d.ts +11 -0
  32. package/dist/types/api.d.ts.map +1 -1
  33. package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
  34. package/dist/web/client/src/pages/BranchDetailPage.js +55 -0
  35. package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
  36. package/dist/web/server/routes/sessions.d.ts.map +1 -1
  37. package/dist/web/server/routes/sessions.js +35 -0
  38. package/dist/web/server/routes/sessions.js.map +1 -1
  39. package/dist/web/server/services/branches.d.ts.map +1 -1
  40. package/dist/web/server/services/branches.js +4 -0
  41. package/dist/web/server/services/branches.js.map +1 -1
  42. package/dist/worktree.d.ts.map +1 -1
  43. package/dist/worktree.js +3 -1
  44. package/dist/worktree.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +20 -0
  47. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +53 -0
  48. package/src/cli/ui/__tests__/integration/navigation.test.tsx +51 -0
  49. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +37 -0
  50. package/src/cli/ui/components/App.tsx +21 -0
  51. package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +27 -2
  52. package/src/cli/ui/components/screens/BranchListScreen.tsx +6 -1
  53. package/src/cli/ui/hooks/useGitData.ts +15 -4
  54. package/src/cli/ui/types.ts +5 -0
  55. package/src/cli/ui/utils/branchFormatter.ts +47 -0
  56. package/src/config/index.ts +87 -1
  57. package/src/git.ts +114 -30
  58. package/src/index.ts +4 -1
  59. package/src/types/api.ts +12 -0
  60. package/src/web/client/src/pages/BranchDetailPage.tsx +69 -1
  61. package/src/web/server/routes/sessions.ts +39 -0
  62. package/src/web/server/services/branches.ts +5 -0
  63. package/src/worktree.ts +7 -1
@@ -307,6 +307,57 @@ describe("Protected Branch Navigation (T103)", () => {
307
307
  branchActionSpy.mockRestore();
308
308
  });
309
309
 
310
+ it("prefills AI tool selector with last used tool for the branch", async () => {
311
+ const branchesWithUsage: BranchInfo[] = [
312
+ {
313
+ name: "feature/with-usage",
314
+ type: "local",
315
+ branchType: "feature",
316
+ isCurrent: false,
317
+ lastToolUsage: {
318
+ branch: "feature/with-usage",
319
+ worktreePath: "/repo/.worktrees/feature-with-usage",
320
+ toolId: "codex-cli",
321
+ toolLabel: "Codex",
322
+ timestamp: Date.now(),
323
+ },
324
+ },
325
+ ];
326
+
327
+ mockedGetAllBranches.mockResolvedValue(branchesWithUsage);
328
+ mockedListAdditionalWorktrees.mockResolvedValue([]);
329
+
330
+ const onExit = vi.fn();
331
+ render(<App onExit={onExit} />);
332
+
333
+ await waitFor(() => {
334
+ const latestProps = branchListProps.find((p) => p.branches?.length);
335
+ expect(latestProps?.branches?.length ?? 0).toBeGreaterThan(0);
336
+ });
337
+
338
+ // Simulate selecting the branch from the list
339
+ const listProps =
340
+ branchListProps.find((p) => p.branches?.length) ??
341
+ branchListProps.at(-1);
342
+ expect(listProps?.branches?.length).toBeGreaterThan(0);
343
+ listProps.onSelect(listProps.branches[0]);
344
+
345
+ await waitFor(() => {
346
+ expect(branchActionProps.length).toBeGreaterThan(0);
347
+ });
348
+
349
+ // Use existing branch (non-protected) to navigate to AI tool selector
350
+ const actionProps = branchActionProps.at(-1);
351
+ actionProps.onUseExisting();
352
+
353
+ await waitFor(() => {
354
+ expect(aiToolScreenProps.length).toBeGreaterThan(0);
355
+ });
356
+
357
+ const props = aiToolScreenProps.at(-1) as { initialToolId?: string };
358
+ expect(props.initialToolId).toBe("codex-cli");
359
+ });
360
+
310
361
  it("switches local protected branches via root workflow and navigates to AI tool", async () => {
311
362
  mockedGetAllBranches.mockResolvedValue(baseBranches);
312
363
  mockedListAdditionalWorktrees.mockResolvedValue([]);
@@ -45,6 +45,30 @@ describe("branchFormatter", () => {
45
45
  expect(result.value).toBe("feature/new-ui");
46
46
  });
47
47
 
48
+ it("should include last tool usage label when present", () => {
49
+ const branchInfo: BranchInfo = {
50
+ name: "feature/tool",
51
+ type: "local",
52
+ branchType: "feature",
53
+ isCurrent: false,
54
+ lastToolUsage: {
55
+ branch: "feature/tool",
56
+ worktreePath: "/tmp/wt",
57
+ toolId: "codex-cli",
58
+ toolLabel: "Codex",
59
+ mode: "normal",
60
+ timestamp: Date.UTC(2025, 10, 26, 14, 3), // 2025-11-26 14:03 UTC
61
+ model: "gpt-5.1-codex",
62
+ },
63
+ };
64
+
65
+ const result = formatBranchItem(branchInfo);
66
+
67
+ expect(result.lastToolUsageLabel).toContain("Codex");
68
+ expect(result.lastToolUsageLabel).toContain("New");
69
+ expect(result.lastToolUsageLabel).toContain("2025-11-26");
70
+ });
71
+
48
72
  it("should format a bugfix branch", () => {
49
73
  const branchInfo: BranchInfo = {
50
74
  name: "bugfix/security-issue",
@@ -59,6 +83,19 @@ describe("branchFormatter", () => {
59
83
  expect(result.label).toContain("bugfix/security-issue");
60
84
  });
61
85
 
86
+ it("should set lastToolUsageLabel to null when no usage exists", () => {
87
+ const branchInfo: BranchInfo = {
88
+ name: "feature/no-usage",
89
+ type: "local",
90
+ branchType: "feature",
91
+ isCurrent: false,
92
+ };
93
+
94
+ const result = formatBranchItem(branchInfo);
95
+
96
+ expect(result.lastToolUsageLabel).toBeNull();
97
+ });
98
+
62
99
  it("should format a hotfix branch", () => {
63
100
  const branchInfo: BranchInfo = {
64
101
  name: "hotfix/critical-bug",
@@ -110,6 +110,7 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
110
110
  const [lastModelByTool, setLastModelByTool] = useState<
111
111
  Record<AITool, ModelSelectionResult | undefined>
112
112
  >({});
113
+ const [preferredToolId, setPreferredToolId] = useState<AITool | null>(null);
113
114
 
114
115
  // PR cleanup feedback
115
116
  const [cleanupIndicators, setCleanupIndicators] = useState<
@@ -200,6 +201,19 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
200
201
  }
201
202
  }, [branches, hiddenBranches]);
202
203
 
204
+ // Update preferred tool when branch or data changes
205
+ useEffect(() => {
206
+ if (!selectedBranch) return;
207
+ const branchMatch =
208
+ branches.find((b) => b.name === selectedBranch.name) ||
209
+ branches.find(
210
+ (b) =>
211
+ selectedBranch.branchType === "remote" &&
212
+ b.name === selectedBranch.displayName,
213
+ );
214
+ setPreferredToolId(branchMatch?.lastToolUsage?.toolId ?? null);
215
+ }, [branches, selectedBranch]);
216
+
203
217
  useEffect(
204
218
  () => () => {
205
219
  if (completionTimerRef.current) {
@@ -387,6 +401,7 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
387
401
  setSelectedTool(null);
388
402
  setSelectedModel(null);
389
403
  setCreationSourceBranch(null);
404
+ setPreferredToolId(item.lastToolUsage?.toolId ?? null);
390
405
 
391
406
  if (protectedSelected) {
392
407
  setCleanupFooterMessage({
@@ -419,6 +434,8 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
419
434
 
420
435
  const handleWorktreeSelect = useCallback(
421
436
  (worktree: WorktreeItem) => {
437
+ const lastTool = branches.find((b) => b.name === worktree.branch)
438
+ ?.lastToolUsage?.toolId;
422
439
  setSelectedBranch({
423
440
  name: worktree.branch,
424
441
  displayName: worktree.branch,
@@ -428,6 +445,7 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
428
445
  setSelectedTool(null);
429
446
  setSelectedModel(null);
430
447
  setCreationSourceBranch(null);
448
+ setPreferredToolId(lastTool ?? null);
431
449
  setCleanupFooterMessage(null);
432
450
  navigateTo("ai-tool-selector");
433
451
  },
@@ -436,6 +454,7 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
436
454
  navigateTo,
437
455
  setCleanupFooterMessage,
438
456
  setCreationSourceBranch,
457
+ branches,
439
458
  ],
440
459
  );
441
460
 
@@ -541,6 +560,7 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
541
560
  });
542
561
  setSelectedTool(null);
543
562
  setSelectedModel(null);
563
+ setPreferredToolId(null);
544
564
  setCleanupFooterMessage(null);
545
565
 
546
566
  navigateTo("ai-tool-selector");
@@ -900,6 +920,7 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
900
920
  onBack={goBack}
901
921
  onSelect={handleToolSelect}
902
922
  version={version}
923
+ initialToolId={selectedTool ?? preferredToolId ?? null}
903
924
  />
904
925
  );
905
926
 
@@ -18,6 +18,7 @@ export interface AIToolSelectorScreenProps {
18
18
  onBack: () => void;
19
19
  onSelect: (tool: AITool) => void;
20
20
  version?: string | null;
21
+ initialToolId?: AITool | null;
21
22
  }
22
23
 
23
24
  /**
@@ -30,10 +31,12 @@ export function AIToolSelectorScreen({
30
31
  onBack,
31
32
  onSelect,
32
33
  version,
34
+ initialToolId,
33
35
  }: AIToolSelectorScreenProps) {
34
36
  const { rows } = useTerminalSize();
35
37
  const [toolItems, setToolItems] = useState<AIToolItem[]>([]);
36
38
  const [isLoading, setIsLoading] = useState(true);
39
+ const [selectedIndex, setSelectedIndex] = useState<number>(0);
37
40
 
38
41
  // Load tools from getAllTools()
39
42
  useEffect(() => {
@@ -61,6 +64,13 @@ export function AIToolSelectorScreen({
61
64
  });
62
65
 
63
66
  setToolItems(items);
67
+
68
+ // Decide initial cursor position based on last used tool
69
+ const idx =
70
+ initialToolId && items.length > 0
71
+ ? items.findIndex((item) => item.value === initialToolId)
72
+ : 0;
73
+ setSelectedIndex(idx >= 0 ? idx : 0);
64
74
  } catch (error) {
65
75
  // If loading fails, show error in console but don't crash
66
76
  console.error("Failed to load tools:", error);
@@ -72,7 +82,17 @@ export function AIToolSelectorScreen({
72
82
  };
73
83
 
74
84
  loadTools();
75
- }, []);
85
+ }, [initialToolId]);
86
+
87
+ // Update selection when props or items change
88
+ useEffect(() => {
89
+ if (isLoading || toolItems.length === 0) return;
90
+ const idx =
91
+ initialToolId && toolItems.length > 0
92
+ ? toolItems.findIndex((item) => item.value === initialToolId)
93
+ : 0;
94
+ setSelectedIndex(idx >= 0 ? idx : 0);
95
+ }, [initialToolId, toolItems, isLoading]);
76
96
 
77
97
  // Handle keyboard input
78
98
  // Note: Select component handles Enter and arrow keys
@@ -110,7 +130,12 @@ export function AIToolSelectorScreen({
110
130
  No tools available. Please check your configuration.
111
131
  </Text>
112
132
  ) : (
113
- <Select items={toolItems} onSelect={handleSelect} />
133
+ <Select
134
+ items={toolItems}
135
+ onSelect={handleSelect}
136
+ selectedIndex={selectedIndex}
137
+ onSelectedIndexChange={setSelectedIndex}
138
+ />
114
139
  )}
115
140
  </Box>
116
141
 
@@ -279,7 +279,12 @@ export function BranchListScreen({
279
279
  // Use a small safety margin to avoid terminal-dependent wrapping
280
280
  const columns = Math.max(20, context.columns - 1);
281
281
  const arrow = isSelected ? ">" : " ";
282
- const timestampText = formatLatestCommit(item.latestCommitTimestamp);
282
+ const commitText = formatLatestCommit(item.latestCommitTimestamp);
283
+ const infoText =
284
+ item.lastToolUsage && item.lastToolUsageLabel
285
+ ? item.lastToolUsageLabel
286
+ : `${chalk.gray("Unknown")}${commitText !== "---" ? ` | ${commitText}` : ""}`;
287
+ const timestampText = infoText;
283
288
  const timestampWidth = stringWidth(timestampText);
284
289
 
285
290
  const indicatorInfo = cleanupUI?.indicators?.[item.name];
@@ -9,6 +9,7 @@ import { listAdditionalWorktrees } from "../../../worktree.js";
9
9
  import { getPullRequestByBranch } from "../../../github.js";
10
10
  import type { BranchInfo, WorktreeInfo } from "../types.js";
11
11
  import type { WorktreeInfo as GitWorktreeInfo } from "../../../worktree.js";
12
+ import { getLastToolUsageMap } from "../../../config/index.js";
12
13
 
13
14
  export interface UseGitDataOptions {
14
15
  enableAutoRefresh?: boolean;
@@ -52,10 +53,17 @@ export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
52
53
  }
53
54
  }
54
55
 
55
- const [branchesData, worktreesData] = await Promise.all([
56
- getAllBranches(),
57
- listAdditionalWorktrees(),
58
- ]);
56
+ const branchesData = await getAllBranches();
57
+ let worktreesData: GitWorktreeInfo[] = [];
58
+ try {
59
+ worktreesData = await listAdditionalWorktrees();
60
+ } catch (err) {
61
+ if (process.env.DEBUG) {
62
+ console.error("Failed to list additional worktrees:", err);
63
+ }
64
+ worktreesData = [];
65
+ }
66
+ const lastToolUsageMap = await getLastToolUsageMap(repoRoot);
59
67
 
60
68
  // Store worktrees separately
61
69
  setWorktrees(worktreesData);
@@ -117,6 +125,9 @@ export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
117
125
  },
118
126
  }
119
127
  : {}),
128
+ ...(lastToolUsageMap.get(branch.name)
129
+ ? { lastToolUsage: lastToolUsageMap.get(branch.name) ?? null }
130
+ : {}),
120
131
  };
121
132
  }),
122
133
  );
@@ -5,8 +5,11 @@ export interface WorktreeInfo {
5
5
  isAccessible?: boolean;
6
6
  }
7
7
 
8
+ import type { LastToolUsage } from "../../types/api.js";
9
+
8
10
  export type AITool = string;
9
11
  export type InferenceLevel = "low" | "medium" | "high" | "xhigh";
12
+ export type { LastToolUsage } from "../../types/api.js";
10
13
 
11
14
  export interface ModelOption {
12
15
  id: string;
@@ -35,6 +38,7 @@ export interface BranchInfo {
35
38
  openPR?: { number: number; title: string };
36
39
  mergedPR?: { number: number; mergedAt: string };
37
40
  latestCommitTimestamp?: number;
41
+ lastToolUsage?: LastToolUsage | null;
38
42
  }
39
43
 
40
44
  export interface BranchChoice {
@@ -204,6 +208,7 @@ export interface BranchItem extends BranchInfo {
204
208
  hasChanges: boolean;
205
209
  label: string;
206
210
  value: string;
211
+ lastToolUsageLabel?: string | null;
207
212
  }
208
213
 
209
214
  /**
@@ -6,6 +6,7 @@ import type {
6
6
  WorktreeInfo,
7
7
  } from "../types.js";
8
8
  import stringWidth from "string-width";
9
+ import chalk from "chalk";
9
10
 
10
11
  // Icon mappings
11
12
  const branchIcons: Record<BranchType, string> = {
@@ -69,6 +70,51 @@ export interface FormatOptions {
69
70
  hasChanges?: boolean;
70
71
  }
71
72
 
73
+ function mapToolLabel(toolId: string, toolLabel?: string): string {
74
+ if (toolId === "claude-code") return "Claude";
75
+ if (toolId === "codex-cli") return "Codex";
76
+ if (toolId === "gemini-cli") return "Gemini";
77
+ if (toolId === "qwen-cli") return "Qwen";
78
+ if (toolLabel) return toolLabel;
79
+ return "Custom";
80
+ }
81
+
82
+ function mapModeLabel(
83
+ mode?: "normal" | "continue" | "resume" | null,
84
+ ): string | null {
85
+ if (mode === "normal") return "New";
86
+ if (mode === "continue") return "Continue";
87
+ if (mode === "resume") return "Resume";
88
+ return null;
89
+ }
90
+
91
+ function formatTimestamp(ts: number): string {
92
+ const date = new Date(ts);
93
+ const year = date.getFullYear();
94
+ const month = String(date.getMonth() + 1).padStart(2, "0");
95
+ const day = String(date.getDate()).padStart(2, "0");
96
+ const hours = String(date.getHours()).padStart(2, "0");
97
+ const minutes = String(date.getMinutes()).padStart(2, "0");
98
+ return `${year}-${month}-${day} ${hours}:${minutes}`;
99
+ }
100
+
101
+ function buildLastToolUsageLabel(
102
+ usage?: BranchInfo["lastToolUsage"] | null,
103
+ ): string | null {
104
+ if (!usage) return null;
105
+ const toolText = mapToolLabel(usage.toolId, usage.toolLabel);
106
+ const modeText = mapModeLabel(usage.mode);
107
+ const timestamp = usage.timestamp ? formatTimestamp(usage.timestamp) : null;
108
+ const parts = [toolText];
109
+ if (modeText) {
110
+ parts.push(modeText);
111
+ }
112
+ if (timestamp) {
113
+ parts.push(timestamp);
114
+ }
115
+ return parts.join(" | ");
116
+ }
117
+
72
118
  /**
73
119
  * Converts BranchInfo to BranchItem with display properties
74
120
  */
@@ -171,6 +217,7 @@ export function formatBranchItem(
171
217
  hasChanges,
172
218
  label,
173
219
  value: branch.name,
220
+ lastToolUsageLabel: buildLastToolUsageLabel(branch.lastToolUsage),
174
221
  };
175
222
  }
176
223
 
@@ -16,6 +16,20 @@ export interface SessionData {
16
16
  lastUsedTool?: string;
17
17
  timestamp: number;
18
18
  repositoryRoot: string;
19
+ mode?: "normal" | "continue" | "resume";
20
+ model?: string | null;
21
+ toolLabel?: string | null;
22
+ history?: ToolSessionEntry[];
23
+ }
24
+
25
+ export interface ToolSessionEntry {
26
+ branch: string;
27
+ worktreePath: string | null;
28
+ toolId: string;
29
+ toolLabel: string;
30
+ mode?: "normal" | "continue" | "resume" | null;
31
+ model?: string | null;
32
+ timestamp: number;
19
33
  }
20
34
 
21
35
  const DEFAULT_CONFIG: AppConfig = {
@@ -104,7 +118,38 @@ export async function saveSession(sessionData: SessionData): Promise<void> {
104
118
  // ディレクトリを作成
105
119
  await mkdir(sessionDir, { recursive: true });
106
120
 
107
- await writeFile(sessionPath, JSON.stringify(sessionData, null, 2), "utf-8");
121
+ // 既存履歴を読み込み(後方互換のため失敗は無視)
122
+ let existingHistory: ToolSessionEntry[] = [];
123
+ try {
124
+ const currentContent = await readFile(sessionPath, "utf-8");
125
+ const parsed = JSON.parse(currentContent) as SessionData;
126
+ if (Array.isArray(parsed.history)) {
127
+ existingHistory = parsed.history;
128
+ }
129
+ } catch {
130
+ // ignore
131
+ }
132
+
133
+ // 新しい履歴エントリを追加(branch/worktree/toolが揃っている場合のみ)
134
+ if (sessionData.lastBranch && sessionData.lastWorktreePath) {
135
+ const entry: ToolSessionEntry = {
136
+ branch: sessionData.lastBranch,
137
+ worktreePath: sessionData.lastWorktreePath,
138
+ toolId: sessionData.lastUsedTool ?? "unknown",
139
+ toolLabel: sessionData.toolLabel ?? sessionData.lastUsedTool ?? "Custom",
140
+ mode: sessionData.mode ?? null,
141
+ model: sessionData.model ?? null,
142
+ timestamp: sessionData.timestamp,
143
+ };
144
+ existingHistory = [...existingHistory, entry].slice(-100); // keep latest 100
145
+ }
146
+
147
+ const payload: SessionData = {
148
+ ...sessionData,
149
+ history: existingHistory,
150
+ };
151
+
152
+ await writeFile(sessionPath, JSON.stringify(payload, null, 2), "utf-8");
108
153
  } catch (error) {
109
154
  // セッション保存の失敗は致命的ではないため、エラーをログに出力するのみ
110
155
  if (process.env.DEBUG_SESSION) {
@@ -192,3 +237,44 @@ export async function getAllSessions(): Promise<SessionData[]> {
192
237
  return [];
193
238
  }
194
239
  }
240
+
241
+ /**
242
+ * 各ブランチの最新ツール利用履歴を取得
243
+ */
244
+ export async function getLastToolUsageMap(
245
+ repositoryRoot: string,
246
+ ): Promise<Map<string, ToolSessionEntry>> {
247
+ const map = new Map<string, ToolSessionEntry>();
248
+ try {
249
+ const sessionPath = getSessionFilePath(repositoryRoot);
250
+ const content = await readFile(sessionPath, "utf-8");
251
+ const parsed = JSON.parse(content) as SessionData;
252
+
253
+ const history: ToolSessionEntry[] = Array.isArray(parsed.history)
254
+ ? parsed.history
255
+ : [];
256
+
257
+ // 後方互換: historyが無い場合はlastUsedToolを1件扱い
258
+ if (!history.length && parsed.lastBranch && parsed.lastWorktreePath) {
259
+ history.push({
260
+ branch: parsed.lastBranch,
261
+ worktreePath: parsed.lastWorktreePath,
262
+ toolId: parsed.lastUsedTool ?? "unknown",
263
+ toolLabel: parsed.toolLabel ?? parsed.lastUsedTool ?? "Custom",
264
+ mode: parsed.mode ?? null,
265
+ model: parsed.model ?? null,
266
+ timestamp: parsed.timestamp ?? Date.now(),
267
+ });
268
+ }
269
+
270
+ for (const entry of history) {
271
+ const existing = map.get(entry.branch);
272
+ if (!existing || existing.timestamp < entry.timestamp) {
273
+ map.set(entry.branch, entry);
274
+ }
275
+ }
276
+ } catch {
277
+ // セッションファイルが無い/壊れている場合は空のMapを返す
278
+ }
279
+ return map;
280
+ }