From 0fb471ca15f3df12ab69f0131e9f41f71f490496 Mon Sep 17 00:00:00 2001 From: Shirone Date: Sun, 25 Jan 2026 18:11:48 +0100 Subject: [PATCH] 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. --- .../components/dialogs/new-project-modal.tsx | 2 +- .../components/edit-project-dialog.tsx | 8 +-- apps/ui/src/components/ui/git-diff-panel.tsx | 14 +++- .../src/components/ui/task-progress-panel.tsx | 34 ++++++---- .../ui/src/components/views/analysis-view.tsx | 8 +-- apps/ui/src/components/views/board-view.tsx | 13 ++-- .../components/kanban-card/card-badges.tsx | 2 - .../board-view/dialogs/agent-output-modal.tsx | 14 ++-- .../board-view/hooks/use-board-effects.ts | 9 +-- .../board-view/shared/model-selector.tsx | 8 +-- apps/ui/src/components/views/chat-history.tsx | 4 +- .../src/components/views/graph-view-page.tsx | 5 +- .../src/components/views/interview-view.tsx | 6 +- .../setup-view/hooks/use-cli-installation.ts | 38 +++++++---- .../views/setup-view/hooks/use-cli-status.ts | 31 +++++++-- .../views/setup-view/steps/cli-setup-step.tsx | 32 ++++++++- apps/ui/src/hooks/use-settings-sync.ts | 6 +- apps/ui/src/lib/electron.ts | 41 ++++++----- apps/ui/src/lib/file-picker.ts | 18 +++-- apps/ui/src/lib/http-api-client.ts | 28 ++++---- apps/ui/src/lib/workspace-config.ts | 4 +- apps/ui/src/store/app-store.ts | 8 ++- apps/ui/src/types/electron.d.ts | 21 +++++- apps/ui/src/types/global.d.ts | 68 +++++++++++++++++++ .../feature-manual-review-flow.spec.ts | 4 +- .../projects/open-existing-project.spec.ts | 4 +- apps/ui/tests/utils/project/setup.ts | 14 ++-- package.json | 1 + 28 files changed, 320 insertions(+), 125 deletions(-) create mode 100644 apps/ui/src/types/global.d.ts diff --git a/apps/ui/src/components/dialogs/new-project-modal.tsx b/apps/ui/src/components/dialogs/new-project-modal.tsx index 55df0a1c..c27f7807 100644 --- a/apps/ui/src/components/dialogs/new-project-modal.tsx +++ b/apps/ui/src/components/dialogs/new-project-modal.tsx @@ -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 ? '\\' : '/' diff --git a/apps/ui/src/components/layout/project-switcher/components/edit-project-dialog.tsx b/apps/ui/src/components/layout/project-switcher/components/edit-project-dialog.tsx index 31e39367..0cb598b2 100644 --- a/apps/ui/src/components/layout/project-switcher/components/edit-project-dialog.tsx +++ b/apps/ui/src/components/layout/project-switcher/components/edit-project-dialog.tsx @@ -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((project as any).icon || null); + const [icon, setIcon] = useState(project.icon || null); const [customIconPath, setCustomIconPath] = useState( - (project as any).customIconPath || null + project.customIconPath || null ); const [isUploadingIcon, setIsUploadingIcon] = useState(false); const fileInputRef = useRef(null); @@ -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); diff --git a/apps/ui/src/components/ui/git-diff-panel.tsx b/apps/ui/src/components/ui/git-diff-panel.tsx index 6a4d7e03..cce517b7 100644 --- a/apps/ui/src/components/ui/git-diff-panel.tsx +++ b/apps/ui/src/components/ui/git-diff-panel.tsx @@ -479,7 +479,12 @@ export function GitDiffPanel({
{error} - @@ -550,7 +555,12 @@ export function GitDiffPanel({ > Collapse All - diff --git a/apps/ui/src/components/ui/task-progress-panel.tsx b/apps/ui/src/components/ui/task-progress-panel.tsx index b936cb9b..f72d6174 100644 --- a/apps/ui/src/components/ui/task-progress-panel.tsx +++ b/apps/ui/src/components/ui/task-progress-panel.tsx @@ -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 { @@ -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); diff --git a/apps/ui/src/components/views/analysis-view.tsx b/apps/ui/src/components/views/analysis-view.tsx index ff1745e3..8d74c71c 100644 --- a/apps/ui/src/components/views/analysis-view.tsx +++ b/apps/ui/src/components/views/analysis-view.tsx @@ -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'; @@ -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 diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index dcb6ead6..39c8e59b 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -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 { createLogger } from '@automaker/utils/logger'; import { @@ -9,6 +8,8 @@ import { rectIntersection, pointerWithin, type PointerEvent as DndPointerEvent, + type CollisionDetection, + type Collision, } from '@dnd-kit/core'; // 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 { 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'; @@ -348,12 +349,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[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-'); }); @@ -363,7 +364,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) ); @@ -1094,7 +1095,7 @@ export function BoardView() { const columns = getColumnsWithPipeline(pipelineConfig); const map: Record = {}; 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]); diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/card-badges.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/card-badges.tsx index 4f543a90..d9df8ad9 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/card-badges.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/card-badges.tsx @@ -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]); diff --git a/apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx b/apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx index a074ceb8..767ca59d 100644 --- a/apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx @@ -17,6 +17,7 @@ import { useAppStore } from '@/store/app-store'; import { extractSummary } from '@/lib/log-parser'; import { useAgentOutput } from '@/hooks/queries'; import type { AutoModeEvent } from '@/types/electron'; +import type { BacklogPlanEvent } from '@automaker/types'; interface AgentOutputModalProps { open: boolean; @@ -48,18 +49,16 @@ export function AgentOutputModal({ const isBacklogPlan = featureId.startsWith('backlog-plan:'); // 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) const [streamedContent, setStreamedContent] = useState(''); const [viewMode, setViewMode] = useState(null); // Use React Query for initial output loading - const { data: initialOutput = '', isLoading } = useAgentOutput( - resolvedProjectPath, - featureId, - open && !!resolvedProjectPath - ); + const { data: initialOutput = '', isLoading } = useAgentOutput(resolvedProjectPath, featureId, { + enabled: open && !!resolvedProjectPath, + }); // Reset streamed content when modal opens or featureId changes useEffect(() => { @@ -262,7 +261,8 @@ export function AgentOutputModal({ const api = getElectronAPI(); 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; let newContent = ''; diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-effects.ts b/apps/ui/src/components/views/board-view/hooks/use-board-effects.ts index df352b01..0c5adc70 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-effects.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-effects.ts @@ -1,15 +1,16 @@ import { useEffect, useRef } from 'react'; import { getElectronAPI } from '@/lib/electron'; import { createLogger } from '@automaker/utils/logger'; +import type { Feature } from '@/store/app-store'; const logger = createLogger('BoardEffects'); interface UseBoardEffectsProps { - currentProject: { path: string; id: string } | null; + currentProject: { path: string; id: string; name?: string } | null; specCreatingForProject: string | null; setSpecCreatingForProject: (path: string | null) => void; checkContextExists: (featureId: string) => Promise; - features: any[]; + features: Feature[]; isLoading: boolean; featuresWithContext: Set; setFeaturesWithContext: (set: Set) => void; @@ -33,10 +34,10 @@ export function useBoardEffects({ // Make current project available globally for modal useEffect(() => { if (currentProject) { - (window as any).__currentProject = currentProject; + window.__currentProject = currentProject; } return () => { - (window as any).__currentProject = null; + window.__currentProject = null; }; }, [currentProject]); diff --git a/apps/ui/src/components/views/board-view/shared/model-selector.tsx b/apps/ui/src/components/views/board-view/shared/model-selector.tsx index a40623ea..28ff540f 100644 --- a/apps/ui/src/components/views/board-view/shared/model-selector.tsx +++ b/apps/ui/src/components/views/board-view/shared/model-selector.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck - model selector with provider-specific model options and validation import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Brain, AlertTriangle } from 'lucide-react'; @@ -7,7 +6,7 @@ import { cn } from '@/lib/utils'; import { useAppStore } from '@/store/app-store'; import { useSetupStore } from '@/store/setup-store'; 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 { useEffect } from 'react'; import { Spinner } from '@/components/ui/spinner'; @@ -40,6 +39,7 @@ export function ModelSelector({ const isCursorAvailable = cursorCliStatus?.installed && cursorCliStatus?.auth?.authenticated; // 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; // 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) const unprefixedId = model.id.startsWith('cursor-') ? model.id.slice(7) : model.id; return ( - enabledCursorModels.includes(model.id as any) || - enabledCursorModels.includes(unprefixedId as any) + enabledCursorModels.includes(model.id as CursorModelId) || + enabledCursorModels.includes(unprefixedId as CursorModelId) ); }); diff --git a/apps/ui/src/components/views/chat-history.tsx b/apps/ui/src/components/views/chat-history.tsx index eed0b062..ef107d3f 100644 --- a/apps/ui/src/components/views/chat-history.tsx +++ b/apps/ui/src/components/views/chat-history.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } 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 { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -156,7 +156,7 @@ export function ChatHistory() { createChatSession(); }; - const handleSelectSession = (session: any) => { + const handleSelectSession = (session: ChatSession) => { setCurrentChatSession(session); }; diff --git a/apps/ui/src/components/views/graph-view-page.tsx b/apps/ui/src/components/views/graph-view-page.tsx index 0f6d7d24..9e0fc3cd 100644 --- a/apps/ui/src/components/views/graph-view-page.tsx +++ b/apps/ui/src/components/views/graph-view-page.tsx @@ -1,6 +1,5 @@ -// @ts-nocheck - graph view page with feature filtering and visualization state 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 { GraphView } from './graph-view'; import { @@ -236,7 +235,7 @@ export function GraphViewPage() { // Follow-up state (simplified for graph view) const [followUpFeature, setFollowUpFeature] = useState(null); const [followUpPrompt, setFollowUpPrompt] = useState(''); - const [followUpImagePaths, setFollowUpImagePaths] = useState([]); + const [followUpImagePaths, setFollowUpImagePaths] = useState([]); const [, setFollowUpPreviewMap] = useState>(new Map()); // In-progress features for shortcuts diff --git a/apps/ui/src/components/views/interview-view.tsx b/apps/ui/src/components/views/interview-view.tsx index 5771103f..4abd5f79 100644 --- a/apps/ui/src/components/views/interview-view.tsx +++ b/apps/ui/src/components/views/interview-view.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck - interview flow state machine with dynamic question handling import { useState, useCallback, useRef, useEffect } from 'react'; import { createLogger } from '@automaker/utils/logger'; import { useAppStore, Feature } from '@/store/app-store'; @@ -324,7 +323,7 @@ export function InterviewView() { const api = getElectronAPI(); // Use platform-specific path separator const pathSep = - typeof window !== 'undefined' && (window as any).electronAPI + typeof window !== 'undefined' && window.electronAPI ? navigator.platform.indexOf('Win') !== -1 ? '\\' : '/' @@ -349,8 +348,9 @@ export function InterviewView() { id: generateUUID(), category: 'Core', description: 'Initial project setup', - status: 'backlog' as const, + status: 'backlog', skipTests: true, + steps: [], }; if (!api.features) { diff --git a/apps/ui/src/components/views/setup-view/hooks/use-cli-installation.ts b/apps/ui/src/components/views/setup-view/hooks/use-cli-installation.ts index aeb57d53..6e632530 100644 --- a/apps/ui/src/components/views/setup-view/hooks/use-cli-installation.ts +++ b/apps/ui/src/components/views/setup-view/hooks/use-cli-installation.ts @@ -1,15 +1,31 @@ import { useState, useCallback } from 'react'; import { toast } from 'sonner'; import { createLogger } from '@automaker/utils/logger'; +import type { ModelProvider } from '@automaker/types'; +import type { CliStatus } from '@/store/setup-store'; const logger = createLogger('CliInstallation'); +interface InstallApiResult { + success: boolean; + message?: string; + error?: string; +} + +interface InstallProgressEvent { + cli?: string; + data?: string; + type?: string; +} + interface UseCliInstallationOptions { - cliType: 'claude'; - installApi: () => Promise; - onProgressEvent?: (callback: (progress: any) => void) => (() => void) | undefined; + cliType: ModelProvider; + installApi: () => Promise; + onProgressEvent?: ( + callback: (progress: InstallProgressEvent) => void + ) => (() => void) | undefined; onSuccess?: () => void; - getStoreState?: () => any; + getStoreState?: () => CliStatus | null; } export function useCliInstallation({ @@ -32,15 +48,13 @@ export function useCliInstallation({ let unsubscribe: (() => void) | undefined; if (onProgressEvent) { - unsubscribe = onProgressEvent( - (progress: { cli?: string; data?: string; type?: string }) => { - if (progress.cli === cliType) { - setInstallProgress((prev) => ({ - output: [...prev.output, progress.data || progress.type || ''], - })); - } + unsubscribe = onProgressEvent((progress: InstallProgressEvent) => { + if (progress.cli === cliType) { + setInstallProgress((prev) => ({ + output: [...prev.output, progress.data || progress.type || ''], + })); } - ); + }); } const result = await installApi(); diff --git a/apps/ui/src/components/views/setup-view/hooks/use-cli-status.ts b/apps/ui/src/components/views/setup-view/hooks/use-cli-status.ts index 44f56795..79b6dedc 100644 --- a/apps/ui/src/components/views/setup-view/hooks/use-cli-status.ts +++ b/apps/ui/src/components/views/setup-view/hooks/use-cli-status.ts @@ -1,11 +1,34 @@ import { useState, useCallback } from 'react'; 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 { - cliType: 'claude' | 'codex'; - statusApi: () => Promise; - setCliStatus: (status: any) => void; - setAuthStatus: (status: any) => void; + cliType: ModelProvider; + statusApi: () => Promise; + setCliStatus: (status: CliStatus | null) => void; + setAuthStatus: (status: ClaudeAuthStatus | CodexAuthStatus | null) => void; } const VALID_AUTH_METHODS = { diff --git a/apps/ui/src/components/views/setup-view/steps/cli-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/cli-setup-step.tsx index cc17f390..5a486458 100644 --- a/apps/ui/src/components/views/setup-view/steps/cli-setup-step.tsx +++ b/apps/ui/src/components/views/setup-view/steps/cli-setup-step.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck - CLI setup wizard with step validation and setup store state import { useState, useEffect, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -45,6 +44,33 @@ type VerificationStatus = 'idle' | 'verifying' | 'verified' | 'error'; 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 { cliType: ModelProvider; displayName: string; @@ -73,8 +99,8 @@ interface CliSetupConfig { buildCliAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus; buildApiKeyAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus; buildClearedAuthStatus: (previous: CliSetupAuthStatus | null) => CliSetupAuthStatus; - statusApi: () => Promise; - installApi: () => Promise; + statusApi: () => Promise; + installApi: () => Promise; verifyAuthApi: ( method: 'cli' | 'api_key', apiKey?: string diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index c9729805..8bf384b3 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -282,8 +282,10 @@ export function useSettingsSync(): SettingsSyncState { } logger.info('[SYNC_SEND] Sending settings update to server:', { - projects: (updates.projects as any)?.length ?? 0, - trashedProjects: (updates.trashedProjects as any)?.length ?? 0, + projects: Array.isArray(updates.projects) ? updates.projects.length : 0, + trashedProjects: Array.isArray(updates.trashedProjects) + ? updates.trashedProjects.length + : 0, }); const result = await api.settings.updateGlobal(updates); diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index a98bc2c9..ab84ec32 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -28,7 +28,11 @@ import type { UpdateIdeaInput, ConvertToFeatureOptions, IdeationContextSources, + Feature, + IdeationStreamEvent, + IdeationAnalysisEvent, } from '@automaker/types'; +import type { InstallProgress } from '@/store/setup-store'; import { DEFAULT_MAX_CONCURRENCY } from '@automaker/types'; import { getJSON, setJSON, removeItem } from './storage'; @@ -124,7 +128,7 @@ export interface IdeationAPI { projectPath: string, ideaId: string, 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 addSuggestionToBoard: ( @@ -141,8 +145,8 @@ export interface IdeationAPI { }>; // Event subscriptions - onStream: (callback: (event: any) => void) => () => void; - onAnalysisEvent: (callback: (event: any) => void) => () => void; + onStream: (callback: (event: IdeationStreamEvent) => void) => () => void; + onAnalysisEvent: (callback: (event: IdeationAnalysisEvent) => void) => () => void; } export interface FileEntry { @@ -186,6 +190,16 @@ export interface StatResult { 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 export type { AutoModeEvent, @@ -212,9 +226,6 @@ import type { // Import HTTP API client (ES module) import { getHttpApiClient, getServerUrlSync } from './http-api-client'; -// Feature type - Import from app-store -import type { Feature } from '@/store/app-store'; - // Running Agent type export interface RunningAgent { featureId: string; @@ -749,7 +760,7 @@ export interface ElectronAPI { }; // 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. - setup?: any; + setup?: SetupAPI; agent?: { start: ( sessionId: string, @@ -950,13 +961,11 @@ export const isElectron = (): boolean => { return false; } - const w = window as any; - - if (w.isElectron === true) { + if (window.isElectron === true) { return true; } - return !!w.electronAPI?.isElectron; + return !!window.electronAPI?.isElectron; }; // Check if backend server is available @@ -1030,7 +1039,7 @@ export const getCurrentApiMode = (): 'http' => { // Debug helpers if (typeof window !== 'undefined') { - (window as any).__checkApiMode = () => { + window.__checkApiMode = () => { console.log('Current API mode:', getCurrentApiMode()); console.log('isElectron():', isElectron()); }; @@ -1413,8 +1422,8 @@ interface SetupAPI { user: string | null; error?: string; }>; - onInstallProgress?: (callback: (progress: any) => void) => () => void; - onAuthProgress?: (callback: (progress: any) => void) => () => void; + onInstallProgress?: (callback: (progress: InstallProgress) => void) => () => void; + onAuthProgress?: (callback: (progress: InstallProgress) => void) => () => void; } // 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 }); return { success: true, @@ -2927,7 +2936,7 @@ function createMockFeaturesAPI(): FeaturesAPI { console.log('[Mock] Getting all features for:', projectPath); // Check if test has set mock features via global variable - const testFeatures = (window as any).__mockFeatures; + const testFeatures = window.__mockFeatures; if (testFeatures !== undefined) { return { success: true, features: testFeatures }; } diff --git a/apps/ui/src/lib/file-picker.ts b/apps/ui/src/lib/file-picker.ts index f3dc6bf9..6a22fe27 100644 --- a/apps/ui/src/lib/file-picker.ts +++ b/apps/ui/src/lib/file-picker.ts @@ -162,9 +162,13 @@ export async function openDirectoryPicker(): Promise void }).showPicker === 'function' + ) { try { - (input as any).showPicker(); + (input as { showPicker: () => void }).showPicker(); logger.info('Using showPicker()'); } catch (error) { logger.info('showPicker() failed, using click()', error); @@ -263,11 +267,13 @@ export async function openFilePicker(options?: { document.body.appendChild(input); // Try to show picker programmatically - // Note: showPicker() is available in modern browsers but TypeScript types it as void - // In practice, it may return a Promise in some implementations, but we'll handle errors via try/catch - 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 { - (input as any).showPicker(); + (input as { showPicker: () => void }).showPicker(); } catch { // Fallback to click if showPicker fails input.click(); diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index c78a8642..44a39971 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -31,9 +31,15 @@ import type { ConvertToFeatureOptions, NotificationsAPI, EventHistoryAPI, + CreatePROptions, } from './electron'; -import type { IdeationContextSources } from '@automaker/types'; -import type { EventHistoryFilter } from '@automaker/types'; +import type { + IdeationContextSources, + EventHistoryFilter, + IdeationStreamEvent, + IdeationAnalysisEvent, + Notification, +} from '@automaker/types'; import type { Message, SessionListItem } from '@/types/electron'; import type { Feature, ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store'; 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. */ export const initServerUrl = async (): Promise => { - // window.electronAPI is typed as ElectronAPI, but some Electron-only helpers - // (like getServerUrl) are not part of the shared interface. Narrow via `any`. - const electron = typeof window !== 'undefined' ? (window.electronAPI as any) : null; + const electron = typeof window !== 'undefined' ? window.electronAPI : null; if (electron?.getServerUrl) { try { cachedServerUrl = await electron.getServerUrl(); @@ -249,7 +253,7 @@ export const isElectronMode = (): boolean => { // Prefer a stable runtime marker from preload. // In some dev/electron setups, method availability can be temporarily undefined // during early startup, but `isElectron` remains reliable. - const api = window.electronAPI as any; + const api = window.electronAPI; return api?.isElectron === true || !!api?.getApiKey; }; @@ -266,7 +270,7 @@ export const checkExternalServerMode = async (): Promise => { } if (typeof window !== 'undefined') { - const api = window.electronAPI as any; + const api = window.electronAPI; if (api?.isExternalServerMode) { try { cachedExternalServerMode = Boolean(await api.isExternalServerMode()); @@ -2035,7 +2039,7 @@ export class HttpApiClient implements ElectronAPI { this.post('/api/worktree/generate-commit-message', { worktreePath }), push: (worktreePath: string, force?: boolean, remote?: string) => 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 }), getDiffs: (projectPath: string, featureId: string) => this.post('/api/worktree/diffs', { projectPath, featureId }), @@ -2762,18 +2766,18 @@ export class HttpApiClient implements ElectronAPI { 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); }, - onAnalysisEvent: (callback: (event: any) => void): (() => void) => { + onAnalysisEvent: (callback: (event: IdeationAnalysisEvent) => void): (() => void) => { return this.subscribeToEvent('ideation:analysis', callback as EventCallback); }, }; // Notifications API - project-level notifications notifications: NotificationsAPI & { - onNotificationCreated: (callback: (notification: any) => void) => () => void; + onNotificationCreated: (callback: (notification: Notification) => void) => () => void; } = { list: (projectPath: string) => this.post('/api/notifications/list', { projectPath }), @@ -2786,7 +2790,7 @@ export class HttpApiClient implements ElectronAPI { dismiss: (projectPath: string, notificationId?: string) => 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); }, }; diff --git a/apps/ui/src/lib/workspace-config.ts b/apps/ui/src/lib/workspace-config.ts index e1d32837..596de564 100644 --- a/apps/ui/src/lib/workspace-config.ts +++ b/apps/ui/src/lib/workspace-config.ts @@ -35,8 +35,8 @@ async function getDefaultDocumentsPath(): Promise { // In Electron mode, use the native getPath API directly from the preload script // This returns the actual system Documents folder (e.g., C:\Users\\Documents on Windows) // Note: The HTTP client's getPath returns incorrect Unix-style paths for 'documents' - if (typeof window !== 'undefined' && (window as any).electronAPI?.getPath) { - const documentsPath = await (window as any).electronAPI.getPath('documents'); + if (typeof window !== 'undefined' && window.electronAPI?.getPath) { + const documentsPath = await window.electronAPI.getPath('documents'); return joinPath(documentsPath, 'Automaker'); } diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 6c73d5cc..010be300 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -153,8 +153,12 @@ export function getStoredTheme(): ThemeMode | null { try { const legacy = getItem('automaker-storage'); if (!legacy) return null; - const parsed = JSON.parse(legacy) as { state?: { theme?: unknown } } | { theme?: unknown }; - const theme = (parsed as any)?.state?.theme ?? (parsed as any)?.theme; + interface LegacyStorageFormat { + 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) { return theme as ThemeMode; } diff --git a/apps/ui/src/types/electron.d.ts b/apps/ui/src/types/electron.d.ts index cf6f81c5..7c2fda9e 100644 --- a/apps/ui/src/types/electron.d.ts +++ b/apps/ui/src/types/electron.d.ts @@ -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; + /** Get the API key (Electron-only) */ + getApiKey?: () => Promise; + /** Check if running in external server mode (Electron-only) */ + isExternalServerMode?: () => Promise; + /** Get system paths (Electron-only) */ + getPath?: (name: 'documents' | 'home' | 'appData' | 'userData') => Promise; +} + declare global { interface Window { - electronAPI: ElectronAPI; - isElectron: boolean; + electronAPI?: ExtendedElectronAPI; + isElectron?: boolean; } } diff --git a/apps/ui/src/types/global.d.ts b/apps/ui/src/types/global.d.ts new file mode 100644 index 00000000..eee79ca1 --- /dev/null +++ b/apps/ui/src/types/global.d.ts @@ -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; + getApiKey?: () => Promise; + isExternalServerMode?: () => Promise; + getPath?: (name: 'documents' | 'home' | 'appData' | 'userData') => Promise; + }; + } +} + +export {}; diff --git a/apps/ui/tests/features/feature-manual-review-flow.spec.ts b/apps/ui/tests/features/feature-manual-review-flow.spec.ts index 10c044d9..aad819f1 100644 --- a/apps/ui/tests/features/feature-manual-review-flow.spec.ts +++ b/apps/ui/tests/features/feature-manual-review-flow.spec.ts @@ -90,7 +90,9 @@ test.describe('Feature Manual Review Flow', () => { // Add to projects if not already there 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) { json.settings.projects = [testProject, ...existingProjects]; } diff --git a/apps/ui/tests/projects/open-existing-project.spec.ts b/apps/ui/tests/projects/open-existing-project.spec.ts index 8dc1fce8..d9fd6862 100644 --- a/apps/ui/tests/projects/open-existing-project.spec.ts +++ b/apps/ui/tests/projects/open-existing-project.spec.ts @@ -114,7 +114,9 @@ test.describe('Open Project', () => { // Add to existing projects (or create array) 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) { json.settings.projects = [testProject, ...existingProjects]; } diff --git a/apps/ui/tests/utils/project/setup.ts b/apps/ui/tests/utils/project/setup.ts index 6ce62e73..412d7d0c 100644 --- a/apps/ui/tests/utils/project/setup.ts +++ b/apps/ui/tests/utils/project/setup.ts @@ -348,7 +348,7 @@ export async function setupMockProjectWithFeatures( // 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 - (window as any).__mockFeatures = mockFeatures; + (window as { __mockFeatures?: unknown[] }).__mockFeatures = mockFeatures; // Disable splash screen in tests 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 // This will be used by the mock electron API // Now uses features/{id}/agent-output.md path - (window as any).__mockContextFile = { + ( + window as { __mockContextFile?: { featureId: string; path: string; content: string } } + ).__mockContextFile = { featureId, path: `/mock/test-project/.automaker/features/${featureId}/agent-output.md`, content: contextContent, @@ -455,7 +457,7 @@ export async function setupMockProjectWithInProgressFeatures( // 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 - (window as any).__mockFeatures = mockFeatures; + (window as { __mockFeatures?: unknown[] }).__mockFeatures = mockFeatures; }, options); } @@ -687,7 +689,9 @@ export async function setupMockProjectWithAgentOutput( // Set up mock file system with output content for the feature // Now uses features/{id}/agent-output.md path - (window as any).__mockContextFile = { + ( + window as { __mockContextFile?: { featureId: string; path: string; content: string } } + ).__mockContextFile = { featureId, path: `/mock/test-project/.automaker/features/${featureId}/agent-output.md`, content: outputContent, @@ -747,7 +751,7 @@ export async function setupMockProjectWithWaitingApprovalFeatures( localStorage.setItem('automaker-storage', JSON.stringify(mockState)); // 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); } diff --git a/package.json b/package.json index 7c5f4d88..e2340d20 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "lint": "npm run lint --workspace=apps/ui", "lint:errors": "npm run lint --workspace=apps/ui -- --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:headed": "npm run test:headed --workspace=apps/ui", "test:ui": "npm run test --workspace=apps/ui -- --ui",