ccmanager 0.0.1

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.
Files changed (40) hide show
  1. package/README.md +85 -0
  2. package/dist/app.d.ts +6 -0
  3. package/dist/app.js +57 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +24 -0
  6. package/dist/components/App.d.ts +3 -0
  7. package/dist/components/App.js +228 -0
  8. package/dist/components/ConfigureShortcuts.d.ts +6 -0
  9. package/dist/components/ConfigureShortcuts.js +139 -0
  10. package/dist/components/Confirmation.d.ts +12 -0
  11. package/dist/components/Confirmation.js +42 -0
  12. package/dist/components/DeleteWorktree.d.ts +7 -0
  13. package/dist/components/DeleteWorktree.js +116 -0
  14. package/dist/components/Menu.d.ts +9 -0
  15. package/dist/components/Menu.js +154 -0
  16. package/dist/components/MergeWorktree.d.ts +7 -0
  17. package/dist/components/MergeWorktree.js +142 -0
  18. package/dist/components/NewWorktree.d.ts +7 -0
  19. package/dist/components/NewWorktree.js +49 -0
  20. package/dist/components/Session.d.ts +10 -0
  21. package/dist/components/Session.js +121 -0
  22. package/dist/constants/statusIcons.d.ts +18 -0
  23. package/dist/constants/statusIcons.js +27 -0
  24. package/dist/services/sessionManager.d.ts +16 -0
  25. package/dist/services/sessionManager.js +190 -0
  26. package/dist/services/sessionManager.test.d.ts +1 -0
  27. package/dist/services/sessionManager.test.js +99 -0
  28. package/dist/services/shortcutManager.d.ts +17 -0
  29. package/dist/services/shortcutManager.js +167 -0
  30. package/dist/services/worktreeService.d.ts +24 -0
  31. package/dist/services/worktreeService.js +220 -0
  32. package/dist/types/index.d.ts +36 -0
  33. package/dist/types/index.js +4 -0
  34. package/dist/utils/logger.d.ts +14 -0
  35. package/dist/utils/logger.js +21 -0
  36. package/dist/utils/promptDetector.d.ts +1 -0
  37. package/dist/utils/promptDetector.js +20 -0
  38. package/dist/utils/promptDetector.test.d.ts +1 -0
  39. package/dist/utils/promptDetector.test.js +81 -0
  40. package/package.json +70 -0
