mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-17 10:03:08 +00:00
Fix orphaned features when deleting worktrees (#820)
* Changes from fix/orphaned-features * fix: Handle feature migration failures and improve UI accessibility * feat: Add event emission for worktree deletion and feature migration * fix: Handle OpenCode model errors and prevent duplicate model IDs * feat: Add summary dialog and async verify with loading state * fix: Add type attributes to buttons and improve OpenCode model selection * fix: Add null checks for onVerify callback and opencode model selection
This commit is contained in:
@@ -493,7 +493,7 @@ app.use(
|
||||
);
|
||||
app.use('/api/auto-mode', createAutoModeRoutes(autoModeService));
|
||||
app.use('/api/enhance-prompt', createEnhancePromptRoutes(settingsService));
|
||||
app.use('/api/worktree', createWorktreeRoutes(events, settingsService));
|
||||
app.use('/api/worktree', createWorktreeRoutes(events, settingsService, featureLoader));
|
||||
app.use('/api/git', createGitRoutes());
|
||||
app.use('/api/models', createModelsRoutes());
|
||||
app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events, settingsService));
|
||||
|
||||
@@ -1189,8 +1189,26 @@ export class OpencodeProvider extends CliProvider {
|
||||
* Format a display name for a model
|
||||
*/
|
||||
private formatModelDisplayName(model: OpenCodeModelInfo): string {
|
||||
// Extract the last path segment for nested model IDs
|
||||
// e.g., "arcee-ai/trinity-large-preview:free" → "trinity-large-preview:free"
|
||||
let rawName = model.name;
|
||||
if (rawName.includes('/')) {
|
||||
rawName = rawName.split('/').pop()!;
|
||||
}
|
||||
|
||||
// Strip tier/pricing suffixes like ":free", ":extended"
|
||||
const colonIdx = rawName.indexOf(':');
|
||||
let suffix = '';
|
||||
if (colonIdx !== -1) {
|
||||
const tierPart = rawName.slice(colonIdx + 1);
|
||||
if (/^(free|extended|beta|preview)$/i.test(tierPart)) {
|
||||
suffix = ` (${tierPart.charAt(0).toUpperCase() + tierPart.slice(1)})`;
|
||||
}
|
||||
rawName = rawName.slice(0, colonIdx);
|
||||
}
|
||||
|
||||
// Capitalize and format the model name
|
||||
const formattedName = model.name
|
||||
const formattedName = rawName
|
||||
.split('-')
|
||||
.map((part) => {
|
||||
// Handle version numbers like "4-5" -> "4.5"
|
||||
@@ -1218,7 +1236,7 @@ export class OpencodeProvider extends CliProvider {
|
||||
};
|
||||
|
||||
const providerDisplay = providerNames[model.provider] || model.provider;
|
||||
return `${formattedName} (${providerDisplay})`;
|
||||
return `${formattedName}${suffix} (${providerDisplay})`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -71,10 +71,12 @@ import { createSetTrackingHandler } from './routes/set-tracking.js';
|
||||
import { createSyncHandler } from './routes/sync.js';
|
||||
import { createUpdatePRNumberHandler } from './routes/update-pr-number.js';
|
||||
import type { SettingsService } from '../../services/settings-service.js';
|
||||
import type { FeatureLoader } from '../../services/feature-loader.js';
|
||||
|
||||
export function createWorktreeRoutes(
|
||||
events: EventEmitter,
|
||||
settingsService?: SettingsService
|
||||
settingsService?: SettingsService,
|
||||
featureLoader?: FeatureLoader
|
||||
): Router {
|
||||
const router = Router();
|
||||
|
||||
@@ -94,7 +96,11 @@ export function createWorktreeRoutes(
|
||||
validatePathParams('projectPath'),
|
||||
createCreateHandler(events, settingsService)
|
||||
);
|
||||
router.post('/delete', validatePathParams('projectPath', 'worktreePath'), createDeleteHandler());
|
||||
router.post(
|
||||
'/delete',
|
||||
validatePathParams('projectPath', 'worktreePath'),
|
||||
createDeleteHandler(events, featureLoader)
|
||||
);
|
||||
router.post('/create-pr', createCreatePRHandler());
|
||||
router.post('/pr-info', createPRInfoHandler());
|
||||
router.post(
|
||||
|
||||
@@ -10,11 +10,13 @@ import { isGitRepo } from '@automaker/git-utils';
|
||||
import { getErrorMessage, logError, isValidBranchName } from '../common.js';
|
||||
import { execGitCommand } from '../../../lib/git.js';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import type { FeatureLoader } from '../../../services/feature-loader.js';
|
||||
import type { EventEmitter } from '../../../lib/events.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const logger = createLogger('Worktree');
|
||||
|
||||
export function createDeleteHandler() {
|
||||
export function createDeleteHandler(events: EventEmitter, featureLoader?: FeatureLoader) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, worktreePath, deleteBranch } = req.body as {
|
||||
@@ -134,12 +136,65 @@ export function createDeleteHandler() {
|
||||
}
|
||||
}
|
||||
|
||||
// Emit worktree:deleted event after successful deletion
|
||||
events.emit('worktree:deleted', {
|
||||
worktreePath,
|
||||
projectPath,
|
||||
branchName,
|
||||
branchDeleted,
|
||||
});
|
||||
|
||||
// Move features associated with the deleted branch to the main worktree
|
||||
// This prevents features from being orphaned when a worktree is deleted
|
||||
let featuresMovedToMain = 0;
|
||||
if (featureLoader && branchName) {
|
||||
try {
|
||||
const allFeatures = await featureLoader.getAll(projectPath);
|
||||
const affectedFeatures = allFeatures.filter((f) => f.branchName === branchName);
|
||||
for (const feature of affectedFeatures) {
|
||||
try {
|
||||
await featureLoader.update(projectPath, feature.id, {
|
||||
branchName: null,
|
||||
});
|
||||
featuresMovedToMain++;
|
||||
// Emit feature:migrated event for each successfully migrated feature
|
||||
events.emit('feature:migrated', {
|
||||
featureId: feature.id,
|
||||
status: 'migrated',
|
||||
fromBranch: branchName,
|
||||
toWorktreeId: null, // migrated to main worktree (no specific worktree)
|
||||
projectPath,
|
||||
});
|
||||
} catch (featureUpdateError) {
|
||||
// Non-fatal: log per-feature failure but continue migrating others
|
||||
logger.warn('Failed to move feature to main worktree after deletion', {
|
||||
error: getErrorMessage(featureUpdateError),
|
||||
featureId: feature.id,
|
||||
branchName,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (featuresMovedToMain > 0) {
|
||||
logger.info(
|
||||
`Moved ${featuresMovedToMain} feature(s) to main worktree after deleting worktree with branch: ${branchName}`
|
||||
);
|
||||
}
|
||||
} catch (featureError) {
|
||||
// Non-fatal: log but don't fail the deletion (getAll failed)
|
||||
logger.warn('Failed to load features for migration to main worktree after deletion', {
|
||||
error: getErrorMessage(featureError),
|
||||
branchName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
deleted: {
|
||||
worktreePath,
|
||||
branch: branchDeleted ? branchName : null,
|
||||
branchDeleted,
|
||||
featuresMovedToMain,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -378,6 +378,7 @@ export class FeatureLoader {
|
||||
description: featureData.description || '',
|
||||
...featureData,
|
||||
id: featureId,
|
||||
createdAt: featureData.createdAt || new Date().toISOString(),
|
||||
imagePaths: migratedImagePaths,
|
||||
descriptionHistory: initialHistory,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user