From 54e9f2287802ab467c328bcc3c198ea0f5f9f7ec Mon Sep 17 00:00:00 2001 From: Andy <119136210+AndyMik90@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:57:16 +0100 Subject: [PATCH] fix(terminal): improve worktree name input UX (#1012) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(terminal): improve worktree name input to not strip trailing characters while typing - Allow trailing hyphens/underscores during input (only trim on submit) - Add preview name that shows the final sanitized value for branch preview - Remove invalid characters instead of replacing with hyphens - Collapse consecutive underscores in addition to hyphens - Final sanitization happens on submit to match backend WORKTREE_NAME_REGEX Co-Authored-By: Claude Opus 4.5 * fix(terminal): address PR review findings for worktree name validation - Fix submit button disabled check to use sanitized name instead of raw input - Simplify trailing trim logic (apply once after all transformations) - Apply lowercase in handleNameChange to reduce input/preview gap - Internationalize 'name' fallback using existing translation key Co-Authored-By: Claude Opus 4.5 * fix(terminal): improve header responsiveness for multiple terminals - Hide text labels (Claude, Open in IDE) when ≥4 terminals, show icon only - Add dynamic max-width to worktree name badge with truncation - Add tooltips to all icon-only elements for accessibility - Maintain full functionality while reducing header width requirements Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 Co-authored-by: Test User --- .../terminal/CreateWorktreeDialog.tsx | 46 +++++++++++++------ .../components/terminal/TerminalHeader.tsx | 39 +++++++++++----- 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/apps/frontend/src/renderer/components/terminal/CreateWorktreeDialog.tsx b/apps/frontend/src/renderer/components/terminal/CreateWorktreeDialog.tsx index a6179280..32ce51b9 100644 --- a/apps/frontend/src/renderer/components/terminal/CreateWorktreeDialog.tsx +++ b/apps/frontend/src/renderer/components/terminal/CreateWorktreeDialog.tsx @@ -32,20 +32,27 @@ const PROJECT_DEFAULT_BRANCH = '__project_default__'; * - Converts to lowercase * - Replaces spaces and invalid characters with hyphens * - Collapses consecutive hyphens - * - Trims leading/trailing hyphens + * - Trims leading hyphens (but allows trailing during input) * - Ensures name ends with alphanumeric (matching backend WORKTREE_NAME_REGEX) + * + * @param trimTrailing - If true, trims trailing hyphens/underscores (for final validation) */ -function sanitizeWorktreeName(value: string, maxLength?: number): string { +function sanitizeWorktreeName(value: string, maxLength?: number, trimTrailing = false): string { let sanitized = value .toLowerCase() .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/[^a-z0-9_-]/g, '-') // Replace invalid chars (including dots) with hyphens + .replace(/[^a-z0-9_-]/g, '') // Remove invalid chars (only allow letters, numbers, hyphens, underscores) .replace(/-{2,}/g, '-') // Collapse consecutive hyphens - .replace(/^[-_]+|[-_]+$/g, ''); // Trim leading and trailing hyphens/underscores + .replace(/_{2,}/g, '_') // Collapse consecutive underscores + .replace(/^[-_]+/, ''); // Trim leading hyphens/underscores only if (maxLength) { sanitized = sanitized.slice(0, maxLength); - // Trim trailing hyphens/underscores again after slicing + } + + // Only trim trailing hyphens/underscores when explicitly requested (final validation) + // Applied once at the end after all other transformations including maxLength slice + if (trimTrailing) { sanitized = sanitized.replace(/[-_]+$/, ''); } @@ -93,6 +100,12 @@ export function CreateWorktreeDialog({ const [baseBranch, setBaseBranch] = useState(PROJECT_DEFAULT_BRANCH); const [projectDefaultBranch, setProjectDefaultBranch] = useState(''); + // Sanitized name for validation (without display fallback) + const sanitizedName = useMemo(() => sanitizeWorktreeName(name, undefined, true), [name]); + + // Preview name with fallback for display (using i18n) + const previewName = sanitizedName || t('terminal:worktree.namePlaceholder'); + // Fetch branches when dialog opens useEffect(() => { if (!open || !projectPath) return; @@ -138,8 +151,11 @@ export function CreateWorktreeDialog({ }, [open, projectPath, project?.settings?.mainBranch]); const handleNameChange = useCallback((e: React.ChangeEvent) => { - const sanitized = sanitizeWorktreeName(e.target.value); - setName(sanitized); + // Apply lowercase and convert spaces to hyphens as user types + // This reduces the visual gap between input and preview + // Full sanitization (removing invalid chars) happens on submit + const rawValue = e.target.value.toLowerCase().replace(/\s+/g, '-'); + setName(rawValue); setError(null); }, []); @@ -153,21 +169,25 @@ export function CreateWorktreeDialog({ if (!name) { const task = backlogTasks.find(t => t.id === taskId); if (task) { - const autoName = sanitizeWorktreeName(task.title, 40); + // Trim trailing when auto-filling from task title (complete value) + const autoName = sanitizeWorktreeName(task.title, 40, true); setName(autoName); } } }, [backlogTasks, name]); const handleCreate = async () => { - if (!name.trim()) { + // Final sanitization: trim trailing hyphens/underscores for submission + const finalName = sanitizeWorktreeName(name, undefined, true); + + if (!finalName) { setError(t('terminal:worktree.nameRequired')); return; } // Validate name format - allow letters, numbers, dashes, and underscores // Must start and end with letter or number (matching backend WORKTREE_NAME_REGEX) - if (!/^[a-z0-9][a-z0-9_-]*[a-z0-9]$/.test(name) && !/^[a-z0-9]$/.test(name)) { + if (!/^[a-z0-9][a-z0-9_-]*[a-z0-9]$/.test(finalName) && !/^[a-z0-9]$/.test(finalName)) { setError(t('terminal:worktree.nameInvalid')); return; } @@ -178,7 +198,7 @@ export function CreateWorktreeDialog({ try { const result = await window.electronAPI.createTerminalWorktree({ terminalId, - name: name.trim(), + name: finalName, taskId: selectedTaskId, createGitBranch, projectPath, @@ -297,7 +317,7 @@ export function CreateWorktreeDialog({ {t('terminal:worktree.createBranch')}

- {t('terminal:worktree.branchHelp', { branch: `terminal/${name || 'name'}` })} + {t('terminal:worktree.branchHelp', { branch: `terminal/${previewName}` })}

onOpenChange(false)} disabled={isCreating}> {t('common:buttons.cancel')} - )} {!isClaudeMode && status !== 'exited' && ( )} {/* Expand/collapse button */}