@akiojin/gwt 4.7.0 → 4.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 (69) hide show
  1. package/README.ja.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/claude.js +1 -1
  4. package/dist/claude.js.map +1 -1
  5. package/dist/cli/ui/components/App.js +1 -1
  6. package/dist/cli/ui/components/App.js.map +1 -1
  7. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  8. package/dist/cli/ui/components/screens/BranchListScreen.js +8 -7
  9. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  10. package/dist/cli/ui/components/screens/LogDatePickerScreen.js +1 -1
  11. package/dist/cli/ui/components/screens/LogDatePickerScreen.js.map +1 -1
  12. package/dist/cli/ui/components/screens/LogDetailScreen.js +1 -1
  13. package/dist/cli/ui/components/screens/LogDetailScreen.js.map +1 -1
  14. package/dist/cli/ui/components/screens/LogListScreen.js +1 -1
  15. package/dist/cli/ui/components/screens/LogListScreen.js.map +1 -1
  16. package/dist/cli/ui/utils/branchFormatter.d.ts +5 -0
  17. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  18. package/dist/cli/ui/utils/branchFormatter.js +18 -5
  19. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  20. package/dist/codex.d.ts.map +1 -1
  21. package/dist/codex.js +0 -1
  22. package/dist/codex.js.map +1 -1
  23. package/dist/config/index.d.ts.map +1 -1
  24. package/dist/config/index.js +3 -7
  25. package/dist/config/index.js.map +1 -1
  26. package/dist/config/profiles.d.ts +2 -2
  27. package/dist/config/profiles.d.ts.map +1 -1
  28. package/dist/config/profiles.js +4 -7
  29. package/dist/config/profiles.js.map +1 -1
  30. package/dist/config/tools.d.ts +1 -1
  31. package/dist/config/tools.d.ts.map +1 -1
  32. package/dist/config/tools.js +3 -43
  33. package/dist/config/tools.js.map +1 -1
  34. package/dist/gemini.d.ts.map +1 -1
  35. package/dist/gemini.js +1 -2
  36. package/dist/gemini.js.map +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +106 -90
  39. package/dist/index.js.map +1 -1
  40. package/dist/utils/command.d.ts +11 -0
  41. package/dist/utils/command.d.ts.map +1 -1
  42. package/dist/utils/command.js +33 -0
  43. package/dist/utils/command.js.map +1 -1
  44. package/dist/web/client/src/pages/ConfigPage.js +1 -1
  45. package/dist/web/client/src/pages/ConfigPage.js.map +1 -1
  46. package/package.json +2 -2
  47. package/src/claude.ts +1 -1
  48. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +1 -1
  49. package/src/cli/ui/__tests__/components/App.test.tsx +65 -3
  50. package/src/cli/ui/__tests__/components/screens/LogDetailScreen.test.tsx +1 -1
  51. package/src/cli/ui/__tests__/components/screens/LogListScreen.test.tsx +1 -1
  52. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +83 -22
  53. package/src/cli/ui/__tests__/integration/navigation.test.tsx +57 -37
  54. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +105 -0
  55. package/src/cli/ui/components/App.tsx +1 -1
  56. package/src/cli/ui/components/screens/BranchListScreen.tsx +9 -7
  57. package/src/cli/ui/components/screens/LogDatePickerScreen.tsx +1 -1
  58. package/src/cli/ui/components/screens/LogDetailScreen.tsx +1 -1
  59. package/src/cli/ui/components/screens/LogListScreen.tsx +1 -1
  60. package/src/cli/ui/utils/branchFormatter.ts +19 -5
  61. package/src/codex.ts +0 -1
  62. package/src/config/index.ts +3 -7
  63. package/src/config/profiles.ts +4 -7
  64. package/src/config/tools.ts +3 -56
  65. package/src/gemini.ts +1 -2
  66. package/src/index.ts +148 -133
  67. package/src/utils/command.ts +37 -0
  68. package/src/web/client/src/pages/ConfigPage.tsx +2 -2
  69. package/src/index.ts.backup +0 -1543
@@ -7,16 +7,55 @@ import React from "react";
7
7
  import { Window } from "happy-dom";
8
8
  import type { BranchInfo, BranchItem } from "../../types.js";
9
9
  import type { BranchListScreenProps } from "../../components/screens/BranchListScreen.js";
