ccmanager 4.1.24 → 4.1.25
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.
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Worktree } from '../types/index.js';
|
|
2
|
+
/** Clear the module-level git-status cache. Intended for tests. */
|
|
3
|
+
export declare function clearGitStatusCache(): void;
|
|
2
4
|
/**
|
|
3
5
|
* Custom hook for polling git status and commit dates of worktrees with Effect-based execution
|
|
4
6
|
*
|
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { Effect, Exit, Cause, Option } from 'effect';
|
|
3
3
|
import { getGitStatusLimited, getLastCommitDateLimited, } from '../utils/gitStatus.js';
|
|
4
|
+
/**
|
|
5
|
+
* Module-level cache of the last fetched git status, keyed by worktree path.
|
|
6
|
+
*
|
|
7
|
+
* The menu is fully unmounted and remounted on every return to it (App renders
|
|
8
|
+
* `<Menu key={menuKey} />`, and the key changes on navigation), which would
|
|
9
|
+
* otherwise wipe this hook's state and force every worktree to flash
|
|
10
|
+
* "[fetching...]" and refetch from scratch each time. Seeding state from this
|
|
11
|
+
* cache shows each worktree's last known status instantly; background polling
|
|
12
|
+
* still refreshes it. Entries are intentionally not pruned so switching between
|
|
13
|
+
* projects keeps each project's cached status.
|
|
14
|
+
*/
|
|
15
|
+
const statusCache = new Map();
|
|
16
|
+
/** Clear the module-level git-status cache. Intended for tests. */
|
|
17
|
+
export function clearGitStatusCache() {
|
|
18
|
+
statusCache.clear();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Merge any cached status onto the given worktrees so a remount can show the
|
|
22
|
+
* last known status immediately instead of "[fetching...]". Returns the same
|
|
23
|
+
* array reference when nothing was cached, so React can skip a re-render.
|
|
24
|
+
*/
|
|
25
|
+
function hydrateFromCache(worktrees) {
|
|
26
|
+
let changed = false;
|
|
27
|
+
const next = worktrees.map(wt => {
|
|
28
|
+
const cached = statusCache.get(wt.path);
|
|
29
|
+
if (!cached) {
|
|
30
|
+
return wt;
|
|
31
|
+
}
|
|
32
|
+
changed = true;
|
|
33
|
+
return {
|
|
34
|
+
...wt,
|
|
35
|
+
gitStatus: cached.gitStatus,
|
|
36
|
+
gitStatusError: cached.gitStatusError,
|
|
37
|
+
lastCommitDate: cached.lastCommitDate,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
return changed ? next : worktrees;
|
|
41
|
+
}
|
|
42
|
+
/** Record a worktree's latest status update into the module-level cache. */
|
|
43
|
+
function cacheStatusUpdate(path, update) {
|
|
44
|
+
const next = { ...statusCache.get(path) };
|
|
45
|
+
if ('gitStatus' in update) {
|
|
46
|
+
next.gitStatus = update.gitStatus;
|
|
47
|
+
}
|
|
48
|
+
if ('gitStatusError' in update) {
|
|
49
|
+
next.gitStatusError = update.gitStatusError;
|
|
50
|
+
}
|
|
51
|
+
if ('lastCommitDate' in update) {
|
|
52
|
+
next.lastCommitDate = update.lastCommitDate;
|
|
53
|
+
}
|
|
54
|
+
statusCache.set(path, next);
|
|
55
|
+
}
|
|
4
56
|
/**
|
|
5
57
|
* Custom hook for polling git status and commit dates of worktrees with Effect-based execution
|
|
6
58
|
*
|
|
@@ -20,7 +72,7 @@ import { getGitStatusLimited, getLastCommitDateLimited, } from '../utils/gitStat
|
|
|
20
72
|
* @returns Array of worktrees with updated gitStatus, gitStatusError, and lastCommitDate fields
|
|
21
73
|
*/
|
|
22
74
|
export function useGitStatus(worktrees, defaultBranch, updateInterval = 5000) {
|
|
23
|
-
const [worktreesWithStatus, setWorktreesWithStatus] = useState(worktrees);
|
|
75
|
+
const [worktreesWithStatus, setWorktreesWithStatus] = useState(() => hydrateFromCache(worktrees));
|
|
24
76
|
useEffect(() => {
|
|
25
77
|
if (!defaultBranch) {
|
|
26
78
|
return;
|
|
@@ -66,7 +118,7 @@ export function useGitStatus(worktrees, defaultBranch, updateInterval = 5000) {
|
|
|
66
118
|
}, updateInterval);
|
|
67
119
|
}
|
|
68
120
|
};
|
|
69
|
-
setWorktreesWithStatus(worktrees);
|
|
121
|
+
setWorktreesWithStatus(hydrateFromCache(worktrees));
|
|
70
122
|
runCycle().catch(() => {
|
|
71
123
|
// Ignore errors - the fetch failed or was aborted
|
|
72
124
|
});
|
|
@@ -97,6 +149,7 @@ function applyStatusResults(results, setWorktreesWithStatus) {
|
|
|
97
149
|
const update = buildStatusUpdate(result.statusExit, result.dateExit);
|
|
98
150
|
if (update) {
|
|
99
151
|
updatesByPath.set(result.path, update);
|
|
152
|
+
cacheStatusUpdate(result.path, update);
|
|
100
153
|
}
|
|
101
154
|
}
|
|
102
155
|
if (updatesByPath.size === 0) {
|
|
@@ -3,7 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import { render, cleanup } from 'ink-testing-library';
|
|
4
4
|
import { Text } from 'ink';
|
|
5
5
|
import { Effect, Exit } from 'effect';
|
|
6
|
-
import { useGitStatus } from './useGitStatus.js';
|
|
6
|
+
import { useGitStatus, clearGitStatusCache } from './useGitStatus.js';
|
|
7
7
|
import { getGitStatusLimited, getLastCommitDateLimited, } from '../utils/gitStatus.js';
|
|
8
8
|
import { GitError } from '../types/errors.js';
|
|
9
9
|
// Mock the gitStatus module
|
|
@@ -29,6 +29,9 @@ describe('useGitStatus', () => {
|
|
|
29
29
|
});
|
|
30
30
|
beforeEach(() => {
|
|
31
31
|
vi.useFakeTimers();
|
|
32
|
+
// The status cache is module-level and persists across tests; clear it so
|
|
33
|
+
// each test starts from a cold cache.
|
|
34
|
+
clearGitStatusCache();
|
|
32
35
|
mockGetGitStatus.mockClear();
|
|
33
36
|
mockGetLastCommitDate.mockClear();
|
|
34
37
|
// Default: return a date for all worktrees
|
|
@@ -66,6 +69,27 @@ describe('useGitStatus', () => {
|
|
|
66
69
|
expect(hookResult[0]?.gitStatus).toEqual(gitStatus1);
|
|
67
70
|
expect(hookResult[1]?.gitStatus).toEqual(gitStatus2);
|
|
68
71
|
});
|
|
72
|
+
it('should hydrate from cache on remount so status shows immediately', async () => {
|
|
73
|
+
const worktrees = [createWorktree('/path1')];
|
|
74
|
+
const gitStatus1 = createGitStatus(7, 2);
|
|
75
|
+
mockGetGitStatus.mockReturnValue(Effect.succeed(gitStatus1));
|
|
76
|
+
let hookResult = [];
|
|
77
|
+
const TestComponent = () => {
|
|
78
|
+
hookResult = useGitStatus(worktrees, 'main', 100);
|
|
79
|
+
return React.createElement(Text, null, 'test');
|
|
80
|
+
};
|
|
81
|
+
// First mount: fetch populates the module-level cache.
|
|
82
|
+
const first = render(React.createElement(TestComponent));
|
|
83
|
+
await vi.waitFor(() => {
|
|
84
|
+
expect(hookResult[0]?.gitStatus).toEqual(gitStatus1);
|
|
85
|
+
});
|
|
86
|
+
first.unmount();
|
|
87
|
+
// Remount: cached status must be present on the very first render, before
|
|
88
|
+
// any new fetch resolves, so the menu does not flash "[fetching...]".
|
|
89
|
+
hookResult = [];
|
|
90
|
+
render(React.createElement(TestComponent));
|
|
91
|
+
expect(hookResult[0]?.gitStatus).toEqual(gitStatus1);
|
|
92
|
+
});
|
|
69
93
|
it('should handle empty worktree array', () => {
|
|
70
94
|
let hookResult = [];
|
|
71
95
|
const TestComponent = () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.25",
|
|
4
4
|
"description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kodai Kabasawa",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"bin"
|
|
42
42
|
],
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@kodaikabasawa/ccmanager-darwin-arm64": "4.1.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "4.1.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "4.1.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "4.1.25",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.25",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.25",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "4.1.25",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "4.1.25"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|