diff --git a/apps/server/src/routes/backlog-plan/common.ts b/apps/server/src/routes/backlog-plan/common.ts index 74fb44c8..98142c30 100644 --- a/apps/server/src/routes/backlog-plan/common.ts +++ b/apps/server/src/routes/backlog-plan/common.ts @@ -3,12 +3,31 @@ */ import { createLogger } from '@automaker/utils'; +import { ensureAutomakerDir, getAutomakerDir } from '@automaker/platform'; +import * as secureFs from '../../lib/secure-fs.js'; +import path from 'path'; +import type { BacklogPlanResult } from '@automaker/types'; const logger = createLogger('BacklogPlan'); // State for tracking running generation let isRunning = false; let currentAbortController: AbortController | null = null; +let runningDetails: { + projectPath: string; + prompt: string; + model?: string; + startedAt: string; +} | null = null; + +const BACKLOG_PLAN_FILENAME = 'backlog-plan.json'; + +export interface StoredBacklogPlan { + savedAt: string; + prompt: string; + model?: string; + result: BacklogPlanResult; +} export function getBacklogPlanStatus(): { isRunning: boolean } { return { isRunning }; @@ -16,11 +35,67 @@ export function getBacklogPlanStatus(): { isRunning: boolean } { export function setRunningState(running: boolean, abortController?: AbortController | null): void { isRunning = running; + if (!running) { + runningDetails = null; + } if (abortController !== undefined) { currentAbortController = abortController; } } +export function setRunningDetails( + details: { + projectPath: string; + prompt: string; + model?: string; + startedAt: string; + } | null +): void { + runningDetails = details; +} + +export function getRunningDetails(): { + projectPath: string; + prompt: string; + model?: string; + startedAt: string; +} | null { + return runningDetails; +} + +function getBacklogPlanPath(projectPath: string): string { + return path.join(getAutomakerDir(projectPath), BACKLOG_PLAN_FILENAME); +} + +export async function saveBacklogPlan(projectPath: string, plan: StoredBacklogPlan): Promise { + await ensureAutomakerDir(projectPath); + const filePath = getBacklogPlanPath(projectPath); + await secureFs.writeFile(filePath, JSON.stringify(plan, null, 2), 'utf-8'); +} + +export async function loadBacklogPlan(projectPath: string): Promise { + try { + const filePath = getBacklogPlanPath(projectPath); + const raw = await secureFs.readFile(filePath, 'utf-8'); + const parsed = JSON.parse(raw) as StoredBacklogPlan; + if (!parsed?.result?.changes) { + return null; + } + return parsed; + } catch { + return null; + } +} + +export async function clearBacklogPlan(projectPath: string): Promise { + try { + const filePath = getBacklogPlanPath(projectPath); + await secureFs.unlink(filePath); + } catch { + // ignore missing file + } +} + export function getAbortController(): AbortController | null { return currentAbortController; } diff --git a/apps/server/src/routes/backlog-plan/generate-plan.ts b/apps/server/src/routes/backlog-plan/generate-plan.ts index d8235e50..bf99d2a4 100644 --- a/apps/server/src/routes/backlog-plan/generate-plan.ts +++ b/apps/server/src/routes/backlog-plan/generate-plan.ts @@ -17,7 +17,7 @@ import { resolvePhaseModel } from '@automaker/model-resolver'; import { FeatureLoader } from '../../services/feature-loader.js'; import { ProviderFactory } from '../../providers/provider-factory.js'; import { extractJsonWithArray } from '../../lib/json-extractor.js'; -import { logger, setRunningState, getErrorMessage } from './common.js'; +import { logger, setRunningState, getErrorMessage, saveBacklogPlan } from './common.js'; import type { SettingsService } from '../../services/settings-service.js'; import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js'; @@ -200,6 +200,13 @@ ${userPrompt}`; // Parse the response const result = parsePlanResponse(responseText); + await saveBacklogPlan(projectPath, { + savedAt: new Date().toISOString(), + prompt, + model: effectiveModel, + result, + }); + events.emit('backlog-plan:event', { type: 'backlog_plan_complete', result, diff --git a/apps/server/src/routes/backlog-plan/index.ts b/apps/server/src/routes/backlog-plan/index.ts index 393296df..4ab9e71d 100644 --- a/apps/server/src/routes/backlog-plan/index.ts +++ b/apps/server/src/routes/backlog-plan/index.ts @@ -9,6 +9,7 @@ import { createGenerateHandler } from './routes/generate.js'; import { createStopHandler } from './routes/stop.js'; import { createStatusHandler } from './routes/status.js'; import { createApplyHandler } from './routes/apply.js'; +import { createClearHandler } from './routes/clear.js'; import type { SettingsService } from '../../services/settings-service.js'; export function createBacklogPlanRoutes( @@ -23,8 +24,9 @@ export function createBacklogPlanRoutes( createGenerateHandler(events, settingsService) ); router.post('/stop', createStopHandler()); - router.get('/status', createStatusHandler()); + router.get('/status', validatePathParams('projectPath'), createStatusHandler()); router.post('/apply', validatePathParams('projectPath'), createApplyHandler()); + router.post('/clear', validatePathParams('projectPath'), createClearHandler()); return router; } diff --git a/apps/server/src/routes/backlog-plan/routes/apply.ts b/apps/server/src/routes/backlog-plan/routes/apply.ts index b6c257a0..d2b45f40 100644 --- a/apps/server/src/routes/backlog-plan/routes/apply.ts +++ b/apps/server/src/routes/backlog-plan/routes/apply.ts @@ -5,7 +5,7 @@ import type { Request, Response } from 'express'; import type { BacklogPlanResult, BacklogChange, Feature } from '@automaker/types'; import { FeatureLoader } from '../../../services/feature-loader.js'; -import { getErrorMessage, logError, logger } from '../common.js'; +import { clearBacklogPlan, getErrorMessage, logError, logger } from '../common.js'; const featureLoader = new FeatureLoader(); @@ -151,6 +151,8 @@ export function createApplyHandler() { success: true, appliedChanges, }); + + await clearBacklogPlan(projectPath); } catch (error) { logError(error, 'Apply backlog plan failed'); res.status(500).json({ success: false, error: getErrorMessage(error) }); diff --git a/apps/server/src/routes/backlog-plan/routes/clear.ts b/apps/server/src/routes/backlog-plan/routes/clear.ts new file mode 100644 index 00000000..855dc507 --- /dev/null +++ b/apps/server/src/routes/backlog-plan/routes/clear.ts @@ -0,0 +1,25 @@ +/** + * POST /clear endpoint - Clear saved backlog plan + */ + +import type { Request, Response } from 'express'; +import { clearBacklogPlan, getErrorMessage, logError } from '../common.js'; + +export function createClearHandler() { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath } = req.body as { projectPath: string }; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath required' }); + return; + } + + await clearBacklogPlan(projectPath); + res.json({ success: true }); + } catch (error) { + logError(error, 'Clear backlog plan failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/backlog-plan/routes/generate.ts b/apps/server/src/routes/backlog-plan/routes/generate.ts index b596576f..59438538 100644 --- a/apps/server/src/routes/backlog-plan/routes/generate.ts +++ b/apps/server/src/routes/backlog-plan/routes/generate.ts @@ -4,7 +4,13 @@ import type { Request, Response } from 'express'; import type { EventEmitter } from '../../../lib/events.js'; -import { getBacklogPlanStatus, setRunningState, getErrorMessage, logError } from '../common.js'; +import { + getBacklogPlanStatus, + setRunningState, + setRunningDetails, + getErrorMessage, + logError, +} from '../common.js'; import { generateBacklogPlan } from '../generate-plan.js'; import type { SettingsService } from '../../../services/settings-service.js'; @@ -37,6 +43,12 @@ export function createGenerateHandler(events: EventEmitter, settingsService?: Se } setRunningState(true); + setRunningDetails({ + projectPath, + prompt, + model, + startedAt: new Date().toISOString(), + }); const abortController = new AbortController(); setRunningState(true, abortController); diff --git a/apps/server/src/routes/backlog-plan/routes/status.ts b/apps/server/src/routes/backlog-plan/routes/status.ts index 3b1684d3..5f20f1e2 100644 --- a/apps/server/src/routes/backlog-plan/routes/status.ts +++ b/apps/server/src/routes/backlog-plan/routes/status.ts @@ -3,13 +3,15 @@ */ import type { Request, Response } from 'express'; -import { getBacklogPlanStatus, getErrorMessage, logError } from '../common.js'; +import { getBacklogPlanStatus, loadBacklogPlan, getErrorMessage, logError } from '../common.js'; export function createStatusHandler() { - return async (_req: Request, res: Response): Promise => { + return async (req: Request, res: Response): Promise => { try { const status = getBacklogPlanStatus(); - res.json({ success: true, ...status }); + const projectPath = typeof req.query.projectPath === 'string' ? req.query.projectPath : ''; + const savedPlan = projectPath ? await loadBacklogPlan(projectPath) : null; + res.json({ success: true, ...status, savedPlan }); } catch (error) { logError(error, 'Get backlog plan status failed'); res.status(500).json({ success: false, error: getErrorMessage(error) }); diff --git a/apps/server/src/routes/running-agents/routes/index.ts b/apps/server/src/routes/running-agents/routes/index.ts index 693eba8b..955ac93d 100644 --- a/apps/server/src/routes/running-agents/routes/index.ts +++ b/apps/server/src/routes/running-agents/routes/index.ts @@ -4,12 +4,27 @@ import type { Request, Response } from 'express'; import type { AutoModeService } from '../../../services/auto-mode-service.js'; +import { getBacklogPlanStatus, getRunningDetails } from '../../backlog-plan/common.js'; +import path from 'path'; import { getErrorMessage, logError } from '../common.js'; export function createIndexHandler(autoModeService: AutoModeService) { return async (_req: Request, res: Response): Promise => { try { - const runningAgents = await autoModeService.getRunningAgents(); + const runningAgents = [...(await autoModeService.getRunningAgents())]; + const backlogPlanStatus = getBacklogPlanStatus(); + const backlogPlanDetails = getRunningDetails(); + + if (backlogPlanStatus.isRunning && backlogPlanDetails) { + runningAgents.push({ + featureId: `backlog-plan:${backlogPlanDetails.projectPath}`, + projectPath: backlogPlanDetails.projectPath, + projectName: path.basename(backlogPlanDetails.projectPath), + isAutoMode: false, + title: 'Backlog plan', + description: backlogPlanDetails.prompt, + }); + } res.json({ success: true, diff --git a/apps/ui/src/components/layout/project-switcher/project-switcher.tsx b/apps/ui/src/components/layout/project-switcher/project-switcher.tsx index 442413fd..0713df72 100644 --- a/apps/ui/src/components/layout/project-switcher/project-switcher.tsx +++ b/apps/ui/src/components/layout/project-switcher/project-switcher.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useEffect } from 'react'; -import { Plus, Bug, FolderOpen } from 'lucide-react'; -import { useNavigate } from '@tanstack/react-router'; +import { Plus, Bug, FolderOpen, BookOpen } from 'lucide-react'; +import { useNavigate, useLocation } from '@tanstack/react-router'; import { cn } from '@/lib/utils'; import { useAppStore, type ThemeMode } from '@/store/app-store'; import { useOSDetection } from '@/hooks/use-os-detection'; @@ -10,6 +10,7 @@ import { EditProjectDialog } from './components/edit-project-dialog'; import { NewProjectModal } from '@/components/dialogs/new-project-modal'; import { OnboardingDialog } from '@/components/layout/sidebar/dialogs'; import { useProjectCreation, useProjectTheme } from '@/components/layout/sidebar/hooks'; +import { SIDEBAR_FEATURE_FLAGS } from '@/components/layout/sidebar/constants'; import type { Project } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron'; import { initializeProject, hasAppSpec, hasAutomakerDir } from '@/lib/project-init'; @@ -31,6 +32,9 @@ function getOSAbbreviation(os: string): string { export function ProjectSwitcher() { const navigate = useNavigate(); + const location = useLocation(); + const { hideWiki } = SIDEBAR_FEATURE_FLAGS; + const isWikiActive = location.pathname === '/wiki'; const { projects, currentProject, @@ -124,6 +128,10 @@ export function ProjectSwitcher() { api.openExternalLink('https://github.com/AutoMaker-Org/automaker/issues'); }, []); + const handleWikiClick = useCallback(() => { + navigate({ to: '/wiki' }); + }, [navigate]); + /** * Opens the system folder selection dialog and initializes the selected project. */ @@ -405,8 +413,37 @@ export function ProjectSwitcher() { )} - {/* Bug Report Button at the very bottom */} -
+ {/* Wiki and Bug Report Buttons at the very bottom */} +
+ {/* Wiki Button */} + {!hideWiki && ( + + )} + {/* Bug Report Button */} -
- )} {/* Running Agents Link */} {!hideRunningAgents && (
diff --git a/apps/ui/src/components/layout/sidebar/hooks/use-running-agents.ts b/apps/ui/src/components/layout/sidebar/hooks/use-running-agents.ts index 2e88fec5..e6e79cd8 100644 --- a/apps/ui/src/components/layout/sidebar/hooks/use-running-agents.ts +++ b/apps/ui/src/components/layout/sidebar/hooks/use-running-agents.ts @@ -12,10 +12,20 @@ export function useRunningAgents() { try { const api = getElectronAPI(); if (api.runningAgents) { + logger.debug('Fetching running agents count'); const result = await api.runningAgents.getAll(); if (result.success && result.runningAgents) { + logger.debug('Running agents count fetched', { + count: result.runningAgents.length, + }); setRunningAgentsCount(result.runningAgents.length); + } else { + logger.debug('Running agents count fetch returned empty/failed', { + success: result.success, + }); } + } else { + logger.debug('Running agents API not available'); } } catch (error) { logger.error('Error fetching running agents count:', error); @@ -26,6 +36,7 @@ export function useRunningAgents() { useEffect(() => { const api = getElectronAPI(); if (!api.autoMode) { + logger.debug('Auto mode API not available for running agents hook'); // If autoMode is not available, still fetch initial count fetchRunningAgentsCount(); return; @@ -35,6 +46,9 @@ export function useRunningAgents() { fetchRunningAgentsCount(); const unsubscribe = api.autoMode.onEvent((event) => { + logger.debug('Auto mode event for running agents hook', { + type: event.type, + }); // When a feature starts, completes, or errors, refresh the count if ( event.type === 'auto_mode_feature_complete' || @@ -50,6 +64,22 @@ export function useRunningAgents() { }; }, [fetchRunningAgentsCount]); + // Subscribe to backlog plan events to update running agents count + useEffect(() => { + const api = getElectronAPI(); + if (!api.backlogPlan) return; + + fetchRunningAgentsCount(); + + const unsubscribe = api.backlogPlan.onEvent(() => { + fetchRunningAgentsCount(); + }); + + return () => { + unsubscribe(); + }; + }, [fetchRunningAgentsCount]); + return { runningAgentsCount, }; diff --git a/apps/ui/src/components/ui/dependency-selector.tsx b/apps/ui/src/components/ui/dependency-selector.tsx new file mode 100644 index 00000000..d2a51e74 --- /dev/null +++ b/apps/ui/src/components/ui/dependency-selector.tsx @@ -0,0 +1,245 @@ +import * as React from 'react'; +import { ChevronsUpDown, X, GitBranch, ArrowUp, ArrowDown } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Badge } from '@/components/ui/badge'; +import { wouldCreateCircularDependency } from '@automaker/dependency-resolver'; +import type { Feature } from '@automaker/types'; + +interface DependencySelectorProps { + /** The current feature being edited (null for add mode) */ + currentFeatureId?: string; + /** Selected feature IDs */ + value: string[]; + /** Callback when selection changes */ + onChange: (ids: string[]) => void; + /** All available features to select from */ + features: Feature[]; + /** Type of dependency - 'parent' means features this depends on, 'child' means features that depend on this */ + type: 'parent' | 'child'; + /** Placeholder text */ + placeholder?: string; + /** Disabled state */ + disabled?: boolean; + /** Test ID for testing */ + 'data-testid'?: string; +} + +export function DependencySelector({ + currentFeatureId, + value, + onChange, + features, + type, + placeholder, + disabled = false, + 'data-testid': testId, +}: DependencySelectorProps) { + const [open, setOpen] = React.useState(false); + const [inputValue, setInputValue] = React.useState(''); + const [triggerWidth, setTriggerWidth] = React.useState(0); + const triggerRef = React.useRef(null); + + // Update trigger width when component mounts or value changes + React.useEffect(() => { + if (triggerRef.current) { + const updateWidth = () => { + setTriggerWidth(triggerRef.current?.offsetWidth || 0); + }; + + updateWidth(); + + const resizeObserver = new ResizeObserver(updateWidth); + resizeObserver.observe(triggerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + } + }, [value]); + + // Get display label for a feature + const getFeatureLabel = (feature: Feature): string => { + if (feature.title && feature.title.trim()) { + return feature.title; + } + // Truncate description to 50 chars + const desc = feature.description || ''; + return desc.length > 50 ? desc.slice(0, 47) + '...' : desc; + }; + + // Filter out current feature and already selected features from options + const availableFeatures = React.useMemo(() => { + return features.filter((f) => { + // Don't show current feature + if (currentFeatureId && f.id === currentFeatureId) return false; + // Don't show already selected features + if (value.includes(f.id)) return false; + return true; + }); + }, [features, currentFeatureId, value]); + + // Filter by search input + const filteredFeatures = React.useMemo(() => { + if (!inputValue) return availableFeatures; + const lower = inputValue.toLowerCase(); + return availableFeatures.filter((f) => { + const label = getFeatureLabel(f).toLowerCase(); + return label.includes(lower) || f.id.toLowerCase().includes(lower); + }); + }, [availableFeatures, inputValue]); + + // Check if selecting a feature would create a circular dependency + const wouldCreateCycle = React.useCallback( + (featureId: string): boolean => { + if (!currentFeatureId) return false; + + // For parent dependencies: we're adding featureId to currentFeature.dependencies + // This would create a cycle if featureId already depends on currentFeatureId + if (type === 'parent') { + return wouldCreateCircularDependency(features, featureId, currentFeatureId); + } + + // For child dependencies: we're adding currentFeatureId to featureId.dependencies + // This would create a cycle if currentFeatureId already depends on featureId + return wouldCreateCircularDependency(features, currentFeatureId, featureId); + }, + [features, currentFeatureId, type] + ); + + // Get selected features for display + const selectedFeatures = React.useMemo(() => { + return value + .map((id) => features.find((f) => f.id === id)) + .filter((f): f is Feature => f !== undefined); + }, [value, features]); + + const handleSelect = (featureId: string) => { + if (!value.includes(featureId)) { + onChange([...value, featureId]); + } + setInputValue(''); + }; + + const handleRemove = (featureId: string) => { + onChange(value.filter((id) => id !== featureId)); + }; + + const defaultPlaceholder = + type === 'parent' ? 'Select parent dependencies...' : 'Select child dependencies...'; + + const Icon = type === 'parent' ? ArrowUp : ArrowDown; + + return ( +
+ + + + + e.stopPropagation()} + onTouchMove={(e) => e.stopPropagation()} + > + + + + No features found. + + {filteredFeatures.map((feature) => { + const willCreateCycle = wouldCreateCycle(feature.id); + const label = getFeatureLabel(feature); + + return ( + { + if (!willCreateCycle) { + handleSelect(feature.id); + } + }} + disabled={willCreateCycle} + className={cn(willCreateCycle && 'opacity-50 cursor-not-allowed')} + data-testid={`${testId}-option-${feature.id}`} + > + + {label} + {willCreateCycle && ( + (circular) + )} + {feature.status && ( + + {feature.status} + + )} + + ); + })} + + + + + + + {/* Selected items as badges */} + {selectedFeatures.length > 0 && ( +
+ {selectedFeatures.map((feature) => ( + + {getFeatureLabel(feature)} + + + ))} +
+ )} +
+ ); +} diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 20b9e258..046ab4bd 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -951,6 +951,32 @@ export function BoardView() { return unsubscribe; }, []); + // Load any saved plan from disk when opening the board + useEffect(() => { + if (!currentProject || pendingBacklogPlan) return; + + let isActive = true; + const loadSavedPlan = async () => { + const api = getElectronAPI(); + if (!api?.backlogPlan) return; + + const result = await api.backlogPlan.status(currentProject.path); + if ( + isActive && + result.success && + result.savedPlan?.result && + result.savedPlan.result.changes?.length > 0 + ) { + setPendingBacklogPlan(result.savedPlan.result); + } + }; + + loadSavedPlan(); + return () => { + isActive = false; + }; + }, [currentProject, pendingBacklogPlan]); + useEffect(() => { logger.info( '[AutoMode] Effect triggered - isRunning:', @@ -1384,6 +1410,8 @@ export function BoardView() { } }} onOpenPlanDialog={() => setShowPlanDialog(true)} + hasPendingPlan={Boolean(pendingBacklogPlan)} + onOpenPendingPlan={() => setShowPlanDialog(true)} isMounted={isMounted} searchQuery={searchQuery} onSearchChange={setSearchQuery} diff --git a/apps/ui/src/components/views/board-view/board-header.tsx b/apps/ui/src/components/views/board-view/board-header.tsx index a5fc9961..29dce66a 100644 --- a/apps/ui/src/components/views/board-view/board-header.tsx +++ b/apps/ui/src/components/views/board-view/board-header.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; -import { Wand2, GitBranch } from 'lucide-react'; +import { Wand2, GitBranch, ClipboardCheck } from 'lucide-react'; import { UsagePopover } from '@/components/usage-popover'; import { useAppStore } from '@/store/app-store'; import { useSetupStore } from '@/store/setup-store'; @@ -25,6 +25,8 @@ interface BoardHeaderProps { isAutoModeRunning: boolean; onAutoModeToggle: (enabled: boolean) => void; onOpenPlanDialog: () => void; + hasPendingPlan?: boolean; + onOpenPendingPlan?: () => void; isMounted: boolean; // Search bar props searchQuery: string; @@ -50,6 +52,8 @@ export function BoardHeader({ isAutoModeRunning, onAutoModeToggle, onOpenPlanDialog, + hasPendingPlan, + onOpenPendingPlan, isMounted, searchQuery, onSearchChange, @@ -192,6 +196,15 @@ export function BoardHeader({ {/* Plan Button with Settings - only show on desktop, mobile has it in the menu */} {isMounted && !isMobile && (
+ {hasPendingPlan && ( + + )}
+ + {/* Dependencies - only show when not in spawn mode */} + {!isSpawnMode && allFeatures.length > 0 && ( +
+
+ + +
+
+ + +
+
+ )}
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 ab2b0732..84870675 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 @@ -40,6 +40,7 @@ export function AgentOutputModal({ onNumberKeyPress, projectPath: projectPathProp, }: AgentOutputModalProps) { + const isBacklogPlan = featureId.startsWith('backlog-plan:'); const [output, setOutput] = useState(''); const [isLoading, setIsLoading] = useState(true); const [viewMode, setViewMode] = useState(null); @@ -83,6 +84,11 @@ export function AgentOutputModal({ projectPathRef.current = resolvedProjectPath; setProjectPath(resolvedProjectPath); + if (isBacklogPlan) { + setOutput(''); + return; + } + // Use features API to get agent output if (api.features) { const result = await api.features.getAgentOutput(resolvedProjectPath, featureId); @@ -104,14 +110,14 @@ export function AgentOutputModal({ }; loadOutput(); - }, [open, featureId, projectPathProp]); + }, [open, featureId, projectPathProp, isBacklogPlan]); // Listen to auto mode events and update output useEffect(() => { if (!open) return; const api = getElectronAPI(); - if (!api?.autoMode) return; + if (!api?.autoMode || isBacklogPlan) return; console.log('[AgentOutputModal] Subscribing to events for featureId:', featureId); @@ -272,7 +278,43 @@ export function AgentOutputModal({ return () => { unsubscribe(); }; - }, [open, featureId]); + }, [open, featureId, isBacklogPlan]); + + // Listen to backlog plan events and update output + useEffect(() => { + if (!open || !isBacklogPlan) return; + + const api = getElectronAPI(); + if (!api?.backlogPlan) return; + + const unsubscribe = api.backlogPlan.onEvent((event: any) => { + if (!event?.type) return; + + let newContent = ''; + switch (event.type) { + case 'backlog_plan_progress': + newContent = `\n🧭 ${event.content || 'Backlog plan progress update'}\n`; + break; + case 'backlog_plan_error': + newContent = `\nāŒ Backlog plan error: ${event.error || 'Unknown error'}\n`; + break; + case 'backlog_plan_complete': + newContent = `\nāœ… Backlog plan completed\n`; + break; + default: + newContent = `\nā„¹ļø ${event.type}\n`; + break; + } + + if (newContent) { + setOutput((prev) => `${prev}${newContent}`); + } + }); + + return () => { + unsubscribe(); + }; + }, [open, isBacklogPlan]); // Handle scroll to detect if user scrolled up const handleScroll = () => { @@ -369,7 +411,7 @@ export function AgentOutputModal({ {featureDescription} @@ -377,11 +419,13 @@ export function AgentOutputModal({ {/* Task Progress Panel - shows when tasks are being executed */} - + {!isBacklogPlan && ( + + )} {effectiveViewMode === 'changes' ? (
@@ -423,11 +467,11 @@ export function AgentOutputModal({ ) : effectiveViewMode === 'parsed' ? ( ) : ( -
{output}
+
{output}
)}
-
+
{autoScrollRef.current ? 'Auto-scrolling enabled' : 'Scroll to bottom to enable auto-scroll'} diff --git a/apps/ui/src/components/views/board-view/dialogs/backlog-plan-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/backlog-plan-dialog.tsx index 4f909e55..c82b7157 100644 --- a/apps/ui/src/components/views/board-view/dialogs/backlog-plan-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/backlog-plan-dialog.tsx @@ -1,4 +1,5 @@ import { useEffect, useState, useCallback } from 'react'; +import { createLogger } from '@automaker/utils/logger'; import { Dialog, DialogContent, @@ -43,16 +44,6 @@ function normalizeEntry(entry: PhaseModelEntry | string): PhaseModelEntry { return entry; } -/** - * Extract model string from PhaseModelEntry or string - */ -function extractModel(entry: PhaseModelEntry | string): ModelAlias | CursorModelId { - if (typeof entry === 'string') { - return entry as ModelAlias | CursorModelId; - } - return entry.model; -} - interface BacklogPlanDialogProps { open: boolean; onClose: () => void; @@ -80,6 +71,7 @@ export function BacklogPlanDialog({ setIsGeneratingPlan, currentBranch, }: BacklogPlanDialogProps) { + const logger = createLogger('BacklogPlanDialog'); const [mode, setMode] = useState('input'); const [prompt, setPrompt] = useState(''); const [expandedChanges, setExpandedChanges] = useState>(new Set()); @@ -110,11 +102,17 @@ export function BacklogPlanDialog({ const api = getElectronAPI(); if (!api?.backlogPlan) { + logger.warn('Backlog plan API not available'); toast.error('API not available'); return; } // Start generation in background + logger.debug('Starting backlog plan generation', { + projectPath, + promptLength: prompt.length, + hasModelOverride: Boolean(modelOverride), + }); setIsGeneratingPlan(true); // Use model override if set, otherwise use global default (extract model string from PhaseModelEntry) @@ -122,12 +120,20 @@ export function BacklogPlanDialog({ const effectiveModel = effectiveModelEntry.model; const result = await api.backlogPlan.generate(projectPath, prompt, effectiveModel); if (!result.success) { + logger.error('Backlog plan generation failed to start', { + error: result.error, + projectPath, + }); setIsGeneratingPlan(false); toast.error(result.error || 'Failed to start plan generation'); return; } // Show toast and close dialog - generation runs in background + logger.debug('Backlog plan generation started', { + projectPath, + model: effectiveModel, + }); toast.info('Generating plan... This will be ready soon!', { duration: 3000, }); @@ -194,10 +200,15 @@ export function BacklogPlanDialog({ currentBranch, ]); - const handleDiscard = useCallback(() => { + const handleDiscard = useCallback(async () => { setPendingPlanResult(null); setMode('input'); - }, [setPendingPlanResult]); + + const api = getElectronAPI(); + if (api?.backlogPlan) { + await api.backlogPlan.clear(projectPath); + } + }, [setPendingPlanResult, projectPath]); const toggleChangeExpanded = (index: number) => { setExpandedChanges((prev) => { @@ -260,11 +271,11 @@ export function BacklogPlanDialog({ return (
- Describe the changes you want to make to your backlog. The AI will analyze your - current features and propose additions, updates, or deletions. + Describe the changes you want to make across your features. The AI will analyze your + current feature list and propose additions, updates, deletions, or restructuring.