From 43481c2bab3295f74802af1d48761d24454c106f Mon Sep 17 00:00:00 2001 From: webdevcody Date: Mon, 19 Jan 2026 17:35:01 -0500 Subject: [PATCH] refactor: sanitize featureId for worktree paths across multiple handlers - Updated createDiffsHandler, createFileDiffHandler, createInfoHandler, createStatusHandler, and auto-mode service to sanitize featureId when constructing worktree paths. - Ensured consistent handling of featureId to prevent issues with invalid characters in branch names. - Added branchName support in UI components to enhance feature visibility and management. This change improves the robustness of worktree operations and enhances user experience by ensuring valid paths are used throughout the application. --- apps/server/src/routes/worktree/routes/diffs.ts | 5 ++++- .../src/routes/worktree/routes/file-diff.ts | 5 ++++- apps/server/src/routes/worktree/routes/info.ts | 5 ++++- apps/server/src/routes/worktree/routes/status.ts | 5 ++++- apps/server/src/services/auto-mode-service.ts | 16 ++++++++++++---- apps/ui/src/components/views/board-view.tsx | 1 + .../board-view/dialogs/agent-output-modal.tsx | 5 ++++- apps/ui/src/components/views/graph-view-page.tsx | 1 + .../src/components/views/running-agents-view.tsx | 1 + 9 files changed, 35 insertions(+), 9 deletions(-) diff --git a/apps/server/src/routes/worktree/routes/diffs.ts b/apps/server/src/routes/worktree/routes/diffs.ts index 75f43d7f..314fa8ce 100644 --- a/apps/server/src/routes/worktree/routes/diffs.ts +++ b/apps/server/src/routes/worktree/routes/diffs.ts @@ -39,7 +39,10 @@ export function createDiffsHandler() { } // Git worktrees are stored in project directory - const worktreePath = path.join(projectPath, '.worktrees', featureId); + // Sanitize featureId the same way it's sanitized when creating worktrees + // (see create.ts: branchName.replace(/[^a-zA-Z0-9_-]/g, '-')) + const sanitizedFeatureId = featureId.replace(/[^a-zA-Z0-9_-]/g, '-'); + const worktreePath = path.join(projectPath, '.worktrees', sanitizedFeatureId); try { // Check if worktree exists diff --git a/apps/server/src/routes/worktree/routes/file-diff.ts b/apps/server/src/routes/worktree/routes/file-diff.ts index 4d29eb26..f3d4ed1a 100644 --- a/apps/server/src/routes/worktree/routes/file-diff.ts +++ b/apps/server/src/routes/worktree/routes/file-diff.ts @@ -37,7 +37,10 @@ export function createFileDiffHandler() { } // Git worktrees are stored in project directory - const worktreePath = path.join(projectPath, '.worktrees', featureId); + // Sanitize featureId the same way it's sanitized when creating worktrees + // (see create.ts: branchName.replace(/[^a-zA-Z0-9_-]/g, '-')) + const sanitizedFeatureId = featureId.replace(/[^a-zA-Z0-9_-]/g, '-'); + const worktreePath = path.join(projectPath, '.worktrees', sanitizedFeatureId); try { await secureFs.access(worktreePath); diff --git a/apps/server/src/routes/worktree/routes/info.ts b/apps/server/src/routes/worktree/routes/info.ts index 3d512452..5c2eb808 100644 --- a/apps/server/src/routes/worktree/routes/info.ts +++ b/apps/server/src/routes/worktree/routes/info.ts @@ -28,7 +28,10 @@ export function createInfoHandler() { } // Check if worktree exists (git worktrees are stored in project directory) - const worktreePath = path.join(projectPath, '.worktrees', featureId); + // Sanitize featureId the same way it's sanitized when creating worktrees + // (see create.ts: branchName.replace(/[^a-zA-Z0-9_-]/g, '-')) + const sanitizedFeatureId = featureId.replace(/[^a-zA-Z0-9_-]/g, '-'); + const worktreePath = path.join(projectPath, '.worktrees', sanitizedFeatureId); try { await secureFs.access(worktreePath); const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', { diff --git a/apps/server/src/routes/worktree/routes/status.ts b/apps/server/src/routes/worktree/routes/status.ts index f9d6bf88..b44c5ae4 100644 --- a/apps/server/src/routes/worktree/routes/status.ts +++ b/apps/server/src/routes/worktree/routes/status.ts @@ -28,7 +28,10 @@ export function createStatusHandler() { } // Git worktrees are stored in project directory - const worktreePath = path.join(projectPath, '.worktrees', featureId); + // Sanitize featureId the same way it's sanitized when creating worktrees + // (see create.ts: branchName.replace(/[^a-zA-Z0-9_-]/g, '-')) + const sanitizedFeatureId = featureId.replace(/[^a-zA-Z0-9_-]/g, '-'); + const worktreePath = path.join(projectPath, '.worktrees', sanitizedFeatureId); try { await secureFs.access(worktreePath); diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 606660c3..28498829 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -2060,7 +2060,9 @@ Address the follow-up instructions above. Review the previous work and make the const feature = await this.loadFeature(projectPath, featureId); // Worktrees are in project dir - const worktreePath = path.join(projectPath, '.worktrees', featureId); + // Sanitize featureId the same way it's sanitized when creating worktrees + const sanitizedFeatureId = featureId.replace(/[^a-zA-Z0-9_-]/g, '-'); + const worktreePath = path.join(projectPath, '.worktrees', sanitizedFeatureId); let workDir = projectPath; try { @@ -2143,7 +2145,9 @@ Address the follow-up instructions above. Review the previous work and make the } } else { // Fallback: try to find worktree at legacy location - const legacyWorktreePath = path.join(projectPath, '.worktrees', featureId); + // Sanitize featureId the same way it's sanitized when creating worktrees + const sanitizedFeatureId = featureId.replace(/[^a-zA-Z0-9_-]/g, '-'); + const legacyWorktreePath = path.join(projectPath, '.worktrees', sanitizedFeatureId); try { await secureFs.access(legacyWorktreePath); workDir = legacyWorktreePath; @@ -2429,22 +2433,25 @@ Format your response as a structured markdown document.`; provider?: ModelProvider; title?: string; description?: string; + branchName?: string; }> > { const agents = await Promise.all( Array.from(this.runningFeatures.values()).map(async (rf) => { - // Try to fetch feature data to get title and description + // Try to fetch feature data to get title, description, and branchName let title: string | undefined; let description: string | undefined; + let branchName: string | undefined; try { const feature = await this.featureLoader.get(rf.projectPath, rf.featureId); if (feature) { title = feature.title; description = feature.description; + branchName = feature.branchName; } } catch (error) { - // Silently ignore errors - title/description are optional + // Silently ignore errors - title/description/branchName are optional } return { @@ -2456,6 +2463,7 @@ Format your response as a structured markdown document.`; provider: rf.provider, title, description, + branchName, }; }) ); diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 17d44d2b..c72fc8de 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -1415,6 +1415,7 @@ export function BoardView() { featureId={outputFeature?.id || ''} featureStatus={outputFeature?.status} onNumberKeyPress={handleOutputModalNumberKeyPress} + branchName={outputFeature?.branchName} /> {/* Archive All Verified Dialog */} diff --git a/apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx b/apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx index ba78f1c8..cfb34f18 100644 --- a/apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx @@ -28,6 +28,8 @@ interface AgentOutputModalProps { onNumberKeyPress?: (key: string) => void; /** Project path - if not provided, falls back to window.__currentProject for backward compatibility */ projectPath?: string; + /** Branch name for the feature worktree - used when viewing changes */ + branchName?: string; } type ViewMode = 'summary' | 'parsed' | 'raw' | 'changes'; @@ -40,6 +42,7 @@ export function AgentOutputModal({ featureStatus, onNumberKeyPress, projectPath: projectPathProp, + branchName, }: AgentOutputModalProps) { const isBacklogPlan = featureId.startsWith('backlog-plan:'); const [output, setOutput] = useState(''); @@ -433,7 +436,7 @@ export function AgentOutputModal({ {projectPath ? ( {/* Backlog Plan Dialog */} diff --git a/apps/ui/src/components/views/running-agents-view.tsx b/apps/ui/src/components/views/running-agents-view.tsx index b77518d0..883609db 100644 --- a/apps/ui/src/components/views/running-agents-view.tsx +++ b/apps/ui/src/components/views/running-agents-view.tsx @@ -280,6 +280,7 @@ export function RunningAgentsView() { } featureId={selectedAgent.featureId} featureStatus="running" + branchName={selectedAgent.branchName} /> )}