diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 37b8089b..ab61b641 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -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)); diff --git a/apps/server/src/providers/opencode-provider.ts b/apps/server/src/providers/opencode-provider.ts index 8c58da15..c38e3399 100644 --- a/apps/server/src/providers/opencode-provider.ts +++ b/apps/server/src/providers/opencode-provider.ts @@ -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})`; } /** diff --git a/apps/server/src/routes/worktree/index.ts b/apps/server/src/routes/worktree/index.ts index 2525c831..d786616b 100644 --- a/apps/server/src/routes/worktree/index.ts +++ b/apps/server/src/routes/worktree/index.ts @@ -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( diff --git a/apps/server/src/routes/worktree/routes/delete.ts b/apps/server/src/routes/worktree/routes/delete.ts index fcb42f59..034be28e 100644 --- a/apps/server/src/routes/worktree/routes/delete.ts +++ b/apps/server/src/routes/worktree/routes/delete.ts @@ -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 => { 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) { diff --git a/apps/server/src/services/feature-loader.ts b/apps/server/src/services/feature-loader.ts index 5b21e44b..31eca4a7 100644 --- a/apps/server/src/services/feature-loader.ts +++ b/apps/server/src/services/feature-loader.ts @@ -378,6 +378,7 @@ export class FeatureLoader { description: featureData.description || '', ...featureData, id: featureId, + createdAt: featureData.createdAt || new Date().toISOString(), imagePaths: migratedImagePaths, descriptionHistory: initialHistory, }; diff --git a/apps/ui/src/components/dialogs/board-background-modal.tsx b/apps/ui/src/components/dialogs/board-background-modal.tsx index 208d2059..c923e751 100644 --- a/apps/ui/src/components/dialogs/board-background-modal.tsx +++ b/apps/ui/src/components/dialogs/board-background-modal.tsx @@ -312,7 +312,12 @@ export function BoardBackgroundModal({ open, onOpenChange }: BoardBackgroundModa return ( - + Board Background Settings diff --git a/apps/ui/src/components/ui/sheet.tsx b/apps/ui/src/components/ui/sheet.tsx index 08e4d70c..fe58101e 100644 --- a/apps/ui/src/components/ui/sheet.tsx +++ b/apps/ui/src/components/ui/sheet.tsx @@ -57,6 +57,8 @@ const SheetContent = ({ className, children, side = 'right', ...props }: SheetCo const Close = SheetPrimitive.Close as React.ComponentType<{ className: string; children: React.ReactNode; + 'data-slot'?: string; + style?: React.CSSProperties; }>; return ( @@ -79,7 +81,13 @@ const SheetContent = ({ className, children, side = 'right', ...props }: SheetCo {...props} > {children} - + Close diff --git a/apps/ui/src/components/views/agent-view/components/agent-header.tsx b/apps/ui/src/components/views/agent-view/components/agent-header.tsx index cfb8cd0a..a1e7aa69 100644 --- a/apps/ui/src/components/views/agent-view/components/agent-header.tsx +++ b/apps/ui/src/components/views/agent-view/components/agent-header.tsx @@ -27,22 +27,22 @@ export function AgentHeader({ worktreeBranch, }: AgentHeaderProps) { return ( -
-
-
+
+
+
-
+

AI Agent

-
- +
+ {projectName} {currentSessionId && !isConnected && ' - Connecting...'} {worktreeBranch && ( - + - {worktreeBranch} + {worktreeBranch} )}
@@ -50,9 +50,9 @@ export function AgentHeader({
{/* Status indicators & actions */} -
+
{currentTool && ( -
+
{currentTool}
@@ -63,10 +63,11 @@ export function AgentHeader({ size="sm" onClick={onClearChat} disabled={isProcessing} - className="text-muted-foreground hover:text-foreground" + aria-label="Clear chat" + className="text-muted-foreground hover:text-foreground h-8 w-8 p-0 sm:w-auto sm:px-3" > - - Clear + + Clear )}
+ diff --git a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx index f0dde39d..bbbafdd7 100644 --- a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx @@ -85,7 +85,10 @@ export function PlanApprovalDialog({ return ( - + {viewOnly ? 'View Plan' : 'Review Plan'} @@ -146,12 +149,12 @@ export function PlanApprovalDialog({ )} {/* Plan Content */} -
+
{isEditMode && !viewOnly ? (