diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json index 0637a088..8b253166 100644 --- a/.automaker/feature_list.json +++ b/.automaker/feature_list.json @@ -1 +1,24 @@ -[] \ No newline at end of file +[ + { + "id": "feature-1765376066671-lmchhkgez", + "category": "Agent Runner", + "description": "On the Agent Runner, the markdown is not being formatted properly. Can you please bring in a React library to format markdown so that on a session window of the agent, it'll properly format the markdown?", + "steps": [], + "status": "waiting_approval", + "startedAt": "2025-12-10T14:15:24.929Z", + "imagePaths": [], + "skipTests": true, + "summary": "Added markdown formatting to Agent Runner session messages. Modified: agent-view.tsx. Used existing Markdown component with react-markdown library to properly render headings, code blocks, lists, links, and other markdown elements in assistant messages." + }, + { + "id": "feature-1765376191866-n4r0s1l6q", + "category": "Agent Runner", + "description": "On the Agent Runner page, change the hotkey for making a session from W to New. I want all new feature buttons to be the hotkey New.", + "steps": [], + "status": "waiting_approval", + "startedAt": "2025-12-10T14:16:34.207Z", + "imagePaths": [], + "skipTests": true, + "summary": "Changed the new session hotkey from \"W\" to \"N\" on the Agent Runner page. Modified: use-keyboard-shortcuts.ts (line 125). This aligns with the user's request to have all \"new\" feature buttons use the \"N\" hotkey consistently." + } +] \ No newline at end of file diff --git a/app/electron/services/project-analyzer.js b/app/electron/services/project-analyzer.js index de91e4dd..1922e415 100644 --- a/app/electron/services/project-analyzer.js +++ b/app/electron/services/project-analyzer.js @@ -27,7 +27,7 @@ class ProjectAnalyzer { systemPrompt: promptBuilder.getProjectAnalysisSystemPrompt(), maxTurns: 50, cwd: projectPath, - allowedTools: ["Read", "Glob", "Grep", "Bash"], + allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"], permissionMode: "acceptEdits", sandbox: { enabled: true, diff --git a/app/electron/services/prompt-builder.js b/app/electron/services/prompt-builder.js index f5ece767..de0c6b00 100644 --- a/app/electron/services/prompt-builder.js +++ b/app/electron/services/prompt-builder.js @@ -359,6 +359,7 @@ Analyze this project's codebase and update the .automaker/app_spec.txt file with 3. **Technology Stack** - Languages, frameworks, libraries detected 4. **Core Capabilities** - Main features and functionality 5. **Implemented Features** - What features are already built +6. **Implementation Roadmap** - Break down remaining work into phases with individual features **Steps to Follow:** @@ -406,10 +407,42 @@ Analyze this project's codebase and update the .automaker/app_spec.txt file with + + + + + + + + + + + + \`\`\` -4. Ensure .automaker/feature_list.json exists (create as empty array [] if not) +4. **IMPORTANT - Generate Feature List:** + After writing the app_spec.txt, you MUST update .automaker/feature_list.json with features from the implementation_roadmap section: + - Read the app_spec.txt you just created + - For EVERY feature in each phase of the implementation_roadmap, create an entry + - Write ALL features to .automaker/feature_list.json + + The feature_list.json format should be: + \`\`\`json + [ + { + "id": "feature--", + "category": "", + "description": "", + "status": "backlog", + "steps": ["Step 1", "Step 2", "..."], + "skipTests": true + } + ] + \`\`\` + + Generate unique IDs using the current timestamp and index (e.g., "feature-1234567890-0", "feature-1234567890-1", etc.) 5. Ensure .automaker/context/ directory exists @@ -420,6 +453,7 @@ Analyze this project's codebase and update the .automaker/app_spec.txt file with - Only include information you can verify from the codebase - If unsure about something, note it as "to be determined" - Don't make up features that don't exist +- Include EVERY feature from the roadmap in feature_list.json - do not skip any Begin by exploring the project structure.`; } @@ -618,6 +652,7 @@ Your goal is to: - Identify programming languages, frameworks, and libraries - Detect existing features and capabilities - Update the .automaker/app_spec.txt with accurate information +- Generate a feature list in .automaker/feature_list.json based on the implementation roadmap - Ensure all required .automaker files and directories exist Be efficient - don't read every file, focus on: @@ -626,7 +661,13 @@ Be efficient - don't read every file, focus on: - Directory structure - README and documentation -You have read access to files and can run basic bash commands to explore the structure.`; +**CRITICAL - Feature List Generation:** +After creating/updating the app_spec.txt, you MUST also update .automaker/feature_list.json: +1. Read the app_spec.txt you just wrote +2. Extract all features from the implementation_roadmap section +3. Write them to .automaker/feature_list.json in the correct format + +You have access to Read, Write, Edit, Glob, Grep, and Bash tools. Use them to explore the structure and write the necessary files.`; } } diff --git a/app/src/components/session-manager.tsx b/app/src/components/session-manager.tsx index 2b8df243..88144679 100644 --- a/app/src/components/session-manager.tsx +++ b/app/src/components/session-manager.tsx @@ -67,6 +67,29 @@ export function SessionManager({ const [editingName, setEditingName] = useState(""); const [isCreating, setIsCreating] = useState(false); const [newSessionName, setNewSessionName] = useState(""); + const [runningSessions, setRunningSessions] = useState>(new Set()); + + // Check running state for all sessions + const checkRunningSessions = async (sessionList: SessionListItem[]) => { + if (!window.electronAPI?.agent) return; + + const runningIds = new Set(); + + // Check each session's running state + for (const session of sessionList) { + try { + const result = await window.electronAPI.agent.getHistory(session.id); + if (result.success && result.isRunning) { + runningIds.add(session.id); + } + } catch (err) { + // Ignore errors for individual session checks + console.warn(`[SessionManager] Failed to check running state for ${session.id}:`, err); + } + } + + setRunningSessions(runningIds); + }; // Load sessions const loadSessions = async () => { @@ -76,6 +99,8 @@ export function SessionManager({ const result = await window.electronAPI.sessions.list(true); if (result.success && result.sessions) { setSessions(result.sessions); + // Check running state for all sessions + await checkRunningSessions(result.sessions); } }; @@ -83,6 +108,20 @@ export function SessionManager({ loadSessions(); }, []); + // Periodically check running state for sessions (useful for detecting when agents finish) + useEffect(() => { + // Only poll if there are running sessions + if (runningSessions.size === 0 && !isCurrentSessionThinking) return; + + const interval = setInterval(async () => { + if (sessions.length > 0) { + await checkRunningSessions(sessions); + } + }, 3000); // Check every 3 seconds + + return () => clearInterval(interval); + }, [sessions, runningSessions.size, isCurrentSessionThinking]); + // Create new session with random name const handleCreateSession = async () => { if (!window.electronAPI?.sessions) return; @@ -328,13 +367,14 @@ export function SessionManager({ ) : ( <>
- {currentSessionId === session.id && isCurrentSessionThinking ? ( + {/* Show loading indicator if this session is running (either current session thinking or any session in runningSessions) */} + {((currentSessionId === session.id && isCurrentSessionThinking) || runningSessions.has(session.id)) ? ( ) : ( )}

{session.name}

- {currentSessionId === session.id && isCurrentSessionThinking && ( + {((currentSessionId === session.id && isCurrentSessionThinking) || runningSessions.has(session.id)) && ( thinking... diff --git a/app/src/components/views/agent-view.tsx b/app/src/components/views/agent-view.tsx index d75decf6..7e59a123 100644 --- a/app/src/components/views/agent-view.tsx +++ b/app/src/components/views/agent-view.tsx @@ -22,6 +22,7 @@ import { import { cn } from "@/lib/utils"; import { useElectronAgent } from "@/hooks/use-electron-agent"; import { SessionManager } from "@/components/session-manager"; +import { Markdown } from "@/components/ui/markdown"; import type { ImageAttachment } from "@/store/app-store"; import { useKeyboardShortcuts, @@ -30,7 +31,7 @@ import { } from "@/hooks/use-keyboard-shortcuts"; export function AgentView() { - const { currentProject } = useAppStore(); + const { currentProject, setLastSelectedSession, getLastSelectedSession } = useAppStore(); const [input, setInput] = useState(""); const [selectedImages, setSelectedImages] = useState([]); const [showImageDropZone, setShowImageDropZone] = useState(false); @@ -39,6 +40,9 @@ export function AgentView() { const [showSessionManager, setShowSessionManager] = useState(true); const [isDragOver, setIsDragOver] = useState(false); + // Track if initial session has been loaded + const initialSessionLoadedRef = useRef(false); + // Scroll management for auto-scroll const messagesContainerRef = useRef(null); const [isUserAtBottom, setIsUserAtBottom] = useState(true); @@ -66,6 +70,40 @@ export function AgentView() { }, }); + // Handle session selection with persistence + const handleSelectSession = useCallback((sessionId: string | null) => { + setCurrentSessionId(sessionId); + // Persist the selection for this project + if (currentProject?.path) { + setLastSelectedSession(currentProject.path, sessionId); + } + }, [currentProject?.path, setLastSelectedSession]); + + // Restore last selected session when switching to Agent view or when project changes + useEffect(() => { + if (!currentProject?.path) { + // No project, reset + setCurrentSessionId(null); + initialSessionLoadedRef.current = false; + return; + } + + // Only restore once per project + if (initialSessionLoadedRef.current) return; + initialSessionLoadedRef.current = true; + + const lastSessionId = getLastSelectedSession(currentProject.path); + if (lastSessionId) { + console.log("[AgentView] Restoring last selected session:", lastSessionId); + setCurrentSessionId(lastSessionId); + } + }, [currentProject?.path, getLastSelectedSession]); + + // Reset initialSessionLoadedRef when project changes + useEffect(() => { + initialSessionLoadedRef.current = false; + }, [currentProject?.path]); + const handleSend = useCallback(async () => { if ((!input.trim() && selectedImages.length === 0) || isProcessing) return; @@ -441,7 +479,7 @@ export function AgentView() {
-

- {message.content} -

+ {message.role === "assistant" ? ( + {message.content} + ) : ( +

+ {message.content} +

+ )}

([]); const [showSuggestionsDialog, setShowSuggestionsDialog] = useState(false); const [suggestionsCount, setSuggestionsCount] = useState(0); + const [featureSuggestions, setFeatureSuggestions] = useState< + import("@/lib/electron").FeatureSuggestion[] + >([]); + const [isGeneratingSuggestions, setIsGeneratingSuggestions] = useState(false); // Make current project available globally for modal useEffect(() => { @@ -138,7 +142,7 @@ export function BoardView() { }; }, [currentProject]); - // Listen for suggestions events to update count + // Listen for suggestions events to update count (persists even when dialog is closed) useEffect(() => { const api = getElectronAPI(); if (!api?.suggestions) return; @@ -146,6 +150,10 @@ export function BoardView() { const unsubscribe = api.suggestions.onEvent((event) => { if (event.type === "suggestions_complete" && event.suggestions) { setSuggestionsCount(event.suggestions.length); + setFeatureSuggestions(event.suggestions); + setIsGeneratingSuggestions(false); + } else if (event.type === "suggestions_error") { + setIsGeneratingSuggestions(false); } }); @@ -1809,9 +1817,15 @@ export function BoardView() { open={showSuggestionsDialog} onClose={() => { setShowSuggestionsDialog(false); - // Clear the count when dialog is closed (suggestions were either imported or dismissed) }} projectPath={currentProject.path} + suggestions={featureSuggestions} + setSuggestions={(suggestions) => { + setFeatureSuggestions(suggestions); + setSuggestionsCount(suggestions.length); + }} + isGenerating={isGeneratingSuggestions} + setIsGenerating={setIsGeneratingSuggestions} />

); diff --git a/app/src/components/views/feature-suggestions-dialog.tsx b/app/src/components/views/feature-suggestions-dialog.tsx index e1b5bf10..62a747e3 100644 --- a/app/src/components/views/feature-suggestions-dialog.tsx +++ b/app/src/components/views/feature-suggestions-dialog.tsx @@ -28,16 +28,23 @@ interface FeatureSuggestionsDialogProps { open: boolean; onClose: () => void; projectPath: string; + // Props to persist state across dialog open/close + suggestions: FeatureSuggestion[]; + setSuggestions: (suggestions: FeatureSuggestion[]) => void; + isGenerating: boolean; + setIsGenerating: (generating: boolean) => void; } export function FeatureSuggestionsDialog({ open, onClose, projectPath, + suggestions, + setSuggestions, + isGenerating, + setIsGenerating, }: FeatureSuggestionsDialogProps) { - const [isGenerating, setIsGenerating] = useState(false); const [progress, setProgress] = useState([]); - const [suggestions, setSuggestions] = useState([]); const [selectedIds, setSelectedIds] = useState>(new Set()); const [expandedIds, setExpandedIds] = useState>(new Set()); const [isImporting, setIsImporting] = useState(false); @@ -46,6 +53,13 @@ export function FeatureSuggestionsDialog({ const { features, setFeatures } = useAppStore(); + // Initialize selectedIds when suggestions change + useEffect(() => { + if (suggestions.length > 0 && selectedIds.size === 0) { + setSelectedIds(new Set(suggestions.map((s) => s.id))); + } + }, [suggestions, selectedIds.size]); + // Auto-scroll progress when new content arrives useEffect(() => { if (autoScrollRef.current && scrollRef.current && isGenerating) { @@ -53,7 +67,7 @@ export function FeatureSuggestionsDialog({ } }, [progress, isGenerating]); - // Listen for suggestion events + // Listen for suggestion events when dialog is open useEffect(() => { if (!open) return; @@ -85,7 +99,7 @@ export function FeatureSuggestionsDialog({ return () => { unsubscribe(); }; - }, [open]); + }, [open, setSuggestions, setIsGenerating]); // Start generating suggestions const handleGenerate = useCallback(async () => { @@ -111,7 +125,7 @@ export function FeatureSuggestionsDialog({ toast.error("Failed to start generation"); setIsGenerating(false); } - }, [projectPath]); + }, [projectPath, setIsGenerating, setSuggestions]); // Stop generating const handleStop = useCallback(async () => { @@ -125,7 +139,7 @@ export function FeatureSuggestionsDialog({ } catch (error) { console.error("Failed to stop generation:", error); } - }, []); + }, [setIsGenerating]); // Toggle suggestion selection const toggleSelection = useCallback((id: string) => { @@ -198,6 +212,12 @@ export function FeatureSuggestionsDialog({ setFeatures(updatedFeatures); toast.success(`Imported ${newFeatures.length} features to backlog!`); + + // Clear suggestions after importing + setSuggestions([]); + setSelectedIds(new Set()); + setProgress([]); + onClose(); } catch (error) { console.error("Failed to import features:", error); @@ -205,7 +225,7 @@ export function FeatureSuggestionsDialog({ } finally { setIsImporting(false); } - }, [selectedIds, suggestions, features, setFeatures, projectPath, onClose]); + }, [selectedIds, suggestions, features, setFeatures, setSuggestions, projectPath, onClose]); // Handle scroll to detect if user scrolled up const handleScroll = () => { diff --git a/app/src/hooks/use-electron-agent.ts b/app/src/hooks/use-electron-agent.ts index f187c37a..69d16e45 100644 --- a/app/src/hooks/use-electron-agent.ts +++ b/app/src/hooks/use-electron-agent.ts @@ -137,8 +137,7 @@ export function useElectronAgent({ let mounted = true; const initialize = async () => { - // Reset state when switching sessions - setIsProcessing(false); + // Reset error state when switching sessions setError(null); try { @@ -154,13 +153,23 @@ export function useElectronAgent({ console.log("[useElectronAgent] Loaded", result.messages.length, "messages"); setMessages(result.messages); setIsConnected(true); + + // Check if the agent is currently running for this session + const historyResult = await window.electronAPI.agent.getHistory(sessionId); + if (mounted && historyResult.success) { + const isRunning = historyResult.isRunning || false; + console.log("[useElectronAgent] Session running state:", isRunning); + setIsProcessing(isRunning); + } } else { setError(result.error || "Failed to start session"); + setIsProcessing(false); } } catch (err) { if (!mounted) return; console.error("[useElectronAgent] Failed to initialize:", err); setError(err instanceof Error ? err.message : "Failed to initialize"); + setIsProcessing(false); } }; diff --git a/app/src/hooks/use-keyboard-shortcuts.ts b/app/src/hooks/use-keyboard-shortcuts.ts index 34ebe104..233003fb 100644 --- a/app/src/hooks/use-keyboard-shortcuts.ts +++ b/app/src/hooks/use-keyboard-shortcuts.ts @@ -122,7 +122,7 @@ export const ACTION_SHORTCUTS: Record = { addFeature: "N", // N for New feature addContextFile: "F", // F for File (add context file) startNext: "G", // G for Grab (start next features from backlog) - newSession: "W", // W for new session (in agent view) + newSession: "N", // N for New session (in agent view) openProject: "O", // O for Open project (navigate to welcome view) projectPicker: "P", // P for Project picker cyclePrevProject: "Q", // Q for previous project (cycle back through MRU) diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index 771b817f..cde3748d 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -101,6 +101,9 @@ export interface AppState { currentView: ViewMode; sidebarOpen: boolean; + // Agent Session state (per-project, keyed by project path) + lastSelectedSessionByProject: Record; // projectPath -> sessionId + // Theme theme: ThemeMode; @@ -223,6 +226,10 @@ export interface AppActions { // Feature Default Settings actions setDefaultSkipTests: (skip: boolean) => void; + // Agent Session actions + setLastSelectedSession: (projectPath: string, sessionId: string | null) => void; + getLastSelectedSession: (projectPath: string) => string | null; + // Reset reset: () => void; } @@ -235,6 +242,7 @@ const initialState: AppState = { projectHistoryIndex: -1, currentView: "welcome", sidebarOpen: true, + lastSelectedSessionByProject: {}, theme: "dark", features: [], appSpec: "", @@ -682,6 +690,27 @@ export const useAppStore = create()( // Feature Default Settings actions setDefaultSkipTests: (skip) => set({ defaultSkipTests: skip }), + // Agent Session actions + setLastSelectedSession: (projectPath, sessionId) => { + const current = get().lastSelectedSessionByProject; + if (sessionId === null) { + // Remove the entry for this project + const { [projectPath]: _, ...rest } = current; + set({ lastSelectedSessionByProject: rest }); + } else { + set({ + lastSelectedSessionByProject: { + ...current, + [projectPath]: sessionId, + }, + }); + } + }, + + getLastSelectedSession: (projectPath) => { + return get().lastSelectedSessionByProject[projectPath] || null; + }, + // Reset reset: () => set(initialState), }), @@ -702,6 +731,7 @@ export const useAppStore = create()( maxConcurrency: state.maxConcurrency, kanbanCardDetailLevel: state.kanbanCardDetailLevel, defaultSkipTests: state.defaultSkipTests, + lastSelectedSessionByProject: state.lastSelectedSessionByProject, }), } ) diff --git a/app/src/types/electron.d.ts b/app/src/types/electron.d.ts index 5d008c9e..7e87f5e0 100644 --- a/app/src/types/electron.d.ts +++ b/app/src/types/electron.d.ts @@ -291,6 +291,24 @@ export interface AutoModeAPI { error?: string; }>; + resumeFeature: (projectPath: string, featureId: string) => Promise<{ + success: boolean; + passes?: boolean; + error?: string; + }>; + + contextExists: (projectPath: string, featureId: string) => Promise<{ + success: boolean; + exists?: boolean; + error?: string; + }>; + + analyzeProject: (projectPath: string) => Promise<{ + success: boolean; + message?: string; + error?: string; + }>; + followUpFeature: (projectPath: string, featureId: string, prompt: string, imagePaths?: string[]) => Promise<{ success: boolean; passes?: boolean;