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.
This commit is contained in:
webdevcody
2026-01-19 17:35:01 -05:00
parent d7f6e72a9e
commit 43481c2bab
9 changed files with 35 additions and 9 deletions

View File

@@ -39,7 +39,10 @@ export function createDiffsHandler() {
} }
// Git worktrees are stored in project directory // 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 { try {
// Check if worktree exists // Check if worktree exists

View File

@@ -37,7 +37,10 @@ export function createFileDiffHandler() {
} }
// Git worktrees are stored in project directory // 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 { try {
await secureFs.access(worktreePath); await secureFs.access(worktreePath);

View File

@@ -28,7 +28,10 @@ export function createInfoHandler() {
} }
// Check if worktree exists (git worktrees are stored in project directory) // 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 { try {
await secureFs.access(worktreePath); await secureFs.access(worktreePath);
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', { const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {

View File

@@ -28,7 +28,10 @@ export function createStatusHandler() {
} }
// Git worktrees are stored in project directory // 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 { try {
await secureFs.access(worktreePath); await secureFs.access(worktreePath);

View File

@@ -2060,7 +2060,9 @@ Address the follow-up instructions above. Review the previous work and make the
const feature = await this.loadFeature(projectPath, featureId); const feature = await this.loadFeature(projectPath, featureId);
// Worktrees are in project dir // 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; let workDir = projectPath;
try { try {
@@ -2143,7 +2145,9 @@ Address the follow-up instructions above. Review the previous work and make the
} }
} else { } else {
// Fallback: try to find worktree at legacy location // 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 { try {
await secureFs.access(legacyWorktreePath); await secureFs.access(legacyWorktreePath);
workDir = legacyWorktreePath; workDir = legacyWorktreePath;
@@ -2429,22 +2433,25 @@ Format your response as a structured markdown document.`;
provider?: ModelProvider; provider?: ModelProvider;
title?: string; title?: string;
description?: string; description?: string;
branchName?: string;
}> }>
> { > {
const agents = await Promise.all( const agents = await Promise.all(
Array.from(this.runningFeatures.values()).map(async (rf) => { 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 title: string | undefined;
let description: string | undefined; let description: string | undefined;
let branchName: string | undefined;
try { try {
const feature = await this.featureLoader.get(rf.projectPath, rf.featureId); const feature = await this.featureLoader.get(rf.projectPath, rf.featureId);
if (feature) { if (feature) {
title = feature.title; title = feature.title;
description = feature.description; description = feature.description;
branchName = feature.branchName;
} }
} catch (error) { } catch (error) {
// Silently ignore errors - title/description are optional // Silently ignore errors - title/description/branchName are optional
} }
return { return {
@@ -2456,6 +2463,7 @@ Format your response as a structured markdown document.`;
provider: rf.provider, provider: rf.provider,
title, title,
description, description,
branchName,
}; };
}) })
); );

View File

@@ -1415,6 +1415,7 @@ export function BoardView() {
featureId={outputFeature?.id || ''} featureId={outputFeature?.id || ''}
featureStatus={outputFeature?.status} featureStatus={outputFeature?.status}
onNumberKeyPress={handleOutputModalNumberKeyPress} onNumberKeyPress={handleOutputModalNumberKeyPress}
branchName={outputFeature?.branchName}
/> />
{/* Archive All Verified Dialog */} {/* Archive All Verified Dialog */}

View File

@@ -28,6 +28,8 @@ interface AgentOutputModalProps {
onNumberKeyPress?: (key: string) => void; onNumberKeyPress?: (key: string) => void;
/** Project path - if not provided, falls back to window.__currentProject for backward compatibility */ /** Project path - if not provided, falls back to window.__currentProject for backward compatibility */
projectPath?: string; projectPath?: string;
/** Branch name for the feature worktree - used when viewing changes */
branchName?: string;
} }
type ViewMode = 'summary' | 'parsed' | 'raw' | 'changes'; type ViewMode = 'summary' | 'parsed' | 'raw' | 'changes';
@@ -40,6 +42,7 @@ export function AgentOutputModal({
featureStatus, featureStatus,
onNumberKeyPress, onNumberKeyPress,
projectPath: projectPathProp, projectPath: projectPathProp,
branchName,
}: AgentOutputModalProps) { }: AgentOutputModalProps) {
const isBacklogPlan = featureId.startsWith('backlog-plan:'); const isBacklogPlan = featureId.startsWith('backlog-plan:');
const [output, setOutput] = useState<string>(''); const [output, setOutput] = useState<string>('');
@@ -433,7 +436,7 @@ export function AgentOutputModal({
{projectPath ? ( {projectPath ? (
<GitDiffPanel <GitDiffPanel
projectPath={projectPath} projectPath={projectPath}
featureId={featureId} featureId={branchName || featureId}
compact={false} compact={false}
useWorktrees={useWorktrees} useWorktrees={useWorktrees}
className="border-0 rounded-lg" className="border-0 rounded-lg"

View File

@@ -418,6 +418,7 @@ export function GraphViewPage() {
featureId={outputFeature?.id || ''} featureId={outputFeature?.id || ''}
featureStatus={outputFeature?.status} featureStatus={outputFeature?.status}
onNumberKeyPress={handleOutputModalNumberKeyPress} onNumberKeyPress={handleOutputModalNumberKeyPress}
branchName={outputFeature?.branchName}
/> />
{/* Backlog Plan Dialog */} {/* Backlog Plan Dialog */}

View File

@@ -280,6 +280,7 @@ export function RunningAgentsView() {
} }
featureId={selectedAgent.featureId} featureId={selectedAgent.featureId}
featureStatus="running" featureStatus="running"
branchName={selectedAgent.branchName}
/> />
)} )}
</div> </div>