- {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;