ccmanager 3.9.0 → 3.11.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.
Files changed (31) hide show
  1. package/dist/components/App.js +159 -44
  2. package/dist/components/App.test.js +96 -5
  3. package/dist/components/Dashboard.d.ts +12 -0
  4. package/dist/components/Dashboard.js +443 -0
  5. package/dist/components/Dashboard.test.js +348 -0
  6. package/dist/components/Menu.recent-projects.test.js +19 -19
  7. package/dist/components/NewWorktree.d.ts +20 -1
  8. package/dist/components/NewWorktree.js +103 -56
  9. package/dist/components/NewWorktree.test.js +17 -4
  10. package/dist/services/globalSessionOrchestrator.d.ts +1 -0
  11. package/dist/services/globalSessionOrchestrator.js +3 -0
  12. package/dist/services/projectManager.d.ts +7 -1
  13. package/dist/services/projectManager.js +26 -10
  14. package/dist/services/sessionManager.d.ts +3 -2
  15. package/dist/services/sessionManager.js +37 -40
  16. package/dist/services/sessionManager.test.js +38 -0
  17. package/dist/services/worktreeNameGenerator.d.ts +8 -0
  18. package/dist/services/worktreeNameGenerator.js +184 -0
  19. package/dist/services/worktreeNameGenerator.test.js +35 -0
  20. package/dist/utils/presetPrompt.d.ts +11 -0
  21. package/dist/utils/presetPrompt.js +71 -0
  22. package/dist/utils/presetPrompt.test.d.ts +1 -0
  23. package/dist/utils/presetPrompt.test.js +167 -0
  24. package/dist/utils/worktreeUtils.d.ts +1 -2
  25. package/package.json +6 -6
  26. package/dist/components/ProjectList.d.ts +0 -10
  27. package/dist/components/ProjectList.js +0 -233
  28. package/dist/components/ProjectList.recent-projects.test.js +0 -193
  29. package/dist/components/ProjectList.test.js +0 -620
  30. /package/dist/components/{ProjectList.recent-projects.test.d.ts → Dashboard.test.d.ts} +0 -0
  31. /package/dist/{components/ProjectList.test.d.ts → services/worktreeNameGenerator.test.d.ts} +0 -0
@@ -9,18 +9,15 @@ import { generateWorktreeDirectory } from '../utils/worktreeUtils.js';
9
9
  import { WorktreeService } from '../services/worktreeService.js';
10
10
  import { useSearchMode } from '../hooks/useSearchMode.js';
11
11
  import { Effect } from 'effect';
12
+ import { describePromptInjection, getPromptInjectionMethod, } from '../utils/presetPrompt.js';
12
13
  const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
13
14
  const worktreeConfig = configReader.getWorktreeConfig();
15
+ const presetsConfig = configReader.getCommandPresets();
14
16
  const isAutoDirectory = worktreeConfig.autoDirectory;
15
17
  const isAutoUseDefaultBranch = worktreeConfig.autoUseDefaultBranch ?? false;
16
18
  const limit = 10;
