mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
chore: Enhance type safety and improve code consistency across components
- Added a new `typecheck` script in `package.json` for better type checking in the UI workspace. - Refactored several components to remove unnecessary type assertions and improve type safety, particularly in `new-project-modal.tsx`, `edit-project-dialog.tsx`, and `task-progress-panel.tsx`. - Updated event handling in `git-diff-panel.tsx` to use async functions for better error handling. - Improved type definitions in various files, including `setup-view` and `electron.ts`, to ensure consistent usage of types across the codebase. - Cleaned up global type definitions for better clarity and maintainability. These changes aim to streamline the development process and reduce potential runtime errors.
This commit is contained in:
@@ -191,7 +191,7 @@ export function NewProjectModal({
|
|||||||
|
|
||||||
// Use platform-specific path separator
|
// Use platform-specific path separator
|
||||||
const pathSep =
|
const pathSep =
|
||||||
typeof window !== 'undefined' && (window as any).electronAPI
|
typeof window !== 'undefined' && window.electronAPI
|
||||||
? navigator.platform.indexOf('Win') !== -1
|
? navigator.platform.indexOf('Win') !== -1
|
||||||
? '\\'
|
? '\\'
|
||||||
: '/'
|
: '/'
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ interface EditProjectDialogProps {
|
|||||||
export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDialogProps) {
|
export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDialogProps) {
|
||||||
const { setProjectName, setProjectIcon, setProjectCustomIcon } = useAppStore();
|
const { setProjectName, setProjectIcon, setProjectCustomIcon } = useAppStore();
|
||||||
const [name, setName] = useState(project.name);
|
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>(
|
const [customIconPath, setCustomIconPath] = useState<string | null>(
|
||||||
(project as any).customIconPath || null
|
project.customIconPath || null
|
||||||
);
|
);
|
||||||
const [isUploadingIcon, setIsUploadingIcon] = useState(false);
|
const [isUploadingIcon, setIsUploadingIcon] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -36,10 +36,10 @@ export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDi
|
|||||||
if (name.trim() !== project.name) {
|
if (name.trim() !== project.name) {
|
||||||
setProjectName(project.id, name.trim());
|
setProjectName(project.id, name.trim());
|
||||||
}
|
}
|
||||||
if (icon !== (project as any).icon) {
|
if (icon !== project.icon) {
|
||||||
setProjectIcon(project.id, icon);
|
setProjectIcon(project.id, icon);
|
||||||
}
|
}
|
||||||
if (customIconPath !== (project as any).customIconPath) {
|
if (customIconPath !== project.customIconPath) {
|
||||||
setProjectCustomIcon(project.id, customIconPath);
|
setProjectCustomIcon(project.id, customIconPath);
|
||||||
}
|
}
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
|||||||
@@ -479,7 +479,12 @@ export function GitDiffPanel({
|
|||||||
<div className="flex flex-col items-center justify-center gap-2 py-8 text-muted-foreground">
|
<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" />
|
<AlertCircle className="w-5 h-5 text-amber-500" />
|
||||||
<span className="text-sm">{error}</span>
|
<span className="text-sm">{error}</span>
|
||||||
<Button variant="ghost" size="sm" onClick={loadDiffs} className="mt-2">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => await loadDiffs()}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
@@ -550,7 +555,12 @@ export function GitDiffPanel({
|
|||||||
>
|
>
|
||||||
Collapse All
|
Collapse All
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" onClick={loadDiffs} className="text-xs h-7">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => await loadDiffs()}
|
||||||
|
className="text-xs h-7"
|
||||||
|
>
|
||||||
<RefreshCw className="w-3 h-3 mr-1" />
|
<RefreshCw className="w-3 h-3 mr-1" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Check, Circle, ChevronDown, ChevronRight, FileCode } from 'lucide-react
|
|||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
import type { AutoModeEvent } from '@/types/electron';
|
import type { AutoModeEvent } from '@/types/electron';
|
||||||
|
import type { Feature, ParsedTask } from '@automaker/types';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
interface TaskInfo {
|
interface TaskInfo {
|
||||||
@@ -53,26 +54,29 @@ export function TaskProgressPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await api.features.get(projectPath, featureId);
|
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) {
|
if (result.success && feature?.planSpec?.tasks) {
|
||||||
const planSpec = feature.planSpec as any;
|
const planSpec = feature.planSpec;
|
||||||
const planTasks = planSpec.tasks;
|
const planTasks = planSpec.tasks; // Already guarded by the if condition above
|
||||||
const currentId = planSpec.currentTaskId;
|
const currentId = planSpec.currentTaskId;
|
||||||
const completedCount = planSpec.tasksCompleted || 0;
|
const completedCount = planSpec.tasksCompleted || 0;
|
||||||
|
|
||||||
// Convert planSpec tasks to TaskInfo with proper status
|
// Convert planSpec tasks to TaskInfo with proper status
|
||||||
const initialTasks: TaskInfo[] = planTasks.map((t: any, index: number) => ({
|
// planTasks is guaranteed to be defined due to the if condition check
|
||||||
id: t.id,
|
const initialTasks: TaskInfo[] = (planTasks as ParsedTask[]).map(
|
||||||
description: t.description,
|
(t: ParsedTask, index: number) => ({
|
||||||
filePath: t.filePath,
|
id: t.id,
|
||||||
phase: t.phase,
|
description: t.description,
|
||||||
status:
|
filePath: t.filePath,
|
||||||
index < completedCount
|
phase: t.phase,
|
||||||
? ('completed' as const)
|
status:
|
||||||
: t.id === currentId
|
index < completedCount
|
||||||
? ('in_progress' as const)
|
? ('completed' as const)
|
||||||
: ('pending' as const),
|
: t.id === currentId
|
||||||
}));
|
? ('in_progress' as const)
|
||||||
|
: ('pending' as const),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
setTasks(initialTasks);
|
setTasks(initialTasks);
|
||||||
setCurrentTaskId(currentId || null);
|
setCurrentTaskId(currentId || null);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
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 { getElectronAPI } from '@/lib/electron';
|
||||||
import { queryKeys } from '@/lib/query-keys';
|
import { queryKeys } from '@/lib/query-keys';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
@@ -640,14 +640,14 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const detectedFeature of detectedFeatures) {
|
for (const detectedFeature of detectedFeatures) {
|
||||||
await api.features.create(currentProject.path, {
|
const newFeature: Feature = {
|
||||||
id: generateUUID(),
|
id: generateUUID(),
|
||||||
category: detectedFeature.category,
|
category: detectedFeature.category,
|
||||||
description: detectedFeature.description,
|
description: detectedFeature.description,
|
||||||
status: 'backlog',
|
status: 'backlog',
|
||||||
// Initialize with empty steps so the object satisfies the Feature type
|
|
||||||
steps: [],
|
steps: [],
|
||||||
} as any);
|
};
|
||||||
|
await api.features.create(currentProject.path, newFeature);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate React Query cache to sync UI
|
// Invalidate React Query cache to sync UI
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-nocheck - dnd-kit type incompatibilities with collision detection and complex state management
|
|
||||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +8,8 @@ import {
|
|||||||
rectIntersection,
|
rectIntersection,
|
||||||
pointerWithin,
|
pointerWithin,
|
||||||
type PointerEvent as DndPointerEvent,
|
type PointerEvent as DndPointerEvent,
|
||||||
|
type CollisionDetection,
|
||||||
|
type Collision,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
|
|
||||||
// Custom pointer sensor that ignores drag events from within dialogs
|
// Custom pointer sensor that ignores drag events from within dialogs
|
||||||
@@ -29,7 +30,7 @@ class DialogAwarePointerSensor extends PointerSensor {
|
|||||||
import { useAppStore, Feature } from '@/store/app-store';
|
import { useAppStore, Feature } from '@/store/app-store';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
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 { pathsEqual } from '@/lib/utils';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { BoardBackgroundModal } from '@/components/dialogs/board-background-modal';
|
import { BoardBackgroundModal } from '@/components/dialogs/board-background-modal';
|
||||||
@@ -348,12 +349,12 @@ export function BoardView() {
|
|||||||
}, [currentProject, worktreeRefreshKey]);
|
}, [currentProject, worktreeRefreshKey]);
|
||||||
|
|
||||||
// Custom collision detection that prioritizes specific drop targets (cards, worktrees) over columns
|
// 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);
|
const pointerCollisions = pointerWithin(args);
|
||||||
|
|
||||||
// Priority 1: Specific drop targets (cards for dependency links, worktrees)
|
// Priority 1: Specific drop targets (cards for dependency links, worktrees)
|
||||||
// These need to be detected even if they are inside a column
|
// 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);
|
const id = String(collision.id);
|
||||||
return id.startsWith('card-drop-') || id.startsWith('worktree-drop-');
|
return id.startsWith('card-drop-') || id.startsWith('worktree-drop-');
|
||||||
});
|
});
|
||||||
@@ -363,7 +364,7 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Priority 2: Columns
|
// Priority 2: Columns
|
||||||
const columnCollisions = pointerCollisions.filter((collision: any) =>
|
const columnCollisions = pointerCollisions.filter((collision: Collision) =>
|
||||||
COLUMNS.some((col) => col.id === collision.id)
|
COLUMNS.some((col) => col.id === collision.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1094,7 +1095,7 @@ export function BoardView() {
|
|||||||
const columns = getColumnsWithPipeline(pipelineConfig);
|
const columns = getColumnsWithPipeline(pipelineConfig);
|
||||||
const map: Record<string, typeof hookFeatures> = {};
|
const map: Record<string, typeof hookFeatures> = {};
|
||||||
for (const column of columns) {
|
for (const column of columns) {
|
||||||
map[column.id] = getColumnFeatures(column.id as any);
|
map[column.id] = getColumnFeatures(column.id as FeatureStatusWithPipeline);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [pipelineConfig, getColumnFeatures]);
|
}, [pipelineConfig, getColumnFeatures]);
|
||||||
|
|||||||
@@ -98,13 +98,11 @@ export const PriorityBadges = memo(function PriorityBadges({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setCurrentTime(Date.now());
|
setCurrentTime(Date.now());
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, [feature.justFinishedAt, feature.status, currentTime]);
|
}, [feature.justFinishedAt, feature.status, currentTime]);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useAppStore } from '@/store/app-store';
|
|||||||
import { extractSummary } from '@/lib/log-parser';
|
import { extractSummary } from '@/lib/log-parser';
|
||||||
import { useAgentOutput } from '@/hooks/queries';
|
import { useAgentOutput } from '@/hooks/queries';
|
||||||
import type { AutoModeEvent } from '@/types/electron';
|
import type { AutoModeEvent } from '@/types/electron';
|
||||||
|
import type { BacklogPlanEvent } from '@automaker/types';
|
||||||
|
|
||||||
interface AgentOutputModalProps {
|
interface AgentOutputModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -48,18 +49,16 @@ export function AgentOutputModal({
|
|||||||
const isBacklogPlan = featureId.startsWith('backlog-plan:');
|
const isBacklogPlan = featureId.startsWith('backlog-plan:');
|
||||||
|
|
||||||
// Resolve project path - prefer prop, fallback to window.__currentProject
|
// Resolve project path - prefer prop, fallback to window.__currentProject
|
||||||
const resolvedProjectPath = projectPathProp || (window as any).__currentProject?.path || '';
|
const resolvedProjectPath = projectPathProp || window.__currentProject?.path || '';
|
||||||
|
|
||||||
// Track additional content from WebSocket events (appended to query data)
|
// Track additional content from WebSocket events (appended to query data)
|
||||||
const [streamedContent, setStreamedContent] = useState<string>('');
|
const [streamedContent, setStreamedContent] = useState<string>('');
|
||||||
const [viewMode, setViewMode] = useState<ViewMode | null>(null);
|
const [viewMode, setViewMode] = useState<ViewMode | null>(null);
|
||||||
|
|
||||||
// Use React Query for initial output loading
|
// Use React Query for initial output loading
|
||||||
const { data: initialOutput = '', isLoading } = useAgentOutput(
|
const { data: initialOutput = '', isLoading } = useAgentOutput(resolvedProjectPath, featureId, {
|
||||||
resolvedProjectPath,
|
enabled: open && !!resolvedProjectPath,
|
||||||
featureId,
|
});
|
||||||
open && !!resolvedProjectPath
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset streamed content when modal opens or featureId changes
|
// Reset streamed content when modal opens or featureId changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -262,7 +261,8 @@ export function AgentOutputModal({
|
|||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.backlogPlan) return;
|
if (!api?.backlogPlan) return;
|
||||||
|
|
||||||
const unsubscribe = api.backlogPlan.onEvent((event: any) => {
|
const unsubscribe = api.backlogPlan.onEvent((data: unknown) => {
|
||||||
|
const event = data as BacklogPlanEvent;
|
||||||
if (!event?.type) return;
|
if (!event?.type) return;
|
||||||
|
|
||||||
let newContent = '';
|
let newContent = '';
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
|
import type { Feature } from '@/store/app-store';
|
||||||
|
|
||||||
const logger = createLogger('BoardEffects');
|
const logger = createLogger('BoardEffects');
|
||||||
|
|
||||||
interface UseBoardEffectsProps {
|
interface UseBoardEffectsProps {
|
||||||
currentProject: { path: string; id: string } | null;
|
currentProject: { path: string; id: string; name?: string } | null;
|
||||||
specCreatingForProject: string | null;
|
specCreatingForProject: string | null;
|
||||||
setSpecCreatingForProject: (path: string | null) => void;
|
setSpecCreatingForProject: (path: string | null) => void;
|
||||||
checkContextExists: (featureId: string) => Promise<boolean>;
|
checkContextExists: (featureId: string) => Promise<boolean>;
|
||||||
features: any[];
|
features: Feature[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
featuresWithContext: Set<string>;
|
featuresWithContext: Set<string>;
|
||||||
setFeaturesWithContext: (set: Set<string>) => void;
|
setFeaturesWithContext: (set: Set<string>) => void;
|
||||||
@@ -33,10 +34,10 @@ export function useBoardEffects({
|
|||||||
// Make current project available globally for modal
|
// Make current project available globally for modal
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentProject) {
|
if (currentProject) {
|
||||||
(window as any).__currentProject = currentProject;
|
window.__currentProject = currentProject;
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
(window as any).__currentProject = null;
|
window.__currentProject = null;
|
||||||
};
|
};
|
||||||
}, [currentProject]);
|
}, [currentProject]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-nocheck - model selector with provider-specific model options and validation
|
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Brain, AlertTriangle } from 'lucide-react';
|
import { Brain, AlertTriangle } from 'lucide-react';
|
||||||
@@ -7,7 +6,7 @@ import { cn } from '@/lib/utils';
|
|||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { useSetupStore } from '@/store/setup-store';
|
import { useSetupStore } from '@/store/setup-store';
|
||||||
import { getModelProvider } from '@automaker/types';
|
import { getModelProvider } from '@automaker/types';
|
||||||
import type { ModelProvider } from '@automaker/types';
|
import type { ModelProvider, CursorModelId } from '@automaker/types';
|
||||||
import { CLAUDE_MODELS, CURSOR_MODELS, ModelOption } from './model-constants';
|
import { CLAUDE_MODELS, CURSOR_MODELS, ModelOption } from './model-constants';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
@@ -40,6 +39,7 @@ export function ModelSelector({
|
|||||||
const isCursorAvailable = cursorCliStatus?.installed && cursorCliStatus?.auth?.authenticated;
|
const isCursorAvailable = cursorCliStatus?.installed && cursorCliStatus?.auth?.authenticated;
|
||||||
|
|
||||||
// Check if Codex CLI is available
|
// Check if Codex CLI is available
|
||||||
|
// @ts-expect-error - codexCliStatus uses CliStatus type but should use CodexCliStatus which has auth
|
||||||
const isCodexAvailable = codexCliStatus?.installed && codexCliStatus?.auth?.authenticated;
|
const isCodexAvailable = codexCliStatus?.installed && codexCliStatus?.auth?.authenticated;
|
||||||
|
|
||||||
// Fetch Codex models on mount
|
// Fetch Codex models on mount
|
||||||
@@ -75,8 +75,8 @@ export function ModelSelector({
|
|||||||
// Check both the full ID (for GPT models) and the unprefixed version (for non-GPT models)
|
// Check both the full ID (for GPT models) and the unprefixed version (for non-GPT models)
|
||||||
const unprefixedId = model.id.startsWith('cursor-') ? model.id.slice(7) : model.id;
|
const unprefixedId = model.id.startsWith('cursor-') ? model.id.slice(7) : model.id;
|
||||||
return (
|
return (
|
||||||
enabledCursorModels.includes(model.id as any) ||
|
enabledCursorModels.includes(model.id as CursorModelId) ||
|
||||||
enabledCursorModels.includes(unprefixedId as any)
|
enabledCursorModels.includes(unprefixedId as CursorModelId)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { UIEvent } from 'react';
|
import type { UIEvent } from 'react';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore, ChatSession } from '@/store/app-store';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -156,7 +156,7 @@ export function ChatHistory() {
|
|||||||
createChatSession();
|
createChatSession();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectSession = (session: any) => {
|
const handleSelectSession = (session: ChatSession) => {
|
||||||
setCurrentChatSession(session);
|
setCurrentChatSession(session);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// @ts-nocheck - graph view page with feature filtering and visualization state
|
|
||||||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||||
import { useAppStore, Feature } from '@/store/app-store';
|
import { useAppStore, Feature, FeatureImagePath } from '@/store/app-store';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { GraphView } from './graph-view';
|
import { GraphView } from './graph-view';
|
||||||
import {
|
import {
|
||||||
@@ -236,7 +235,7 @@ export function GraphViewPage() {
|
|||||||
// Follow-up state (simplified for graph view)
|
// Follow-up state (simplified for graph view)
|
||||||
const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null);
|
const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null);
|
||||||
const [followUpPrompt, setFollowUpPrompt] = useState('');
|
const [followUpPrompt, setFollowUpPrompt] = useState('');
|
||||||
const [followUpImagePaths, setFollowUpImagePaths] = useState<any[]>([]);
|
const [followUpImagePaths, setFollowUpImagePaths] = useState<FeatureImagePath[]>([]);
|
||||||
const [, setFollowUpPreviewMap] = useState<Map<string, string>>(new Map());
|
const [, setFollowUpPreviewMap] = useState<Map<string, string>>(new Map());
|
||||||
|
|
||||||
// In-progress features for shortcuts
|
// In-progress features for shortcuts
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-nocheck - interview flow state machine with dynamic question handling
|
|
||||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
import { useAppStore, Feature } from '@/store/app-store';
|
import { useAppStore, Feature } from '@/store/app-store';
|
||||||
@@ -324,7 +323,7 @@ export function InterviewView() {
|
|||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
// Use platform-specific path separator
|
// Use platform-specific path separator
|
||||||
const pathSep =
|
const pathSep =
|
||||||
typeof window !== 'undefined' && (window as any).electronAPI
|
typeof window !== 'undefined' && window.electronAPI
|
||||||
? navigator.platform.indexOf('Win') !== -1
|
? navigator.platform.indexOf('Win') !== -1
|
||||||
? '\\'
|
? '\\'
|
||||||
: '/'
|
: '/'
|
||||||
@@ -349,8 +348,9 @@ export function InterviewView() {
|
|||||||
id: generateUUID(),
|
id: generateUUID(),
|
||||||
category: 'Core',
|
category: 'Core',
|
||||||
description: 'Initial project setup',
|
description: 'Initial project setup',
|
||||||
status: 'backlog' as const,
|
status: 'backlog',
|
||||||
skipTests: true,
|
skipTests: true,
|
||||||
|
steps: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!api.features) {
|
if (!api.features) {
|
||||||
|
|||||||
@@ -1,15 +1,31 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
|
import type { ModelProvider } from '@automaker/types';
|
||||||
|
import type { CliStatus } from '@/store/setup-store';
|
||||||
|
|
||||||
const logger = createLogger('CliInstallation');
|
const logger = createLogger('CliInstallation');
|
||||||
|
|
||||||
|
interface InstallApiResult {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InstallProgressEvent {
|
||||||
|
cli?: string;
|
||||||
|
data?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface UseCliInstallationOptions {
|
interface UseCliInstallationOptions {
|
||||||
cliType: 'claude';
|
cliType: ModelProvider;
|
||||||
installApi: () => Promise<any>;
|
installApi: () => Promise<InstallApiResult>;
|
||||||
onProgressEvent?: (callback: (progress: any) => void) => (() => void) | undefined;
|
onProgressEvent?: (
|
||||||
|
callback: (progress: InstallProgressEvent) => void
|
||||||
|
) => (() => void) | undefined;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
getStoreState?: () => any;
|
getStoreState?: () => CliStatus | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCliInstallation({
|
export function useCliInstallation({
|
||||||
@@ -32,15 +48,13 @@ export function useCliInstallation({
|
|||||||
let unsubscribe: (() => void) | undefined;
|
let unsubscribe: (() => void) | undefined;
|
||||||
|
|
||||||
if (onProgressEvent) {
|
if (onProgressEvent) {
|
||||||
unsubscribe = onProgressEvent(
|
unsubscribe = onProgressEvent((progress: InstallProgressEvent) => {
|
||||||
(progress: { cli?: string; data?: string; type?: string }) => {
|
if (progress.cli === cliType) {
|
||||||
if (progress.cli === cliType) {
|
setInstallProgress((prev) => ({
|
||||||
setInstallProgress((prev) => ({
|
output: [...prev.output, progress.data || progress.type || ''],
|
||||||
output: [...prev.output, progress.data || progress.type || ''],
|
}));
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await installApi();
|
const result = await installApi();
|
||||||
|
|||||||
@@ -1,11 +1,34 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
|
import type { ModelProvider } from '@automaker/types';
|
||||||
|
import type { CliStatus, ClaudeAuthStatus, CodexAuthStatus } from '@/store/setup-store';
|
||||||
|
|
||||||
|
interface CliStatusApiResponse {
|
||||||
|
success: boolean;
|
||||||
|
status?: 'installed' | 'not_installed';
|
||||||
|
installed?: boolean;
|
||||||
|
method?: string;
|
||||||
|
version?: string;
|
||||||
|
path?: string;
|
||||||
|
auth?: {
|
||||||
|
authenticated: boolean;
|
||||||
|
method: string;
|
||||||
|
hasCredentialsFile?: boolean;
|
||||||
|
hasStoredOAuthToken?: boolean;
|
||||||
|
hasStoredApiKey?: boolean;
|
||||||
|
hasEnvApiKey?: boolean;
|
||||||
|
hasEnvOAuthToken?: boolean;
|
||||||
|
hasAuthFile?: boolean;
|
||||||
|
hasApiKey?: boolean;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface UseCliStatusOptions {
|
interface UseCliStatusOptions {
|
||||||
cliType: 'claude' | 'codex';
|
cliType: ModelProvider;
|
||||||
statusApi: () => Promise<any>;
|
statusApi: () => Promise<CliStatusApiResponse>;
|
||||||
setCliStatus: (status: any) => void;
|
setCliStatus: (status: CliStatus | null) => void;
|
||||||
setAuthStatus: (status: any) => void;
|
setAuthStatus: (status: ClaudeAuthStatus | CodexAuthStatus | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VALID_AUTH_METHODS = {
|
const VALID_AUTH_METHODS = {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-nocheck - CLI setup wizard with step validation and setup store state
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -45,6 +44,33 @@ type VerificationStatus = 'idle' | 'verifying' | 'verified' | 'error';
|
|||||||
|
|
||||||
type CliSetupAuthStatus = ClaudeAuthStatus | CodexAuthStatus;
|
type CliSetupAuthStatus = ClaudeAuthStatus | CodexAuthStatus;
|
||||||
|
|
||||||
|
interface CliStatusApiResponse {
|
||||||
|
success: boolean;
|
||||||
|
status?: 'installed' | 'not_installed';
|
||||||
|
installed?: boolean;
|
||||||
|
method?: string;
|
||||||
|
version?: string;
|
||||||
|
path?: string;
|
||||||
|
auth?: {
|
||||||
|
authenticated: boolean;
|
||||||
|
method: string;
|
||||||
|
hasCredentialsFile?: boolean;
|
||||||
|
hasStoredOAuthToken?: boolean;
|
||||||
|
hasStoredApiKey?: boolean;
|
||||||
|
hasEnvApiKey?: boolean;
|
||||||
|
hasEnvOAuthToken?: boolean;
|
||||||
|
hasAuthFile?: boolean;
|
||||||
|
hasApiKey?: boolean;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InstallApiResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface CliSetupConfig {
|
interface CliSetupConfig {
|
||||||
cliType: ModelProvider;
|
cliType: ModelProvider;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@@ -73,8 +99,8 @@ interface CliSetupConfig {
|
|||||||
buildCliAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus;
|
buildCliAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus;
|
||||||
buildApiKeyAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus;
|
buildApiKeyAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus;
|
||||||
buildClearedAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus;
|
buildClearedAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus;
|
||||||
statusApi: () => Promise<any>;
|
statusApi: () => Promise<CliStatusApiResponse>;
|
||||||
installApi: () => Promise<any>;
|
installApi: () => Promise<InstallApiResponse>;
|
||||||
verifyAuthApi: (
|
verifyAuthApi: (
|
||||||
method: 'cli' | 'api_key',
|
method: 'cli' | 'api_key',
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
|
|||||||
@@ -282,8 +282,10 @@ export function useSettingsSync(): SettingsSyncState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info('[SYNC_SEND] Sending settings update to server:', {
|
logger.info('[SYNC_SEND] Sending settings update to server:', {
|
||||||
projects: (updates.projects as any)?.length ?? 0,
|
projects: Array.isArray(updates.projects) ? updates.projects.length : 0,
|
||||||
trashedProjects: (updates.trashedProjects as any)?.length ?? 0,
|
trashedProjects: Array.isArray(updates.trashedProjects)
|
||||||
|
? updates.trashedProjects.length
|
||||||
|
: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await api.settings.updateGlobal(updates);
|
const result = await api.settings.updateGlobal(updates);
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ import type {
|
|||||||
UpdateIdeaInput,
|
UpdateIdeaInput,
|
||||||
ConvertToFeatureOptions,
|
ConvertToFeatureOptions,
|
||||||
IdeationContextSources,
|
IdeationContextSources,
|
||||||
|
Feature,
|
||||||
|
IdeationStreamEvent,
|
||||||
|
IdeationAnalysisEvent,
|
||||||
} from '@automaker/types';
|
} from '@automaker/types';
|
||||||
|
import type { InstallProgress } from '@/store/setup-store';
|
||||||
import { DEFAULT_MAX_CONCURRENCY } from '@automaker/types';
|
import { DEFAULT_MAX_CONCURRENCY } from '@automaker/types';
|
||||||
import { getJSON, setJSON, removeItem } from './storage';
|
import { getJSON, setJSON, removeItem } from './storage';
|
||||||
|
|
||||||
@@ -124,7 +128,7 @@ export interface IdeationAPI {
|
|||||||
projectPath: string,
|
projectPath: string,
|
||||||
ideaId: string,
|
ideaId: string,
|
||||||
options?: ConvertToFeatureOptions
|
options?: ConvertToFeatureOptions
|
||||||
) => Promise<{ success: boolean; feature?: any; featureId?: string; error?: string }>;
|
) => Promise<{ success: boolean; feature?: Feature; featureId?: string; error?: string }>;
|
||||||
|
|
||||||
// Add suggestion directly to board as feature
|
// Add suggestion directly to board as feature
|
||||||
addSuggestionToBoard: (
|
addSuggestionToBoard: (
|
||||||
@@ -141,8 +145,8 @@ export interface IdeationAPI {
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
// Event subscriptions
|
// Event subscriptions
|
||||||
onStream: (callback: (event: any) => void) => () => void;
|
onStream: (callback: (event: IdeationStreamEvent) => void) => () => void;
|
||||||
onAnalysisEvent: (callback: (event: any) => void) => () => void;
|
onAnalysisEvent: (callback: (event: IdeationAnalysisEvent) => void) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileEntry {
|
export interface FileEntry {
|
||||||
@@ -186,6 +190,16 @@ export interface StatResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Options for creating a pull request
|
||||||
|
export interface CreatePROptions {
|
||||||
|
projectPath?: string;
|
||||||
|
commitMessage?: string;
|
||||||
|
prTitle?: string;
|
||||||
|
prBody?: string;
|
||||||
|
baseBranch?: string;
|
||||||
|
draft?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// Re-export types from electron.d.ts for external use
|
// Re-export types from electron.d.ts for external use
|
||||||
export type {
|
export type {
|
||||||
AutoModeEvent,
|
AutoModeEvent,
|
||||||
@@ -212,9 +226,6 @@ import type {
|
|||||||
// Import HTTP API client (ES module)
|
// Import HTTP API client (ES module)
|
||||||
import { getHttpApiClient, getServerUrlSync } from './http-api-client';
|
import { getHttpApiClient, getServerUrlSync } from './http-api-client';
|
||||||
|
|
||||||
// Feature type - Import from app-store
|
|
||||||
import type { Feature } from '@/store/app-store';
|
|
||||||
|
|
||||||
// Running Agent type
|
// Running Agent type
|
||||||
export interface RunningAgent {
|
export interface RunningAgent {
|
||||||
featureId: string;
|
featureId: string;
|
||||||
@@ -749,7 +760,7 @@ export interface ElectronAPI {
|
|||||||
};
|
};
|
||||||
// Setup API surface is implemented by the main process and mirrored by HttpApiClient.
|
// Setup API surface is implemented by the main process and mirrored by HttpApiClient.
|
||||||
// Keep this intentionally loose to avoid tight coupling between front-end and server types.
|
// Keep this intentionally loose to avoid tight coupling between front-end and server types.
|
||||||
setup?: any;
|
setup?: SetupAPI;
|
||||||
agent?: {
|
agent?: {
|
||||||
start: (
|
start: (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
@@ -950,13 +961,11 @@ export const isElectron = (): boolean => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = window as any;
|
if (window.isElectron === true) {
|
||||||
|
|
||||||
if (w.isElectron === true) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!w.electronAPI?.isElectron;
|
return !!window.electronAPI?.isElectron;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if backend server is available
|
// Check if backend server is available
|
||||||
@@ -1030,7 +1039,7 @@ export const getCurrentApiMode = (): 'http' => {
|
|||||||
|
|
||||||
// Debug helpers
|
// Debug helpers
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
(window as any).__checkApiMode = () => {
|
window.__checkApiMode = () => {
|
||||||
console.log('Current API mode:', getCurrentApiMode());
|
console.log('Current API mode:', getCurrentApiMode());
|
||||||
console.log('isElectron():', isElectron());
|
console.log('isElectron():', isElectron());
|
||||||
};
|
};
|
||||||
@@ -1413,8 +1422,8 @@ interface SetupAPI {
|
|||||||
user: string | null;
|
user: string | null;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
onInstallProgress?: (callback: (progress: any) => void) => () => void;
|
onInstallProgress?: (callback: (progress: InstallProgress) => void) => () => void;
|
||||||
onAuthProgress?: (callback: (progress: any) => void) => () => void;
|
onAuthProgress?: (callback: (progress: InstallProgress) => void) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock Setup API implementation
|
// Mock Setup API implementation
|
||||||
@@ -1665,7 +1674,7 @@ function createMockWorktreeAPI(): WorktreeAPI {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
createPR: async (worktreePath: string, options?: any) => {
|
createPR: async (worktreePath: string, options?: CreatePROptions) => {
|
||||||
console.log('[Mock] Creating PR:', { worktreePath, options });
|
console.log('[Mock] Creating PR:', { worktreePath, options });
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -2927,7 +2936,7 @@ function createMockFeaturesAPI(): FeaturesAPI {
|
|||||||
console.log('[Mock] Getting all features for:', projectPath);
|
console.log('[Mock] Getting all features for:', projectPath);
|
||||||
|
|
||||||
// Check if test has set mock features via global variable
|
// Check if test has set mock features via global variable
|
||||||
const testFeatures = (window as any).__mockFeatures;
|
const testFeatures = window.__mockFeatures;
|
||||||
if (testFeatures !== undefined) {
|
if (testFeatures !== undefined) {
|
||||||
return { success: true, features: testFeatures };
|
return { success: true, features: testFeatures };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,9 +162,13 @@ export async function openDirectoryPicker(): Promise<DirectoryPickerResult | nul
|
|||||||
logger.info('Opening directory picker...');
|
logger.info('Opening directory picker...');
|
||||||
|
|
||||||
// Try to show picker programmatically
|
// Try to show picker programmatically
|
||||||
if ('showPicker' in HTMLInputElement.prototype) {
|
// Note: showPicker() is available in modern browsers but not in standard TypeScript types
|
||||||
|
if (
|
||||||
|
'showPicker' in input &&
|
||||||
|
typeof (input as { showPicker?: () => void }).showPicker === 'function'
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
(input as any).showPicker();
|
(input as { showPicker: () => void }).showPicker();
|
||||||
logger.info('Using showPicker()');
|
logger.info('Using showPicker()');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.info('showPicker() failed, using click()', error);
|
logger.info('showPicker() failed, using click()', error);
|
||||||
@@ -263,11 +267,13 @@ export async function openFilePicker(options?: {
|
|||||||
document.body.appendChild(input);
|
document.body.appendChild(input);
|
||||||
|
|
||||||
// Try to show picker programmatically
|
// Try to show picker programmatically
|
||||||
// Note: showPicker() is available in modern browsers but TypeScript types it as void
|
// Note: showPicker() is available in modern browsers but not in standard TypeScript types
|
||||||
// In practice, it may return a Promise in some implementations, but we'll handle errors via try/catch
|
if (
|
||||||
if ('showPicker' in HTMLInputElement.prototype) {
|
'showPicker' in input &&
|
||||||
|
typeof (input as { showPicker?: () => void }).showPicker === 'function'
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
(input as any).showPicker();
|
(input as { showPicker: () => void }).showPicker();
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback to click if showPicker fails
|
// Fallback to click if showPicker fails
|
||||||
input.click();
|
input.click();
|
||||||
|
|||||||
@@ -31,9 +31,15 @@ import type {
|
|||||||
ConvertToFeatureOptions,
|
ConvertToFeatureOptions,
|
||||||
NotificationsAPI,
|
NotificationsAPI,
|
||||||
EventHistoryAPI,
|
EventHistoryAPI,
|
||||||
|
CreatePROptions,
|
||||||
} from './electron';
|
} from './electron';
|
||||||
import type { IdeationContextSources } from '@automaker/types';
|
import type {
|
||||||
import type { EventHistoryFilter } from '@automaker/types';
|
IdeationContextSources,
|
||||||
|
EventHistoryFilter,
|
||||||
|
IdeationStreamEvent,
|
||||||
|
IdeationAnalysisEvent,
|
||||||
|
Notification,
|
||||||
|
} from '@automaker/types';
|
||||||
import type { Message, SessionListItem } from '@/types/electron';
|
import type { Message, SessionListItem } from '@/types/electron';
|
||||||
import type { Feature, ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store';
|
import type { Feature, ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store';
|
||||||
import type { WorktreeAPI, GitAPI, ModelDefinition, ProviderStatus } from '@/types/electron';
|
import type { WorktreeAPI, GitAPI, ModelDefinition, ProviderStatus } from '@/types/electron';
|
||||||
@@ -131,9 +137,7 @@ export const handleServerOffline = (): void => {
|
|||||||
* Must be called early in Electron mode before making API requests.
|
* Must be called early in Electron mode before making API requests.
|
||||||
*/
|
*/
|
||||||
export const initServerUrl = async (): Promise<void> => {
|
export const initServerUrl = async (): Promise<void> => {
|
||||||
// window.electronAPI is typed as ElectronAPI, but some Electron-only helpers
|
const electron = typeof window !== 'undefined' ? window.electronAPI : null;
|
||||||
// (like getServerUrl) are not part of the shared interface. Narrow via `any`.
|
|
||||||
const electron = typeof window !== 'undefined' ? (window.electronAPI as any) : null;
|
|
||||||
if (electron?.getServerUrl) {
|
if (electron?.getServerUrl) {
|
||||||
try {
|
try {
|
||||||
cachedServerUrl = await electron.getServerUrl();
|
cachedServerUrl = await electron.getServerUrl();
|
||||||
@@ -249,7 +253,7 @@ export const isElectronMode = (): boolean => {
|
|||||||
// Prefer a stable runtime marker from preload.
|
// Prefer a stable runtime marker from preload.
|
||||||
// In some dev/electron setups, method availability can be temporarily undefined
|
// In some dev/electron setups, method availability can be temporarily undefined
|
||||||
// during early startup, but `isElectron` remains reliable.
|
// during early startup, but `isElectron` remains reliable.
|
||||||
const api = window.electronAPI as any;
|
const api = window.electronAPI;
|
||||||
return api?.isElectron === true || !!api?.getApiKey;
|
return api?.isElectron === true || !!api?.getApiKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -266,7 +270,7 @@ export const checkExternalServerMode = async (): Promise<boolean> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const api = window.electronAPI as any;
|
const api = window.electronAPI;
|
||||||
if (api?.isExternalServerMode) {
|
if (api?.isExternalServerMode) {
|
||||||
try {
|
try {
|
||||||
cachedExternalServerMode = Boolean(await api.isExternalServerMode());
|
cachedExternalServerMode = Boolean(await api.isExternalServerMode());
|
||||||
@@ -2035,7 +2039,7 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
this.post('/api/worktree/generate-commit-message', { worktreePath }),
|
this.post('/api/worktree/generate-commit-message', { worktreePath }),
|
||||||
push: (worktreePath: string, force?: boolean, remote?: string) =>
|
push: (worktreePath: string, force?: boolean, remote?: string) =>
|
||||||
this.post('/api/worktree/push', { worktreePath, force, remote }),
|
this.post('/api/worktree/push', { worktreePath, force, remote }),
|
||||||
createPR: (worktreePath: string, options?: any) =>
|
createPR: (worktreePath: string, options?: CreatePROptions) =>
|
||||||
this.post('/api/worktree/create-pr', { worktreePath, ...options }),
|
this.post('/api/worktree/create-pr', { worktreePath, ...options }),
|
||||||
getDiffs: (projectPath: string, featureId: string) =>
|
getDiffs: (projectPath: string, featureId: string) =>
|
||||||
this.post('/api/worktree/diffs', { projectPath, featureId }),
|
this.post('/api/worktree/diffs', { projectPath, featureId }),
|
||||||
@@ -2762,18 +2766,18 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
|
|
||||||
getPrompts: () => this.get('/api/ideation/prompts'),
|
getPrompts: () => this.get('/api/ideation/prompts'),
|
||||||
|
|
||||||
onStream: (callback: (event: any) => void): (() => void) => {
|
onStream: (callback: (event: IdeationStreamEvent) => void): (() => void) => {
|
||||||
return this.subscribeToEvent('ideation:stream', callback as EventCallback);
|
return this.subscribeToEvent('ideation:stream', callback as EventCallback);
|
||||||
},
|
},
|
||||||
|
|
||||||
onAnalysisEvent: (callback: (event: any) => void): (() => void) => {
|
onAnalysisEvent: (callback: (event: IdeationAnalysisEvent) => void): (() => void) => {
|
||||||
return this.subscribeToEvent('ideation:analysis', callback as EventCallback);
|
return this.subscribeToEvent('ideation:analysis', callback as EventCallback);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Notifications API - project-level notifications
|
// Notifications API - project-level notifications
|
||||||
notifications: NotificationsAPI & {
|
notifications: NotificationsAPI & {
|
||||||
onNotificationCreated: (callback: (notification: any) => void) => () => void;
|
onNotificationCreated: (callback: (notification: Notification) => void) => () => void;
|
||||||
} = {
|
} = {
|
||||||
list: (projectPath: string) => this.post('/api/notifications/list', { projectPath }),
|
list: (projectPath: string) => this.post('/api/notifications/list', { projectPath }),
|
||||||
|
|
||||||
@@ -2786,7 +2790,7 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
dismiss: (projectPath: string, notificationId?: string) =>
|
dismiss: (projectPath: string, notificationId?: string) =>
|
||||||
this.post('/api/notifications/dismiss', { projectPath, notificationId }),
|
this.post('/api/notifications/dismiss', { projectPath, notificationId }),
|
||||||
|
|
||||||
onNotificationCreated: (callback: (notification: any) => void): (() => void) => {
|
onNotificationCreated: (callback: (notification: Notification) => void): (() => void) => {
|
||||||
return this.subscribeToEvent('notification:created', callback as EventCallback);
|
return this.subscribeToEvent('notification:created', callback as EventCallback);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ async function getDefaultDocumentsPath(): Promise<string | null> {
|
|||||||
// In Electron mode, use the native getPath API directly from the preload script
|
// In Electron mode, use the native getPath API directly from the preload script
|
||||||
// This returns the actual system Documents folder (e.g., C:\Users\<user>\Documents on Windows)
|
// This returns the actual system Documents folder (e.g., C:\Users\<user>\Documents on Windows)
|
||||||
// Note: The HTTP client's getPath returns incorrect Unix-style paths for 'documents'
|
// Note: The HTTP client's getPath returns incorrect Unix-style paths for 'documents'
|
||||||
if (typeof window !== 'undefined' && (window as any).electronAPI?.getPath) {
|
if (typeof window !== 'undefined' && window.electronAPI?.getPath) {
|
||||||
const documentsPath = await (window as any).electronAPI.getPath('documents');
|
const documentsPath = await window.electronAPI.getPath('documents');
|
||||||
return joinPath(documentsPath, 'Automaker');
|
return joinPath(documentsPath, 'Automaker');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,8 +153,12 @@ export function getStoredTheme(): ThemeMode | null {
|
|||||||
try {
|
try {
|
||||||
const legacy = getItem('automaker-storage');
|
const legacy = getItem('automaker-storage');
|
||||||
if (!legacy) return null;
|
if (!legacy) return null;
|
||||||
const parsed = JSON.parse(legacy) as { state?: { theme?: unknown } } | { theme?: unknown };
|
interface LegacyStorageFormat {
|
||||||
const theme = (parsed as any)?.state?.theme ?? (parsed as any)?.theme;
|
state?: { theme?: string };
|
||||||
|
theme?: string;
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(legacy) as LegacyStorageFormat;
|
||||||
|
const theme = parsed.state?.theme ?? parsed.theme;
|
||||||
if (typeof theme === 'string' && theme.length > 0) {
|
if (typeof theme === 'string' && theme.length > 0) {
|
||||||
return theme as ThemeMode;
|
return theme as ThemeMode;
|
||||||
}
|
}
|
||||||
|
|||||||
21
apps/ui/src/types/electron.d.ts
vendored
21
apps/ui/src/types/electron.d.ts
vendored
@@ -1437,10 +1437,27 @@ export interface ProviderStatus {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended Electron API with additional Electron-specific methods
|
||||||
|
* that are exposed via the preload script but not part of the shared interface.
|
||||||
|
*/
|
||||||
|
export interface ExtendedElectronAPI extends ElectronAPI {
|
||||||
|
/** Runtime marker indicating Electron environment */
|
||||||
|
isElectron?: boolean;
|
||||||
|
/** Get the server URL (Electron-only) */
|
||||||
|
getServerUrl?: () => Promise<string>;
|
||||||
|
/** Get the API key (Electron-only) */
|
||||||
|
getApiKey?: () => Promise<string | null>;
|
||||||
|
/** Check if running in external server mode (Electron-only) */
|
||||||
|
isExternalServerMode?: () => Promise<boolean>;
|
||||||
|
/** Get system paths (Electron-only) */
|
||||||
|
getPath?: (name: 'documents' | 'home' | 'appData' | 'userData') => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electronAPI: ElectronAPI;
|
electronAPI?: ExtendedElectronAPI;
|
||||||
isElectron: boolean;
|
isElectron?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
68
apps/ui/src/types/global.d.ts
vendored
Normal file
68
apps/ui/src/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Global type augmentations for Window interface
|
||||||
|
*
|
||||||
|
* These augmentations extend the Window interface with properties
|
||||||
|
* used in testing and development contexts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Feature } from '@automaker/types';
|
||||||
|
import type { ElectronAPI } from '../lib/electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock context file data for testing
|
||||||
|
*/
|
||||||
|
interface MockContextFile {
|
||||||
|
featureId: string;
|
||||||
|
path: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock project data for testing
|
||||||
|
*/
|
||||||
|
export interface MockProject {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
path: string;
|
||||||
|
lastOpened?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
/**
|
||||||
|
* Mock features array used in E2E tests
|
||||||
|
* Set via page.addInitScript() to simulate features loaded from disk
|
||||||
|
*/
|
||||||
|
__mockFeatures?: Feature[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock current project used in E2E tests
|
||||||
|
* Set via page.addInitScript() to simulate the currently open project
|
||||||
|
*/
|
||||||
|
__currentProject?: MockProject | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock context file data used in E2E tests
|
||||||
|
* Set via page.addInitScript() to simulate agent output files
|
||||||
|
*/
|
||||||
|
__mockContextFile?: MockContextFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug helper to check API mode
|
||||||
|
*/
|
||||||
|
__checkApiMode?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Electron API exposed via preload script
|
||||||
|
*/
|
||||||
|
electronAPI?: ElectronAPI & {
|
||||||
|
isElectron?: boolean;
|
||||||
|
getServerUrl?: () => Promise<string>;
|
||||||
|
getApiKey?: () => Promise<string | null>;
|
||||||
|
isExternalServerMode?: () => Promise<boolean>;
|
||||||
|
getPath?: (name: 'documents' | 'home' | 'appData' | 'userData') => Promise<string>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -90,7 +90,9 @@ test.describe('Feature Manual Review Flow', () => {
|
|||||||
|
|
||||||
// Add to projects if not already there
|
// Add to projects if not already there
|
||||||
const existingProjects = json.settings.projects || [];
|
const existingProjects = json.settings.projects || [];
|
||||||
const hasProject = existingProjects.some((p: any) => p.path === projectPath);
|
const hasProject = existingProjects.some(
|
||||||
|
(p: { id: string; path: string }) => p.path === projectPath
|
||||||
|
);
|
||||||
if (!hasProject) {
|
if (!hasProject) {
|
||||||
json.settings.projects = [testProject, ...existingProjects];
|
json.settings.projects = [testProject, ...existingProjects];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,9 @@ test.describe('Open Project', () => {
|
|||||||
|
|
||||||
// Add to existing projects (or create array)
|
// Add to existing projects (or create array)
|
||||||
const existingProjects = json.settings.projects || [];
|
const existingProjects = json.settings.projects || [];
|
||||||
const hasProject = existingProjects.some((p: any) => p.id === projectId);
|
const hasProject = existingProjects.some(
|
||||||
|
(p: { id: string; path: string }) => p.id === projectId
|
||||||
|
);
|
||||||
if (!hasProject) {
|
if (!hasProject) {
|
||||||
json.settings.projects = [testProject, ...existingProjects];
|
json.settings.projects = [testProject, ...existingProjects];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ export async function setupMockProjectWithFeatures(
|
|||||||
|
|
||||||
// Also store features in a global variable that the mock electron API can use
|
// Also store features in a global variable that the mock electron API can use
|
||||||
// This is needed because the board-view loads features from the file system
|
// This is needed because the board-view loads features from the file system
|
||||||
(window as any).__mockFeatures = mockFeatures;
|
(window as { __mockFeatures?: unknown[] }).__mockFeatures = mockFeatures;
|
||||||
|
|
||||||
// Disable splash screen in tests
|
// Disable splash screen in tests
|
||||||
sessionStorage.setItem('automaker-splash-shown', 'true');
|
sessionStorage.setItem('automaker-splash-shown', 'true');
|
||||||
@@ -395,7 +395,9 @@ export async function setupMockProjectWithContextFile(
|
|||||||
// Set up mock file system with a context file for the feature
|
// Set up mock file system with a context file for the feature
|
||||||
// This will be used by the mock electron API
|
// This will be used by the mock electron API
|
||||||
// Now uses features/{id}/agent-output.md path
|
// Now uses features/{id}/agent-output.md path
|
||||||
(window as any).__mockContextFile = {
|
(
|
||||||
|
window as { __mockContextFile?: { featureId: string; path: string; content: string } }
|
||||||
|
).__mockContextFile = {
|
||||||
featureId,
|
featureId,
|
||||||
path: `/mock/test-project/.automaker/features/${featureId}/agent-output.md`,
|
path: `/mock/test-project/.automaker/features/${featureId}/agent-output.md`,
|
||||||
content: contextContent,
|
content: contextContent,
|
||||||
@@ -455,7 +457,7 @@ export async function setupMockProjectWithInProgressFeatures(
|
|||||||
|
|
||||||
// Also store features in a global variable that the mock electron API can use
|
// Also store features in a global variable that the mock electron API can use
|
||||||
// This is needed because the board-view loads features from the file system
|
// This is needed because the board-view loads features from the file system
|
||||||
(window as any).__mockFeatures = mockFeatures;
|
(window as { __mockFeatures?: unknown[] }).__mockFeatures = mockFeatures;
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,7 +689,9 @@ export async function setupMockProjectWithAgentOutput(
|
|||||||
|
|
||||||
// Set up mock file system with output content for the feature
|
// Set up mock file system with output content for the feature
|
||||||
// Now uses features/{id}/agent-output.md path
|
// Now uses features/{id}/agent-output.md path
|
||||||
(window as any).__mockContextFile = {
|
(
|
||||||
|
window as { __mockContextFile?: { featureId: string; path: string; content: string } }
|
||||||
|
).__mockContextFile = {
|
||||||
featureId,
|
featureId,
|
||||||
path: `/mock/test-project/.automaker/features/${featureId}/agent-output.md`,
|
path: `/mock/test-project/.automaker/features/${featureId}/agent-output.md`,
|
||||||
content: outputContent,
|
content: outputContent,
|
||||||
@@ -747,7 +751,7 @@ export async function setupMockProjectWithWaitingApprovalFeatures(
|
|||||||
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
|
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
|
||||||
|
|
||||||
// Also store features in a global variable that the mock electron API can use
|
// Also store features in a global variable that the mock electron API can use
|
||||||
(window as any).__mockFeatures = mockFeatures;
|
(window as { __mockFeatures?: unknown[] }).__mockFeatures = mockFeatures;
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"lint": "npm run lint --workspace=apps/ui",
|
"lint": "npm run lint --workspace=apps/ui",
|
||||||
"lint:errors": "npm run lint --workspace=apps/ui -- --quiet",
|
"lint:errors": "npm run lint --workspace=apps/ui -- --quiet",
|
||||||
"lint:server:errors": "npm run lint --workspace=apps/server -- --quiet",
|
"lint:server:errors": "npm run lint --workspace=apps/server -- --quiet",
|
||||||
|
"typecheck": "npm run typecheck --workspace=apps/ui",
|
||||||
"test": "npm run test --workspace=apps/ui",
|
"test": "npm run test --workspace=apps/ui",
|
||||||
"test:headed": "npm run test:headed --workspace=apps/ui",
|
"test:headed": "npm run test:headed --workspace=apps/ui",
|
||||||
"test:ui": "npm run test --workspace=apps/ui -- --ui",
|
"test:ui": "npm run test --workspace=apps/ui -- --ui",
|
||||||
|
|||||||
Reference in New Issue
Block a user