mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
adding a worktree switch feature
This commit is contained in:
1
.worktrees/feature-model-select
Submodule
1
.worktrees/feature-model-select
Submodule
Submodule .worktrees/feature-model-select added at b95c54a539
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: (
|
||||
|
||||
3
apps/app/src/types/electron.d.ts
vendored
3
apps/app/src/types/electron.d.ts
vendored
@@ -84,7 +84,8 @@ export interface AgentAPI {
|
||||
sessionId: string,
|
||||
message: string,
|
||||
workingDirectory?: string,
|
||||
imagePaths?: string[]
|
||||
imagePaths?: string[],
|
||||
model?: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
|
||||
Reference in New Issue
Block a user