ccmanager 4.1.23 → 4.1.24
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/hooks/useGitStatus.d.ts +5 -0
- package/dist/hooks/useGitStatus.js +121 -48
- package/package.json +6 -6
|
@@ -7,6 +7,11 @@ import { Worktree } from '../types/index.js';
|
|
|
7
7
|
* Both are fetched together so they appear at the same time.
|
|
8
8
|
* Handles cancellation via AbortController.
|
|
9
9
|
*
|
|
10
|
+
* Each poll cycle fetches every worktree concurrently and commits the results in
|
|
11
|
+
* a single state update, skipping the update entirely when nothing changed. This
|
|
12
|
+
* keeps a poll cycle to at most one re-render of the consumer (the menu) instead
|
|
13
|
+
* of one per worktree, so background polling does not stutter keyboard navigation.
|
|
14
|
+
*
|
|
10
15
|
* @param worktrees - Array of worktrees to monitor
|
|
11
16
|
* @param defaultBranch - Default branch for comparisons (null disables polling)
|
|
12
17
|
* @param updateInterval - Polling interval in milliseconds (default: 5000)
|
|
@@ -9,6 +9,11 @@ import { getGitStatusLimited, getLastCommitDateLimited, } from '../utils/gitStat
|
|
|
9
9
|
* Both are fetched together so they appear at the same time.
|
|
10
10
|
* Handles cancellation via AbortController.
|
|
11
11
|
*
|
|
12
|
+
* Each poll cycle fetches every worktree concurrently and commits the results in
|
|
13
|
+
* a single state update, skipping the update entirely when nothing changed. This
|
|
14
|
+
* keeps a poll cycle to at most one re-render of the consumer (the menu) instead
|
|
15
|
+
* of one per worktree, so background polling does not stutter keyboard navigation.
|
|
16
|
+
*
|
|
12
17
|
* @param worktrees - Array of worktrees to monitor
|
|
13
18
|
* @param defaultBranch - Default branch for comparisons (null disables polling)
|
|
14
19
|
* @param updateInterval - Polling interval in milliseconds (default: 5000)
|
|
@@ -20,67 +25,103 @@ export function useGitStatus(worktrees, defaultBranch, updateInterval = 5000) {
|
|
|
20
25
|
if (!defaultBranch) {
|
|
21
26
|
return;
|
|
22
27
|
}
|
|
23
|
-
const
|
|
24
|
-
const activeRequests = new Map();
|
|
28
|
+
const activeRequests = new Set();
|
|
25
29
|
let isCleanedUp = false;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const [statusExit, dateExit] = await Promise.all([
|
|
29
|
-
Effect.runPromiseExit(getGitStatusLimited(worktree.path), {
|
|
30
|
-
signal: abortController.signal,
|
|
31
|
-
}),
|
|
32
|
-
Effect.runPromiseExit(getLastCommitDateLimited(worktree.path), {
|
|
33
|
-
signal: abortController.signal,
|
|
34
|
-
}),
|
|
35
|
-
]);
|
|
36
|
-
// Update worktree state with both results at once
|
|
37
|
-
handleStatusExit(statusExit, dateExit, worktree.path, setWorktreesWithStatus);
|
|
38
|
-
};
|
|
39
|
-
const scheduleUpdate = (worktree) => {
|
|
30
|
+
let cycleTimeout;
|
|
31
|
+
const fetchStatus = async (worktree) => {
|
|
40
32
|
const abortController = new AbortController();
|
|
41
|
-
activeRequests.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
33
|
+
activeRequests.add(abortController);
|
|
34
|
+
try {
|
|
35
|
+
// Fetch git status and last commit date in parallel
|
|
36
|
+
const [statusExit, dateExit] = await Promise.all([
|
|
37
|
+
Effect.runPromiseExit(getGitStatusLimited(worktree.path), {
|
|
38
|
+
signal: abortController.signal,
|
|
39
|
+
}),
|
|
40
|
+
Effect.runPromiseExit(getLastCommitDateLimited(worktree.path), {
|
|
41
|
+
signal: abortController.signal,
|
|
42
|
+
}),
|
|
43
|
+
]);
|
|
44
|
+
return { path: worktree.path, statusExit, dateExit };
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
activeRequests.delete(abortController);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const runCycle = async () => {
|
|
51
|
+
// Fetch all worktrees concurrently. The underlying Effects are already
|
|
52
|
+
// concurrency-limited, so this does not spawn an unbounded number of git
|
|
53
|
+
// subprocesses at once.
|
|
54
|
+
const results = await Promise.all(worktrees.map(worktree => fetchStatus(worktree)));
|
|
55
|
+
if (isCleanedUp) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Apply every worktree's result in one state update so the consumer
|
|
59
|
+
// re-renders at most once per cycle.
|
|
60
|
+
applyStatusResults(results, setWorktreesWithStatus);
|
|
61
|
+
if (!isCleanedUp) {
|
|
62
|
+
cycleTimeout = setTimeout(() => {
|
|
63
|
+
if (!isCleanedUp) {
|
|
64
|
+
runCycle();
|
|
65
|
+
}
|
|
66
|
+
}, updateInterval);
|
|
67
|
+
}
|
|
57
68
|
};
|
|
58
69
|
setWorktreesWithStatus(worktrees);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
scheduleUpdate(worktree);
|
|
70
|
+
runCycle().catch(() => {
|
|
71
|
+
// Ignore errors - the fetch failed or was aborted
|
|
62
72
|
});
|
|
63
73
|
return () => {
|
|
64
74
|
isCleanedUp = true;
|
|
65
|
-
|
|
75
|
+
if (cycleTimeout) {
|
|
76
|
+
clearTimeout(cycleTimeout);
|
|
77
|
+
}
|
|
66
78
|
activeRequests.forEach(controller => controller.abort());
|
|
67
79
|
};
|
|
68
80
|
}, [worktrees, defaultBranch, updateInterval]);
|
|
69
81
|
return worktreesWithStatus;
|
|
70
82
|
}
|
|
71
83
|
/**
|
|
72
|
-
*
|
|
84
|
+
* Apply a cycle's worth of status results in a single state update.
|
|
73
85
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
86
|
+
* Builds the next worktree array by merging each result onto the matching
|
|
87
|
+
* worktree, but only when the result actually changes a field. When nothing
|
|
88
|
+
* changed, the previous array reference is returned so React skips the re-render
|
|
89
|
+
* (and any downstream items rebuild) entirely.
|
|
76
90
|
*
|
|
77
|
-
* @param
|
|
78
|
-
* @param dateExit - Exit result from commit date Effect
|
|
79
|
-
* @param worktreePath - Path of the worktree being updated
|
|
91
|
+
* @param results - Per-worktree status/date Exit results for this cycle
|
|
80
92
|
* @param setWorktreesWithStatus - State setter function
|
|
81
93
|
*/
|
|
82
|
-
function
|
|
83
|
-
|
|
94
|
+
function applyStatusResults(results, setWorktreesWithStatus) {
|
|
95
|
+
const updatesByPath = new Map();
|
|
96
|
+
for (const result of results) {
|
|
97
|
+
const update = buildStatusUpdate(result.statusExit, result.dateExit);
|
|
98
|
+
if (update) {
|
|
99
|
+
updatesByPath.set(result.path, update);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (updatesByPath.size === 0) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
setWorktreesWithStatus(prev => {
|
|
106
|
+
let changed = false;
|
|
107
|
+
const next = prev.map(wt => {
|
|
108
|
+
const update = updatesByPath.get(wt.path);
|
|
109
|
+
if (!update || isUpdateNoop(wt, update)) {
|
|
110
|
+
return wt;
|
|
111
|
+
}
|
|
112
|
+
changed = true;
|
|
113
|
+
return { ...wt, ...update };
|
|
114
|
+
});
|
|
115
|
+
return changed ? next : prev;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build the update object for a single worktree from its Exit results.
|
|
120
|
+
*
|
|
121
|
+
* @returns A partial worktree to merge, or null when there is nothing to update
|
|
122
|
+
* (e.g. the status fetch was interrupted and the commit date failed).
|
|
123
|
+
*/
|
|
124
|
+
function buildStatusUpdate(statusExit, dateExit) {
|
|
84
125
|
const update = {};
|
|
85
126
|
let hasUpdate = false;
|
|
86
127
|
if (Exit.isSuccess(statusExit)) {
|
|
@@ -91,9 +132,8 @@ function handleStatusExit(statusExit, dateExit, worktreePath, setWorktreesWithSt
|
|
|
91
132
|
else if (Exit.isFailure(statusExit)) {
|
|
92
133
|
const failure = Cause.failureOption(statusExit.cause);
|
|
93
134
|
if (Option.isSome(failure)) {
|
|
94
|
-
const gitError = failure.value;
|
|
95
135
|
update.gitStatus = undefined;
|
|
96
|
-
update.gitStatusError = formatGitError(
|
|
136
|
+
update.gitStatusError = formatGitError(failure.value);
|
|
97
137
|
hasUpdate = true;
|
|
98
138
|
}
|
|
99
139
|
}
|
|
@@ -102,9 +142,42 @@ function handleStatusExit(statusExit, dateExit, worktreePath, setWorktreesWithSt
|
|
|
102
142
|
hasUpdate = true;
|
|
103
143
|
}
|
|
104
144
|
// Silently ignore commit date errors (e.g., empty repo)
|
|
105
|
-
|
|
106
|
-
|
|
145
|
+
return hasUpdate ? update : null;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Determine whether applying `update` to `wt` would change any field, so that
|
|
149
|
+
* no-op updates can be dropped before they trigger a re-render.
|
|
150
|
+
*/
|
|
151
|
+
function isUpdateNoop(wt, update) {
|
|
152
|
+
if ('gitStatus' in update &&
|
|
153
|
+
!isSameGitStatus(wt.gitStatus, update.gitStatus)) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
if ('gitStatusError' in update &&
|
|
157
|
+
wt.gitStatusError !== update.gitStatusError) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
if ('lastCommitDate' in update) {
|
|
161
|
+
const prevTime = wt.lastCommitDate?.getTime();
|
|
162
|
+
const nextTime = update.lastCommitDate?.getTime();
|
|
163
|
+
if (prevTime !== nextTime) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
function isSameGitStatus(a, b) {
|
|
170
|
+
if (a === b) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
if (!a || !b) {
|
|
174
|
+
return false;
|
|
107
175
|
}
|
|
176
|
+
return (a.filesAdded === b.filesAdded &&
|
|
177
|
+
a.filesDeleted === b.filesDeleted &&
|
|
178
|
+
a.aheadCount === b.aheadCount &&
|
|
179
|
+
a.behindCount === b.behindCount &&
|
|
180
|
+
a.parentBranch === b.parentBranch);
|
|
108
181
|
}
|
|
109
182
|
/**
|
|
110
183
|
* Format GitError into a user-friendly error message
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.24",
|
|
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.24",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.24",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.24",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "4.1.24",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "4.1.24"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|