adding a worktree switch feature

This commit is contained in:
Cody Seibert
2025-12-16 02:39:11 -05:00
parent b95c54a539
commit 166679cd36
9 changed files with 119 additions and 15 deletions

Submodule .worktrees/feature-model-select added at b95c54a539

View File

@@ -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<string | null>(null);
const [showSessionManager, setShowSessionManager] = useState(true);
const [isDragOver, setIsDragOver] = useState(false);
const [selectedModel, setSelectedModel] = useState<AgentModel>("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 */}
<div className="flex items-center gap-3">
{/* Model Selector */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8 gap-1.5 text-xs font-medium"
disabled={isProcessing}
data-testid="model-selector"
>
<Bot className="w-3.5 h-3.5" />
{CLAUDE_MODELS.find((m) => m.id === selectedModel)?.label.replace("Claude ", "") || "Sonnet"}
<ChevronDown className="w-3 h-3 opacity-50" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{CLAUDE_MODELS.map((model) => (
<DropdownMenuItem
key={model.id}
onClick={() => setSelectedModel(model.id)}
className={cn(
"cursor-pointer",
selectedModel === model.id && "bg-accent"
)}
data-testid={`model-option-${model.id}`}
>
<div className="flex flex-col">
<span className="font-medium">{model.label}</span>
<span className="text-xs text-muted-foreground">
{model.description}
</span>
</div>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{currentTool && (
<div className="flex items-center gap-2 text-xs text-muted-foreground bg-muted/50 px-3 py-1.5 rounded-full border border-border">
<Wrench className="w-3 h-3 text-primary" />

View File

@@ -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

View File

@@ -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) => {

View File

@@ -14,6 +14,8 @@ interface UseBoardDragDropProps {
updates: Partial<Feature>
) => Promise<void>;
handleStartImplementation: (feature: Feature) => Promise<boolean>;
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<Feature | null>(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,
]
);

View File

@@ -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<Message[]>([]);
@@ -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

View File

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

View File

@@ -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: (

View File

@@ -84,7 +84,8 @@ export interface AgentAPI {
sessionId: string,
message: string,
workingDirectory?: string,
imagePaths?: string[]
imagePaths?: string[],
model?: string
) => Promise<{
success: boolean;
error?: string;