ccmanager 3.12.5 → 4.0.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/dist/components/App.js +137 -63
- package/dist/components/App.test.js +16 -30
- package/dist/components/ConfigureStatusHooks.js +1 -1
- package/dist/components/Dashboard.js +3 -3
- package/dist/components/Menu.d.ts +2 -2
- package/dist/components/Menu.js +66 -140
- package/dist/components/Menu.recent-projects.test.js +8 -8
- package/dist/components/Menu.test.js +17 -17
- package/dist/components/Session.js +3 -3
- package/dist/components/SessionActions.d.ts +9 -0
- package/dist/components/SessionActions.js +29 -0
- package/dist/components/SessionRename.d.ts +8 -0
- package/dist/components/SessionRename.js +18 -0
- package/dist/constants/statusIcons.d.ts +3 -0
- package/dist/constants/statusIcons.js +3 -0
- package/dist/services/globalSessionOrchestrator.test.js +11 -5
- package/dist/services/sessionManager.autoApproval.test.js +1 -4
- package/dist/services/sessionManager.d.ts +7 -7
- package/dist/services/sessionManager.effect.test.js +17 -16
- package/dist/services/sessionManager.js +43 -48
- package/dist/services/sessionManager.statePersistence.test.js +3 -6
- package/dist/services/sessionManager.test.js +21 -24
- package/dist/services/worktreeService.d.ts +1 -15
- package/dist/services/worktreeService.js +1 -39
- package/dist/services/worktreeService.sort.test.js +141 -303
- package/dist/types/index.d.ts +37 -6
- package/dist/utils/hookExecutor.js +2 -0
- package/dist/utils/hookExecutor.test.js +8 -0
- package/dist/utils/worktreeUtils.d.ts +12 -6
- package/dist/utils/worktreeUtils.js +116 -50
- package/dist/utils/worktreeUtils.test.js +9 -7
- package/package.json +6 -6
package/dist/components/App.js
CHANGED
|
@@ -12,6 +12,8 @@ import Configuration from './Configuration.js';
|
|
|
12
12
|
import PresetSelector from './PresetSelector.js';
|
|
13
13
|
import RemoteBranchSelector from './RemoteBranchSelector.js';
|
|
14
14
|
import LoadingSpinner from './LoadingSpinner.js';
|
|
15
|
+
import SessionRename from './SessionRename.js';
|
|
16
|
+
import SessionActions from './SessionActions.js';
|
|
15
17
|
import { globalSessionOrchestrator } from '../services/globalSessionOrchestrator.js';
|
|
16
18
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
17
19
|
import { worktreeNameGenerator, generateFallbackBranchName, } from '../services/worktreeNameGenerator.js';
|
|
@@ -31,6 +33,8 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
31
33
|
const [error, setError] = useState(null);
|
|
32
34
|
const [menuKey, setMenuKey] = useState(0); // Force menu refresh
|
|
33
35
|
const [selectedWorktree, setSelectedWorktree] = useState(null); // Store selected worktree for preset selection
|
|
36
|
+
const [renameTarget, setRenameTarget] = useState(null);
|
|
37
|
+
const [sessionActionsTarget, setSessionActionsTarget] = useState(null);
|
|
34
38
|
const [selectedProject, setSelectedProject] = useState(null); // Store selected project in multi-project mode
|
|
35
39
|
const [configScope, setConfigScope] = useState('global'); // Store config scope for configuration view
|
|
36
40
|
const [pendingMenuSessionLaunch, setPendingMenuSessionLaunch] = useState(null);
|
|
@@ -58,7 +62,6 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
58
62
|
const sessionEffect = devcontainerConfig
|
|
59
63
|
? sessionManager.createSessionWithDevcontainerEffect(worktreePath, devcontainerConfig, presetId, initialPrompt)
|
|
60
64
|
: sessionManager.createSessionWithPresetEffect(worktreePath, presetId, initialPrompt);
|
|
61
|
-
// Execute the Effect and handle both success and failure cases
|
|
62
65
|
const result = await Effect.runPromise(Effect.either(sessionEffect));
|
|
63
66
|
if (result._tag === 'Left') {
|
|
64
67
|
const errorMessage = formatErrorMessage(result.left);
|
|
@@ -97,23 +100,34 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
97
100
|
}, 10);
|
|
98
101
|
}, []);
|
|
99
102
|
const startSessionForWorktree = useCallback(async (worktree, options) => {
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
// If a specific session is provided, navigate to it directly
|
|
104
|
+
if (options?.session) {
|
|
105
|
+
navigateToSession(options.session);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Check if there are running sessions for this worktree.
|
|
109
|
+
// Navigate to the first one found (matches old getSession(path) behavior).
|
|
110
|
+
// Skip when forceNew is set (S key — always create new session).
|
|
111
|
+
if (!options?.forceNew) {
|
|
112
|
+
const wtSessions = sessionManager.getSessionsForWorktree(worktree.path);
|
|
113
|
+
if (wtSessions.length > 0 && wtSessions[0]) {
|
|
114
|
+
navigateToSession(wtSessions[0]);
|
|
112
115
|
return;
|
|
113
116
|
}
|
|
114
|
-
session = result.session;
|
|
115
117
|
}
|
|
116
|
-
|
|
118
|
+
if (!options?.presetId && configReader.getSelectPresetOnStart()) {
|
|
119
|
+
setSelectedWorktree(worktree);
|
|
120
|
+
navigateWithClear('preset-selector');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
setView(options?.presetId ? 'creating-session-preset' : 'creating-session');
|
|
124
|
+
const result = await createSessionWithEffect(worktree.path, options?.presetId, options?.initialPrompt);
|
|
125
|
+
if (!result.success) {
|
|
126
|
+
setError(result.errorMessage);
|
|
127
|
+
navigateWithClear('menu');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
navigateToSession(result.session);
|
|
117
131
|
}, [
|
|
118
132
|
sessionManager,
|
|
119
133
|
navigateWithClear,
|
|
@@ -228,54 +242,62 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
228
242
|
setView('new-worktree');
|
|
229
243
|
}
|
|
230
244
|
};
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
245
|
+
const handleMenuAction = async (action) => {
|
|
246
|
+
switch (action.type) {
|
|
247
|
+
case 'newWorktree':
|
|
248
|
+
navigateWithClear('new-worktree');
|
|
249
|
+
return;
|
|
250
|
+
case 'newSession':
|
|
251
|
+
await startSessionForWorktree({
|
|
252
|
+
path: action.worktreePath,
|
|
253
|
+
branch: '',
|
|
254
|
+
isMainWorktree: false,
|
|
255
|
+
hasSession: true,
|
|
256
|
+
}, { forceNew: true });
|
|
257
|
+
return;
|
|
258
|
+
case 'renameSession':
|
|
259
|
+
setRenameTarget({
|
|
260
|
+
id: action.session.id,
|
|
261
|
+
name: action.session.sessionName,
|
|
262
|
+
});
|
|
263
|
+
navigateWithClear('rename-session');
|
|
264
|
+
return;
|
|
265
|
+
case 'killSession':
|
|
266
|
+
sessionManager.destroySession(action.sessionId);
|
|
267
|
+
setMenuKey(prev => prev + 1);
|
|
268
|
+
return;
|
|
269
|
+
case 'sessionActions':
|
|
270
|
+
setSessionActionsTarget({
|
|
271
|
+
session: action.session,
|
|
272
|
+
worktreePath: action.worktreePath,
|
|
273
|
+
});
|
|
274
|
+
navigateWithClear('session-actions');
|
|
275
|
+
return;
|
|
276
|
+
case 'deleteWorktree':
|
|
277
|
+
navigateWithClear('delete-worktree');
|
|
278
|
+
return;
|
|
279
|
+
case 'mergeWorktree':
|
|
280
|
+
navigateWithClear('merge-worktree');
|
|
281
|
+
return;
|
|
282
|
+
case 'configuration':
|
|
283
|
+
setConfigScope(action.scope);
|
|
284
|
+
navigateWithClear('configuration');
|
|
285
|
+
return;
|
|
286
|
+
case 'exit':
|
|
287
|
+
if (multiProject && selectedProject) {
|
|
288
|
+
handleBackToProjectList();
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
globalSessionOrchestrator.destroyAllSessions();
|
|
292
|
+
exit();
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
case 'selectWorktree':
|
|
296
|
+
await startSessionForWorktree(action.worktree, {
|
|
297
|
+
session: action.session,
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
277
300
|
}
|
|
278
|
-
await startSessionForWorktree(worktree);
|
|
279
301
|
};
|
|
280
302
|
const handlePresetSelected = async (presetId) => {
|
|
281
303
|
if (!selectedWorktree)
|
|
@@ -430,6 +452,11 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
430
452
|
// Delete the worktrees sequentially using Effect
|
|
431
453
|
let hasError = false;
|
|
432
454
|
for (const path of worktreePaths) {
|
|
455
|
+
// Destroy any running sessions for this worktree
|
|
456
|
+
const wtSessions = sessionManager.getSessionsForWorktree(path);
|
|
457
|
+
for (const s of wtSessions) {
|
|
458
|
+
sessionManager.destroySession(s.id);
|
|
459
|
+
}
|
|
433
460
|
const result = await Effect.runPromise(Effect.either(worktreeService.deleteWorktreeEffect(path, { deleteBranch })));
|
|
434
461
|
if (result._tag === 'Left') {
|
|
435
462
|
// Handle error using pattern matching on _tag
|
|
@@ -495,7 +522,7 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
495
522
|
return (_jsx(Dashboard, { projectsDir: projectsDir, onSelectSession: handleSelectSessionFromDashboard, onSelectProject: handleSelectProject, error: error, onDismissError: () => setError(null), version: version }));
|
|
496
523
|
}
|
|
497
524
|
if (view === 'menu') {
|
|
498
|
-
return (_jsx(Menu, { sessionManager: sessionManager, worktreeService: worktreeService,
|
|
525
|
+
return (_jsx(Menu, { sessionManager: sessionManager, worktreeService: worktreeService, onMenuAction: handleMenuAction, onSelectRecentProject: handleSelectProject, error: error, onDismissError: () => setError(null), projectName: selectedProject?.name, multiProject: multiProject, version: version }, menuKey));
|
|
499
526
|
}
|
|
500
527
|
if (view === 'session' && activeSession) {
|
|
501
528
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(Session, { session: activeSession, sessionManager: sessionManager, onReturnToMenu: handleReturnToMenu }, activeSession.id) }));
|
|
@@ -530,6 +557,53 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
530
557
|
if (view === 'configuration') {
|
|
531
558
|
return (_jsx(Configuration, { scope: configScope, onComplete: handleReturnToMenu }));
|
|
532
559
|
}
|
|
560
|
+
if (view === 'rename-session' && renameTarget) {
|
|
561
|
+
return (_jsx(SessionRename, { currentName: renameTarget.name, onRename: name => {
|
|
562
|
+
const session = sessionManager.getSessionById(renameTarget.id);
|
|
563
|
+
if (session) {
|
|
564
|
+
session.sessionName = name;
|
|
565
|
+
}
|
|
566
|
+
setRenameTarget(null);
|
|
567
|
+
handleReturnToMenu();
|
|
568
|
+
}, onCancel: () => {
|
|
569
|
+
setRenameTarget(null);
|
|
570
|
+
handleReturnToMenu();
|
|
571
|
+
} }));
|
|
572
|
+
}
|
|
573
|
+
if (view === 'session-actions' && sessionActionsTarget) {
|
|
574
|
+
const { session: targetSession, worktreePath } = sessionActionsTarget;
|
|
575
|
+
const label = targetSession.sessionName
|
|
576
|
+
? `${worktreePath} : ${targetSession.sessionName}`
|
|
577
|
+
: `${worktreePath} #${targetSession.sessionNumber}`;
|
|
578
|
+
const handleSessionAction = async (action) => {
|
|
579
|
+
setSessionActionsTarget(null);
|
|
580
|
+
switch (action) {
|
|
581
|
+
case 'newSession':
|
|
582
|
+
await startSessionForWorktree({
|
|
583
|
+
path: worktreePath,
|
|
584
|
+
branch: '',
|
|
585
|
+
isMainWorktree: false,
|
|
586
|
+
hasSession: true,
|
|
587
|
+
}, { forceNew: true });
|
|
588
|
+
return;
|
|
589
|
+
case 'rename':
|
|
590
|
+
setRenameTarget({
|
|
591
|
+
id: targetSession.id,
|
|
592
|
+
name: targetSession.sessionName,
|
|
593
|
+
});
|
|
594
|
+
navigateWithClear('rename-session');
|
|
595
|
+
return;
|
|
596
|
+
case 'kill':
|
|
597
|
+
sessionManager.destroySession(targetSession.id);
|
|
598
|
+
handleReturnToMenu();
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
return (_jsx(SessionActions, { sessionLabel: label, onSelect: handleSessionAction, onCancel: () => {
|
|
603
|
+
setSessionActionsTarget(null);
|
|
604
|
+
handleReturnToMenu();
|
|
605
|
+
} }));
|
|
606
|
+
}
|
|
533
607
|
if (view === 'preset-selector') {
|
|
534
608
|
return (_jsx(PresetSelector, { onSelect: handlePresetSelected, onCancel: handlePresetSelectorCancel }));
|
|
535
609
|
}
|
|
@@ -16,8 +16,11 @@ const mockSession = {
|
|
|
16
16
|
class MockSessionManager {
|
|
17
17
|
on = vi.fn((_, __) => this);
|
|
18
18
|
off = vi.fn((_, __) => this);
|
|
19
|
-
|
|
19
|
+
getSessionById = vi.fn((_) => null);
|
|
20
|
+
getSessionsForWorktree = vi.fn((_) => []);
|
|
20
21
|
getAllSessions = vi.fn(() => []);
|
|
22
|
+
destroySession = vi.fn((_) => { });
|
|
23
|
+
cancelAutoApproval = vi.fn((_, __) => { });
|
|
21
24
|
createSessionWithPresetEffect = vi.fn((_, __) => Effect.succeed(mockSession));
|
|
22
25
|
createSessionWithDevcontainerEffect = vi.fn((_, __) => Effect.succeed(mockSession));
|
|
23
26
|
}
|
|
@@ -179,12 +182,7 @@ describe('App component loading state machine', () => {
|
|
|
179
182
|
const { lastFrame, unmount } = render(_jsx(App, { version: "test" }));
|
|
180
183
|
await waitForCondition(() => Boolean(menuProps));
|
|
181
184
|
const menu = menuProps;
|
|
182
|
-
const selectPromise = Promise.resolve(menu.
|
|
183
|
-
path: '',
|
|
184
|
-
branch: '',
|
|
185
|
-
isMainWorktree: false,
|
|
186
|
-
hasSession: false,
|
|
187
|
-
}));
|
|
185
|
+
const selectPromise = Promise.resolve(menu.onMenuAction({ type: 'newWorktree' }));
|
|
188
186
|
await waitForCondition(() => Boolean(newWorktreeProps));
|
|
189
187
|
const newWorktree = newWorktreeProps;
|
|
190
188
|
const createPromise = Promise.resolve(newWorktree.onComplete({
|
|
@@ -210,12 +208,7 @@ describe('App component loading state machine', () => {
|
|
|
210
208
|
expect(sessionManagers).toHaveLength(1);
|
|
211
209
|
const sessionManager = sessionManagers[0];
|
|
212
210
|
const menu = menuProps;
|
|
213
|
-
await Promise.resolve(menu.
|
|
214
|
-
path: '',
|
|
215
|
-
branch: '',
|
|
216
|
-
isMainWorktree: false,
|
|
217
|
-
hasSession: false,
|
|
218
|
-
}));
|
|
211
|
+
await Promise.resolve(menu.onMenuAction({ type: 'newWorktree' }));
|
|
219
212
|
await waitForCondition(() => Boolean(newWorktreeProps));
|
|
220
213
|
await Promise.resolve(newWorktreeProps.onComplete({
|
|
221
214
|
creationMode: 'prompt',
|
|
@@ -245,12 +238,7 @@ describe('App component loading state machine', () => {
|
|
|
245
238
|
const { unmount } = render(_jsx(App, { version: "test" }));
|
|
246
239
|
await waitForCondition(() => Boolean(menuProps));
|
|
247
240
|
const sessionManager = sessionManagers[0];
|
|
248
|
-
await Promise.resolve(menuProps.
|
|
249
|
-
path: '',
|
|
250
|
-
branch: '',
|
|
251
|
-
isMainWorktree: false,
|
|
252
|
-
hasSession: false,
|
|
253
|
-
}));
|
|
241
|
+
await Promise.resolve(menuProps.onMenuAction({ type: 'newWorktree' }));
|
|
254
242
|
await waitForCondition(() => Boolean(newWorktreeProps));
|
|
255
243
|
await Promise.resolve(newWorktreeProps.onComplete({
|
|
256
244
|
creationMode: 'prompt',
|
|
@@ -278,12 +266,7 @@ describe('App component loading state machine', () => {
|
|
|
278
266
|
const { lastFrame, unmount } = render(_jsx(App, { version: "test" }));
|
|
279
267
|
await waitForCondition(() => Boolean(menuProps));
|
|
280
268
|
const menu = menuProps;
|
|
281
|
-
const selectPromise = Promise.resolve(menu.
|
|
282
|
-
path: 'DELETE_WORKTREE',
|
|
283
|
-
branch: '',
|
|
284
|
-
isMainWorktree: false,
|
|
285
|
-
hasSession: false,
|
|
286
|
-
}));
|
|
269
|
+
const selectPromise = Promise.resolve(menu.onMenuAction({ type: 'deleteWorktree' }));
|
|
287
270
|
await waitForCondition(() => Boolean(deleteWorktreeProps));
|
|
288
271
|
const deleteWorktree = deleteWorktreeProps;
|
|
289
272
|
const deletePromise = Promise.resolve(deleteWorktree.onComplete(['/tmp/test'], true));
|
|
@@ -313,11 +296,14 @@ describe('App component loading state machine', () => {
|
|
|
313
296
|
catch: (error) => error,
|
|
314
297
|
}));
|
|
315
298
|
const menu = menuProps;
|
|
316
|
-
const selectPromise = Promise.resolve(menu.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
299
|
+
const selectPromise = Promise.resolve(menu.onMenuAction({
|
|
300
|
+
type: 'selectWorktree',
|
|
301
|
+
worktree: {
|
|
302
|
+
path: '/project/worktree',
|
|
303
|
+
branch: 'feature',
|
|
304
|
+
isMainWorktree: false,
|
|
305
|
+
hasSession: false,
|
|
306
|
+
},
|
|
321
307
|
}));
|
|
322
308
|
await flush();
|
|
323
309
|
expect(lastFrame()).toContain('Starting devcontainer (this may take a moment)...');
|
|
@@ -105,7 +105,7 @@ const ConfigureStatusHooks = ({ onComplete, }) => {
|
|
|
105
105
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "green", children: "\u2713 Configuration saved successfully!" }) }));
|
|
106
106
|
}
|
|
107
107
|
if (view === 'edit') {
|
|
108
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "green", children: ["Configure ", STATUS_LABELS[selectedStatus], " Hook"] }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Command to execute when status changes to", ' ', STATUS_LABELS[selectedStatus], ":"] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(TextInputWrapper, { value: currentCommand, onChange: setCurrentCommand, onSubmit: handleCommandSubmit, placeholder: "Enter command (e.g., notify-send 'Claude is idle')" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Enabled: ", currentEnabled ? '✓' : '✗', " (Press Tab to toggle)"] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Environment variables available: CCMANAGER_OLD_STATE, CCMANAGER_NEW_STATE," }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `CCMANAGER_WORKTREE_PATH, CCMANAGER_WORKTREE_BRANCH, CCMANAGER_SESSION_ID` }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to save, Tab to toggle enabled, Esc to cancel" }) })] }));
|
|
108
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "green", children: ["Configure ", STATUS_LABELS[selectedStatus], " Hook"] }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Command to execute when status changes to", ' ', STATUS_LABELS[selectedStatus], ":"] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(TextInputWrapper, { value: currentCommand, onChange: setCurrentCommand, onSubmit: handleCommandSubmit, placeholder: "Enter command (e.g., notify-send 'Claude is idle')" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Enabled: ", currentEnabled ? '✓' : '✗', " (Press Tab to toggle)"] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Environment variables available: CCMANAGER_OLD_STATE, CCMANAGER_NEW_STATE," }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `CCMANAGER_WORKTREE_PATH, CCMANAGER_WORKTREE_DIR, CCMANAGER_WORKTREE_BRANCH, CCMANAGER_SESSION_ID` }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to save, Tab to toggle enabled, Esc to cancel" }) })] }));
|
|
109
109
|
}
|
|
110
110
|
const scopeLabel = scope === 'project' ? 'Project' : 'Global';
|
|
111
111
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "green", children: ["Configure Status Hooks (", scopeLabel, ")"] }) }), isInheriting && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { backgroundColor: "cyan", color: "black", children: [' ', "\uD83D\uDCCB Inheriting from global configuration", ' '] }) })), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Set commands to run when session status changes:" }) }), _jsx(SelectInput, { items: getMenuItems(), onSelect: handleMenuSelect, isFocused: true, limit: 10 }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Esc to go back" }) })] }));
|
|
@@ -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,
|
|
15
|
+
import { truncateString, calculateColumnPositions, assembleSessionLabel, formatRelativeDate, } 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;
|
|
@@ -208,7 +208,7 @@ const Dashboard = ({ projectsDir, onSelectSession, onSelectProject, error, onDis
|
|
|
208
208
|
let currentIndex = 0;
|
|
209
209
|
// --- Active Sessions section ---
|
|
210
210
|
if (sessionEntries.length > 0) {
|
|
211
|
-
// Build
|
|
211
|
+
// Build SessionItems for column alignment
|
|
212
212
|
const sessionWorkItems = sessionEntries.map(entry => {
|
|
213
213
|
// Use enriched worktree if available (has git status)
|
|
214
214
|
const wt = enrichedWorktrees.find(w => w.path === entry.worktree.path) ||
|
|
@@ -278,7 +278,7 @@ const Dashboard = ({ projectsDir, onSelectSession, onSelectProject, error, onDis
|
|
|
278
278
|
filteredEntries.forEach(entry => {
|
|
279
279
|
const itemIndex = sessionEntries.indexOf(entry);
|
|
280
280
|
const workItem = sessionWorkItems[itemIndex];
|
|
281
|
-
const label =
|
|
281
|
+
const label = assembleSessionLabel(workItem, columns);
|
|
282
282
|
const numberPrefix = !isSearchMode && currentIndex < 10 ? `${currentIndex} ❯ ` : '❯ ';
|
|
283
283
|
const project = {
|
|
284
284
|
path: entry.projectPath,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { GitProject, MenuAction } from '../types/index.js';
|
|
3
3
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
4
4
|
import { SessionManager } from '../services/sessionManager.js';
|
|
5
5
|
interface MenuProps {
|
|
6
6
|
sessionManager: SessionManager;
|
|
7
7
|
worktreeService: WorktreeService;
|
|
8
|
-
|
|
8
|
+
onMenuAction: (action: MenuAction) => void;
|
|
9
9
|
onSelectRecentProject?: (project: GitProject) => void;
|
|
10
10
|
error?: string | null;
|
|
11
11
|
onDismissError?: () => void;
|