ccmanager 4.1.18 → 4.1.19
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/components/App.js
CHANGED
|
@@ -572,6 +572,16 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
572
572
|
setActiveSession(session);
|
|
573
573
|
navigateWithClear('session');
|
|
574
574
|
};
|
|
575
|
+
const handleSessionActionFromDashboard = (session, project) => {
|
|
576
|
+
const projectSessionManager = globalSessionOrchestrator.getManagerForProject(project.path);
|
|
577
|
+
setSessionManager(projectSessionManager);
|
|
578
|
+
setWorktreeService(new WorktreeService(project.path));
|
|
579
|
+
setSessionActionsTarget({
|
|
580
|
+
session,
|
|
581
|
+
worktreePath: session.worktreePath,
|
|
582
|
+
});
|
|
583
|
+
navigateWithClear('session-actions');
|
|
584
|
+
};
|
|
575
585
|
const handleBackToProjectList = () => {
|
|
576
586
|
// Sessions persist in their project-specific managers
|
|
577
587
|
setSelectedProject(null);
|
|
@@ -587,7 +597,7 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
587
597
|
if (!projectsDir) {
|
|
588
598
|
return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["Error: ", MULTI_PROJECT_ERRORS.NO_PROJECTS_DIR] }) }));
|
|
589
599
|
}
|
|
590
|
-
return (_jsx(Dashboard, { projectsDir: projectsDir, onSelectSession: handleSelectSessionFromDashboard, onSelectProject: handleSelectProject, error: error, onDismissError: () => setError(null), version: version }));
|
|
600
|
+
return (_jsx(Dashboard, { projectsDir: projectsDir, onSelectSession: handleSelectSessionFromDashboard, onSelectProject: handleSelectProject, onSessionAction: handleSessionActionFromDashboard, error: error, onDismissError: () => setError(null), version: version }));
|
|
591
601
|
}
|
|
592
602
|
if (view === 'menu') {
|
|
593
603
|
return (_jsx(Menu, { sessionManager: sessionManager, worktreeService: worktreeService, onMenuAction: handleMenuAction, onSelectRecentProject: handleSelectProject, error: error, onDismissError: () => setError(null), projectName: selectedProject?.name, multiProject: multiProject, version: version }, menuKey));
|
|
@@ -4,6 +4,7 @@ interface DashboardProps {
|
|
|
4
4
|
projectsDir: string;
|
|
5
5
|
onSelectSession: (session: ISession, project: GitProject) => void;
|
|
6
6
|
onSelectProject: (project: GitProject) => void;
|
|
7
|
+
onSessionAction?: (session: ISession, project: GitProject) => void;
|
|
7
8
|
error: string | null;
|
|
8
9
|
onDismissError: () => void;
|
|
9
10
|
version: string;
|
|
@@ -12,7 +12,7 @@ import { STATUS_ICONS, STATUS_LABELS, MENU_ICONS, getStatusDisplay, } from '../c
|
|
|
12
12
|
import { useSearchMode } from '../hooks/useSearchMode.js';
|
|
13
13
|
import { useDynamicLimit } from '../hooks/useDynamicLimit.js';
|
|
14
14
|
import { useGitStatus } from '../hooks/useGitStatus.js';
|
|
15
|
-
import { truncateString, calculateColumnPositions, assembleSessionLabel, formatRelativeDate, } from '../utils/worktreeUtils.js';
|
|
15
|
+
import { truncateString, calculateColumnPositions, assembleSessionLabel, formatRelativeDate, displaySuffix, } from '../utils/worktreeUtils.js';
|
|
16
16
|
import { formatGitFileChanges, formatGitAheadBehind, formatParentBranch, } from '../utils/gitStatus.js';
|
|
17
17
|
import SearchableList from './SearchableList.js';
|
|
18
18
|
const MAX_BRANCH_NAME_LENGTH = 70;
|
|
@@ -63,7 +63,7 @@ function resolveProjectDisplayNames(projects) {
|
|
|
63
63
|
}
|
|
64
64
|
return displayNames;
|
|
65
65
|
}
|
|
66
|
-
const Dashboard = ({ projectsDir, onSelectSession, onSelectProject, error, onDismissError, version, }) => {
|
|
66
|
+
const Dashboard = ({ projectsDir, onSelectSession, onSelectProject, onSessionAction, error, onDismissError, version, }) => {
|
|
67
67
|
const [projects, setProjects] = useState([]);
|
|
68
68
|
const [recentProjects, setRecentProjects] = useState([]);
|
|
69
69
|
const [loading, setLoading] = useState(true);
|
|
@@ -73,6 +73,8 @@ const Dashboard = ({ projectsDir, onSelectSession, onSelectProject, error, onDis
|
|
|
73
73
|
const [sessionEntries, setSessionEntries] = useState([]);
|
|
74
74
|
const [baseSessionWorktrees, setBaseSessionWorktrees] = useState([]);
|
|
75
75
|
const [sessionRefreshKey, setSessionRefreshKey] = useState(0);
|
|
76
|
+
const [highlightedSession, setHighlightedSession] = useState(null);
|
|
77
|
+
const [highlightedProject, setHighlightedProject] = useState(null);
|
|
76
78
|
const displayError = error || loadError;
|
|
77
79
|
const { isSearchMode, searchQuery, selectedIndex, setSearchQuery } = useSearchMode(items.length, {
|
|
78
80
|
isDisabled: !!displayError,
|
|
@@ -220,7 +222,10 @@ const Dashboard = ({ projectsDir, onSelectSession, onSelectProject, error, onDis
|
|
|
220
222
|
: wt.path.split('/').pop() || 'detached';
|
|
221
223
|
const branchName = truncateString(fullBranchName, MAX_BRANCH_NAME_LENGTH);
|
|
222
224
|
const isMain = wt.isMainWorktree ? ' (main)' : '';
|
|
223
|
-
const
|
|
225
|
+
const worktreeSessionCount = sessionEntries.filter(e => e.worktree.path === entry.worktree.path &&
|
|
226
|
+
e.projectPath === entry.projectPath).length;
|
|
227
|
+
const sessionSuffix = displaySuffix(entry.session, worktreeSessionCount > 1);
|
|
228
|
+
const baseLabel = `${entry.projectName} :: ${branchName}${isMain}${sessionSuffix}${status}`;
|
|
224
229
|
let fileChanges = '';
|
|
225
230
|
let aheadBehind = '';
|
|
226
231
|
let parentBranch = '';
|
|
@@ -407,6 +412,11 @@ const Dashboard = ({ projectsDir, onSelectSession, onSelectProject, error, onDis
|
|
|
407
412
|
return;
|
|
408
413
|
}
|
|
409
414
|
switch (keyPressed) {
|
|
415
|
+
case ' ':
|
|
416
|
+
if (highlightedSession && highlightedProject && onSessionAction) {
|
|
417
|
+
onSessionAction(highlightedSession, highlightedProject);
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
410
420
|
case 'r':
|
|
411
421
|
refreshAll();
|
|
412
422
|
break;
|
|
@@ -447,10 +457,20 @@ const Dashboard = ({ projectsDir, onSelectSession, onSelectProject, error, onDis
|
|
|
447
457
|
if (!item)
|
|
448
458
|
return;
|
|
449
459
|
handleSelect(item);
|
|
460
|
+
}, onHighlight: raw => {
|
|
461
|
+
const item = items.find(i => i.value === raw?.value);
|
|
462
|
+
if (item?.type === 'session') {
|
|
463
|
+
setHighlightedSession(item.session);
|
|
464
|
+
setHighlightedProject(item.project);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
setHighlightedSession(null);
|
|
468
|
+
setHighlightedProject(null);
|
|
469
|
+
}
|
|
450
470
|
}, isFocused: !displayError, limit: limit, initialIndex: selectedIndex }) })), displayError && (_jsx(Box, { marginTop: 1, paddingX: 1, borderStyle: "round", borderColor: "red", children: _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", bold: true, children: ["Error: ", displayError] }), _jsx(Text, { color: "gray", dimColor: true, children: "Press any key to dismiss" })] }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Status: ", STATUS_ICONS.BUSY, " ", STATUS_LABELS.BUSY, ' ', STATUS_ICONS.WAITING, " ", STATUS_LABELS.WAITING, " ", STATUS_ICONS.IDLE, ' ', STATUS_LABELS.IDLE] }), _jsx(Text, { dimColor: true, children: isSearchMode
|
|
451
471
|
? 'Search Mode: Type to filter, Enter to exit search, ESC to exit search'
|
|
452
472
|
: searchQuery
|
|
453
|
-
? `Filtered: "${searchQuery}" | ↑↓ Navigate Enter Select | /-Search ESC-Clear 0-9 Quick Select R-Refresh Q-Quit`
|
|
454
|
-
: 'Controls: ↑↓ Navigate Enter Select | Hotkeys: 0-9 Quick Select /-Search R-Refresh Q-Quit' })] })] }));
|
|
473
|
+
? `Filtered: "${searchQuery}" | ↑↓ Navigate Enter Select | /-Search ESC-Clear 0-9 Quick Select Space-Session actions (session rows only) R-Refresh Q-Quit`
|
|
474
|
+
: 'Controls: ↑↓ Navigate Enter Select | Hotkeys: 0-9 Quick Select /-Search Space-Session actions (session rows only) R-Refresh Q-Quit' })] })] }));
|
|
455
475
|
};
|
|
456
476
|
export default Dashboard;
|
|
@@ -1,26 +1,50 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from 'ink-testing-library';
|
|
3
3
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
const capturedHandlers = { inputHandlers: [], onHighlight: null };
|
|
5
|
+
const makeKey = (overrides = {}) => ({
|
|
6
|
+
upArrow: false,
|
|
7
|
+
downArrow: false,
|
|
8
|
+
leftArrow: false,
|
|
9
|
+
rightArrow: false,
|
|
10
|
+
pageDown: false,
|
|
11
|
+
pageUp: false,
|
|
12
|
+
home: false,
|
|
13
|
+
end: false,
|
|
14
|
+
return: false,
|
|
15
|
+
escape: false,
|
|
16
|
+
ctrl: false,
|
|
17
|
+
shift: false,
|
|
18
|
+
tab: false,
|
|
19
|
+
backspace: false,
|
|
20
|
+
delete: false,
|
|
21
|
+
meta: false,
|
|
22
|
+
...overrides,
|
|
23
|
+
});
|
|
4
24
|
// Mock bunTerminal to avoid native module loading issues
|
|
5
25
|
vi.mock('../services/bunTerminal.js', () => ({
|
|
6
26
|
spawn: vi.fn(function () {
|
|
7
27
|
return null;
|
|
8
28
|
}),
|
|
9
29
|
}));
|
|
10
|
-
// Import the actual component code but
|
|
30
|
+
// Import the actual component code but capture useInput handlers
|
|
11
31
|
vi.mock('ink', async () => {
|
|
12
32
|
const actual = await vi.importActual('ink');
|
|
13
33
|
return {
|
|
14
34
|
...actual,
|
|
15
|
-
useInput: vi.fn()
|
|
35
|
+
useInput: vi.fn((handler) => {
|
|
36
|
+
capturedHandlers.inputHandlers.push(handler);
|
|
37
|
+
}),
|
|
16
38
|
};
|
|
17
39
|
});
|
|
18
|
-
// Mock SelectInput to render items as simple text
|
|
40
|
+
// Mock SelectInput to render items as simple text and capture onHighlight
|
|
19
41
|
vi.mock('ink-select-input', async () => {
|
|
20
42
|
const React = await vi.importActual('react');
|
|
21
43
|
const { Text, Box } = await vi.importActual('ink');
|
|
22
44
|
return {
|
|
23
|
-
default: ({ items }) => {
|
|
45
|
+
default: ({ items, onHighlight, }) => {
|
|
46
|
+
if (onHighlight)
|
|
47
|
+
capturedHandlers.onHighlight = onHighlight;
|
|
24
48
|
return React.createElement(Box, { flexDirection: 'column' }, items.map((item, index) => React.createElement(Text, { key: index }, item.label)));
|
|
25
49
|
},
|
|
26
50
|
};
|
|
@@ -120,6 +144,8 @@ describe('Dashboard', () => {
|
|
|
120
144
|
];
|
|
121
145
|
beforeEach(() => {
|
|
122
146
|
vi.clearAllMocks();
|
|
147
|
+
capturedHandlers.inputHandlers = [];
|
|
148
|
+
capturedHandlers.onHighlight = null;
|
|
123
149
|
vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed(mockProjects));
|
|
124
150
|
vi.mocked(globalSessionOrchestrator.getProjectPaths).mockReturnValue([]);
|
|
125
151
|
vi.mocked(globalSessionOrchestrator.getProjectSessions).mockReturnValue([]);
|
|
@@ -308,6 +334,7 @@ describe('Dashboard', () => {
|
|
|
308
334
|
const { lastFrame } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
|
|
309
335
|
expect(lastFrame()).toContain('Controls:');
|
|
310
336
|
expect(lastFrame()).toContain('0-9 Quick Select');
|
|
337
|
+
expect(lastFrame()).toContain('Space-Session actions');
|
|
311
338
|
expect(lastFrame()).toContain('R-Refresh');
|
|
312
339
|
expect(lastFrame()).toContain('Q-Quit');
|
|
313
340
|
});
|
|
@@ -345,4 +372,261 @@ describe('Dashboard', () => {
|
|
|
345
372
|
// shared-lib should appear first (index 0) because it's recent
|
|
346
373
|
expect(frame).toContain('0 ❯ shared-lib');
|
|
347
374
|
});
|
|
375
|
+
it('should show session name suffix in label when session has a name', async () => {
|
|
376
|
+
const mockSession = {
|
|
377
|
+
id: 'session-named',
|
|
378
|
+
worktreePath: '/projects/my-app/worktrees/feature-auth',
|
|
379
|
+
sessionNumber: 1,
|
|
380
|
+
sessionName: 'my-feature',
|
|
381
|
+
lastActivity: new Date(),
|
|
382
|
+
isActive: true,
|
|
383
|
+
stateMutex: {
|
|
384
|
+
getSnapshot: () => ({
|
|
385
|
+
state: 'busy',
|
|
386
|
+
backgroundTaskCount: 0,
|
|
387
|
+
teamMemberCount: 0,
|
|
388
|
+
}),
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
vi.mocked(globalSessionOrchestrator.getProjectPaths).mockReturnValue([
|
|
392
|
+
'/projects/my-app',
|
|
393
|
+
]);
|
|
394
|
+
vi.mocked(globalSessionOrchestrator.getProjectSessions).mockReturnValue([
|
|
395
|
+
mockSession,
|
|
396
|
+
]);
|
|
397
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
398
|
+
return {
|
|
399
|
+
getWorktreesEffect: () => Effect.succeed([
|
|
400
|
+
{
|
|
401
|
+
path: '/projects/my-app/worktrees/feature-auth',
|
|
402
|
+
branch: 'feature/auth',
|
|
403
|
+
isMainWorktree: false,
|
|
404
|
+
hasSession: true,
|
|
405
|
+
},
|
|
406
|
+
]),
|
|
407
|
+
getGitRootPath: () => '/projects/my-app',
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
|
|
411
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
412
|
+
rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
|
|
413
|
+
await vi.waitFor(() => {
|
|
414
|
+
return lastFrame()?.includes('Active Sessions') ?? false;
|
|
415
|
+
}, { timeout: 3000 });
|
|
416
|
+
expect(lastFrame()).toContain('my-app :: feature/auth: my-feature');
|
|
417
|
+
});
|
|
418
|
+
it('should show session number suffix when multiple unnamed sessions on same worktree', async () => {
|
|
419
|
+
const mockSessions = [
|
|
420
|
+
{
|
|
421
|
+
id: 'session-1',
|
|
422
|
+
worktreePath: '/projects/my-app/worktrees/feature-auth',
|
|
423
|
+
sessionNumber: 1,
|
|
424
|
+
lastActivity: new Date(),
|
|
425
|
+
isActive: true,
|
|
426
|
+
stateMutex: {
|
|
427
|
+
getSnapshot: () => ({
|
|
428
|
+
state: 'busy',
|
|
429
|
+
backgroundTaskCount: 0,
|
|
430
|
+
teamMemberCount: 0,
|
|
431
|
+
}),
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
id: 'session-2',
|
|
436
|
+
worktreePath: '/projects/my-app/worktrees/feature-auth',
|
|
437
|
+
sessionNumber: 2,
|
|
438
|
+
lastActivity: new Date(),
|
|
439
|
+
isActive: true,
|
|
440
|
+
stateMutex: {
|
|
441
|
+
getSnapshot: () => ({
|
|
442
|
+
state: 'idle',
|
|
443
|
+
backgroundTaskCount: 0,
|
|
444
|
+
teamMemberCount: 0,
|
|
445
|
+
}),
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
];
|
|
449
|
+
vi.mocked(globalSessionOrchestrator.getProjectPaths).mockReturnValue([
|
|
450
|
+
'/projects/my-app',
|
|
451
|
+
]);
|
|
452
|
+
vi.mocked(globalSessionOrchestrator.getProjectSessions).mockReturnValue(mockSessions);
|
|
453
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
454
|
+
return {
|
|
455
|
+
getWorktreesEffect: () => Effect.succeed([
|
|
456
|
+
{
|
|
457
|
+
path: '/projects/my-app/worktrees/feature-auth',
|
|
458
|
+
branch: 'feature/auth',
|
|
459
|
+
isMainWorktree: false,
|
|
460
|
+
hasSession: true,
|
|
461
|
+
},
|
|
462
|
+
]),
|
|
463
|
+
getGitRootPath: () => '/projects/my-app',
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
|
|
467
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
468
|
+
rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
|
|
469
|
+
await vi.waitFor(() => {
|
|
470
|
+
return lastFrame()?.includes('Active Sessions') ?? false;
|
|
471
|
+
}, { timeout: 3000 });
|
|
472
|
+
const frame = lastFrame();
|
|
473
|
+
expect(frame).toContain('#1');
|
|
474
|
+
expect(frame).toContain('#2');
|
|
475
|
+
});
|
|
476
|
+
it('should show no session suffix for single unnamed session', async () => {
|
|
477
|
+
const mockSession = {
|
|
478
|
+
id: 'session-single',
|
|
479
|
+
worktreePath: '/projects/my-app/worktrees/feature-auth',
|
|
480
|
+
sessionNumber: 1,
|
|
481
|
+
lastActivity: new Date(),
|
|
482
|
+
isActive: true,
|
|
483
|
+
stateMutex: {
|
|
484
|
+
getSnapshot: () => ({
|
|
485
|
+
state: 'busy',
|
|
486
|
+
backgroundTaskCount: 0,
|
|
487
|
+
teamMemberCount: 0,
|
|
488
|
+
}),
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
vi.mocked(globalSessionOrchestrator.getProjectPaths).mockReturnValue([
|
|
492
|
+
'/projects/my-app',
|
|
493
|
+
]);
|
|
494
|
+
vi.mocked(globalSessionOrchestrator.getProjectSessions).mockReturnValue([
|
|
495
|
+
mockSession,
|
|
496
|
+
]);
|
|
497
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
498
|
+
return {
|
|
499
|
+
getWorktreesEffect: () => Effect.succeed([
|
|
500
|
+
{
|
|
501
|
+
path: '/projects/my-app/worktrees/feature-auth',
|
|
502
|
+
branch: 'feature/auth',
|
|
503
|
+
isMainWorktree: false,
|
|
504
|
+
hasSession: true,
|
|
505
|
+
},
|
|
506
|
+
]),
|
|
507
|
+
getGitRootPath: () => '/projects/my-app',
|
|
508
|
+
};
|
|
509
|
+
});
|
|
510
|
+
const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
|
|
511
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
512
|
+
rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
|
|
513
|
+
await vi.waitFor(() => {
|
|
514
|
+
return lastFrame()?.includes('Active Sessions') ?? false;
|
|
515
|
+
}, { timeout: 3000 });
|
|
516
|
+
const frame = lastFrame();
|
|
517
|
+
expect(frame).toContain('my-app :: feature/auth');
|
|
518
|
+
expect(frame).not.toContain('feature/auth:');
|
|
519
|
+
expect(frame).not.toContain('feature/auth #');
|
|
520
|
+
});
|
|
521
|
+
it('should call onSessionAction when space is pressed on highlighted session', async () => {
|
|
522
|
+
const mockOnSessionAction = vi.fn();
|
|
523
|
+
const mockSession = {
|
|
524
|
+
id: 'session-action',
|
|
525
|
+
worktreePath: '/projects/my-app/worktrees/feature-auth',
|
|
526
|
+
sessionNumber: 1,
|
|
527
|
+
lastActivity: new Date(),
|
|
528
|
+
isActive: true,
|
|
529
|
+
stateMutex: {
|
|
530
|
+
getSnapshot: () => ({
|
|
531
|
+
state: 'busy',
|
|
532
|
+
backgroundTaskCount: 0,
|
|
533
|
+
teamMemberCount: 0,
|
|
534
|
+
}),
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
vi.mocked(globalSessionOrchestrator.getProjectPaths).mockReturnValue([
|
|
538
|
+
'/projects/my-app',
|
|
539
|
+
]);
|
|
540
|
+
vi.mocked(globalSessionOrchestrator.getProjectSessions).mockReturnValue([
|
|
541
|
+
mockSession,
|
|
542
|
+
]);
|
|
543
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
544
|
+
return {
|
|
545
|
+
getWorktreesEffect: () => Effect.succeed([
|
|
546
|
+
{
|
|
547
|
+
path: '/projects/my-app/worktrees/feature-auth',
|
|
548
|
+
branch: 'feature/auth',
|
|
549
|
+
isMainWorktree: false,
|
|
550
|
+
hasSession: true,
|
|
551
|
+
},
|
|
552
|
+
]),
|
|
553
|
+
getGitRootPath: () => '/projects/my-app',
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, onSessionAction: mockOnSessionAction, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
|
|
557
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
558
|
+
rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, onSessionAction: mockOnSessionAction, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
|
|
559
|
+
await vi.waitFor(() => {
|
|
560
|
+
return lastFrame()?.includes('Active Sessions') ?? false;
|
|
561
|
+
}, { timeout: 3000 });
|
|
562
|
+
// Enable the useInput handler (Dashboard guards on process.stdin.setRawMode)
|
|
563
|
+
const origSetRawMode = process.stdin.setRawMode;
|
|
564
|
+
process.stdin.setRawMode = vi.fn();
|
|
565
|
+
// Simulate highlighting a session item
|
|
566
|
+
expect(capturedHandlers.onHighlight).not.toBeNull();
|
|
567
|
+
capturedHandlers.onHighlight({
|
|
568
|
+
value: 'session-session-action',
|
|
569
|
+
label: 'my-app :: feature/auth',
|
|
570
|
+
});
|
|
571
|
+
// Force re-render to flush state update — new useInput handler captures updated closure
|
|
572
|
+
rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, onSessionAction: mockOnSessionAction, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
|
|
573
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
574
|
+
// Use the latest Dashboard handler (last in array — has updated highlightedSession state)
|
|
575
|
+
const latestHandler = capturedHandlers.inputHandlers[capturedHandlers.inputHandlers.length - 1];
|
|
576
|
+
expect(latestHandler).toBeDefined();
|
|
577
|
+
latestHandler(' ', makeKey());
|
|
578
|
+
expect(mockOnSessionAction).toHaveBeenCalledWith(expect.objectContaining({ id: 'session-action' }), expect.objectContaining({ path: '/projects/my-app' }));
|
|
579
|
+
process.stdin.setRawMode = origSetRawMode;
|
|
580
|
+
});
|
|
581
|
+
it('should not call onSessionAction when space is pressed with no highlighted session', async () => {
|
|
582
|
+
const mockOnSessionAction = vi.fn();
|
|
583
|
+
const mockSession = {
|
|
584
|
+
id: 'session-noop',
|
|
585
|
+
worktreePath: '/projects/my-app/worktrees/feature-auth',
|
|
586
|
+
sessionNumber: 1,
|
|
587
|
+
lastActivity: new Date(),
|
|
588
|
+
isActive: true,
|
|
589
|
+
stateMutex: {
|
|
590
|
+
getSnapshot: () => ({
|
|
591
|
+
state: 'busy',
|
|
592
|
+
backgroundTaskCount: 0,
|
|
593
|
+
teamMemberCount: 0,
|
|
594
|
+
}),
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
vi.mocked(globalSessionOrchestrator.getProjectPaths).mockReturnValue([
|
|
598
|
+
'/projects/my-app',
|
|
599
|
+
]);
|
|
600
|
+
vi.mocked(globalSessionOrchestrator.getProjectSessions).mockReturnValue([
|
|
601
|
+
mockSession,
|
|
602
|
+
]);
|
|
603
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
604
|
+
return {
|
|
605
|
+
getWorktreesEffect: () => Effect.succeed([
|
|
606
|
+
{
|
|
607
|
+
path: '/projects/my-app/worktrees/feature-auth',
|
|
608
|
+
branch: 'feature/auth',
|
|
609
|
+
isMainWorktree: false,
|
|
610
|
+
hasSession: true,
|
|
611
|
+
},
|
|
612
|
+
]),
|
|
613
|
+
getGitRootPath: () => '/projects/my-app',
|
|
614
|
+
};
|
|
615
|
+
});
|
|
616
|
+
const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, onSessionAction: mockOnSessionAction, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
|
|
617
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
618
|
+
rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, onSessionAction: mockOnSessionAction, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
|
|
619
|
+
await vi.waitFor(() => {
|
|
620
|
+
return lastFrame()?.includes('Active Sessions') ?? false;
|
|
621
|
+
}, { timeout: 3000 });
|
|
622
|
+
// Enable the useInput handler (Dashboard guards on process.stdin.setRawMode)
|
|
623
|
+
const origSetRawMode = process.stdin.setRawMode;
|
|
624
|
+
process.stdin.setRawMode = vi.fn();
|
|
625
|
+
// Do NOT call onHighlight — highlightedSession stays null
|
|
626
|
+
const dashboardHandler = capturedHandlers.inputHandlers.find(handler => handler !== capturedHandlers.inputHandlers[0]);
|
|
627
|
+
expect(dashboardHandler).toBeDefined();
|
|
628
|
+
dashboardHandler(' ', makeKey());
|
|
629
|
+
expect(mockOnSessionAction).not.toHaveBeenCalled();
|
|
630
|
+
process.stdin.setRawMode = origSetRawMode;
|
|
631
|
+
});
|
|
348
632
|
});
|
|
@@ -29,6 +29,7 @@ export declare function extractBranchParts(branchName: string): {
|
|
|
29
29
|
prefix?: string;
|
|
30
30
|
name: string;
|
|
31
31
|
};
|
|
32
|
+
export declare function displaySuffix(session: Session, multipleForWorktree: boolean): string;
|
|
32
33
|
/**
|
|
33
34
|
* Prepares session items for display.
|
|
34
35
|
* Supports multiple sessions per worktree.
|
|
@@ -122,7 +122,7 @@ function indexSessionsByWorktree(sessions) {
|
|
|
122
122
|
}
|
|
123
123
|
return { byWorktreePath, maxAccessAt };
|
|
124
124
|
}
|
|
125
|
-
function displaySuffix(session, multipleForWorktree) {
|
|
125
|
+
export function displaySuffix(session, multipleForWorktree) {
|
|
126
126
|
if (multipleForWorktree) {
|
|
127
127
|
return session.sessionName
|
|
128
128
|
? `: ${session.sessionName}`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.19",
|
|
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.19",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.19",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.19",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "4.1.19",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "4.1.19"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|