@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.
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/claude.js +1 -1
- package/dist/claude.js.map +1 -1
- package/dist/cli/ui/components/App.js +1 -1
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +8 -7
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/LogDatePickerScreen.js +1 -1
- package/dist/cli/ui/components/screens/LogDatePickerScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/LogDetailScreen.js +1 -1
- package/dist/cli/ui/components/screens/LogDetailScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/LogListScreen.js +1 -1
- package/dist/cli/ui/components/screens/LogListScreen.js.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.d.ts +5 -0
- package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.js +18 -5
- package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
- package/dist/codex.d.ts.map +1 -1
- package/dist/codex.js +0 -1
- package/dist/codex.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +3 -7
- package/dist/config/index.js.map +1 -1
- package/dist/config/profiles.d.ts +2 -2
- package/dist/config/profiles.d.ts.map +1 -1
- package/dist/config/profiles.js +4 -7
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/tools.d.ts +1 -1
- package/dist/config/tools.d.ts.map +1 -1
- package/dist/config/tools.js +3 -43
- package/dist/config/tools.js.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +1 -2
- package/dist/gemini.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -90
- package/dist/index.js.map +1 -1
- package/dist/utils/command.d.ts +11 -0
- package/dist/utils/command.d.ts.map +1 -1
- package/dist/utils/command.js +33 -0
- package/dist/utils/command.js.map +1 -1
- package/dist/web/client/src/pages/ConfigPage.js +1 -1
- package/dist/web/client/src/pages/ConfigPage.js.map +1 -1
- package/package.json +2 -2
- package/src/claude.ts +1 -1
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +1 -1
- package/src/cli/ui/__tests__/components/App.test.tsx +65 -3
- package/src/cli/ui/__tests__/components/screens/LogDetailScreen.test.tsx +1 -1
- package/src/cli/ui/__tests__/components/screens/LogListScreen.test.tsx +1 -1
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +83 -22
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +57 -37
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +105 -0
- package/src/cli/ui/components/App.tsx +1 -1
- package/src/cli/ui/components/screens/BranchListScreen.tsx +9 -7
- package/src/cli/ui/components/screens/LogDatePickerScreen.tsx +1 -1
- package/src/cli/ui/components/screens/LogDetailScreen.tsx +1 -1
- package/src/cli/ui/components/screens/LogListScreen.tsx +1 -1
- package/src/cli/ui/utils/branchFormatter.ts +19 -5
- package/src/codex.ts +0 -1
- package/src/config/index.ts +3 -7
- package/src/config/profiles.ts +4 -7
- package/src/config/tools.ts +3 -56
- package/src/gemini.ts +1 -2
- package/src/index.ts +148 -133
- package/src/utils/command.ts +37 -0
- package/src/web/client/src/pages/ConfigPage.tsx +2 -2
- 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
|
-
|
|
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(
|
|
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
|
-
|
|
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[] = [
|
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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(
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
|
90
|
-
const
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
73
|
+
<Text dimColor>No logs available.</Text>
|
|
74
74
|
</Box>
|
|
75
75
|
) : (
|
|
76
76
|
<Select items={items} onSelect={handleSelect} limit={limit} />
|