From 166679cd367b6df654e9af860035bbc63aae30ab Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Tue, 16 Dec 2025 02:39:11 -0500 Subject: [PATCH] adding a worktree switch feature --- .worktrees/feature-model-select | 1 + apps/app/src/components/views/agent-view.tsx | 49 ++++++++++++++++++- apps/app/src/components/views/board-view.tsx | 8 +++ .../hooks/use-board-column-features.ts | 33 +++++++++++-- .../board-view/hooks/use-board-drag-drop.ts | 21 ++++++-- apps/app/src/hooks/use-electron-agent.ts | 12 +++-- apps/app/src/lib/electron.ts | 3 +- apps/app/src/lib/http-api-client.ts | 4 +- apps/app/src/types/electron.d.ts | 3 +- 9 files changed, 119 insertions(+), 15 deletions(-) create mode 160000 .worktrees/feature-model-select diff --git a/.worktrees/feature-model-select b/.worktrees/feature-model-select new file mode 160000 index 00000000..b95c54a5 --- /dev/null +++ b/.worktrees/feature-model-select @@ -0,0 +1 @@ +Subproject commit b95c54a5399c125a793b3086abe4129c4e9769e8 diff --git a/apps/app/src/components/views/agent-view.tsx b/apps/app/src/components/views/agent-view.tsx index 8386554d..daa9e48b 100644 --- a/apps/app/src/components/views/agent-view.tsx +++ b/apps/app/src/components/views/agent-view.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useCallback, useRef, useEffect, useMemo } from "react"; -import { useAppStore } from "@/store/app-store"; +import { useAppStore, type AgentModel } from "@/store/app-store"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ImageDropZone } from "@/components/ui/image-drop-zone"; @@ -18,6 +18,7 @@ import { Paperclip, X, ImageIcon, + ChevronDown, } from "lucide-react"; import { cn } from "@/lib/utils"; import { useElectronAgent } from "@/hooks/use-electron-agent"; @@ -29,6 +30,13 @@ import { useKeyboardShortcutsConfig, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { CLAUDE_MODELS } from "@/components/views/board-view/shared/model-constants"; export function AgentView() { const { currentProject, setLastSelectedSession, getLastSelectedSession } = @@ -41,6 +49,7 @@ export function AgentView() { const [currentSessionId, setCurrentSessionId] = useState(null); const [showSessionManager, setShowSessionManager] = useState(true); const [isDragOver, setIsDragOver] = useState(false); + const [selectedModel, setSelectedModel] = useState("sonnet"); // Track if initial session has been loaded const initialSessionLoadedRef = useRef(false); @@ -66,6 +75,7 @@ export function AgentView() { } = useElectronAgent({ sessionId: currentSessionId || "", workingDirectory: currentProject?.path, + model: selectedModel, onToolUse: (toolName) => { setCurrentTool(toolName); setTimeout(() => setCurrentTool(null), 2000); @@ -501,6 +511,43 @@ export function AgentView() { {/* Status indicators & actions */}
+ {/* Model Selector */} + + + + + + {CLAUDE_MODELS.map((model) => ( + setSelectedModel(model.id)} + className={cn( + "cursor-pointer", + selectedModel === model.id && "bg-accent" + )} + data-testid={`model-option-${model.id}`} + > +
+ {model.label} + + {model.description} + +
+
+ ))} +
+
+ {currentTool && (
diff --git a/apps/app/src/components/views/board-view.tsx b/apps/app/src/components/views/board-view.tsx index baa483ad..280ae5d1 100644 --- a/apps/app/src/components/views/board-view.tsx +++ b/apps/app/src/components/views/board-view.tsx @@ -62,6 +62,7 @@ export function BoardView() { setKanbanCardDetailLevel, specCreatingForProject, setSpecCreatingForProject, + getCurrentWorktree, } = useAppStore(); const shortcuts = useKeyboardShortcutsConfig(); const { @@ -298,12 +299,17 @@ export function BoardView() { }); // Use drag and drop hook + // Get current worktree path for filtering features and assigning to cards + const currentWorktreePath = currentProject ? getCurrentWorktree(currentProject.path) : null; + const { activeFeature, handleDragStart, handleDragEnd } = useBoardDragDrop({ features: hookFeatures, currentProject, runningAutoTasks, persistFeatureUpdate, handleStartImplementation, + currentWorktreePath, + projectPath: currentProject?.path || null, }); // Use column features hook @@ -311,6 +317,8 @@ export function BoardView() { features: hookFeatures, runningAutoTasks, searchQuery, + currentWorktreePath, + projectPath: currentProject?.path || null, }); // Use background hook diff --git a/apps/app/src/components/views/board-view/hooks/use-board-column-features.ts b/apps/app/src/components/views/board-view/hooks/use-board-column-features.ts index c3944b5d..8f0c8e84 100644 --- a/apps/app/src/components/views/board-view/hooks/use-board-column-features.ts +++ b/apps/app/src/components/views/board-view/hooks/use-board-column-features.ts @@ -7,12 +7,16 @@ interface UseBoardColumnFeaturesProps { features: Feature[]; runningAutoTasks: string[]; searchQuery: string; + currentWorktreePath: string | null; // Currently selected worktree path + projectPath: string | null; // Main project path (for main worktree) } export function useBoardColumnFeatures({ features, runningAutoTasks, searchQuery, + currentWorktreePath, + projectPath, }: UseBoardColumnFeaturesProps) { // Memoize column features to prevent unnecessary re-renders const columnFeaturesMap = useMemo(() => { @@ -34,16 +38,37 @@ export function useBoardColumnFeatures({ ) : features; + // Determine the effective worktree path for filtering + // If currentWorktreePath is null, we're on the main worktree (use projectPath) + const effectiveWorktreePath = currentWorktreePath || projectPath; + filteredFeatures.forEach((f) => { // If feature has a running agent, always show it in "in_progress" const isRunning = runningAutoTasks.includes(f.id); + + // Check if feature matches the current worktree + // Features without a worktreePath are considered unassigned (backlog items) + // Features with a worktreePath should only show if it matches the selected worktree + const matchesWorktree = !f.worktreePath || f.worktreePath === effectiveWorktreePath; + if (isRunning) { - map.in_progress.push(f); + // Only show running tasks if they match the current worktree + if (matchesWorktree) { + map.in_progress.push(f); + } } else { // Otherwise, use the feature's status (fallback to backlog for unknown statuses) const status = f.status as ColumnId; - if (map[status]) { - map[status].push(f); + + // Backlog items are always visible (they have no worktree assigned) + // For other statuses, filter by worktree + if (status === "backlog") { + map.backlog.push(f); + } else if (map[status]) { + // Only show if matches current worktree or has no worktree assigned + if (matchesWorktree) { + map[status].push(f); + } } else { // Unknown status, default to backlog map.backlog.push(f); @@ -59,7 +84,7 @@ export function useBoardColumnFeatures({ }); return map; - }, [features, runningAutoTasks, searchQuery]); + }, [features, runningAutoTasks, searchQuery, currentWorktreePath, projectPath]); const getColumnFeatures = useCallback( (columnId: ColumnId) => { diff --git a/apps/app/src/components/views/board-view/hooks/use-board-drag-drop.ts b/apps/app/src/components/views/board-view/hooks/use-board-drag-drop.ts index c39ed6d1..aeb6db1e 100644 --- a/apps/app/src/components/views/board-view/hooks/use-board-drag-drop.ts +++ b/apps/app/src/components/views/board-view/hooks/use-board-drag-drop.ts @@ -14,6 +14,8 @@ interface UseBoardDragDropProps { updates: Partial ) => Promise; handleStartImplementation: (feature: Feature) => Promise; + currentWorktreePath: string | null; // Currently selected worktree path + projectPath: string | null; // Main project path } export function useBoardDragDrop({ @@ -22,10 +24,15 @@ export function useBoardDragDrop({ runningAutoTasks, persistFeatureUpdate, handleStartImplementation, + currentWorktreePath, + projectPath, }: UseBoardDragDropProps) { const [activeFeature, setActiveFeature] = useState(null); const { moveFeature } = useAppStore(); + // Determine the effective worktree path for assigning to features + const effectiveWorktreePath = currentWorktreePath || projectPath; + const handleDragStart = useCallback( (event: DragStartEvent) => { const { active } = event; @@ -97,6 +104,10 @@ export function useBoardDragDrop({ if (draggedFeature.status === "backlog") { // From backlog if (targetStatus === "in_progress") { + // Assign the current worktree to this feature when moving to in_progress + if (effectiveWorktreePath) { + await persistFeatureUpdate(featureId, { worktreePath: effectiveWorktreePath }); + } // Use helper function to handle concurrency check and start implementation await handleStartImplementation(draggedFeature); } else { @@ -123,10 +134,11 @@ export function useBoardDragDrop({ } else if (targetStatus === "backlog") { // Allow moving waiting_approval cards back to backlog moveFeature(featureId, "backlog"); - // Clear justFinishedAt timestamp when moving back to backlog + // Clear justFinishedAt timestamp and worktreePath when moving back to backlog persistFeatureUpdate(featureId, { status: "backlog", justFinishedAt: undefined, + worktreePath: undefined, }); toast.info("Feature moved to backlog", { description: `Moved to Backlog: ${draggedFeature.description.slice( @@ -166,7 +178,8 @@ export function useBoardDragDrop({ } else if (targetStatus === "backlog") { // Allow moving skipTests cards back to backlog moveFeature(featureId, "backlog"); - persistFeatureUpdate(featureId, { status: "backlog" }); + // Clear worktreePath when moving back to backlog + persistFeatureUpdate(featureId, { status: "backlog", worktreePath: undefined }); toast.info("Feature moved to backlog", { description: `Moved to Backlog: ${draggedFeature.description.slice( 0, @@ -189,7 +202,8 @@ export function useBoardDragDrop({ } else if (targetStatus === "backlog") { // Allow moving verified cards back to backlog moveFeature(featureId, "backlog"); - persistFeatureUpdate(featureId, { status: "backlog" }); + // Clear worktreePath when moving back to backlog + persistFeatureUpdate(featureId, { status: "backlog", worktreePath: undefined }); toast.info("Feature moved to backlog", { description: `Moved to Backlog: ${draggedFeature.description.slice( 0, @@ -205,6 +219,7 @@ export function useBoardDragDrop({ moveFeature, persistFeatureUpdate, handleStartImplementation, + effectiveWorktreePath, ] ); diff --git a/apps/app/src/hooks/use-electron-agent.ts b/apps/app/src/hooks/use-electron-agent.ts index 39342b83..0a8f4403 100644 --- a/apps/app/src/hooks/use-electron-agent.ts +++ b/apps/app/src/hooks/use-electron-agent.ts @@ -7,6 +7,7 @@ import { getElectronAPI } from "@/lib/electron"; interface UseElectronAgentOptions { sessionId: string; workingDirectory?: string; + model?: string; onToolUse?: (toolName: string, toolInput: unknown) => void; } @@ -33,6 +34,7 @@ interface UseElectronAgentResult { export function useElectronAgent({ sessionId, workingDirectory, + model, onToolUse, }: UseElectronAgentOptions): UseElectronAgentResult { const [messages, setMessages] = useState([]); @@ -88,7 +90,8 @@ export function useElectronAgent({ sessionId, content, workingDirectory, - imagePaths + imagePaths, + model ); if (!result.success) { @@ -104,7 +107,7 @@ export function useElectronAgent({ throw err; } }, - [sessionId, workingDirectory, isProcessing] + [sessionId, workingDirectory, model, isProcessing] ); // Message queue for queuing messages when agent is busy @@ -344,7 +347,8 @@ export function useElectronAgent({ sessionId, content, workingDirectory, - imagePaths + imagePaths, + model ); if (!result.success) { @@ -359,7 +363,7 @@ export function useElectronAgent({ setIsProcessing(false); } }, - [sessionId, workingDirectory, isProcessing] + [sessionId, workingDirectory, model, isProcessing] ); // Stop current execution diff --git a/apps/app/src/lib/electron.ts b/apps/app/src/lib/electron.ts index d3c27faa..bf795748 100644 --- a/apps/app/src/lib/electron.ts +++ b/apps/app/src/lib/electron.ts @@ -393,7 +393,8 @@ export interface ElectronAPI { sessionId: string, message: string, workingDirectory?: string, - imagePaths?: string[] + imagePaths?: string[], + model?: string ) => Promise<{ success: boolean; error?: string }>; getHistory: (sessionId: string) => Promise<{ success: boolean; diff --git a/apps/app/src/lib/http-api-client.ts b/apps/app/src/lib/http-api-client.ts index 969a3383..d1216bd5 100644 --- a/apps/app/src/lib/http-api-client.ts +++ b/apps/app/src/lib/http-api-client.ts @@ -699,13 +699,15 @@ export class HttpApiClient implements ElectronAPI { sessionId: string, message: string, workingDirectory?: string, - imagePaths?: string[] + imagePaths?: string[], + model?: string ): Promise<{ success: boolean; error?: string }> => this.post("/api/agent/send", { sessionId, message, workingDirectory, imagePaths, + model, }), getHistory: ( diff --git a/apps/app/src/types/electron.d.ts b/apps/app/src/types/electron.d.ts index a6d53af9..809d7bf9 100644 --- a/apps/app/src/types/electron.d.ts +++ b/apps/app/src/types/electron.d.ts @@ -84,7 +84,8 @@ export interface AgentAPI { sessionId: string, message: string, workingDirectory?: string, - imagePaths?: string[] + imagePaths?: string[], + model?: string ) => Promise<{ success: boolean; error?: string;