@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
|
@@ -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
|
|
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
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
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
|
);
|
package/src/cli/ui/types.ts
CHANGED
|
@@ -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
|
|
package/src/config/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|