mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +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";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ImageDropZone } from "@/components/ui/image-drop-zone";
|
import { ImageDropZone } from "@/components/ui/image-drop-zone";
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
Paperclip,
|
Paperclip,
|
||||||
X,
|
X,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
|
ChevronDown,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useElectronAgent } from "@/hooks/use-electron-agent";
|
import { useElectronAgent } from "@/hooks/use-electron-agent";
|
||||||
@@ -29,6 +30,13 @@ import {
|
|||||||
useKeyboardShortcutsConfig,
|
useKeyboardShortcutsConfig,
|
||||||
KeyboardShortcut,
|
KeyboardShortcut,
|
||||||
} from "@/hooks/use-keyboard-shortcuts";
|
} 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() {
|
export function AgentView() {
|
||||||
const { currentProject, setLastSelectedSession, getLastSelectedSession } =
|
const { currentProject, setLastSelectedSession, getLastSelectedSession } =
|
||||||
@@ -41,6 +49,7 @@ export function AgentView() {
|
|||||||
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
||||||
const [showSessionManager, setShowSessionManager] = useState(true);
|
const [showSessionManager, setShowSessionManager] = useState(true);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
|
const [selectedModel, setSelectedModel] = useState<AgentModel>("sonnet");
|
||||||
|
|
||||||
// Track if initial session has been loaded
|
// Track if initial session has been loaded
|
||||||
const initialSessionLoadedRef = useRef(false);
|
const initialSessionLoadedRef = useRef(false);
|
||||||
@@ -66,6 +75,7 @@ export function AgentView() {
|
|||||||
} = useElectronAgent({
|
} = useElectronAgent({
|
||||||
sessionId: currentSessionId || "",
|
sessionId: currentSessionId || "",
|
||||||
workingDirectory: currentProject?.path,
|
workingDirectory: currentProject?.path,
|
||||||
|
model: selectedModel,
|
||||||
onToolUse: (toolName) => {
|
onToolUse: (toolName) => {
|
||||||
setCurrentTool(toolName);
|
setCurrentTool(toolName);
|
||||||
setTimeout(() => setCurrentTool(null), 2000);
|
setTimeout(() => setCurrentTool(null), 2000);
|
||||||
@@ -501,6 +511,43 @@ export function AgentView() {
|
|||||||
|
|
||||||
{/* Status indicators & actions */}
|
{/* Status indicators & actions */}
|
||||||
<div className="flex items-center gap-3">
|
<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 && (
|
{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">
|
<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" />
|
<Wrench className="w-3 h-3 text-primary" />
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export function BoardView() {
|
|||||||
setKanbanCardDetailLevel,
|
setKanbanCardDetailLevel,
|
||||||
specCreatingForProject,
|
specCreatingForProject,
|
||||||
setSpecCreatingForProject,
|
setSpecCreatingForProject,
|
||||||
|
getCurrentWorktree,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
const shortcuts = useKeyboardShortcutsConfig();
|
const shortcuts = useKeyboardShortcutsConfig();
|
||||||
const {
|
const {
|
||||||
@@ -298,12 +299,17 @@ export function BoardView() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Use drag and drop hook
|
// 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({
|
const { activeFeature, handleDragStart, handleDragEnd } = useBoardDragDrop({
|
||||||
features: hookFeatures,
|
features: hookFeatures,
|
||||||
currentProject,
|
currentProject,
|
||||||
runningAutoTasks,
|
runningAutoTasks,
|
||||||
persistFeatureUpdate,
|
persistFeatureUpdate,
|
||||||
handleStartImplementation,
|
handleStartImplementation,
|
||||||
|
currentWorktreePath,
|
||||||
|
projectPath: currentProject?.path || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use column features hook
|
// Use column features hook
|
||||||
@@ -311,6 +317,8 @@ export function BoardView() {
|
|||||||
features: hookFeatures,
|
features: hookFeatures,
|
||||||
runningAutoTasks,
|
runningAutoTasks,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
currentWorktreePath,
|
||||||
|
projectPath: currentProject?.path || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use background hook
|
// Use background hook
|
||||||
|
|||||||
@@ -7,12 +7,16 @@ interface UseBoardColumnFeaturesProps {
|
|||||||
features: Feature[];
|
features: Feature[];
|
||||||
runningAutoTasks: string[];
|
runningAutoTasks: string[];
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
|
currentWorktreePath: string | null; // Currently selected worktree path
|
||||||
|
projectPath: string | null; // Main project path (for main worktree)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBoardColumnFeatures({
|
export function useBoardColumnFeatures({
|
||||||
features,
|
features,
|
||||||
runningAutoTasks,
|
runningAutoTasks,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
currentWorktreePath,
|
||||||
|
projectPath,
|
||||||
}: UseBoardColumnFeaturesProps) {
|
}: UseBoardColumnFeaturesProps) {
|
||||||
// Memoize column features to prevent unnecessary re-renders
|
// Memoize column features to prevent unnecessary re-renders
|
||||||
const columnFeaturesMap = useMemo(() => {
|
const columnFeaturesMap = useMemo(() => {
|
||||||
@@ -34,16 +38,37 @@ export function useBoardColumnFeatures({
|
|||||||
)
|
)
|
||||||
: features;
|
: 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) => {
|
filteredFeatures.forEach((f) => {
|
||||||
// If feature has a running agent, always show it in "in_progress"
|
// If feature has a running agent, always show it in "in_progress"
|
||||||
const isRunning = runningAutoTasks.includes(f.id);
|
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) {
|
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 {
|
} else {
|
||||||
// Otherwise, use the feature's status (fallback to backlog for unknown statuses)
|
// Otherwise, use the feature's status (fallback to backlog for unknown statuses)
|
||||||
const status = f.status as ColumnId;
|
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 {
|
} else {
|
||||||
// Unknown status, default to backlog
|
// Unknown status, default to backlog
|
||||||
map.backlog.push(f);
|
map.backlog.push(f);
|
||||||
@@ -59,7 +84,7 @@ export function useBoardColumnFeatures({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}, [features, runningAutoTasks, searchQuery]);
|
}, [features, runningAutoTasks, searchQuery, currentWorktreePath, projectPath]);
|
||||||
|
|
||||||
const getColumnFeatures = useCallback(
|
const getColumnFeatures = useCallback(
|
||||||
(columnId: ColumnId) => {
|
(columnId: ColumnId) => {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ interface UseBoardDragDropProps {
|
|||||||
updates: Partial<Feature>
|
updates: Partial<Feature>
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
handleStartImplementation: (feature: Feature) => Promise<boolean>;
|
handleStartImplementation: (feature: Feature) => Promise<boolean>;
|
||||||
|
currentWorktreePath: string | null; // Currently selected worktree path
|
||||||
|
projectPath: string | null; // Main project path
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBoardDragDrop({
|
export function useBoardDragDrop({
|
||||||
@@ -22,10 +24,15 @@ export function useBoardDragDrop({
|
|||||||
runningAutoTasks,
|
runningAutoTasks,
|
||||||
persistFeatureUpdate,
|
persistFeatureUpdate,
|
||||||
handleStartImplementation,
|
handleStartImplementation,
|
||||||
|
currentWorktreePath,
|
||||||
|
projectPath,
|
||||||
}: UseBoardDragDropProps) {
|
}: UseBoardDragDropProps) {
|
||||||
const [activeFeature, setActiveFeature] = useState<Feature | null>(null);
|
const [activeFeature, setActiveFeature] = useState<Feature | null>(null);
|
||||||
const { moveFeature } = useAppStore();
|
const { moveFeature } = useAppStore();
|
||||||
|
|
||||||
|
// Determine the effective worktree path for assigning to features
|
||||||
|
const effectiveWorktreePath = currentWorktreePath || projectPath;
|
||||||
|
|
||||||
const handleDragStart = useCallback(
|
const handleDragStart = useCallback(
|
||||||
(event: DragStartEvent) => {
|
(event: DragStartEvent) => {
|
||||||
const { active } = event;
|
const { active } = event;
|
||||||
@@ -97,6 +104,10 @@ export function useBoardDragDrop({
|
|||||||
if (draggedFeature.status === "backlog") {
|
if (draggedFeature.status === "backlog") {
|
||||||
// From backlog
|
// From backlog
|
||||||
if (targetStatus === "in_progress") {
|
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
|
// Use helper function to handle concurrency check and start implementation
|
||||||
await handleStartImplementation(draggedFeature);
|
await handleStartImplementation(draggedFeature);
|
||||||
} else {
|
} else {
|
||||||
@@ -123,10 +134,11 @@ export function useBoardDragDrop({
|
|||||||
} else if (targetStatus === "backlog") {
|
} else if (targetStatus === "backlog") {
|
||||||
// Allow moving waiting_approval cards back to backlog
|
// Allow moving waiting_approval cards back to backlog
|
||||||
moveFeature(featureId, "backlog");
|
moveFeature(featureId, "backlog");
|
||||||
// Clear justFinishedAt timestamp when moving back to backlog
|
// Clear justFinishedAt timestamp and worktreePath when moving back to backlog
|
||||||
persistFeatureUpdate(featureId, {
|
persistFeatureUpdate(featureId, {
|
||||||
status: "backlog",
|
status: "backlog",
|
||||||
justFinishedAt: undefined,
|
justFinishedAt: undefined,
|
||||||
|
worktreePath: undefined,
|
||||||
});
|
});
|
||||||
toast.info("Feature moved to backlog", {
|
toast.info("Feature moved to backlog", {
|
||||||
description: `Moved to Backlog: ${draggedFeature.description.slice(
|
description: `Moved to Backlog: ${draggedFeature.description.slice(
|
||||||
@@ -166,7 +178,8 @@ export function useBoardDragDrop({
|
|||||||
} else if (targetStatus === "backlog") {
|
} else if (targetStatus === "backlog") {
|
||||||
// Allow moving skipTests cards back to backlog
|
// Allow moving skipTests cards back to backlog
|
||||||
moveFeature(featureId, "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", {
|
toast.info("Feature moved to backlog", {
|
||||||
description: `Moved to Backlog: ${draggedFeature.description.slice(
|
description: `Moved to Backlog: ${draggedFeature.description.slice(
|
||||||
0,
|
0,
|
||||||
@@ -189,7 +202,8 @@ export function useBoardDragDrop({
|
|||||||
} else if (targetStatus === "backlog") {
|
} else if (targetStatus === "backlog") {
|
||||||
// Allow moving verified cards back to backlog
|
// Allow moving verified cards back to backlog
|
||||||
moveFeature(featureId, "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", {
|
toast.info("Feature moved to backlog", {
|
||||||
description: `Moved to Backlog: ${draggedFeature.description.slice(
|
description: `Moved to Backlog: ${draggedFeature.description.slice(
|
||||||
0,
|
0,
|
||||||
@@ -205,6 +219,7 @@ export function useBoardDragDrop({
|
|||||||
moveFeature,
|
moveFeature,
|
||||||
persistFeatureUpdate,
|
persistFeatureUpdate,
|
||||||
handleStartImplementation,
|
handleStartImplementation,
|
||||||
|
effectiveWorktreePath,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { getElectronAPI } from "@/lib/electron";
|
|||||||
interface UseElectronAgentOptions {
|
interface UseElectronAgentOptions {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
workingDirectory?: string;
|
workingDirectory?: string;
|
||||||
|
model?: string;
|
||||||
onToolUse?: (toolName: string, toolInput: unknown) => void;
|
onToolUse?: (toolName: string, toolInput: unknown) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ interface UseElectronAgentResult {
|
|||||||
export function useElectronAgent({
|
export function useElectronAgent({
|
||||||
sessionId,
|
sessionId,
|
||||||
workingDirectory,
|
workingDirectory,
|
||||||
|
model,
|
||||||
onToolUse,
|
onToolUse,
|
||||||
}: UseElectronAgentOptions): UseElectronAgentResult {
|
}: UseElectronAgentOptions): UseElectronAgentResult {
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
@@ -88,7 +90,8 @@ export function useElectronAgent({
|
|||||||
sessionId,
|
sessionId,
|
||||||
content,
|
content,
|
||||||
workingDirectory,
|
workingDirectory,
|
||||||
imagePaths
|
imagePaths,
|
||||||
|
model
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@@ -104,7 +107,7 @@ export function useElectronAgent({
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[sessionId, workingDirectory, isProcessing]
|
[sessionId, workingDirectory, model, isProcessing]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Message queue for queuing messages when agent is busy
|
// Message queue for queuing messages when agent is busy
|
||||||
@@ -344,7 +347,8 @@ export function useElectronAgent({
|
|||||||
sessionId,
|
sessionId,
|
||||||
content,
|
content,
|
||||||
workingDirectory,
|
workingDirectory,
|
||||||
imagePaths
|
imagePaths,
|
||||||
|
model
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@@ -359,7 +363,7 @@ export function useElectronAgent({
|
|||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[sessionId, workingDirectory, isProcessing]
|
[sessionId, workingDirectory, model, isProcessing]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Stop current execution
|
// Stop current execution
|
||||||
|
|||||||
@@ -393,7 +393,8 @@ export interface ElectronAPI {
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
message: string,
|
message: string,
|
||||||
workingDirectory?: string,
|
workingDirectory?: string,
|
||||||
imagePaths?: string[]
|
imagePaths?: string[],
|
||||||
|
model?: string
|
||||||
) => Promise<{ success: boolean; error?: string }>;
|
) => Promise<{ success: boolean; error?: string }>;
|
||||||
getHistory: (sessionId: string) => Promise<{
|
getHistory: (sessionId: string) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|||||||
@@ -699,13 +699,15 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
message: string,
|
message: string,
|
||||||
workingDirectory?: string,
|
workingDirectory?: string,
|
||||||
imagePaths?: string[]
|
imagePaths?: string[],
|
||||||
|
model?: string
|
||||||
): Promise<{ success: boolean; error?: string }> =>
|
): Promise<{ success: boolean; error?: string }> =>
|
||||||
this.post("/api/agent/send", {
|
this.post("/api/agent/send", {
|
||||||
sessionId,
|
sessionId,
|
||||||
message,
|
message,
|
||||||
workingDirectory,
|
workingDirectory,
|
||||||
imagePaths,
|
imagePaths,
|
||||||
|
model,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getHistory: (
|
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,
|
sessionId: string,
|
||||||
message: string,
|
message: string,
|
||||||
workingDirectory?: string,
|
workingDirectory?: string,
|
||||||
imagePaths?: string[]
|
imagePaths?: string[],
|
||||||
|
model?: string
|
||||||
) => Promise<{
|
) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user