Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/ui/src/components/dialogs/new-project-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export function NewProjectModal({

// Use platform-specific path separator
const pathSep =
typeof window !== 'undefined' && (window as any).electronAPI
typeof window !== 'undefined' && window.electronAPI
? navigator.platform.indexOf('Win') !== -1
? '\\'
: '/'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ interface EditProjectDialogProps {
export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDialogProps) {
const { setProjectName, setProjectIcon, setProjectCustomIcon } = useAppStore();
const [name, setName] = useState(project.name);
const [icon, setIcon] = useState<string | null>((project as any).icon || null);
const [icon, setIcon] = useState<string | null>(project.icon || null);
const [customIconPath, setCustomIconPath] = useState<string | null>(
(project as any).customIconPath || null
project.customIconPath || null
);
const [isUploadingIcon, setIsUploadingIcon] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
Expand All @@ -36,10 +36,10 @@ export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDi
if (name.trim() !== project.name) {
setProjectName(project.id, name.trim());
}
if (icon !== (project as any).icon) {
if (icon !== project.icon) {
setProjectIcon(project.id, icon);
}
if (customIconPath !== (project as any).customIconPath) {
if (customIconPath !== project.customIconPath) {
setProjectCustomIcon(project.id, customIconPath);
}
onOpenChange(false);
Expand Down
9 changes: 7 additions & 2 deletions apps/ui/src/components/ui/git-diff-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ export function GitDiffPanel({
<div className="flex flex-col items-center justify-center gap-2 py-8 text-muted-foreground">
<AlertCircle className="w-5 h-5 text-amber-500" />
<span className="text-sm">{error}</span>
<Button variant="ghost" size="sm" onClick={loadDiffs} className="mt-2">
<Button variant="ghost" size="sm" onClick={() => void loadDiffs()} className="mt-2">
<RefreshCw className="w-4 h-4 mr-2" />
Retry
</Button>
Expand Down Expand Up @@ -550,7 +550,12 @@ export function GitDiffPanel({
>
Collapse All
</Button>
<Button variant="ghost" size="sm" onClick={loadDiffs} className="text-xs h-7">
<Button
variant="ghost"
size="sm"
onClick={() => void loadDiffs()}
className="text-xs h-7"
>
<RefreshCw className="w-3 h-3 mr-1" />
Refresh
</Button>
Expand Down
34 changes: 19 additions & 15 deletions apps/ui/src/components/ui/task-progress-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Check, Circle, ChevronDown, ChevronRight, FileCode } from 'lucide-react
import { Spinner } from '@/components/ui/spinner';
import { getElectronAPI } from '@/lib/electron';
import type { AutoModeEvent } from '@/types/electron';
import type { Feature, ParsedTask } from '@automaker/types';
import { Badge } from '@/components/ui/badge';

interface TaskInfo {
Expand Down Expand Up @@ -53,26 +54,29 @@ export function TaskProgressPanel({
}

const result = await api.features.get(projectPath, featureId);
const feature: any = (result as any).feature;
const feature = (result as { success: boolean; feature?: Feature }).feature;
if (result.success && feature?.planSpec?.tasks) {
const planSpec = feature.planSpec as any;
const planTasks = planSpec.tasks;
const planSpec = feature.planSpec;
const planTasks = planSpec.tasks; // Already guarded by the if condition above
const currentId = planSpec.currentTaskId;
const completedCount = planSpec.tasksCompleted || 0;

// Convert planSpec tasks to TaskInfo with proper status
const initialTasks: TaskInfo[] = planTasks.map((t: any, index: number) => ({
id: t.id,
description: t.description,
filePath: t.filePath,
phase: t.phase,
status:
index < completedCount
? ('completed' as const)
: t.id === currentId
? ('in_progress' as const)
: ('pending' as const),
}));
// planTasks is guaranteed to be defined due to the if condition check
const initialTasks: TaskInfo[] = (planTasks as ParsedTask[]).map(
(t: ParsedTask, index: number) => ({
id: t.id,
description: t.description,
filePath: t.filePath,
phase: t.phase,
status:
index < completedCount
? ('completed' as const)
: t.id === currentId
? ('in_progress' as const)
: ('pending' as const),
})
);

setTasks(initialTasks);
setCurrentTaskId(currentId || null);
Expand Down
8 changes: 4 additions & 4 deletions apps/ui/src/components/views/analysis-view.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback, useState } from 'react';
import { createLogger } from '@automaker/utils/logger';
import { useQueryClient } from '@tanstack/react-query';
import { useAppStore, FileTreeNode, ProjectAnalysis } from '@/store/app-store';
import { useAppStore, FileTreeNode, ProjectAnalysis, Feature } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
Expand Down Expand Up @@ -640,14 +640,14 @@ ${Object.entries(projectAnalysis.filesByExtension)
}

for (const detectedFeature of detectedFeatures) {
await api.features.create(currentProject.path, {
const newFeature: Feature = {
id: generateUUID(),
category: detectedFeature.category,
description: detectedFeature.description,
status: 'backlog',
// Initialize with empty steps so the object satisfies the Feature type
steps: [],
} as any);
};
await api.features.create(currentProject.path, newFeature);
}

// Invalidate React Query cache to sync UI
Expand Down
91 changes: 48 additions & 43 deletions apps/ui/src/components/views/board-view.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
// @ts-nocheck - dnd-kit type incompatibilities with collision detection and complex state management
import { useEffect, useState, useCallback, useMemo } from 'react';
import { createLogger } from '@automaker/utils/logger';
import type { PointerEvent as ReactPointerEvent } from 'react';
import {
DndContext,
PointerSensor,
useSensor,
useSensors,
rectIntersection,
pointerWithin,
type PointerEvent as DndPointerEvent,
type CollisionDetection,
type Collision,
} from '@dnd-kit/core';

// Custom pointer sensor that ignores drag events from within dialogs
class DialogAwarePointerSensor extends PointerSensor {
static activators = [
{
eventName: 'onPointerDown' as const,
handler: ({ nativeEvent: event }: { nativeEvent: DndPointerEvent }) => {
handler: ({ nativeEvent: event }: ReactPointerEvent) => {
// Don't start drag if the event originated from inside a dialog
if ((event.target as Element)?.closest?.('[role="dialog"]')) {
return false;
Expand All @@ -29,7 +30,7 @@ class DialogAwarePointerSensor extends PointerSensor {
import { useAppStore, Feature } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron';
import { getHttpApiClient } from '@/lib/http-api-client';
import type { BacklogPlanResult } from '@automaker/types';
import type { BacklogPlanResult, FeatureStatusWithPipeline } from '@automaker/types';
import { pathsEqual } from '@/lib/utils';
import { toast } from 'sonner';
import { BoardBackgroundModal } from '@/components/dialogs/board-background-modal';
Expand Down Expand Up @@ -171,13 +172,9 @@ export function BoardView() {
const [showCreatePRDialog, setShowCreatePRDialog] = useState(false);
const [showCreateBranchDialog, setShowCreateBranchDialog] = useState(false);
const [showPullResolveConflictsDialog, setShowPullResolveConflictsDialog] = useState(false);
const [selectedWorktreeForAction, setSelectedWorktreeForAction] = useState<{
path: string;
branch: string;
isMain: boolean;
hasChanges?: boolean;
changedFilesCount?: number;
} | null>(null);
const [selectedWorktreeForAction, setSelectedWorktreeForAction] = useState<WorktreeInfo | null>(
null
);
const [worktreeRefreshKey, setWorktreeRefreshKey] = useState(0);

// Backlog plan dialog state
Expand Down Expand Up @@ -348,12 +345,12 @@ export function BoardView() {
}, [currentProject, worktreeRefreshKey]);

// Custom collision detection that prioritizes specific drop targets (cards, worktrees) over columns
const collisionDetectionStrategy = useCallback((args: any) => {
const collisionDetectionStrategy = useCallback((args: Parameters<CollisionDetection>[0]) => {
const pointerCollisions = pointerWithin(args);

// Priority 1: Specific drop targets (cards for dependency links, worktrees)
// These need to be detected even if they are inside a column
const specificTargetCollisions = pointerCollisions.filter((collision: any) => {
const specificTargetCollisions = pointerCollisions.filter((collision: Collision) => {
const id = String(collision.id);
return id.startsWith('card-drop-') || id.startsWith('worktree-drop-');
});
Expand All @@ -363,7 +360,7 @@ export function BoardView() {
}

// Priority 2: Columns
const columnCollisions = pointerCollisions.filter((collision: any) =>
const columnCollisions = pointerCollisions.filter((collision: Collision) =>
COLUMNS.some((col) => col.id === collision.id)
);

Expand Down Expand Up @@ -417,19 +414,29 @@ export function BoardView() {

// Get the branch for the currently selected worktree
// Find the worktree that matches the current selection, or use main worktree
const selectedWorktree = useMemo(() => {
const selectedWorktree = useMemo((): WorktreeInfo | undefined => {
let found;
if (currentWorktreePath === null) {
// Primary worktree selected - find the main worktree
return worktrees.find((w) => w.isMain);
found = worktrees.find((w) => w.isMain);
} else {
// Specific worktree selected - find it by path
return worktrees.find((w) => !w.isMain && pathsEqual(w.path, currentWorktreePath));
found = worktrees.find((w) => !w.isMain && pathsEqual(w.path, currentWorktreePath));
}
if (!found) return undefined;
// Ensure all required WorktreeInfo fields are present
return {
...found,
isCurrent:
found.isCurrent ??
(currentWorktreePath !== null ? pathsEqual(found.path, currentWorktreePath) : found.isMain),
hasWorktree: found.hasWorktree ?? true,
};
}, [worktrees, currentWorktreePath]);

// Auto mode hook - pass current worktree to get worktree-specific state
// Must be after selectedWorktree is defined
const autoMode = useAutoMode(selectedWorktree ?? undefined);
const autoMode = useAutoMode(selectedWorktree);
// Get runningTasks from the hook (scoped to current project/worktree)
const runningAutoTasks = autoMode.runningTasks;
// Get worktree-specific maxConcurrency from the hook
Expand Down Expand Up @@ -958,28 +965,27 @@ export function BoardView() {
const api = getElectronAPI();
if (!api?.backlogPlan) return;

const unsubscribe = api.backlogPlan.onEvent(
(event: { type: string; result?: BacklogPlanResult; error?: string }) => {
if (event.type === 'backlog_plan_complete') {
setIsGeneratingPlan(false);
if (event.result && event.result.changes?.length > 0) {
setPendingBacklogPlan(event.result);
toast.success('Plan ready! Click to review.', {
duration: 10000,
action: {
label: 'Review',
onClick: () => setShowPlanDialog(true),
},
});
} else {
toast.info('No changes generated. Try again with a different prompt.');
}
} else if (event.type === 'backlog_plan_error') {
setIsGeneratingPlan(false);
toast.error(`Plan generation failed: ${event.error}`);
const unsubscribe = api.backlogPlan.onEvent((data: unknown) => {
const event = data as { type: string; result?: BacklogPlanResult; error?: string };
if (event.type === 'backlog_plan_complete') {
setIsGeneratingPlan(false);
if (event.result && event.result.changes?.length > 0) {
setPendingBacklogPlan(event.result);
toast.success('Plan ready! Click to review.', {
duration: 10000,
action: {
label: 'Review',
onClick: () => setShowPlanDialog(true),
},
});
} else {
toast.info('No changes generated. Try again with a different prompt.');
}
} else if (event.type === 'backlog_plan_error') {
setIsGeneratingPlan(false);
toast.error(`Plan generation failed: ${event.error}`);
}
);
});

return unsubscribe;
}, []);
Expand Down Expand Up @@ -1091,10 +1097,10 @@ export function BoardView() {
// Build columnFeaturesMap for ListView
// pipelineConfig is now from usePipelineConfig React Query hook at the top
const columnFeaturesMap = useMemo(() => {
const columns = getColumnsWithPipeline(pipelineConfig);
const columns = getColumnsWithPipeline(pipelineConfig ?? null);
const map: Record<string, typeof hookFeatures> = {};
for (const column of columns) {
map[column.id] = getColumnFeatures(column.id as any);
map[column.id] = getColumnFeatures(column.id as FeatureStatusWithPipeline);
}
return map;
}, [pipelineConfig, getColumnFeatures]);
Expand Down Expand Up @@ -1444,14 +1450,13 @@ export function BoardView() {
onAddFeature={() => setShowAddDialog(true)}
onShowCompletedModal={() => setShowCompletedModal(true)}
completedCount={completedFeatures.length}
pipelineConfig={pipelineConfig}
pipelineConfig={pipelineConfig ?? null}
onOpenPipelineSettings={() => setShowPipelineSettings(true)}
isSelectionMode={isSelectionMode}
selectionTarget={selectionTarget}
selectedFeatureIds={selectedFeatureIds}
onToggleFeatureSelection={toggleFeatureSelection}
onToggleSelectionMode={toggleSelectionMode}
viewMode={viewMode}
isDragging={activeFeature !== null}
onAiSuggest={() => setShowPlanDialog(true)}
className="transition-opacity duration-200"
Expand Down Expand Up @@ -1604,7 +1609,7 @@ export function BoardView() {
open={showPipelineSettings}
onClose={() => setShowPipelineSettings(false)}
projectPath={currentProject.path}
pipelineConfig={pipelineConfig}
pipelineConfig={pipelineConfig ?? null}
onSave={async (config) => {
const api = getHttpApiClient();
const result = await api.pipeline.saveConfig(currentProject.path, config);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { memo, useEffect, useState, useMemo, useRef } from 'react';
import { Feature, ThinkingLevel, ParsedTask } from '@/store/app-store';
import type { ReasoningEffort } from '@automaker/types';
import { Feature, ThinkingLevel, ReasoningEffort, ParsedTask } from '@/store/app-store';
import { getProviderFromModel } from '@/lib/utils';
import { parseAgentContext, formatModelName, DEFAULT_MODEL } from '@/lib/agent-context-parser';
import { cn } from '@/lib/utils';
Expand Down Expand Up @@ -290,7 +289,8 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({
// Agent Info Panel for non-backlog cards
// Show panel if we have agentInfo OR planSpec.tasks (for spec/full mode)
// Note: hasPlanSpecTasks is already defined above and includes freshPlanSpec
if (feature.status !== 'backlog' && (agentInfo || hasPlanSpecTasks)) {
// (The backlog case was already handled above and returned early)
if (agentInfo || hasPlanSpecTasks) {
return (
<>
<div className="mb-3 space-y-2 overflow-hidden">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,11 @@ export const PriorityBadges = memo(function PriorityBadges({
return;
}

// eslint-disable-next-line no-undef
const interval = setInterval(() => {
setCurrentTime(Date.now());
}, 1000);

return () => {
// eslint-disable-next-line no-undef
clearInterval(interval);
};
}, [feature.justFinishedAt, feature.status, currentTime]);
Expand Down
Loading