17
- // Determine initial step based on config options
18
- // If autoUseDefaultBranch is enabled, we start at a temporary 'loading' state
19
- // and will transition to 'branch-strategy' after branches are loaded
20
19
  const getInitialStep = () => {
21
20
  if (isAutoDirectory) {
22
- // With autoDirectory, skip path input
23
- // If autoUseDefaultBranch is also enabled, we'll skip base-branch after loading
24
21
  return 'base-branch';
25
22
  }
26
23
  return 'path';
@@ -31,17 +28,16 @@ const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
31
28
  const [baseBranch, setBaseBranch] = useState('');
32
29
  const [copyClaudeDirectory, setCopyClaudeDirectory] = useState(true);
33
30
  const [copySessionData, setCopySessionData] = useState(worktreeConfig.copySessionData ?? true);
34
- // Loading and error states for branch data
31
+ const [selectedPresetId, setSelectedPresetId] = useState(presetsConfig.defaultPresetId);
32
+ const [initialPrompt, setInitialPrompt] = useState('');
35
33
  const [isLoadingBranches, setIsLoadingBranches] = useState(true);
36
34
  const [branchLoadError, setBranchLoadError] = useState(null);
37
35
  const [branches, setBranches] = useState([]);
38
36
  const [defaultBranch, setDefaultBranch] = useState('main');
39
- // Initialize worktree service and load branches using Effect
40
37
  useEffect(() => {
41
38
  let cancelled = false;
42
39
  const service = new WorktreeService(projectPath);
43
40
  const loadBranches = async () => {
44
- // Use Effect.all to load branches and defaultBranch in parallel
45
41
  const workflow = Effect.all([service.getAllBranchesEffect(), service.getDefaultBranchEffect()], { concurrency: 2 });
46
42
  const result = await Effect.runPromise(Effect.match(workflow, {
47
43
  onFailure: (error) => ({
@@ -63,13 +59,9 @@ const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
63
59
  setBranches(result.branches);
64
60
  setDefaultBranch(result.defaultBranch);
65
61
  setIsLoadingBranches(false);
66
- // If autoUseDefaultBranch is enabled, auto-set the base branch
67
- // and skip to branch-strategy step
68
62
  if (isAutoUseDefaultBranch && result.defaultBranch) {
69
63
  setBaseBranch(result.defaultBranch);
70
- // Skip base-branch step, go directly to branch-strategy
71
- // (if we're at base-branch step, which happens with autoDirectory)
72
- setStep(currentStep => currentStep === 'base-branch' ? 'branch-strategy' : currentStep);
64
+ setStep(currentStep => currentStep === 'base-branch' ? 'creation-mode' : currentStep);
73
65
  }
74
66
  }
75
67
  }
@@ -84,98 +76,142 @@ const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
84
76
  cancelled = true;
85
77
  };
86
78
  }, [projectPath, isAutoUseDefaultBranch]);
87
- // Create branch items with default branch first (memoized)
88
79
  const allBranchItems = useMemo(() => [
89
80
  { label: `${defaultBranch} (default)`, value: defaultBranch },
90
81
  ...branches
91
82
  .filter(br => br !== defaultBranch)
92
83
  .map(br => ({ label: br, value: br })),
93
84
  ], [branches, defaultBranch]);
94
- // Use search mode for base branch selection
95
85
  const { isSearchMode, searchQuery, selectedIndex, setSearchQuery } = useSearchMode(allBranchItems.length, {
96
86
  isDisabled: step !== 'base-branch',
97
87
  });
98
- // Filter branch items based on search query
99
88
  const branchItems = useMemo(() => {
100
89
  if (!searchQuery)
101
90
  return allBranchItems;
102
91
  return allBranchItems.filter(item => item.value.toLowerCase().includes(searchQuery.toLowerCase()));
103
92
  }, [allBranchItems, searchQuery]);
93
+ const presetItems = useMemo(() => presetsConfig.presets.map(preset => ({
94
+ label: `${preset.name}${preset.id === presetsConfig.defaultPresetId ? ' (default)' : ''}\n Command: ${preset.command}${preset.args?.length ? ` ${preset.args.join(' ')}` : ''}`,
95
+ value: preset.id,
96
+ })), [presetsConfig.defaultPresetId, presetsConfig.presets]);
97
+ const selectedPreset = useMemo(() => presetsConfig.presets.find(preset => preset.id === selectedPresetId) ||
98
+ presetsConfig.presets[0], [selectedPresetId, presetsConfig.presets]);
104
99
  useInput((input, key) => {
105
100
  if (shortcutManager.matchesShortcut('cancel', input, key)) {
106
101
  onCancel();
107
102
  }
108
- // Handle arrow key navigation in search mode for base branch selection
109
103
  if (step === 'base-branch' && isSearchMode) {
110
- // Don't handle any keys here - let useSearchMode handle them
111
- // The hook will handle arrow keys for navigation and Enter to exit search mode
112
104
  return;
113
105
  }
114
106
  });
115
107
  const handlePathSubmit = (value) => {
116
- if (value.trim()) {
117
- setPath(value.trim());
118
- // If autoUseDefaultBranch is enabled and we have the default branch,
119
- // skip base-branch selection and go directly to branch-strategy
120
- if (isAutoUseDefaultBranch && defaultBranch) {
121
- setBaseBranch(defaultBranch);
122
- setStep('branch-strategy');
123
- }
124
- else {
125
- setStep('base-branch');
126
- }
108
+ if (!value.trim())
109
+ return;
110
+ setPath(value.trim());
111
+ if (isAutoUseDefaultBranch && defaultBranch) {
112
+ setBaseBranch(defaultBranch);
113
+ setStep('creation-mode');
127
114
  }
128
- };
129
- const handleBranchSubmit = (value) => {
130
- if (value.trim()) {
131
- setBranch(value.trim());
132
- setStep('copy-settings');
115
+ else {
116
+ setStep('base-branch');
133
117
  }
134
118
  };
135
119
  const handleBaseBranchSelect = (item) => {
136
120
  setBaseBranch(item.value);
137
- setStep('branch-strategy');
121
+ setStep('creation-mode');
122
+ };
123
+ const handleCreationModeSelect = (item) => {
124
+ if (item.value === 'manual') {
125
+ setStep('branch-strategy');
126
+ return;
127
+ }
128
+ setStep('auto-preset');
138
129
  };
139
130
  const handleBranchStrategySelect = (item) => {
140
131
  const useExisting = item.value === 'existing';
141
132
  if (useExisting) {
142
- // Use the base branch as the branch name for existing branch
143
133
  setBranch(baseBranch);
144
134
  setStep('copy-settings');
145
135
  }
146
136
  else {
147
- // Need to input new branch name
148
137
  setStep('branch');
149
138
  }
150
139
  };
140
+ const handleBranchSubmit = (value) => {
141
+ if (!value.trim())
142
+ return;
143
+ setBranch(value.trim());
144
+ setStep('copy-settings');
145
+ };
146
+ const handlePresetSelect = (item) => {
147
+ setSelectedPresetId(item.value);
148
+ setStep('auto-prompt');
149
+ };
150
+ const handlePromptSubmit = (value) => {
151
+ if (!value.trim())
152
+ return;
153
+ setInitialPrompt(value.trim());
154
+ setStep('copy-settings');
155
+ };
151
156
  const handleCopySettingsSelect = (item) => {
152
157
  setCopyClaudeDirectory(item.value);
153
158
  setStep('copy-session');
154
159
  };
160
+ const getResolvedPath = () => {
161
+ if (!isAutoDirectory) {
162
+ return path;
163
+ }
164
+ const branchForPath = step === 'copy-session' && branch ? branch : 'generated-from-prompt';
165
+ return generateWorktreeDirectory(projectPath || process.cwd(), branchForPath, worktreeConfig.autoDirectoryPattern);
166
+ };
155
167
  const handleCopySessionSelect = (item) => {
156
168
  const shouldCopy = item.value === 'yes';
169
+ const resolvedPath = getResolvedPath();
157
170
  setCopySessionData(shouldCopy);
158
- if (isAutoDirectory) {
159
- // Generate path from branch name
160
- const autoPath = generateWorktreeDirectory(projectPath || process.cwd(), branch, worktreeConfig.autoDirectoryPattern);
161
- onComplete(autoPath, branch, baseBranch, shouldCopy, copyClaudeDirectory);
171
+ if (step !== 'copy-session') {
172
+ return;
162
173
  }
163
- else {
164
- onComplete(path, branch, baseBranch, shouldCopy, copyClaudeDirectory);
174
+ if (initialPrompt && selectedPresetId) {
175
+ onComplete({
176
+ creationMode: 'prompt',
177
+ path: isAutoDirectory ? projectPath || process.cwd() : resolvedPath,
178
+ projectPath: projectPath || process.cwd(),
179
+ autoDirectoryPattern: isAutoDirectory
180
+ ? worktreeConfig.autoDirectoryPattern
181
+ : undefined,
182
+ baseBranch,
183
+ presetId: selectedPresetId,
184
+ initialPrompt,
185
+ copySessionData: shouldCopy,
186
+ copyClaudeDirectory,
187
+ });
188
+ return;
165
189
  }
190
+ onComplete({
191
+ creationMode: 'manual',
192
+ path: resolvedPath,
193
+ branch,
194
+ baseBranch,
195
+ copySessionData: shouldCopy,
196
+ copyClaudeDirectory,
197
+ });
166
198
  };
167
- // Calculate generated path for preview (memoized to avoid expensive recalculations)
168
199
  const generatedPath = useMemo(() => {
169
- return isAutoDirectory && branch
170
- ? generateWorktreeDirectory(projectPath || process.cwd(), branch, worktreeConfig.autoDirectoryPattern)
171
- : '';
200
+ if (!isAutoDirectory) {
201
+ return '';
202
+ }
203
+ const branchForPath = branch || (initialPrompt ? 'generated-from-prompt' : '');
204
+ if (!branchForPath) {
205
+ return '';
206
+ }
207
+ return generateWorktreeDirectory(projectPath || process.cwd(), branchForPath, worktreeConfig.autoDirectoryPattern);
172
208
  }, [
173
209
  isAutoDirectory,
174
210
  branch,
211
+ initialPrompt,
175
212
  worktreeConfig.autoDirectoryPattern,
176
213
  projectPath,
177
214
  ]);
178
- // Format errors using TaggedError discrimination
179
215
  const formatError = (error) => {
180
216
  switch (error._tag) {
181
217
  case 'GitError':
@@ -190,17 +226,28 @@ const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
190
226
  return `Validation failed for ${error.field}: ${error.constraint}`;
191
227
  }
192
228
  };
193
- // Show loading indicator while branches load
194
229
  if (isLoadingBranches) {
195
230
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "green", children: "Create New Worktree" }) }), _jsx(Box, { children: _jsx(Text, { children: "Loading branches..." }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Press ", shortcutManager.getShortcutDisplay('cancel'), " to cancel"] }) })] }));
196
231
  }
197
- // Show error message if branch loading failed
198
232
  if (branchLoadError) {
199
233
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "green", children: "Create New Worktree" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "red", children: "Error loading branches:" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "red", children: branchLoadError }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Press ", shortcutManager.getShortcutDisplay('cancel'), " to go back"] }) })] }));
200
234
  }
201
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "green", children: "Create New Worktree" }) }), step === 'path' && !isAutoDirectory ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Enter worktree path (relative to repository root):" }) }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '> ' }), _jsx(TextInputWrapper, { value: path, onChange: setPath, onSubmit: handlePathSubmit, placeholder: "e.g., ../myproject-feature" })] })] })) : null, step === 'base-branch' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Select base branch for the worktree:" }) }), isSearchMode && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { children: "Search: " }), _jsx(TextInputWrapper, { value: searchQuery, onChange: setSearchQuery, focus: true, placeholder: "Type to filter branches..." })] })), isSearchMode && branchItems.length === 0 ? (_jsx(Box, { children: _jsx(Text, { color: "yellow", children: "No branches match your search" }) })) : isSearchMode ? (
202
- // In search mode, show the items as a list without SelectInput
203
- _jsx(Box, { flexDirection: "column", children: branchItems.slice(0, limit).map((item, index) => (_jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, children: [index === selectedIndex ? '❯ ' : ' ', item.label] }, item.value))) })) : (_jsx(SelectInput, { items: branchItems, onSelect: handleBaseBranchSelect, initialIndex: selectedIndex, limit: limit, isFocused: !isSearchMode })), !isSearchMode && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press / to search" }) }))] })), step === 'branch-strategy' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Base branch: ", _jsx(Text, { color: "cyan", children: baseBranch })] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Choose branch creation strategy:" }) }), _jsx(SelectInput, { items: [
235
+ const promptHandlingText = selectedPreset
236
+ ? describePromptInjection(selectedPreset)
237
+ : '';
238
+ const promptMethod = selectedPreset
239
+ ? getPromptInjectionMethod(selectedPreset)
240
+ : 'stdin';
241
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "green", children: "Create New Worktree" }) }), step === 'path' && !isAutoDirectory ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Enter worktree path (relative to repository root):" }) }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '> ' }), _jsx(TextInputWrapper, { value: path, onChange: setPath, onSubmit: handlePathSubmit, placeholder: "e.g., ../myproject-feature" })] })] })) : null, step === 'base-branch' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Select base branch for the worktree:" }) }), isSearchMode && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { children: "Search: " }), _jsx(TextInputWrapper, { value: searchQuery, onChange: setSearchQuery, focus: true, placeholder: "Type to filter branches..." })] })), isSearchMode && branchItems.length === 0 ? (_jsx(Box, { children: _jsx(Text, { color: "yellow", children: "No branches match your search" }) })) : isSearchMode ? (_jsx(Box, { flexDirection: "column", children: branchItems.slice(0, limit).map((item, index) => (_jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, children: [index === selectedIndex ? '❯ ' : ' ', item.label] }, item.value))) })) : (_jsx(SelectInput, { items: branchItems, onSelect: handleBaseBranchSelect, initialIndex: selectedIndex, limit: limit, isFocused: !isSearchMode })), !isSearchMode && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press / to search" }) }))] })), step === 'creation-mode' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Base branch: ", _jsx(Text, { color: "cyan", children: baseBranch })] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "How do you want to create the new worktree?" }) }), _jsx(SelectInput, { items: [
242
+ {
243
+ label: '1. Choose the branch name yourself',
244
+ value: 'manual',
245
+ },
246
+ {
247
+ label: '2. Enter a prompt first and let Claude decide the branch name',
248
+ value: 'prompt',
249
+ },
250
+ ], onSelect: handleCreationModeSelect, initialIndex: 0 })] })), step === 'branch-strategy' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Base branch: ", _jsx(Text, { color: "cyan", children: baseBranch })] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Choose branch creation strategy:" }) }), _jsx(SelectInput, { items: [
204
251
  {
205
252
  label: 'Create new branch from base branch',
206
253
  value: 'new',
@@ -209,13 +256,13 @@ const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
209
256
  label: 'Use existing base branch',
210
257
  value: 'existing',
211
258
  },
212
- ], onSelect: handleBranchStrategySelect, initialIndex: 0 })] })), step === 'branch' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Enter new branch name (will be created from", ' ', _jsx(Text, { color: "cyan", children: baseBranch }), "):"] }) }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '> ' }), _jsx(TextInputWrapper, { value: branch, onChange: setBranch, onSubmit: handleBranchSubmit, placeholder: "e.g., feature/new-feature" })] }), isAutoDirectory && generatedPath && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Worktree will be created at:", ' ', _jsx(Text, { color: "green", children: generatedPath })] }) }))] })), step === 'copy-settings' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Copy .claude directory from base branch (", _jsx(Text, { color: "cyan", children: baseBranch }), ")?"] }) }), _jsx(SelectInput, { items: [
259
+ ], onSelect: handleBranchStrategySelect, initialIndex: 0 })] })), step === 'branch' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Enter new branch name (will be created from", ' ', _jsx(Text, { color: "cyan", children: baseBranch }), "):"] }) }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '> ' }), _jsx(TextInputWrapper, { value: branch, onChange: setBranch, onSubmit: handleBranchSubmit, placeholder: "e.g., feature/new-feature" })] }), isAutoDirectory && generatedPath && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Worktree will be created at:", ' ', _jsx(Text, { color: "green", children: generatedPath })] }) }))] })), step === 'auto-preset' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Select the preset to use for the first session:" }) }), _jsx(SelectInput, { items: presetItems, onSelect: handlePresetSelect, initialIndex: Math.max(0, presetItems.findIndex(item => item.value === selectedPresetId)) })] })), step === 'auto-prompt' && selectedPreset && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Preset: ", _jsx(Text, { color: "cyan", children: selectedPreset.name })] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Enter the prompt for the new session:" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: promptHandlingText }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Examples: Claude/Codex use the final argument, OpenCode uses `--prompt`, and other commands may receive the prompt over stdin." }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "Automatic branch naming requires the `claude` command in your PATH." }) }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '> ' }), _jsx(TextInputWrapper, { value: initialPrompt, onChange: setInitialPrompt, onSubmit: handlePromptSubmit, placeholder: "Describe what you want the agent to do" })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Prompt delivery mode for this preset:", ' ', _jsx(Text, { color: "green", children: promptMethod })] }) })] })), step === 'copy-settings' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Copy .claude directory from base branch (", _jsx(Text, { color: "cyan", children: baseBranch }), ")?"] }) }), initialPrompt ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "The branch name will be generated automatically right before the worktree is created." }) })) : null, _jsx(SelectInput, { items: [
213
260
  {
214
261
  label: 'Yes - Copy .claude directory from base branch',
215
262
  value: true,
216
263
  },
217
264
  { label: 'No - Start without .claude directory', value: false },
218
- ], onSelect: handleCopySettingsSelect, initialIndex: 0 })] })), step === 'copy-session' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Copy Claude Code session data to the new worktree?" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "This will copy conversation history and context from the current worktree" }) }), _jsx(SelectInput, { items: [
265
+ ], onSelect: handleCopySettingsSelect, initialIndex: 0 })] })), step === 'copy-session' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Copy Claude Code session data to the new worktree?" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "This will copy conversation history and context from the current worktree." }) }), isAutoDirectory && generatedPath ? (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Worktree path preview:", ' ', _jsx(Text, { color: "green", children: generatedPath })] }) })) : null, _jsx(SelectInput, { items: [
219
266
  { label: '✅ Yes, copy session data', value: 'yes' },
220
267
  { label: '❌ No, start fresh', value: 'no' },
221
268
  ], onSelect: handleCopySessionSelect, initialIndex: copySessionData ? 0 : 1 })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Press ", shortcutManager.getShortcutDisplay('cancel'), " to cancel"] }) })] }));
