ccmanager 3.10.0 → 3.11.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.
@@ -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');
@@ -5,9 +5,6 @@ interface TextInputWrapperProps {
5
5
  onSubmit?: (value: string) => void;
6
6
  placeholder?: string;
7
7
  focus?: boolean;
8
- mask?: string;
9
- showCursor?: boolean;
10
- highlightPastedText?: boolean;
11
8
  }
12
9
  declare const TextInputWrapper: React.FC<TextInputWrapperProps>;
13
10
  export default TextInputWrapper;
@@ -1,15 +1,124 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import TextInput from 'ink-text-input';
2
+ import { useReducer, useEffect, useRef, useMemo, } from 'react';
3
+ import { Text, useInput } from 'ink';
4
+ import chalk from 'chalk';
3
5
  import stripAnsi from 'strip-ansi';
4
- const TextInputWrapper = ({ value, onChange, ...props }) => {
5
- const handleChange = (newValue) => {
6
- // First strip all ANSI escape sequences
7
- let cleanedValue = stripAnsi(newValue);
8
- // Then specifically remove bracketed paste mode markers that might remain
9
- // These sometimes appear as literal text after ANSI stripping
10
- cleanedValue = cleanedValue.replace(/\[200~/g, '').replace(/\[201~/g, '');
11
- onChange(cleanedValue);
12
- };
13
- return _jsx(TextInput, { value: value, onChange: handleChange, ...props });
6
+ const reducer = (state, action) => {
7
+ switch (action.type) {
8
+ case 'move-cursor-left': {
9
+ return {
10
+ ...state,
11
+ cursorOffset: Math.max(0, state.cursorOffset - 1),
12
+ };
13
+ }
14
+ case 'move-cursor-right': {
15
+ return {
16
+ ...state,
17
+ cursorOffset: Math.min(state.value.length, state.cursorOffset + 1),
18
+ };
19
+ }
20
+ case 'insert': {
21
+ return {
22
+ value: state.value.slice(0, state.cursorOffset) +
23
+ action.text +
24
+ state.value.slice(state.cursorOffset),
25
+ cursorOffset: state.cursorOffset + action.text.length,
26
+ };
27
+ }
28
+ case 'delete': {
29
+ if (state.cursorOffset === 0)
30
+ return state;
31
+ const newOffset = state.cursorOffset - 1;
32
+ return {
33
+ value: state.value.slice(0, newOffset) + state.value.slice(newOffset + 1),
34
+ cursorOffset: newOffset,
35
+ };
36
+ }
37
+ case 'set': {
38
+ return {
39
+ value: action.value,
40
+ cursorOffset: action.value.length,
41
+ };
42
+ }
43
+ }
44
+ };
45
+ function cleanInput(input) {
46
+ let cleaned = stripAnsi(input);
47
+ cleaned = cleaned.replace(/\[200~/g, '').replace(/\[201~/g, '');
48
+ return cleaned;
49
+ }
50
+ const cursor = chalk.inverse(' ');
51
+ const TextInputWrapper = ({ value, onChange, onSubmit, placeholder = '', focus = true, }) => {
52
+ const [state, dispatch] = useReducer(reducer, {
53
+ value,
54
+ cursorOffset: value.length,
55
+ });
56
+ const lastReportedValue = useRef(value);
57
+ // Sync external value changes into internal state
58
+ useEffect(() => {
59
+ if (value !== lastReportedValue.current) {
60
+ lastReportedValue.current = value;
61
+ dispatch({ type: 'set', value });
62
+ }
63
+ }, [value]);
64
+ // Report internal state changes to parent
65
+ useEffect(() => {
66
+ if (state.value !== lastReportedValue.current) {
67
+ lastReportedValue.current = state.value;
68
+ onChange(state.value);
69
+ }
70
+ }, [state.value, onChange]);
71
+ useInput((input, key) => {
72
+ if (key.upArrow ||
73
+ key.downArrow ||
74
+ (key.ctrl && input === 'c') ||
75
+ key.tab ||
76
+ (key.shift && key.tab)) {
77
+ return;
78
+ }
79
+ if (key.return) {
80
+ onSubmit?.(state.value);
81
+ return;
82
+ }
83
+ if (key.leftArrow) {
84
+ dispatch({ type: 'move-cursor-left' });
85
+ }
86
+ else if (key.rightArrow) {
87
+ dispatch({ type: 'move-cursor-right' });
88
+ }
89
+ else if (key.backspace || key.delete) {
90
+ dispatch({ type: 'delete' });
91
+ }
92
+ else {
93
+ const cleaned = cleanInput(input);
94
+ if (cleaned) {
95
+ dispatch({ type: 'insert', text: cleaned });
96
+ }
97
+ }
98
+ }, { isActive: focus });
99
+ const renderedPlaceholder = useMemo(() => {
100
+ if (!focus) {
101
+ return placeholder ? chalk.dim(placeholder) : '';
102
+ }
103
+ return placeholder.length > 0
104
+ ? chalk.inverse(placeholder[0]) + chalk.dim(placeholder.slice(1))
105
+ : cursor;
106
+ }, [focus, placeholder]);
107
+ const renderedValue = useMemo(() => {
108
+ if (!focus) {
109
+ return state.value;
110
+ }
111
+ let result = state.value.length > 0 ? '' : cursor;
112
+ let index = 0;
113
+ for (const char of state.value) {
114
+ result += index === state.cursorOffset ? chalk.inverse(char) : char;
115
+ index++;
116
+ }
117
+ if (state.value.length > 0 && state.cursorOffset === state.value.length) {
118
+ result += cursor;
119
+ }
120
+ return result;
121
+ }, [focus, state.value, state.cursorOffset]);
122
+ return (_jsx(Text, { children: state.value.length > 0 ? renderedValue : renderedPlaceholder }));
14
123
  };
15
124
  export default TextInputWrapper;
@@ -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