10
+ import { App } from "../../components/App.js";
10
11
 
11
12
  const mockRefresh = vi.fn();
12
- let App: typeof import("../../components/App.js").App;
13
+ const exitMock = vi.fn();
13
14
  const branchListProps: BranchListScreenProps[] = [];
14
15
  const useGitDataMock = vi.fn();
16
+ const useProfilesMock = vi.fn();
17
+ const useToolStatusMock = vi.fn();
18
+
19
+ vi.mock("ink", async () => {
20
+ const ReactImport = await import("react");
21
+ const Box = ({ children }: { children?: ReactImport.ReactNode }) =>
22
+ ReactImport.createElement("div", null, children);
23
+ const Text = ({ children }: { children?: ReactImport.ReactNode }) =>
24
+ ReactImport.createElement("span", null, children);
25
+ return {
26
+ Box,
27
+ Text,
28
+ useApp: () => ({ exit: exitMock }),
29
+ useInput: () => {},
30
+ useStdout: () => ({ stdout: process.stdout, write: vi.fn() }),
31
+ };
32
+ });
15
33
 
16
34
  vi.mock("../../hooks/useGitData.js", () => ({
17
35
  useGitData: (...args: unknown[]) => useGitDataMock(...args),
18
36
  }));
19
37
 
38
+ vi.mock("../../hooks/useProfiles.js", () => ({
39
+ useProfiles: (...args: unknown[]) => useProfilesMock(...args),
40
+ }));
41
+
42
+ vi.mock("../../hooks/useToolStatus.js", () => ({
43
+ useToolStatus: (...args: unknown[]) => useToolStatusMock(...args),
44
+ }));
45
+
46
+ vi.mock("../../../utils.js", () => ({
47
+ getPackageVersion: vi.fn(async () => "0.0.0-test"),
48
+ }));
49
+
50
+ vi.mock("../../../git.js", () => ({
51
+ getRepositoryRoot: vi.fn(async () => "/repo"),
52
+ deleteBranch: vi.fn(async () => undefined),
53
+ }));
54
+
55
+ vi.mock("../../../config/index.js", () => ({
56
+ loadSession: vi.fn(async () => null),
57
+ }));
58
+
20
59
  vi.mock("../../components/screens/BranchListScreen.js", () => {
21
60
  return {
22
61
  BranchListScreen: (props: BranchListScreenProps) => {
@@ -47,7 +86,7 @@ vi.mock("../../components/screens/BranchListScreen.js", () => {
47
86
  });
48
87
 
49
88
  describe("App", () => {
50
- beforeEach(async () => {
89
+ beforeEach(() => {
51
90
  // Setup happy-dom
52
91
  const window = new Window();
53
92
  globalThis.window = window as unknown as typeof globalThis.window;
@@ -56,8 +95,31 @@ describe("App", () => {
56
95
 
57
96
  vi.clearAllMocks();
58
97
  useGitDataMock.mockReset();
98
+ useProfilesMock.mockReset();
99
+ useToolStatusMock.mockReset();
59
100
  branchListProps.length = 0;
60
- App = (await import("../../components/App.js")).App;
101
+
102
+ useProfilesMock.mockReturnValue({
103
+ profiles: null,
104
+ loading: false,
105
+ error: null,
106
+ activeProfileName: null,
107
+ activeProfile: null,
108
+ refresh: vi.fn(),
109
+ setActiveProfile: vi.fn(),
110
+ createProfile: vi.fn(),
111
+ updateProfile: vi.fn(),
112
+ deleteProfile: vi.fn(),
113
+ updateEnvVar: vi.fn(),
114
+ deleteEnvVar: vi.fn(),
115
+ });
116
+
117
+ useToolStatusMock.mockReturnValue({
118
+ tools: [],
119
+ loading: false,
120
+ error: null,
121
+ refresh: vi.fn(),
122
+ });
61
123
  });
62
124
 
63
125
  const mockBranches: BranchInfo[] = [
@@ -52,6 +52,6 @@ describe("LogDetailScreen", () => {
52
52
  <LogDetailScreen entry={null} onBack={vi.fn()} onCopy={vi.fn()} />,
53
53
  );
54
54
 
55
- expect(lastFrame()).toContain("ログがありません");
55
+ expect(lastFrame()).toContain("No logs available.");
56
56
  });
57
57
  });
@@ -97,6 +97,6 @@ describe("LogListScreen", () => {
97
97
  />,
98
98
  );
99
99
 
100
- expect(lastFrame()).toContain("ログがありません");
100
+ expect(lastFrame()).toContain("No logs available.");
101
101
  });
102
102
  });
@@ -5,6 +5,7 @@
5
5
  import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
6
6
  import { render } from "@testing-library/react";
7
7
  import React from "react";
8
+ import { App as AppComponent } from "../../components/App.js";
8
9
  import * as BranchListScreenModule from "../../components/screens/BranchListScreen.js";
9
10
  import type { BranchListScreenProps } from "../../components/screens/BranchListScreen.js";
10
11
  import { Window } from "happy-dom";
@@ -13,19 +14,51 @@ import type { BranchInfo, BranchItem, Statistics } from "../../types.js";
13
14
  const mockRefresh = vi.fn();
14
15
  const branchListProps: BranchListScreenProps[] = [];
15
16
  const useGitDataMock = vi.fn();
16
- let App: typeof import("../../components/App.js").App | null = null;
17
-
18
- const loadApp = async () => {
19
- if (!App) {
20
- App = (await import("../../components/App.js")).App;
21
- }
22
- return App;
23
- };
17
+ const useProfilesMock = vi.fn();
18
+ const useToolStatusMock = vi.fn();
19
+ const originalStdoutRows = process.stdout.rows;
20
+ const originalStdoutColumns = process.stdout.columns;
21
+
22
+ vi.mock("ink", async () => {
23
+ const ReactImport = await import("react");
24
+ const Box = ({ children }: { children?: ReactImport.ReactNode }) =>
25
+ ReactImport.createElement("div", null, children);
26
+ const Text = ({ children }: { children?: ReactImport.ReactNode }) =>
27
+ ReactImport.createElement("span", null, children);
28
+ return {
29
+ Box,
30
+ Text,
31
+ useApp: () => ({ exit: vi.fn() }),
32
+ useInput: () => {},
33
+ useStdout: () => ({ stdout: process.stdout, write: vi.fn() }),
34
+ };
35
+ });
24
36
 
25
37
  vi.mock("../../hooks/useGitData.js", () => ({
26
38
  useGitData: (...args: unknown[]) => useGitDataMock(...args),
27
39
  }));
28
40
 
41
+ vi.mock("../../hooks/useProfiles.js", () => ({
42
+ useProfiles: (...args: unknown[]) => useProfilesMock(...args),
43
+ }));
44
+
45
+ vi.mock("../../hooks/useToolStatus.js", () => ({
46
+ useToolStatus: (...args: unknown[]) => useToolStatusMock(...args),
47
+ }));
48
+
49
+ vi.mock("../../../utils.js", () => ({
50
+ getPackageVersion: vi.fn(async () => "0.0.0-test"),
51
+ }));
52
+
53
+ vi.mock("../../../git.js", () => ({
54
+ getRepositoryRoot: vi.fn(async () => "/repo"),
55
+ deleteBranch: vi.fn(async () => undefined),
56
+ }));
57
+
58
+ vi.mock("../../../config/index.js", () => ({
59
+ loadSession: vi.fn(async () => null),
60
+ }));
61
+
29
62
  vi.mock("../../components/screens/BranchListScreen.js", () => ({
30
63
  BranchListScreen: (props: BranchListScreenProps) => {
31
64
  branchListProps.push(props);
@@ -49,7 +82,7 @@ vi.mock("../../components/screens/BranchListScreen.js", () => ({
49
82
  }));
50
83
 
51
84
  describe("Edge Cases Integration Tests", () => {
52
- beforeEach(async () => {
85
+ beforeEach(() => {
53
86
  // Setup happy-dom
54
87
  const window = new Window();
55
88
  globalThis.window = window as unknown as typeof globalThis.window;
@@ -59,7 +92,44 @@ describe("Edge Cases Integration Tests", () => {
59
92
  // Reset mocks
60
93
  vi.clearAllMocks();
61
94
  useGitDataMock.mockReset();
95
+ useProfilesMock.mockReset();
96
+ useToolStatusMock.mockReset();
62
97
  branchListProps.length = 0;
98
+ useGitDataMock.mockReturnValue({
99
+ branches: [],
100
+ worktrees: [],
101
+ loading: false,
102
+ error: null,
103
+ refresh: mockRefresh,
104
+ lastUpdated: null,
105
+ });
106
+
107
+ useProfilesMock.mockReturnValue({
108
+ profiles: null,
109
+ loading: false,
110
+ error: null,
111
+ activeProfileName: null,
112
+ activeProfile: null,
113
+ refresh: vi.fn(),
114
+ setActiveProfile: vi.fn(),
115
+ createProfile: vi.fn(),
116
+ updateProfile: vi.fn(),
117
+ deleteProfile: vi.fn(),
118
+ updateEnvVar: vi.fn(),
119
+ deleteEnvVar: vi.fn(),
120
+ });
121
+
122
+ useToolStatusMock.mockReturnValue({
123
+ tools: [],
124
+ loading: false,
125
+ error: null,
126
+ refresh: vi.fn(),
127
+ });
128
+ });
129
+
130
+ afterEach(() => {
131
+ process.stdout.rows = originalStdoutRows;
132
+ process.stdout.columns = originalStdoutColumns;
63
133
  });
64
134
 
65
135
  /**
@@ -224,7 +294,7 @@ describe("Edge Cases Integration Tests", () => {
224
294
  * testing unreliable in testing-library. The error is thrown but not caught
225
295
  * by the test framework correctly.
226
296
  */
227
- it.skip("[T093] should catch errors in App component", async () => {
297
+ it.skip("[T093] should catch errors in App component", () => {
228
298
  // Mock useGitData to throw an error after initial render
229
299
  let callCount = 0;
230
300
  useGitDataMock.mockImplementation(() => {
@@ -243,14 +313,13 @@ describe("Edge Cases Integration Tests", () => {
243
313
  });
244
314
 
245
315
  const onExit = vi.fn();
246
- const AppComponent = await loadApp();
247
316
  const { container } = render(<AppComponent onExit={onExit} />);
248
317
 
249
318
  // Initial render should work
250
319
  expect(container).toBeDefined();
251
320
  });
252
321
 
253
- it("[T093] should display error message when data loading fails", async () => {
322
+ it("[T093] should display error message when data loading fails", () => {
254
323
  const testError = new Error("Test error: Failed to load Git data");
255
324
  useGitDataMock.mockReturnValue({
256
325
  branches: [],
@@ -262,7 +331,6 @@ describe("Edge Cases Integration Tests", () => {
262
331
  });
263
332
 
264
333
  const onExit = vi.fn();
265
- const AppComponent = await loadApp();
266
334
  const { getByText } = render(<AppComponent onExit={onExit} />);
267
335
 
268
336
  expect(branchListProps).not.toHaveLength(0);
@@ -272,7 +340,7 @@ describe("Edge Cases Integration Tests", () => {
272
340
  expect(getByText(/Failed to load Git data/i)).toBeDefined();
273
341
  });
274
342
 
275
- it("[T093] should handle empty branches list gracefully", async () => {
343
+ it("[T093] should handle empty branches list gracefully", () => {
276
344
  useGitDataMock.mockReturnValue({
277
345
  branches: [],
278
346
  worktrees: [],
@@ -283,7 +351,6 @@ describe("Edge Cases Integration Tests", () => {
283
351
  });
284
352
 
285
353
  const onExit = vi.fn();
286
- const AppComponent = await loadApp();
287
354
  const { container } = render(<AppComponent onExit={onExit} />);
288
355
 
289
356
  // Should render without error even with no branches
@@ -293,7 +360,7 @@ describe("Edge Cases Integration Tests", () => {
293
360
  /**
294
361
  * Additional edge cases
295
362
  */
296
- it("should handle large number of worktrees", async () => {
363
+ it("should handle large number of worktrees", () => {
297
364
  const mockBranches: BranchInfo[] = Array.from({ length: 50 }, (_, i) => ({
298
365
  name: `feature/branch-${i}`,
299
366
  type: "local" as const,
@@ -316,7 +383,6 @@ describe("Edge Cases Integration Tests", () => {
316
383
  });
317
384
 
318
385
  const onExit = vi.fn();
319
- const AppComponent = await loadApp();
320
386
  const { container } = render(<AppComponent onExit={onExit} />);
321
387
 
322
388
  expect(container).toBeDefined();
@@ -367,9 +433,4 @@ describe("Edge Cases Integration Tests", () => {
367
433
 
368
434
  process.stdout.rows = originalRows;
369
435
  });
370
-
371
- afterEach(() => {
372
- useGitDataMock.mockReset();
373
- branchListProps.length = 0;
374
- });
375
436
  });
@@ -1,15 +1,7 @@
1
1
  /**
2
2
  * @vitest-environment happy-dom
3
3
  */
4
- import {
5
- describe,
6
- it,
7
- expect,
8
- beforeEach,
9
- afterEach,
10
- afterAll,
11
- vi,
12
- } from "vitest";
4
+ import { describe, it, expect, beforeEach, afterAll, vi } from "vitest";
13
5
  import type { Mock } from "vitest";
14
6
  import { render, waitFor } from "@testing-library/react";
15
7
  import { act } from "react-dom/test-utils";
@@ -17,11 +9,12 @@ import React from "react";
17
9
  import { App } from "../../components/App.js";
18
10
  import { Window } from "happy-dom";
19
11
  import type { BranchInfo, BranchItem } from "../../types.js";
20
- import * as BranchListScreenModule from "../../components/screens/BranchListScreen.js";
21
12
  import type { BranchListScreenProps } from "../../components/screens/BranchListScreen.js";
22
- import * as BranchActionSelectorScreenModule from "../../screens/BranchActionSelectorScreen.js";
23
13
  import type { BranchActionSelectorScreenProps } from "../../screens/BranchActionSelectorScreen.js";
24
14
 
15
+ const branchListProps: BranchListScreenProps[] = [];
16
+ const branchActionProps: BranchActionSelectorScreenProps[] = [];
17
+
25
18
  vi.mock("../../../../git.ts", () => ({
26
19
  __esModule: true,
27
20
  getAllBranches: vi.fn(),
@@ -32,6 +25,45 @@ vi.mock("../../../../git.ts", () => ({
32
25
  getBranchDivergenceStatuses: vi.fn(async () => []),
33
26
  }));
34
27
 
28
+ vi.mock("../../../../config/index.js", () => ({
29
+ loadSession: vi.fn(),
30
+ getLastToolUsageMap: vi.fn(),
31
+ }));
32
+
33
+ vi.mock(
34
+ "../../components/screens/BranchListScreen.js",
35
+ async (importOriginal) => {
36
+ const actual =
37
+ await importOriginal<
38
+ typeof import("../../components/screens/BranchListScreen.js")
39
+ >();
40
+ return {
41
+ ...actual,
42
+ BranchListScreen: (props: BranchListScreenProps) => {
43
+ branchListProps.push(props);
44
+ return React.createElement(actual.BranchListScreen, props);
45
+ },
46
+ };
47
+ },
48
+ );
49
+
50
+ vi.mock(
51
+ "../../screens/BranchActionSelectorScreen.js",
52
+ async (importOriginal) => {
53
+ const actual =
54
+ await importOriginal<
55
+ typeof import("../../screens/BranchActionSelectorScreen.js")
56
+ >();
57
+ return {
58
+ ...actual,
59
+ BranchActionSelectorScreen: (props: BranchActionSelectorScreenProps) => {
60
+ branchActionProps.push(props);
61
+ return React.createElement(actual.BranchActionSelectorScreen, props);
62
+ },
63
+ };
64
+ },
65
+ );
66
+
35
67
  const { mockIsProtectedBranchName, mockSwitchToProtectedBranch } = vi.hoisted(
36
68
  () => ({
37
69
  mockIsProtectedBranchName: vi.fn(() => false),
@@ -74,6 +106,7 @@ import {
74
106
  getMergedPRWorktrees,
75
107
  removeWorktree,
76
108
  } from "../../../../worktree.ts";
109
+ import { loadSession, getLastToolUsageMap } from "../../../../config/index.js";
77
110
 
78
111
  const mockedGetAllBranches = getAllBranches as Mock;
79
112
  const mockedGetRepositoryRoot = getRepositoryRoot as Mock;
@@ -86,9 +119,8 @@ const mockedGetMergedPRWorktrees = getMergedPRWorktrees as Mock;
86
119
  const mockedRemoveWorktree = removeWorktree as Mock;
87
120
  const mockedIsProtectedBranchName = mockIsProtectedBranchName as Mock;
88
121
  const mockedSwitchToProtectedBranch = mockSwitchToProtectedBranch as Mock;
89
- const originalBranchListScreen = BranchListScreenModule.BranchListScreen;
90
- const originalBranchActionSelectorScreen =
91
- BranchActionSelectorScreenModule.BranchActionSelectorScreen;
122
+ const mockedLoadSession = loadSession as Mock;
123
+ const mockedGetLastToolUsageMap = getLastToolUsageMap as Mock;
92
124
 
93
125
  describe("Navigation Integration Tests", () => {
94
126
  beforeEach(() => {
@@ -110,8 +142,15 @@ describe("Navigation Integration Tests", () => {
110
142
  mockedRemoveWorktree.mockReset();
111
143
  mockedIsProtectedBranchName.mockReset();
112
144
  mockedSwitchToProtectedBranch.mockReset();
145
+ mockedLoadSession.mockReset();
146
+ mockedGetLastToolUsageMap.mockReset();
147
+ branchListProps.length = 0;
148
+ branchActionProps.length = 0;
149
+ aiToolScreenProps.length = 0;
113
150
  mockedGetRepositoryRoot.mockResolvedValue("/repo");
114
151
  mockedSwitchToProtectedBranch.mockResolvedValue("local");
152
+ mockedLoadSession.mockResolvedValue({ history: [] });
153
+ mockedGetLastToolUsageMap.mockResolvedValue(new Map());
115
154
  });
116
155
 
117
156
  const mockBranches: BranchInfo[] = [
@@ -248,11 +287,6 @@ describe("Navigation Integration Tests", () => {
248
287
  });
249
288
 
250
289
  describe("Protected Branch Navigation (T103)", () => {
251
- const branchListProps: BranchListScreenProps[] = [];
252
- const branchActionProps: BranchActionSelectorScreenProps[] = [];
253
- let branchListSpy: ReturnType<typeof vi.spyOn>;
254
- let branchActionSpy: ReturnType<typeof vi.spyOn>;
255
-
256
290
  const baseBranches: BranchInfo[] = [
257
291
  {
258
292
  name: "main",
@@ -284,33 +318,19 @@ describe("Protected Branch Navigation (T103)", () => {
284
318
  mockedRemoveWorktree.mockReset();
285
319
  mockedIsProtectedBranchName.mockReset();
286
320
  mockedSwitchToProtectedBranch.mockReset();
321
+ mockedLoadSession.mockReset();
322
+ mockedGetLastToolUsageMap.mockReset();
287
323
  mockedGetRepositoryRoot.mockResolvedValue("/repo");
288
324
  branchListProps.length = 0;
289
325
  branchActionProps.length = 0;
290
326
  aiToolScreenProps.length = 0;
291
- branchListSpy = vi
292
- .spyOn(BranchListScreenModule, "BranchListScreen")
293
- .mockImplementation((props: BranchListScreenProps) => {
294
- branchListProps.push(props);
295
- return React.createElement(originalBranchListScreen, props);
296
- });
297
- branchActionSpy = vi
298
- .spyOn(BranchActionSelectorScreenModule, "BranchActionSelectorScreen")
299
- .mockImplementation((props: BranchActionSelectorScreenProps) => {
300
- branchActionProps.push(props);
301
- return React.createElement(originalBranchActionSelectorScreen, props);
302
- });
303
-
304
327
  mockedIsProtectedBranchName.mockImplementation((name: string) =>
305
328
  ["main", "develop", "origin/main", "origin/develop"].includes(name),
306
329
  );
307
330
  mockedSwitchToProtectedBranch.mockResolvedValue("local");
308
331
  mockedGetRepositoryRoot.mockResolvedValue("/repo");
309
- });
310
-
311
- afterEach(() => {
312
- branchListSpy.mockRestore();
313
- branchActionSpy.mockRestore();
332
+ mockedLoadSession.mockResolvedValue({ history: [] });
333
+ mockedGetLastToolUsageMap.mockResolvedValue(new Map());
314
334
  });
315
335
 
316
336
  it("prefills AI tool selector with last used tool for the branch", async () => {
@@ -848,5 +848,110 @@ describe("branchFormatter", () => {
848
848
  expect(results[1].name).toBe("hotfix/urgent");
849
849
  expect(results[2].name).toBe("release/v1.0");
850
850
  });
851
+
852
+ it("should sort by latest activity time (max of git commit and tool usage)", () => {
853
+ const branches: BranchInfo[] = [
854
+ {
855
+ name: "feature/git-newer",
856
+ type: "local",
857
+ branchType: "feature",
858
+ isCurrent: false,
859
+ latestCommitTimestamp: 1_800_000_000, // git commit is newer
860
+ lastToolUsage: {
861
+ branch: "feature/git-newer",
862
+ worktreePath: "/tmp/wt1",
863
+ toolId: "claude-code",
864
+ toolLabel: "Claude",
865
+ timestamp: 1_700_000_000_000, // 1_700_000_000 seconds (older)
866
+ },
867
+ },
868
+ {
869
+ name: "feature/tool-newer",
870
+ type: "local",
871
+ branchType: "feature",
872
+ isCurrent: false,
873
+ latestCommitTimestamp: 1_700_000_000, // git commit is older
874
+ lastToolUsage: {
875
+ branch: "feature/tool-newer",
876
+ worktreePath: "/tmp/wt2",
877
+ toolId: "claude-code",
878
+ toolLabel: "Claude",
879
+ timestamp: 1_800_000_000_000, // 1_800_000_000 seconds (newer)
880
+ },
881
+ },
882
+ ];
883
+
884
+ const results = formatBranchItems(branches);
885
+
886
+ // Both have same latest activity time (1_800_000_000), so alphabetical
887
+ // feature/git-newer: max(1_800_000_000, 1_700_000_000) = 1_800_000_000
888
+ // feature/tool-newer: max(1_700_000_000, 1_800_000_000) = 1_800_000_000
889
+ expect(results[0].name).toBe("feature/git-newer");
890
+ expect(results[1].name).toBe("feature/tool-newer");
891
+ });
892
+
893
+ it("should prioritize branch with tool usage over branch with only git commit when tool is newer", () => {
894
+ const branches: BranchInfo[] = [
895
+ {
896
+ name: "feature/git-only",
897
+ type: "local",
898
+ branchType: "feature",
899
+ isCurrent: false,
900
+ latestCommitTimestamp: 1_700_000_000,
901
+ },
902
+ {
903
+ name: "feature/with-tool",
904
+ type: "local",
905
+ branchType: "feature",
906
+ isCurrent: false,
907
+ latestCommitTimestamp: 1_600_000_000,
908
+ lastToolUsage: {
909
+ branch: "feature/with-tool",
910
+ worktreePath: "/tmp/wt",
911
+ toolId: "claude-code",
912
+ toolLabel: "Claude",
913
+ timestamp: 1_800_000_000_000, // 1_800_000_000 seconds (newest)
914
+ },
915
+ },
916
+ ];
917
+
918
+ const results = formatBranchItems(branches);
919
+
920
+ // feature/with-tool has newer activity (tool usage at 1_800_000_000)
921
+ expect(results[0].name).toBe("feature/with-tool");
922
+ expect(results[1].name).toBe("feature/git-only");
923
+ });
924
+
925
+ it("should prioritize branch with newer git commit over branch with older tool usage", () => {
926
+ const branches: BranchInfo[] = [
927
+ {
928
+ name: "feature/old-tool",
929
+ type: "local",
930
+ branchType: "feature",
931
+ isCurrent: false,
932
+ latestCommitTimestamp: 1_600_000_000,
933
+ lastToolUsage: {
934
+ branch: "feature/old-tool",
935
+ worktreePath: "/tmp/wt",
936
+ toolId: "claude-code",
937
+ toolLabel: "Claude",
938
+ timestamp: 1_650_000_000_000, // 1_650_000_000 seconds
939
+ },
940
+ },
941
+ {
942
+ name: "feature/new-git",
943
+ type: "local",
944
+ branchType: "feature",
945
+ isCurrent: false,
946
+ latestCommitTimestamp: 1_800_000_000, // newest
947
+ },
948
+ ];
949
+
950
+ const results = formatBranchItems(branches);
951
+
952
+ // feature/new-git has newer activity (git commit at 1_800_000_000)
953
+ expect(results[0].name).toBe("feature/new-git");
954
+ expect(results[1].name).toBe("feature/old-tool");
955
+ });
851
956
  });
852
957
  });
@@ -926,7 +926,7 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
926
926
  if (selectedBranches.length === 0) {
927
927
  setCleanupIndicators({});
928
928
  setCleanupFooterMessage({
929
- text: "クリーンアップ対象が選択されていません",
929
+ text: "No cleanup targets selected.",
930
930
  color: "yellow",
931
931
  });
932
932
  setCleanupInputLocked(false);
@@ -11,6 +11,7 @@ import { useAppInput } from "../../hooks/useAppInput.js";
11
11
  import { useTerminalSize } from "../../hooks/useTerminalSize.js";
12
12
  import type { BranchItem, Statistics, BranchViewMode } from "../../types.js";
13
13
  import type { ToolStatus } from "../../hooks/useToolStatus.js";
14
+ import { getLatestActivityTimestamp } from "../../utils/branchFormatter.js";
14
15
  import stringWidth from "string-width";
15
16
  import stripAnsi from "strip-ansi";
16
17
  import chalk from "chalk";
@@ -425,12 +426,11 @@ export const BranchListScreen = React.memo(function BranchListScreen({
425
426
  const columns = Math.max(20, context.columns - 1);
426
427
  const visibleWidth = (value: string) =>
427
428
  measureDisplayWidth(stripAnsi(value));
429
+ // FR-041: Display latest activity time (max of git commit and tool usage)
428
430
  let commitText = "---";
429
- if (item.latestCommitTimestamp) {
430
- commitText = formatLatestCommit(item.latestCommitTimestamp);
431
- } else if (item.lastToolUsage?.timestamp) {
432
- const seconds = Math.floor(item.lastToolUsage.timestamp / 1000);
433
- commitText = formatLatestCommit(seconds);
431
+ const latestActivitySec = getLatestActivityTimestamp(item);
432
+ if (latestActivitySec > 0) {
433
+ commitText = formatLatestCommit(latestActivitySec);
434
434
  }
435
435
  const toolLabelRaw =
436
436
  item.lastToolUsageLabel?.split("|")?.[0]?.trim() ??
@@ -640,7 +640,7 @@ export const BranchListScreen = React.memo(function BranchListScreen({
640
640
  )}
641
641
  </Box>
642
642
 
643
- {/* Tool Status - FR-019, FR-021 */}
643
+ {/* Tool Status - FR-019, FR-021, FR-022 */}
644
644
  {toolStatuses && toolStatuses.length > 0 && (
645
645
  <Box>
646
646
  <Text dimColor>Tools: </Text>
@@ -648,7 +648,9 @@ export const BranchListScreen = React.memo(function BranchListScreen({
648
648
  <React.Fragment key={tool.id}>
649
649
  <Text>{tool.name}: </Text>
650
650
  <Text color={tool.status === "installed" ? "green" : "yellow"}>
651
- {tool.status}
651
+ {tool.status === "installed" && tool.version
652
+ ? tool.version
653
+ : tool.status}
652
654
  </Text>
653
655
  {index < toolStatuses.length - 1 && <Text dimColor> | </Text>}
654
656
  </React.Fragment>
@@ -70,7 +70,7 @@ export function LogDatePickerScreen({
70
70
  <Box flexDirection="column" flexGrow={1}>
71
71
  {dates.length === 0 ? (
72
72
  <Box>
73
- <Text dimColor>ログがありません</Text>
73
+ <Text dimColor>No logs available.</Text>
74
74
  </Box>
75
75
  ) : (
76
76
  <Select items={items} onSelect={handleSelect} limit={limit} />
@@ -35,7 +35,7 @@ export function LogDetailScreen({
35
35
  });
36
36
 
37
37
  const jsonLines = useMemo<string[]>(() => {
38
- if (!entry) return ["ログがありません"]; // fallback
38
+ if (!entry) return ["No logs available."]; // fallback
39
39
  return entry.json.split("\n");
40
40
  }, [entry]);
41
41
 
@@ -166,7 +166,7 @@ export function LogListScreen({
166
166
  </Box>
167
167
  ) : entries.length === 0 ? (
168
168
  <Box>
169
- <Text dimColor>ログがありません</Text>
169
+ <Text dimColor>No logs available.</Text>
170
170
  </Box>
171
171
  ) : (
172
172
  <Select