fix(worktree): implement worktree creation for agent isolation

- Add worktree utility functions for git worktree management
- Fix stale closure issue in create-agent-modal causing worktreeName to not be sent
- Refactor create-agent-modal to use useCallback for all functions in proper order
- Add tilde path expansion before worktree creation to fix "Not in a git repository" error
- Add worktreeName parameter to create_agent_request WebSocket message
- Add debug logging for worktree creation flow
- Remove noisy logging (message chunk updates, session updates, agent update notifications)

The worktree feature allows creating isolated git worktrees for each agent,
enabling work on different branches simultaneously without conflicts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-10-26 13:28:05 +01:00
parent 4323d7d193
commit dcfdccf07f
5 changed files with 432 additions and 62 deletions

View File

@@ -60,6 +60,7 @@ export function CreateAgentModal({
const [workingDir, setWorkingDir] = useState("");
const [selectedMode, setSelectedMode] = useState("plan");
const [worktreeName, setWorktreeName] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [pendingRequestId, setPendingRequestId] = useState<string | null>(null);
@@ -92,6 +93,94 @@ export function CreateAgentModal({
[]
);
// Slugify helper function
const slugifyWorktreeName = useCallback((input: string): string => {
return input
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}, []);
// Validate worktree name
const validateWorktreeName = useCallback((name: string): { valid: boolean; error?: string } => {
if (!name) return { valid: true }; // Optional field
if (name.length > 100) {
return { valid: false, error: "Worktree name too long (max 100 characters)" };
}
const validPattern = /^[a-z0-9-]+$/;
if (!validPattern.test(name)) {
return {
valid: false,
error: "Must contain only lowercase letters, numbers, and hyphens",
};
}
if (name.startsWith("-") || name.endsWith("-")) {
return { valid: false, error: "Cannot start or end with a hyphen" };
}
if (name.includes("--")) {
return { valid: false, error: "Cannot have consecutive hyphens" };
}
return { valid: true };
}, []);
const handleCreate = useCallback(async () => {
if (!workingDir.trim()) {
setErrorMessage("Working directory is required");
return;
}
if (isLoading) {
return;
}
const path = workingDir.trim();
const worktree = worktreeName.trim();
// Validate worktree name if provided
if (worktree) {
const validation = validateWorktreeName(worktree);
if (!validation.valid) {
setErrorMessage(`Invalid worktree name: ${validation.error}`);
return;
}
}
// Save to recent paths
try {
await addRecentPath(path);
} catch (error) {
console.error("[CreateAgentModal] Failed to save recent path:", error);
// Continue anyway - don't block agent creation
}
// Generate request ID
const requestId = generateMessageId();
setIsLoading(true);
setPendingRequestId(requestId);
setErrorMessage("");
// Create the agent
try {
createAgent({
cwd: path,
initialMode: selectedMode,
worktreeName: worktree || undefined,
requestId,
});
} catch (error) {
console.error("[CreateAgentModal] Failed to create agent:", error);
setErrorMessage("Failed to create agent. Please try again.");
setIsLoading(false);
setPendingRequestId(null);
}
}, [workingDir, worktreeName, selectedMode, isLoading, validateWorktreeName, addRecentPath, createAgent]);
const renderFooter = useCallback(
(props: BottomSheetFooterProps) => (
<BottomSheetFooter {...props} style={animatedFooterStyle}>
@@ -118,7 +207,7 @@ export function CreateAgentModal({
</Animated.View>
</BottomSheetFooter>
),
[insets.bottom, workingDir, animatedFooterStyle, isLoading]
[insets.bottom, workingDir, animatedFooterStyle, isLoading, handleCreate]
);
useEffect(() => {
@@ -156,48 +245,6 @@ export function CreateAgentModal({
};
}, [pendingRequestId, ws]);
async function handleCreate() {
if (!workingDir.trim()) {
setErrorMessage("Working directory is required");
return;
}
if (isLoading) {
return;
}
const path = workingDir.trim();
// Save to recent paths
try {
await addRecentPath(path);
} catch (error) {
console.error("[CreateAgentModal] Failed to save recent path:", error);
// Continue anyway - don't block agent creation
}
// Generate request ID
const requestId = generateMessageId();
setIsLoading(true);
setPendingRequestId(requestId);
setErrorMessage("");
// Create the agent
try {
createAgent({
cwd: path,
initialMode: selectedMode,
requestId,
});
} catch (error) {
console.error("[CreateAgentModal] Failed to create agent:", error);
setErrorMessage("Failed to create agent. Please try again.");
setIsLoading(false);
setPendingRequestId(null);
}
}
function handleClose() {
bottomSheetRef.current?.dismiss();
}
@@ -207,6 +254,7 @@ export function CreateAgentModal({
// Reset all state
setWorkingDir("");
setWorktreeName("");
setSelectedMode("plan");
setErrorMessage("");
setIsLoading(false);
@@ -293,6 +341,29 @@ export function CreateAgentModal({
)}
</View>
{/* Worktree Name Input (Optional) */}
<View style={styles.formSection}>
<Text style={styles.label}>Worktree Name (Optional)</Text>
<BottomSheetTextInput
style={[styles.input, isLoading && styles.inputDisabled]}
placeholder="feature-branch-name"
placeholderTextColor={defaultTheme.colors.mutedForeground}
value={worktreeName}
onChangeText={(text) => {
// Auto-slugify as user types
const slugified = slugifyWorktreeName(text);
setWorktreeName(slugified);
setErrorMessage("");
}}
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
<Text style={styles.helperText}>
Create a git worktree for isolated development. Must be lowercase, alphanumeric, and hyphens only.
</Text>
</View>
{/* Mode Selector */}
<View style={styles.formSection}>
<Text style={styles.label}>Mode</Text>