@@ -50,6 +50,18 @@ vi.mock('../services/config/configReader.js', () => ({
50
50
  autoDirectoryPattern: '../{project}-{branch}',
51
51
  copySessionData: true,
52
52
  }),
53
+ getCommandPresets: () => ({
54
+ presets: [
55
+ {
56
+ id: 'claude',
57
+ name: 'Claude',
58
+ command: 'claude',
59
+ args: ['--resume'],
60
+ detectionStrategy: 'claude',
61
+ },
62
+ ],
63
+ defaultPresetId: 'claude',
64
+ }),
53
65
  },
54
66
  }));
55
67
  vi.mock('../hooks/useSearchMode.js', () => ({
@@ -259,7 +271,7 @@ describe('NewWorktree component Effect integration', () => {
259
271
  // Verify Effect was executed (Effect.match pattern)
260
272
  expect(effectExecuted).toBe(true);
261
273
  });
262
- it('should skip base branch selection when autoUseDefaultBranch is enabled with autoDirectory', async () => {
274
+ it('should skip base branch selection and show creation mode when autoUseDefaultBranch is enabled with autoDirectory', async () => {
263
275
  const { Effect } = await import('effect');
264
276
  const { WorktreeService } = await import('../services/worktreeService.js');
265
277
  const { configReader } = await import('../services/config/configReader.js');
@@ -285,12 +297,13 @@ describe('NewWorktree component Effect integration', () => {
285
297
  // Wait for Effect to execute and state to update
286
298
  await new Promise(resolve => setTimeout(resolve, 100));
287
299
  const output = lastFrame();
288
- // Should skip base-branch step and show branch-strategy step
289
- // which displays "Base branch:" and "Choose branch creation strategy"
300
+ // Should skip base-branch step and show the manual/prompt creation mode choice
290
301
  expect(output).toContain('Create New Worktree');
291
302
  expect(output).toContain('Base branch:');
292
303
  expect(output).toContain('main');
293
- expect(output).toContain('Choose branch creation strategy');
304
+ expect(output).toContain('How do you want to create the new worktree?');
305
+ expect(output).toContain('Choose the branch name yourself');
306
+ expect(output).toContain('Enter a prompt first');
294
307
  });
295
308
  it('should show base branch selection when autoUseDefaultBranch is disabled', async () => {
296
309
  const { Effect } = await import('effect');
@@ -10,6 +10,7 @@ declare class GlobalSessionOrchestrator {
10
10
  getAllActiveSessions(): Session[];
11
11
  destroyAllSessions(): void;
12
12
  destroyProjectSessions(projectPath: string): void;
13
+ getProjectPaths(): string[];
13
14
  getProjectSessions(projectPath: string): Session[];
14
15
  }
15
16
  export declare const globalSessionOrchestrator: GlobalSessionOrchestrator;
@@ -53,6 +53,9 @@ class GlobalSessionOrchestrator {
53
53
  this.projectManagers.delete(projectPath);
54
54
  }
55
55
  }
56
+ getProjectPaths() {
57
+ return Array.from(this.projectManagers.keys());
58
+ }
56
59
  getProjectSessions(projectPath) {
57
60
  const manager = this.projectManagers.get(projectPath);
58
61
  if (manager) {
@@ -32,9 +32,15 @@ export declare class ProjectManager implements IProjectManager {
32
32
  */
33
33
  private discoverDirectories;
34
34
  /**
35
- * Quick check for .git directory without running git commands
35
+ * Quick check for .git presence (directory or file) without running git commands.
36
+ * Returns true for both main repositories and worktrees.
36
37
  */
37
38
  private hasGitDirectory;
39
+ /**
40
+ * Check if a directory is a main git repository (not a worktree).
41
+ * Main repositories have .git as a directory; worktrees have .git as a file.
42
+ */
43
+ private isMainGitRepository;
38
44
  /**
39
45
  * Process directories in parallel using worker pool pattern
40
46
  */
@@ -167,12 +167,15 @@ export class ProjectManager {
167
167
  // Quick check if this is a git repository
168
168
  const hasGitDir = await this.hasGitDirectory(fullPath);
169
169
  if (hasGitDir) {
170
- // Found a git repository - add to tasks and skip subdirectories
171
- if (!seen.has(fullPath)) {
170
+ // Only add main repositories (.git is a directory),
171
+ // not worktrees (.git is a file pointing to the main repo)
172
+ const isMain = await this.isMainGitRepository(fullPath);
173
+ if (isMain && !seen.has(fullPath)) {
172
174
  seen.add(fullPath);
173
175
  tasks.push({ path: fullPath, relativePath });
174
176
  }
175
- return; // Early termination - don't walk subdirectories
177
+ // Early termination for any git-related dir
178
+ return;
176
179
  }
177
180
  // Not a git repo, continue walking subdirectories
178
181
  await walk(fullPath, depth + 1);
@@ -189,13 +192,28 @@ export class ProjectManager {
189
192
  return tasks;
190
193
  }
191
194
  /**
192
- * Quick check for .git directory without running git commands
195
+ * Quick check for .git presence (directory or file) without running git commands.
196
+ * Returns true for both main repositories and worktrees.
193
197
  */
194
198
  async hasGitDirectory(dirPath) {
195
199
  try {
196
200
  const gitPath = path.join(dirPath, '.git');
197
201
  const stats = await fs.stat(gitPath);
198
- return stats.isDirectory() || stats.isFile(); // File for worktrees
202
+ return stats.isDirectory() || stats.isFile();
203
+ }
204
+ catch {
205
+ return false;
206
+ }
207
+ }
208
+ /**
209
+ * Check if a directory is a main git repository (not a worktree).
210
+ * Main repositories have .git as a directory; worktrees have .git as a file.
211
+ */
212
+ async isMainGitRepository(dirPath) {
213
+ try {
214
+ const gitPath = path.join(dirPath, '.git');
215
+ const stats = await fs.stat(gitPath);
216
+ return stats.isDirectory();
199
217
  }
200
218
  catch {
201
219
  return false;
@@ -242,11 +260,9 @@ export class ProjectManager {
242
260
  name: path.basename(task.path),
243
261
  };
244
262
  try {
245
- // Check if directory has .git (already validated in discoverDirectories)
246
- // Double-check here to ensure it's still valid
247
- const hasGit = await this.hasGitDirectory(task.path);
248
- if (!hasGit) {
249
- // Not a git repo, return null to filter it out
263
+ // Double-check here to ensure it's still a valid main repository
264
+ const isMain = await this.isMainGitRepository(task.path);
265
+ if (!isMain) {
250
266
  return null;
251
267
  }
252
268
  }
@@ -17,6 +17,7 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
17
17
  private busyTimers;
18
18
  private autoApprovalDisabledWorktrees;
19
19
  private spawn;
20
+ private resolvePreset;
20
21
  detectTerminalState(session: Session): SessionState;
21
22
  detectBackgroundTask(session: Session): number;
22
23
  detectTeamMembers(session: Session): number;
@@ -54,7 +55,7 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
54
55
  * );
55
56
  * ```
56
57
  */
57
- createSessionWithPresetEffect(worktreePath: string, presetId?: string): Effect.Effect<Session, ProcessError | ConfigError, never>;
58
+ createSessionWithPresetEffect(worktreePath: string, presetId?: string, initialPrompt?: string): Effect.Effect<Session, ProcessError | ConfigError, never>;
58
59
  private setupDataHandler;
59
60
  /**
60
61
  * Sets up exit handler for the session process.
@@ -95,7 +96,7 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
95
96
  * Create session with devcontainer integration using Effect-based error handling
96
97
  * @returns Effect that may fail with ProcessError (container/spawn failure) or ConfigError (invalid preset)
97
98
  */
98
- createSessionWithDevcontainerEffect(worktreePath: string, devcontainerConfig: DevcontainerConfig, presetId?: string): Effect.Effect<Session, ProcessError | ConfigError, never>;
99
+ createSessionWithDevcontainerEffect(worktreePath: string, devcontainerConfig: DevcontainerConfig, presetId?: string, initialPrompt?: string): Effect.Effect<Session, ProcessError | ConfigError, never>;
99
100
  destroy(): void;
100
101
  static getSessionCounts(sessions: Session[]): SessionCounts;
101
102
  static formatSessionCounts(counts: SessionCounts): string;
@@ -16,6 +16,7 @@ import { Mutex, createInitialSessionStateData } from '../utils/mutex.js';
16
16
  import { getBackgroundTaskTag, getTeamMemberTag, } from '../constants/statusIcons.js';
17
17
  import { getTerminalScreenContent } from '../utils/screenCapture.js';
18
18
  import { injectTeammateMode } from '../utils/commandArgs.js';
19
+ import { preparePresetLaunch } from '../utils/presetPrompt.js';
19
20
  const { Terminal } = pkg;
20
21
  const execAsync = promisify(exec);
21
22
  const TERMINAL_CONTENT_MAX_LINES = 300;
@@ -34,6 +35,24 @@ export class SessionManager extends EventEmitter {
34
35
  };
35
36
  return spawn(command, args, spawnOptions);
36
37
  }
38
+ resolvePreset(presetId) {
39
+ let preset = presetId
40
+ ? Either.getOrElse(configReader.getPresetByIdEffect(presetId), () => null)
41
+ : null;
42
+ if (!preset) {
43
+ preset = configReader.getDefaultPreset();
44
+ }
45
+ if (!preset) {
46
+ throw new ConfigError({
47
+ configPath: 'configuration',
48
+ reason: 'validation',
49
+ details: presetId
50
+ ? `Preset with ID '${presetId}' not found and no default preset available`
51
+ : 'No default preset available',
52
+ });
53
+ }
54
+ return preset;
55
+ }
37
56
  detectTerminalState(session) {
38
57
  const stateData = session.stateMutex.getSnapshot();
39
58
  const detectedState = session.stateDetector.detectState(session.terminal, stateData.state);
@@ -227,7 +246,7 @@ export class SessionManager extends EventEmitter {
227
246
  * );
228
247
  * ```
229
248
  */
230
- createSessionWithPresetEffect(worktreePath, presetId) {
249
+ createSessionWithPresetEffect(worktreePath, presetId, initialPrompt) {
231
250
  return Effect.tryPromise({
232
251
  try: async () => {
233
252
  // Check if session already exists
@@ -235,31 +254,20 @@ export class SessionManager extends EventEmitter {
235
254
  if (existing) {
236
255
  return existing;
237
256
  }
238
- // Get preset configuration using Either-based lookup
239
- let preset = presetId
240
- ? Either.getOrElse(configReader.getPresetByIdEffect(presetId), () => null)
241
- : null;
242
- if (!preset) {
243
- preset = configReader.getDefaultPreset();
244
- }
245
- // Validate preset exists
246
- if (!preset) {
247
- throw new ConfigError({
248
- configPath: 'configuration',
249
- reason: 'validation',
250
- details: presetId
251
- ? `Preset with ID '${presetId}' not found and no default preset available`
252
- : 'No default preset available',
253
- });
254
- }
257
+ const preset = this.resolvePreset(presetId);
255
258
  const command = preset.command;
256
- const args = injectTeammateMode(preset.command, preset.args || [], preset.detectionStrategy);
259
+ const launch = preparePresetLaunch(preset, initialPrompt);
260
+ const args = launch.args;
257
261
  // Spawn the process - fallback will be handled by setupExitHandler
258
262
  const ptyProcess = await this.spawn(command, args, worktreePath);
259
- return this.createSessionInternal(worktreePath, ptyProcess, {
263
+ const session = await this.createSessionInternal(worktreePath, ptyProcess, {
260
264
  isPrimaryCommand: true,
261
265
  detectionStrategy: preset.detectionStrategy,
262
266
  });
267
+ if (launch.stdinPayload) {
268
+ session.process.write(launch.stdinPayload);
269
+ }
270
+ return session;
263
271
  },
264
272
  catch: (error) => {
265
273
  // If it's already a ConfigError, return it
@@ -617,7 +625,7 @@ export class SessionManager extends EventEmitter {
617
625
  * Create session with devcontainer integration using Effect-based error handling
618
626
  * @returns Effect that may fail with ProcessError (container/spawn failure) or ConfigError (invalid preset)
619
627
  */
620
- createSessionWithDevcontainerEffect(worktreePath, devcontainerConfig, presetId) {
628
+ createSessionWithDevcontainerEffect(worktreePath, devcontainerConfig, presetId, initialPrompt) {
621
629
  return Effect.tryPromise({
622
630
  try: async () => {
623
631
  // Check if session already exists
@@ -635,37 +643,26 @@ export class SessionManager extends EventEmitter {
635
643
  message: `Failed to start devcontainer: ${error instanceof Error ? error.message : String(error)}`,
636
644
  });
637
645
  }
638
- // Get preset configuration using Either-based lookup
639
- let preset = presetId
640
- ? Either.getOrElse(configReader.getPresetByIdEffect(presetId), () => null)
641
- : null;
642
- if (!preset) {
643
- preset = configReader.getDefaultPreset();
644
- }
645
- // Validate preset exists
646
- if (!preset) {
647
- throw new ConfigError({
648
- configPath: 'configuration',
649
- reason: 'validation',
650
- details: presetId
651
- ? `Preset with ID '${presetId}' not found and no default preset available`
652
- : 'No default preset available',
653
- });
654
- }
646
+ const preset = this.resolvePreset(presetId);
655
647
  // Parse the exec command to extract arguments
656
648
  const execParts = devcontainerConfig.execCommand.split(/\s+/);
657
649
  const devcontainerCmd = execParts[0] || 'devcontainer';
658
650
  const execArgs = execParts.slice(1);
659
651
  // Build the full command: devcontainer exec [args] -- [preset command] [preset args]
660
- const presetArgs = injectTeammateMode(preset.command, preset.args || [], preset.detectionStrategy);
652
+ const launch = preparePresetLaunch(preset, initialPrompt);
653
+ const presetArgs = launch.args;
661
654
  const fullArgs = [...execArgs, '--', preset.command, ...presetArgs];
662
655
  // Spawn the process within devcontainer
663
656
  const ptyProcess = await this.spawn(devcontainerCmd, fullArgs, worktreePath);
664
- return this.createSessionInternal(worktreePath, ptyProcess, {
657
+ const session = await this.createSessionInternal(worktreePath, ptyProcess, {
665
658
  isPrimaryCommand: true,
666
659
  detectionStrategy: preset.detectionStrategy,
667
660
  devcontainerConfig,
668
661
  });
662
+ if (launch.stdinPayload) {
663
+ session.process.write(launch.stdinPayload);
664
+ }
665
+ return session;
669
666
  },
670
667
  catch: (error) => {
671
668
  // If it's already a ConfigError or ProcessError, return it