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.24",
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.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"
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",