From 3d6add5272cad43da7870c04565d6cf2ff4f3037 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Wed, 10 Dec 2025 09:20:00 -0500 Subject: [PATCH 1/2] feat(agent-runner, session-management): enhance markdown formatting and session hotkey functionality - Added a React library for proper markdown formatting in the Agent Runner, improving the display of session messages. - Changed the hotkey for creating a new session from "W" to "N" to ensure consistency across new feature buttons. - Updated the feature list to include new entries for these enhancements, along with a detailed implementation roadmap in the project specification. These changes improve user experience by enhancing message readability and streamlining session management. --- .automaker/feature_list.json | 25 ++++++++- app/electron/services/project-analyzer.js | 2 +- app/electron/services/prompt-builder.js | 45 +++++++++++++++- app/src/components/session-manager.tsx | 44 +++++++++++++++- app/src/components/views/agent-view.tsx | 52 +++++++++++++++++-- app/src/components/views/board-view.tsx | 18 ++++++- .../views/feature-suggestions-dialog.tsx | 34 +++++++++--- app/src/hooks/use-electron-agent.ts | 13 ++++- app/src/hooks/use-keyboard-shortcuts.ts | 2 +- app/src/store/app-store.ts | 30 +++++++++++ app/src/types/electron.d.ts | 18 +++++++ 11 files changed, 260 insertions(+), 23 deletions(-) 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; From a28d2f5cfebff544bcb5e40791e221067ce95726 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Wed, 10 Dec 2025 09:41:46 -0500 Subject: [PATCH 2/2] refactor(description-image-dropzone, app-store): update styling and enhance project history cycling logic - Changed button styling in the DescriptionImageDropZone component to use primary color classes for better visual consistency. - Improved project history cycling logic in the app store by filtering out invalid projects, ensuring smoother navigation through valid project history. These changes enhance the user interface and improve the reliability of project navigation within the application. --- .../ui/description-image-dropzone.tsx | 2 +- app/src/store/app-store.ts | 56 +++++++++++++++---- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/app/src/components/ui/description-image-dropzone.tsx b/app/src/components/ui/description-image-dropzone.tsx index 17e73bb8..78d661bd 100644 --- a/app/src/components/ui/description-image-dropzone.tsx +++ b/app/src/components/ui/description-image-dropzone.tsx @@ -292,7 +292,7 @@ export function DescriptionImageDropZone({