@@ -0,0 +1,116 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { WorktreeService } from '../services/worktreeService.js';
4
+ import Confirmation from './Confirmation.js';
5
+ import { shortcutManager } from '../services/shortcutManager.js';
6
+ const DeleteWorktree = ({ onComplete, onCancel, }) => {
7
+ const [worktrees, setWorktrees] = useState([]);
8
+ const [selectedIndices, setSelectedIndices] = useState(new Set());
9
+ const [focusedIndex, setFocusedIndex] = useState(0);
10
+ const [confirmMode, setConfirmMode] = useState(false);
11
+ useEffect(() => {
12
+ const worktreeService = new WorktreeService();
13
+ const allWorktrees = worktreeService.getWorktrees();
14
+ // Filter out main worktree - we shouldn't delete it
15
+ const deletableWorktrees = allWorktrees.filter(wt => !wt.isMainWorktree);
16
+ setWorktrees(deletableWorktrees);
17
+ }, []);
18
+ useInput((input, key) => {
19
+ if (key.ctrl && input === 'c') {
20
+ onCancel();
21
+ return;
22
+ }
23
+ if (confirmMode) {
24
+ // Confirmation component handles input
25
+ return;
26
+ }
27
+ if (key.upArrow) {
28
+ setFocusedIndex(prev => Math.max(0, prev - 1));
29
+ }
30
+ else if (key.downArrow) {
31
+ setFocusedIndex(prev => Math.min(worktrees.length - 1, prev + 1));
32
+ }
33
+ else if (input === ' ') {
34
+ // Toggle selection
35
+ setSelectedIndices(prev => {
36
+ const newSet = new Set(prev);
37
+ if (newSet.has(focusedIndex)) {
38
+ newSet.delete(focusedIndex);
39
+ }
40
+ else {
41
+ newSet.add(focusedIndex);
42
+ }
43
+ return newSet;
44
+ });
45
+ }
46
+ else if (key.return) {
47
+ if (selectedIndices.size > 0) {
48
+ setConfirmMode(true);
49
+ }
50
+ }
51
+ else if (shortcutManager.matchesShortcut('cancel', input, key)) {
52
+ onCancel();
53
+ }
54
+ });
55
+ if (worktrees.length === 0) {
56
+ return (React.createElement(Box, { flexDirection: "column" },
57
+ React.createElement(Text, { color: "yellow" }, "No worktrees available to delete."),
58
+ React.createElement(Text, { dimColor: true },
59
+ "Press ",
60
+ shortcutManager.getShortcutDisplay('cancel'),
61
+ " to return to menu")));
62
+ }
63
+ if (confirmMode) {
64
+ const selectedWorktrees = Array.from(selectedIndices).map(index => worktrees[index]);
65
+ const handleConfirm = () => {
66
+ const selectedPaths = Array.from(selectedIndices).map(index => worktrees[index].path);
67
+ onComplete(selectedPaths);
68
+ };
69
+ const handleCancel = () => {
70
+ setConfirmMode(false);
71
+ };
72
+ const confirmMessage = (React.createElement(Box, { flexDirection: "column" },
73
+ React.createElement(Text, { bold: true, color: "red" }, "\u26A0\uFE0F Delete Confirmation"),
74
+ React.createElement(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column" },
75
+ React.createElement(Text, null, "You are about to delete the following worktrees:"),
76
+ selectedWorktrees.map(wt => (React.createElement(Text, { key: wt.path, color: "red" },
77
+ "\u2022 ",
78
+ wt.branch.replace('refs/heads/', ''),
79
+ " (",
80
+ wt.path,
81
+ ")")))),
82
+ React.createElement(Text, { bold: true }, "This will also delete their branches. Are you sure?")));
83
+ return (React.createElement(Confirmation, { message: confirmMessage, onConfirm: handleConfirm, onCancel: handleCancel }));
84
+ }
85
+ return (React.createElement(Box, { flexDirection: "column" },
86
+ React.createElement(Box, { marginBottom: 1 },
87
+ React.createElement(Text, { bold: true, color: "red" }, "Delete Worktrees")),
88
+ React.createElement(Box, { marginBottom: 1 },
89
+ React.createElement(Text, { dimColor: true }, "Select worktrees to delete (Space to select, Enter to confirm):")),
90
+ worktrees.map((worktree, index) => {
91
+ const isSelected = selectedIndices.has(index);
92
+ const isFocused = index === focusedIndex;
93
+ const branchName = worktree.branch.replace('refs/heads/', '');
94
+ return (React.createElement(Box, { key: worktree.path },
95
+ React.createElement(Text, { color: isFocused ? 'green' : undefined, inverse: isFocused, dimColor: !isFocused && !isSelected },
96
+ isSelected ? '[✓]' : '[ ]',
97
+ " ",
98
+ branchName,
99
+ " (",
100
+ worktree.path,
101
+ ")")));
102
+ }),
103
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" },
104
+ React.createElement(Text, { dimColor: true },
105
+ "Controls: \u2191\u2193 Navigate, Space Select, Enter Confirm,",
106
+ ' ',
107
+ shortcutManager.getShortcutDisplay('cancel'),
108
+ " Cancel"),
109
+ selectedIndices.size > 0 && (React.createElement(Text, { color: "yellow" },
110
+ selectedIndices.size,
111
+ " worktree",
112
+ selectedIndices.size > 1 ? 's' : '',
113
+ ' ',
114
+ "selected")))));
115
+ };
116
+ export default DeleteWorktree;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { Worktree } from '../types/index.js';
3
+ import { SessionManager } from '../services/sessionManager.js';
4
+ interface MenuProps {
5
+ sessionManager: SessionManager;
6
+ onSelectWorktree: (worktree: Worktree) => void;
7
+ }
8
+ declare const Menu: React.FC<MenuProps>;
9
+ export default Menu;
@@ -0,0 +1,154 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import SelectInput from 'ink-select-input';
4
+ import { WorktreeService } from '../services/worktreeService.js';
5
+ import { STATUS_ICONS, STATUS_LABELS, MENU_ICONS, getStatusDisplay, } from '../constants/statusIcons.js';
6
+ const Menu = ({ sessionManager, onSelectWorktree }) => {
7
+ const [worktrees, setWorktrees] = useState([]);
8
+ const [sessions, setSessions] = useState([]);
9
+ const [items, setItems] = useState([]);
10
+ useEffect(() => {
11
+ // Load worktrees
12
+ const worktreeService = new WorktreeService();
13
+ const loadedWorktrees = worktreeService.getWorktrees();
14
+ setWorktrees(loadedWorktrees);
15
+ // Update sessions
16
+ const updateSessions = () => {
17
+ const allSessions = sessionManager.getAllSessions();
18
+ setSessions(allSessions);
19
+ // Update worktree session status
20
+ loadedWorktrees.forEach(wt => {
21
+ wt.hasSession = allSessions.some(s => s.worktreePath === wt.path);
22
+ });
23
+ };
24
+ updateSessions();
25
+ // Listen for session changes
26
+ const handleSessionChange = () => updateSessions();
27
+ sessionManager.on('sessionCreated', handleSessionChange);
28
+ sessionManager.on('sessionDestroyed', handleSessionChange);
29
+ sessionManager.on('sessionStateChanged', handleSessionChange);
30
+ return () => {
31
+ sessionManager.off('sessionCreated', handleSessionChange);
32
+ sessionManager.off('sessionDestroyed', handleSessionChange);
33
+ sessionManager.off('sessionStateChanged', handleSessionChange);
34
+ };
35
+ }, [sessionManager]);
36
+ useEffect(() => {
37
+ // Build menu items
38
+ const menuItems = worktrees.map(wt => {
39
+ const session = sessions.find(s => s.worktreePath === wt.path);
40
+ let status = '';
41
+ if (session) {
42
+ status = ` [${getStatusDisplay(session.state)}]`;
43
+ }
44
+ const branchName = wt.branch.replace('refs/heads/', '');
45
+ const isMain = wt.isMainWorktree ? ' (main)' : '';
46
+ return {
47
+ label: `${branchName}${isMain}${status}`,
48
+ value: wt.path,
49
+ worktree: wt,
50
+ };
51
+ });
52
+ // Add menu options
53
+ menuItems.push({
54
+ label: '─────────────',
55
+ value: 'separator',
56
+ });
57
+ menuItems.push({
58
+ label: `${MENU_ICONS.NEW_WORKTREE} New Worktree`,
59
+ value: 'new-worktree',
60
+ });
61
+ menuItems.push({
62
+ label: `${MENU_ICONS.MERGE_WORKTREE} Merge Worktree`,
63
+ value: 'merge-worktree',
64
+ });
65
+ menuItems.push({
66
+ label: `${MENU_ICONS.DELETE_WORKTREE} Delete Worktree`,
67
+ value: 'delete-worktree',
68
+ });
69
+ menuItems.push({
70
+ label: `${MENU_ICONS.CONFIGURE_SHORTCUTS} Configure Shortcuts`,
71
+ value: 'configure-shortcuts',
72
+ });
73
+ menuItems.push({
74
+ label: `${MENU_ICONS.EXIT} Exit`,
75
+ value: 'exit',
76
+ });
77
+ setItems(menuItems);
78
+ }, [worktrees, sessions]);
79
+ const handleSelect = (item) => {
80
+ if (item.value === 'separator') {
81
+ // Do nothing for separator
82
+ }
83
+ else if (item.value === 'new-worktree') {
84
+ // Handle in parent component
85
+ onSelectWorktree({
86
+ path: '',
87
+ branch: '',
88
+ isMainWorktree: false,
89
+ hasSession: false,
90
+ });
91
+ }
92
+ else if (item.value === 'merge-worktree') {
93
+ // Handle in parent component - use special marker
94
+ onSelectWorktree({
95
+ path: 'MERGE_WORKTREE',
96
+ branch: '',
97
+ isMainWorktree: false,
98
+ hasSession: false,
99
+ });
100
+ }
101
+ else if (item.value === 'delete-worktree') {
102
+ // Handle in parent component - use special marker
103
+ onSelectWorktree({
104
+ path: 'DELETE_WORKTREE',
105
+ branch: '',
106
+ isMainWorktree: false,
107
+ hasSession: false,
108
+ });
109
+ }
110
+ else if (item.value === 'configure-shortcuts') {
111
+ // Handle in parent component - use special marker
112
+ onSelectWorktree({
113
+ path: 'CONFIGURE_SHORTCUTS',
114
+ branch: '',
115
+ isMainWorktree: false,
116
+ hasSession: false,
117
+ });
118
+ }
119
+ else if (item.value === 'exit') {
120
+ // Handle in parent component - use special marker
121
+ onSelectWorktree({
122
+ path: 'EXIT_APPLICATION',
123
+ branch: '',
124
+ isMainWorktree: false,
125
+ hasSession: false,
126
+ });
127
+ }
128
+ else if (item.worktree) {
129
+ onSelectWorktree(item.worktree);
130
+ }
131
+ };
132
+ return (React.createElement(Box, { flexDirection: "column" },
133
+ React.createElement(Box, { marginBottom: 1 },
134
+ React.createElement(Text, { bold: true, color: "green" }, "CCManager - Claude Code Worktree Manager")),
135
+ React.createElement(Box, { marginBottom: 1 },
136
+ React.createElement(Text, { dimColor: true }, "Select a worktree to start or resume a Claude Code session:")),
137
+ React.createElement(SelectInput, { items: items, onSelect: handleSelect, isFocused: true }),
138
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" },
139
+ React.createElement(Text, { dimColor: true },
140
+ "Status: ",
141
+ STATUS_ICONS.BUSY,
142
+ " ",
143
+ STATUS_LABELS.BUSY,
144
+ ' ',
145
+ STATUS_ICONS.WAITING,
146
+ " ",
147
+ STATUS_LABELS.WAITING,
148
+ " ",
149
+ STATUS_ICONS.IDLE,
150
+ ' ',
151
+ STATUS_LABELS.IDLE),
152
+ React.createElement(Text, { dimColor: true }, "Controls: \u2191\u2193 Navigate Enter Select"))));
153
+ };
154
+ export default Menu;
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ interface MergeWorktreeProps {
3
+ onComplete: (sourceBranch: string, targetBranch: string, deleteAfterMerge: boolean, useRebase: boolean) => void;
4
+ onCancel: () => void;
5
+ }
6
+ declare const MergeWorktree: React.FC<MergeWorktreeProps>;
7
+ export default MergeWorktree;
@@ -0,0 +1,142 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import SelectInput from 'ink-select-input';
4
+ import { WorktreeService } from '../services/worktreeService.js';
5
+ import Confirmation from './Confirmation.js';
6
+ import { shortcutManager } from '../services/shortcutManager.js';
7
+ const MergeWorktree = ({ onComplete, onCancel, }) => {
8
+ const [step, setStep] = useState('select-source');
9
+ const [sourceBranch, setSourceBranch] = useState('');
10
+ const [targetBranch, setTargetBranch] = useState('');
11
+ const [branchItems, setBranchItems] = useState([]);
12
+ const [useRebase, setUseRebase] = useState(false);
13
+ const [operationFocused, setOperationFocused] = useState(false);
14
+ useEffect(() => {
15
+ const worktreeService = new WorktreeService();
16
+ const loadedWorktrees = worktreeService.getWorktrees();
17
+ // Create branch items for selection
18
+ const items = loadedWorktrees.map(wt => ({
19
+ label: wt.branch.replace('refs/heads/', '') +
20
+ (wt.isMainWorktree ? ' (main)' : ''),
21
+ value: wt.branch.replace('refs/heads/', ''),
22
+ }));
23
+ setBranchItems(items);
24
+ }, []);
25
+ useInput((input, key) => {
26
+ if (shortcutManager.matchesShortcut('cancel', input, key)) {
27
+ onCancel();
28
+ return;
29
+ }
30
+ if (step === 'select-operation') {
31
+ if (key.leftArrow || key.rightArrow) {
32
+ const newOperationFocused = !operationFocused;
33
+ setOperationFocused(newOperationFocused);
34
+ setUseRebase(newOperationFocused);
35
+ }
36
+ else if (key.return) {
37
+ setStep('confirm-merge');
38
+ }
39
+ }
40
+ });
41
+ const handleSelectSource = (item) => {
42
+ setSourceBranch(item.value);
43
+ // Filter out the selected source branch for target selection
44
+ const filteredItems = branchItems.filter(b => b.value !== item.value);
45
+ setBranchItems(filteredItems);
46
+ setStep('select-target');
47
+ };
48
+ const handleSelectTarget = (item) => {
49
+ setTargetBranch(item.value);
50
+ setStep('select-operation');
51
+ };
52
+ if (step === 'select-source') {
53
+ return (React.createElement(Box, { flexDirection: "column" },
54
+ React.createElement(Box, { marginBottom: 1 },
55
+ React.createElement(Text, { bold: true, color: "green" }, "Merge Worktree")),
56
+ React.createElement(Box, { marginBottom: 1 },
57
+ React.createElement(Text, null, "Select the source branch to merge:")),
58
+ React.createElement(SelectInput, { items: branchItems, onSelect: handleSelectSource, isFocused: true }),
59
+ React.createElement(Box, { marginTop: 1 },
60
+ React.createElement(Text, { dimColor: true },
61
+ "Press ",
62
+ shortcutManager.getShortcutDisplay('cancel'),
63
+ " to cancel"))));
64
+ }
65
+ if (step === 'select-target') {
66
+ return (React.createElement(Box, { flexDirection: "column" },
67
+ React.createElement(Box, { marginBottom: 1 },
68
+ React.createElement(Text, { bold: true, color: "green" }, "Merge Worktree")),
69
+ React.createElement(Box, { marginBottom: 1 },
70
+ React.createElement(Text, null,
71
+ "Merging from: ",
72
+ React.createElement(Text, { color: "yellow" }, sourceBranch))),
73
+ React.createElement(Box, { marginBottom: 1 },
74
+ React.createElement(Text, null, "Select the target branch to merge into:")),
75
+ React.createElement(SelectInput, { items: branchItems, onSelect: handleSelectTarget, isFocused: true }),
76
+ React.createElement(Box, { marginTop: 1 },
77
+ React.createElement(Text, { dimColor: true },
78
+ "Press ",
79
+ shortcutManager.getShortcutDisplay('cancel'),
80
+ " to cancel"))));
81
+ }
82
+ if (step === 'select-operation') {
83
+ return (React.createElement(Box, { flexDirection: "column" },
84
+ React.createElement(Box, { marginBottom: 1 },
85
+ React.createElement(Text, { bold: true, color: "green" }, "Select Operation")),
86
+ React.createElement(Box, { marginBottom: 1 },
87
+ React.createElement(Text, null,
88
+ "Choose how to integrate ",
89
+ React.createElement(Text, { color: "yellow" }, sourceBranch),
90
+ ' ',
91
+ "into ",
92
+ React.createElement(Text, { color: "yellow" }, targetBranch),
93
+ ":")),
94
+ React.createElement(Box, null,
95
+ React.createElement(Box, { marginRight: 2 },
96
+ React.createElement(Text, { color: !operationFocused ? 'green' : 'white', inverse: !operationFocused },
97
+ ' ',
98
+ "Merge",
99
+ ' ')),
100
+ React.createElement(Box, null,
101
+ React.createElement(Text, { color: operationFocused ? 'blue' : 'white', inverse: operationFocused },
102
+ ' ',
103
+ "Rebase",
104
+ ' '))),
105
+ React.createElement(Box, { marginTop: 1 },
106
+ React.createElement(Text, { dimColor: true },
107
+ "Use \u2190 \u2192 to navigate, Enter to select,",
108
+ ' ',
109
+ shortcutManager.getShortcutDisplay('cancel'),
110
+ " to cancel"))));
111
+ }
112
+ if (step === 'confirm-merge') {
113
+ const confirmMessage = (React.createElement(Box, { flexDirection: "column" },
114
+ React.createElement(Box, { marginBottom: 1 },
115
+ React.createElement(Text, { bold: true, color: "green" },
116
+ "Confirm ",
117
+ useRebase ? 'Rebase' : 'Merge')),
118
+ React.createElement(Text, null,
119
+ useRebase ? 'Rebase' : 'Merge',
120
+ ' ',
121
+ React.createElement(Text, { color: "yellow" }, sourceBranch),
122
+ ' ',
123
+ useRebase ? 'onto' : 'into',
124
+ ' ',
125
+ React.createElement(Text, { color: "yellow" }, targetBranch),
126
+ "?")));
127
+ return (React.createElement(Confirmation, { message: confirmMessage, onConfirm: () => setStep('delete-confirm'), onCancel: onCancel }));
128
+ }
129
+ if (step === 'delete-confirm') {
130
+ const deleteMessage = (React.createElement(Box, { flexDirection: "column" },
131
+ React.createElement(Box, { marginBottom: 1 },
132
+ React.createElement(Text, { bold: true, color: "green" }, "Delete Source Branch?")),
133
+ React.createElement(Text, null,
134
+ "Delete the merged branch ",
135
+ React.createElement(Text, { color: "yellow" }, sourceBranch),
136
+ ' ',
137
+ "and its worktree?")));
138
+ return (React.createElement(Confirmation, { message: deleteMessage, onConfirm: () => onComplete(sourceBranch, targetBranch, true, useRebase), onCancel: () => onComplete(sourceBranch, targetBranch, false, useRebase) }));
139
+ }
140
+ return null;
141
+ };
142
+ export default MergeWorktree;
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ interface NewWorktreeProps {
3
+ onComplete: (path: string, branch: string) => void;
4
+ onCancel: () => void;
5
+ }
6
+ declare const NewWorktree: React.FC<NewWorktreeProps>;
7
+ export default NewWorktree;
@@ -0,0 +1,49 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import { shortcutManager } from '../services/shortcutManager.js';
5
+ const NewWorktree = ({ onComplete, onCancel }) => {
6
+ const [step, setStep] = useState('path');
7
+ const [path, setPath] = useState('');
8
+ const [branch, setBranch] = useState('');
9
+ useInput((input, key) => {
10
+ if (shortcutManager.matchesShortcut('cancel', input, key)) {
11
+ onCancel();
12
+ }
13
+ });
14
+ const handlePathSubmit = (value) => {
15
+ if (value.trim()) {
16
+ setPath(value.trim());
17
+ setStep('branch');
18
+ }
19
+ };
20
+ const handleBranchSubmit = (value) => {
21
+ if (value.trim()) {
22
+ setBranch(value.trim());
23
+ onComplete(path, value.trim());
24
+ }
25
+ };
26
+ return (React.createElement(Box, { flexDirection: "column" },
27
+ React.createElement(Box, { marginBottom: 1 },
28
+ React.createElement(Text, { bold: true, color: "green" }, "Create New Worktree")),
29
+ step === 'path' ? (React.createElement(Box, { flexDirection: "column" },
30
+ React.createElement(Box, { marginBottom: 1 },
31
+ React.createElement(Text, null, "Enter worktree path (relative to repository root):")),
32
+ React.createElement(Box, null,
33
+ React.createElement(Text, { color: "cyan" }, '> '),
34
+ React.createElement(TextInput, { value: path, onChange: setPath, onSubmit: handlePathSubmit, placeholder: "e.g., ../myproject-feature" })))) : (React.createElement(Box, { flexDirection: "column" },
35
+ React.createElement(Box, { marginBottom: 1 },
36
+ React.createElement(Text, null,
37
+ "Enter branch name for worktree at ",
38
+ React.createElement(Text, { color: "cyan" }, path),
39
+ ":")),
40
+ React.createElement(Box, null,
41
+ React.createElement(Text, { color: "cyan" }, '> '),
42
+ React.createElement(TextInput, { value: branch, onChange: setBranch, onSubmit: handleBranchSubmit, placeholder: "e.g., feature/new-feature" })))),
43
+ React.createElement(Box, { marginTop: 1 },
44
+ React.createElement(Text, { dimColor: true },
45
+ "Press ",
46
+ shortcutManager.getShortcutDisplay('cancel'),
47
+ " to cancel"))));
48
+ };
49
+ export default NewWorktree;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { Session as SessionType } from '../types/index.js';
3
+ import { SessionManager } from '../services/sessionManager.js';
4
+ interface SessionProps {
5
+ session: SessionType;
6
+ sessionManager: SessionManager;
7
+ onReturnToMenu: () => void;
8
+ }
9
+ declare const Session: React.FC<SessionProps>;
10
+ export default Session;
@@ -0,0 +1,121 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useStdout } from 'ink';
3
+ import { shortcutManager } from '../services/shortcutManager.js';
4
+ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
5
+ const { stdout } = useStdout();
6
+ const [isExiting, setIsExiting] = useState(false);
7
+ useEffect(() => {
8
+ if (!stdout)
9
+ return;
10
+ // Clear screen when entering session
11
+ stdout.write('\x1B[2J\x1B[H');
12
+ // Handle session restoration
13
+ const handleSessionRestore = (restoredSession) => {
14
+ if (restoredSession.id === session.id) {
15
+ // Replay all buffered output, but skip the initial clear if present
16
+ for (let i = 0; i < restoredSession.outputHistory.length; i++) {
17
+ const buffer = restoredSession.outputHistory[i];
18
+ if (!buffer)
19
+ continue;
20
+ const str = buffer.toString('utf8');
21
+ // Skip clear screen sequences at the beginning
22
+ if (i === 0 && (str.includes('\x1B[2J') || str.includes('\x1B[H'))) {
23
+ // Skip this buffer or remove the clear sequence
24
+ const cleaned = str
25
+ .replace(/\x1B\[2J/g, '')
26
+ .replace(/\x1B\[H/g, '');
27
+ if (cleaned.length > 0) {
28
+ stdout.write(Buffer.from(cleaned, 'utf8'));
29
+ }
30
+ }
31
+ else {
32
+ stdout.write(buffer);
33
+ }
34
+ }
35
+ }
36
+ };
37
+ // Listen for restore event first
38
+ sessionManager.on('sessionRestore', handleSessionRestore);
39
+ // Mark session as active (this will trigger the restore event)
40
+ sessionManager.setSessionActive(session.worktreePath, true);
41
+ // Listen for session data events
42
+ const handleSessionData = (activeSession, data) => {
43
+ // Only handle data for our session
44
+ if (activeSession.id === session.id && !isExiting) {
45
+ stdout.write(data);
46
+ }
47
+ };
48
+ const handleSessionExit = (exitedSession) => {
49
+ if (exitedSession.id === session.id) {
50
+ setIsExiting(true);
51
+ // Don't call onReturnToMenu here - App component handles it
52
+ }
53
+ };
54
+ sessionManager.on('sessionData', handleSessionData);
55
+ sessionManager.on('sessionExit', handleSessionExit);
56
+ // Handle terminal resize
57
+ const handleResize = () => {
58
+ session.process.resize(process.stdout.columns || 80, process.stdout.rows || 24);
59
+ };
60
+ stdout.on('resize', handleResize);
61
+ // Set up raw input handling
62
+ const stdin = process.stdin;
63
+ // Store original stdin state
64
+ const originalIsRaw = stdin.isRaw;
65
+ const originalIsPaused = stdin.isPaused();
66
+ // Configure stdin for PTY passthrough
67
+ stdin.setRawMode(true);
68
+ stdin.resume();
69
+ stdin.setEncoding('utf8');
70
+ const handleStdinData = (data) => {
71
+ if (isExiting)
72
+ return;
73
+ // Check for return to menu shortcut
74
+ const returnToMenuShortcut = shortcutManager.getShortcuts().returnToMenu;
75
+ const shortcutCode = shortcutManager.getShortcutCode(returnToMenuShortcut);
76
+ if (shortcutCode && data === shortcutCode) {
77
+ // Disable focus reporting mode before returning to menu
78
+ if (stdout) {
79
+ stdout.write('\x1b[?1004l');
80
+ }
81
+ // Restore stdin state before returning to menu
82
+ stdin.removeListener('data', handleStdinData);
83
+ stdin.setRawMode(false);
84
+ stdin.pause();
85
+ onReturnToMenu();
86
+ return;
87
+ }
88
+ // Pass all other input directly to the PTY
89
+ session.process.write(data);
90
+ };
91
+ stdin.on('data', handleStdinData);
92
+ return () => {
93
+ // Remove listener first to prevent any race conditions
94
+ stdin.removeListener('data', handleStdinData);
95
+ // Disable focus reporting mode that might have been enabled by the PTY
96
+ if (stdout) {
97
+ stdout.write('\x1b[?1004l');
98
+ }
99
+ // Restore stdin to its original state
100
+ if (stdin.isTTY) {
101
+ stdin.setRawMode(originalIsRaw || false);
102
+ if (originalIsPaused) {
103
+ stdin.pause();
104
+ }
105
+ else {
106
+ stdin.resume();
107
+ }
108
+ }
109
+ // Mark session as inactive
110
+ sessionManager.setSessionActive(session.worktreePath, false);
111
+ // Remove event listeners
112
+ sessionManager.off('sessionRestore', handleSessionRestore);
113
+ sessionManager.off('sessionData', handleSessionData);
114
+ sessionManager.off('sessionExit', handleSessionExit);
115
+ stdout.off('resize', handleResize);
116
+ };
117
+ }, [session, sessionManager, stdout, onReturnToMenu, isExiting]);
118
+ // Return null to render nothing (PTY output goes directly to stdout)
119
+ return null;
120
+ };
121
+ export default Session;
@@ -0,0 +1,18 @@
1
+ export declare const STATUS_ICONS: {
2
+ readonly BUSY: "●";
3
+ readonly WAITING: "◐";
4
+ readonly IDLE: "○";
5
+ };
6
+ export declare const STATUS_LABELS: {
7
+ readonly BUSY: "Busy";
8
+ readonly WAITING: "Waiting";
9
+ readonly IDLE: "Idle";
10
+ };
11
+ export declare const MENU_ICONS: {
12
+ readonly NEW_WORKTREE: "⊕";
13
+ readonly MERGE_WORKTREE: "⇄";
14
+ readonly DELETE_WORKTREE: "✕";
15
+ readonly CONFIGURE_SHORTCUTS: "⌨";
16
+ readonly EXIT: "⏻";
17
+ };
18
+ export declare const getStatusDisplay: (status: "busy" | "waiting_input" | "idle") => string;
@@ -0,0 +1,27 @@
1
+ export const STATUS_ICONS = {
2
+ BUSY: '●',
3
+ WAITING: '◐',
4
+ IDLE: '○',
5
+ };
6
+ export const STATUS_LABELS = {
7
+ BUSY: 'Busy',
8
+ WAITING: 'Waiting',
9
+ IDLE: 'Idle',
10
+ };
11
+ export const MENU_ICONS = {
12
+ NEW_WORKTREE: '⊕',
13
+ MERGE_WORKTREE: '⇄',
14
+ DELETE_WORKTREE: '✕',
15
+ CONFIGURE_SHORTCUTS: '⌨',
16
+ EXIT: '⏻',
17
+ };
18
+ export const getStatusDisplay = (status) => {
19
+ switch (status) {
20
+ case 'busy':
21
+ return `${STATUS_ICONS.BUSY} ${STATUS_LABELS.BUSY}`;
22
+ case 'waiting_input':
23
+ return `${STATUS_ICONS.WAITING} ${STATUS_LABELS.WAITING}`;
24
+ case 'idle':
25
+ return `${STATUS_ICONS.IDLE} ${STATUS_LABELS.IDLE}`;
26
+ }
27
+ };