chore: Fix all 246 TypeScript errors in UI

- Extended SetupAPI interface with 20+ missing methods for Cursor, Codex,
  OpenCode, Gemini, and Copilot CLI integrations
- Fixed WorktreeInfo type to include isCurrent and hasWorktree fields
- Added null checks for optional API properties across all hooks
- Fixed Feature type conflicts between @automaker/types and local definitions
- Added missing CLI status hooks for all providers
- Fixed type mismatches in mutation callbacks and event handlers
- Removed dead code referencing non-existent GlobalSettings properties
- Updated mock implementations in electron.ts for all new API methods

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Shirone
2026-01-25 18:36:47 +01:00
parent 0fb471ca15
commit 5c335641fa
48 changed files with 1071 additions and 336 deletions

View File

@@ -1,5 +1,6 @@
import { useEffect, useState, useCallback, useMemo } from 'react';
import { createLogger } from '@automaker/utils/logger';
import type { PointerEvent as ReactPointerEvent } from 'react';
import {
DndContext,
PointerSensor,
@@ -7,7 +8,6 @@ import {
useSensors,
rectIntersection,
pointerWithin,
type PointerEvent as DndPointerEvent,
type CollisionDetection,
type Collision,
} from '@dnd-kit/core';
@@ -17,7 +17,7 @@ class DialogAwarePointerSensor extends PointerSensor {
static activators = [
{
eventName: 'onPointerDown' as const,
handler: ({ nativeEvent: event }: { nativeEvent: DndPointerEvent }) => {
handler: ({ nativeEvent: event }: ReactPointerEvent) => {
// Don't start drag if the event originated from inside a dialog
if ((event.target as Element)?.closest?.('[role="dialog"]')) {
return false;
@@ -172,13 +172,9 @@ export function BoardView() {
const [showCreatePRDialog, setShowCreatePRDialog] = useState(false);
const [showCreateBranchDialog, setShowCreateBranchDialog] = useState(false);
const [showPullResolveConflictsDialog, setShowPullResolveConflictsDialog] = useState(false);
const [selectedWorktreeForAction, setSelectedWorktreeForAction] = useState<{
path: string;
branch: string;
isMain: boolean;
hasChanges?: boolean;
changedFilesCount?: number;
} | null>(null);
const [selectedWorktreeForAction, setSelectedWorktreeForAction] = useState<WorktreeInfo | null>(
null
);
const [worktreeRefreshKey, setWorktreeRefreshKey] = useState(0);
// Backlog plan dialog state
@@ -418,19 +414,29 @@ export function BoardView() {
// Get the branch for the currently selected worktree
// Find the worktree that matches the current selection, or use main worktree
const selectedWorktree = useMemo(() => {
const selectedWorktree = useMemo((): WorktreeInfo | undefined => {
let found;
if (currentWorktreePath === null) {
// Primary worktree selected - find the main worktree
return worktrees.find((w) => w.isMain);
found = worktrees.find((w) => w.isMain);
} else {
// Specific worktree selected - find it by path
return worktrees.find((w) => !w.isMain && pathsEqual(w.path, currentWorktreePath));
found = worktrees.find((w) => !w.isMain && pathsEqual(w.path, currentWorktreePath));
}
if (!found) return undefined;
// Ensure all required WorktreeInfo fields are present
return {
...found,
isCurrent:
found.isCurrent ??
(currentWorktreePath !== null ? pathsEqual(found.path, currentWorktreePath) : found.isMain),
hasWorktree: found.hasWorktree ?? true,
};
}, [worktrees, currentWorktreePath]);
// Auto mode hook - pass current worktree to get worktree-specific state
// Must be after selectedWorktree is defined
const autoMode = useAutoMode(selectedWorktree ?? undefined);
const autoMode = useAutoMode(selectedWorktree);
// Get runningTasks from the hook (scoped to current project/worktree)
const runningAutoTasks = autoMode.runningTasks;
// Get worktree-specific maxConcurrency from the hook
@@ -959,28 +965,27 @@ export function BoardView() {
const api = getElectronAPI();
if (!api?.backlogPlan) return;
const unsubscribe = api.backlogPlan.onEvent(
(event: { type: string; result?: BacklogPlanResult; error?: string }) => {
if (event.type === 'backlog_plan_complete') {
setIsGeneratingPlan(false);
if (event.result && event.result.changes?.length > 0) {
setPendingBacklogPlan(event.result);
toast.success('Plan ready! Click to review.', {
duration: 10000,
action: {
label: 'Review',
onClick: () => setShowPlanDialog(true),
},
});
} else {
toast.info('No changes generated. Try again with a different prompt.');
}
} else if (event.type === 'backlog_plan_error') {
setIsGeneratingPlan(false);
toast.error(`Plan generation failed: ${event.error}`);
const unsubscribe = api.backlogPlan.onEvent((data: unknown) => {
const event = data as { type: string; result?: BacklogPlanResult; error?: string };
if (event.type === 'backlog_plan_complete') {
setIsGeneratingPlan(false);
if (event.result && event.result.changes?.length > 0) {
setPendingBacklogPlan(event.result);
toast.success('Plan ready! Click to review.', {
duration: 10000,
action: {
label: 'Review',
onClick: () => setShowPlanDialog(true),
},
});
} else {
toast.info('No changes generated. Try again with a different prompt.');
}
} else if (event.type === 'backlog_plan_error') {
setIsGeneratingPlan(false);
toast.error(`Plan generation failed: ${event.error}`);
}
);
});
return unsubscribe;
}, []);
@@ -1092,7 +1097,7 @@ export function BoardView() {
// Build columnFeaturesMap for ListView
// pipelineConfig is now from usePipelineConfig React Query hook at the top
const columnFeaturesMap = useMemo(() => {
const columns = getColumnsWithPipeline(pipelineConfig);
const columns = getColumnsWithPipeline(pipelineConfig ?? null);
const map: Record<string, typeof hookFeatures> = {};
for (const column of columns) {
map[column.id] = getColumnFeatures(column.id as FeatureStatusWithPipeline);
@@ -1445,14 +1450,13 @@ export function BoardView() {
onAddFeature={() => setShowAddDialog(true)}
onShowCompletedModal={() => setShowCompletedModal(true)}
completedCount={completedFeatures.length}
pipelineConfig={pipelineConfig}
pipelineConfig={pipelineConfig ?? null}
onOpenPipelineSettings={() => setShowPipelineSettings(true)}
isSelectionMode={isSelectionMode}
selectionTarget={selectionTarget}
selectedFeatureIds={selectedFeatureIds}
onToggleFeatureSelection={toggleFeatureSelection}
onToggleSelectionMode={toggleSelectionMode}
viewMode={viewMode}
isDragging={activeFeature !== null}
onAiSuggest={() => setShowPlanDialog(true)}
className="transition-opacity duration-200"
@@ -1605,7 +1609,7 @@ export function BoardView() {
open={showPipelineSettings}
onClose={() => setShowPipelineSettings(false)}
projectPath={currentProject.path}
pipelineConfig={pipelineConfig}
pipelineConfig={pipelineConfig ?? null}
onSave={async (config) => {
const api = getHttpApiClient();
const result = await api.pipeline.saveConfig(currentProject.path, config);

View File

@@ -1,6 +1,5 @@
import { memo, useEffect, useState, useMemo, useRef } from 'react';
import { Feature, ThinkingLevel, ParsedTask } from '@/store/app-store';
import type { ReasoningEffort } from '@automaker/types';
import { Feature, ThinkingLevel, ReasoningEffort, ParsedTask } from '@/store/app-store';
import { getProviderFromModel } from '@/lib/utils';
import { parseAgentContext, formatModelName, DEFAULT_MODEL } from '@/lib/agent-context-parser';
import { cn } from '@/lib/utils';
@@ -290,7 +289,8 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({
// Agent Info Panel for non-backlog cards
// Show panel if we have agentInfo OR planSpec.tasks (for spec/full mode)
// Note: hasPlanSpecTasks is already defined above and includes freshPlanSpec
if (feature.status !== 'backlog' && (agentInfo || hasPlanSpecTasks)) {
// (The backlog case was already handled above and returned early)
if (agentInfo || hasPlanSpecTasks) {
return (
<>
<div className="mb-3 space-y-2 overflow-hidden">

View File

@@ -23,14 +23,7 @@ import { getHttpApiClient } from '@/lib/http-api-client';
import { toast } from 'sonner';
import { GitMerge, RefreshCw, AlertTriangle } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
interface WorktreeInfo {
path: string;
branch: string;
isMain: boolean;
hasChanges?: boolean;
changedFilesCount?: number;
}
import type { WorktreeInfo } from '../worktree-panel/types';
interface RemoteBranch {
name: string;
@@ -49,7 +42,7 @@ interface PullResolveConflictsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
worktree: WorktreeInfo | null;
onConfirm: (worktree: WorktreeInfo, remoteBranch: string) => void;
onConfirm: (worktree: WorktreeInfo, remoteBranch: string) => void | Promise<void>;
}
export function PullResolveConflictsDialog({

View File

@@ -128,10 +128,9 @@ export function useBoardDragDrop({
const targetBranch = worktreeData.branch;
const currentBranch = draggedFeature.branchName;
// For main worktree, set branchName to null to indicate it should use main
// (must use null not undefined so it serializes to JSON for the API call)
// For main worktree, set branchName to undefined to indicate it should use main
// For other worktrees, set branchName to the target branch
const newBranchName = worktreeData.isMain ? null : targetBranch;
const newBranchName: string | undefined = worktreeData.isMain ? undefined : targetBranch;
// If already on the same branch, nothing to do
// For main worktree: feature with null/undefined branchName is already on main

View File

@@ -185,8 +185,8 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
features,
isLoading,
persistedCategories,
loadFeatures: () => {
queryClient.invalidateQueries({
loadFeatures: async () => {
await queryClient.invalidateQueries({
queryKey: queryKeys.features.all(currentProject?.path ?? ''),
});
},

View File

@@ -1,5 +1,6 @@
import { useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import type { Feature as ApiFeature } from '@automaker/types';
import { Feature } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron';
import { useAppStore } from '@/store/app-store';
@@ -48,14 +49,14 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
feature: result.feature,
});
if (result.success && result.feature) {
const updatedFeature = result.feature;
updateFeature(updatedFeature.id, updatedFeature);
const updatedFeature = result.feature as Feature;
updateFeature(updatedFeature.id, updatedFeature as Partial<Feature>);
queryClient.setQueryData<Feature[]>(
queryKeys.features.all(currentProject.path),
(features) => {
if (!features) return features;
return features.map((feature) =>
feature.id === updatedFeature.id ? updatedFeature : feature
feature.id === updatedFeature.id ? { ...feature, ...updatedFeature } : feature
);
}
);
@@ -85,9 +86,9 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
return;
}
const result = await api.features.create(currentProject.path, feature);
const result = await api.features.create(currentProject.path, feature as ApiFeature);
if (result.success && result.feature) {
updateFeature(result.feature.id, result.feature);
updateFeature(result.feature.id, result.feature as Partial<Feature>);
// Invalidate React Query cache to sync UI
queryClient.invalidateQueries({
queryKey: queryKeys.features.all(currentProject.path),

View File

@@ -6,6 +6,7 @@ import {
useEffect,
type RefObject,
type ReactNode,
type UIEvent,
} from 'react';
import { DragOverlay } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
@@ -79,7 +80,7 @@ const REDUCED_CARD_OPACITY_PERCENT = 85;
type VirtualListItem = { id: string };
interface VirtualListState<Item extends VirtualListItem> {
contentRef: RefObject<HTMLDivElement>;
contentRef: RefObject<HTMLDivElement | null>;
onScroll: (event: UIEvent<HTMLDivElement>) => void;
itemIds: string[];
visibleItems: Item[];

View File

@@ -26,6 +26,9 @@ export function useAvailableEditors() {
const { mutate: refreshMutate, isPending: isRefreshing } = useMutation({
mutationFn: async () => {
const api = getElectronAPI();
if (!api.worktree) {
throw new Error('Worktree API not available');
}
const result = await api.worktree.refreshEditors();
if (!result.success) {
throw new Error(result.error || 'Failed to refresh editors');

View File

@@ -149,33 +149,32 @@ export function GraphViewPage() {
return;
}
const unsubscribe = api.backlogPlan.onEvent(
(event: { type: string; result?: BacklogPlanResult; error?: string }) => {
logger.debug('Backlog plan event received', {
type: event.type,
hasResult: Boolean(event.result),
hasError: Boolean(event.error),
});
if (event.type === 'backlog_plan_complete') {
setIsGeneratingPlan(false);
if (event.result && event.result.changes?.length > 0) {
setPendingBacklogPlan(event.result);
toast.success('Plan ready! Click to review.', {
duration: 10000,
action: {
label: 'Review',
onClick: () => setShowPlanDialog(true),
},
});
} else {
toast.info('No changes generated. Try again with a different prompt.');
}
} else if (event.type === 'backlog_plan_error') {
setIsGeneratingPlan(false);
toast.error(`Plan generation failed: ${event.error}`);
const unsubscribe = api.backlogPlan.onEvent((data: unknown) => {
const event = data as { type: string; result?: BacklogPlanResult; error?: string };
logger.debug('Backlog plan event received', {
type: event.type,
hasResult: Boolean(event.result),
hasError: Boolean(event.error),
});
if (event.type === 'backlog_plan_complete') {
setIsGeneratingPlan(false);
if (event.result && event.result.changes?.length > 0) {
setPendingBacklogPlan(event.result);
toast.success('Plan ready! Click to review.', {
duration: 10000,
action: {
label: 'Review',
onClick: () => setShowPlanDialog(true),
},
});
} else {
toast.info('No changes generated. Try again with a different prompt.');
}
} else if (event.type === 'backlog_plan_error') {
setIsGeneratingPlan(false);
toast.error(`Plan generation failed: ${event.error}`);
}
);
});
return unsubscribe;
}, []);
@@ -211,7 +210,7 @@ export function GraphViewPage() {
return hookFeatures.reduce(
(counts, feature) => {
if (feature.status !== 'completed') {
const branch = feature.branchName ?? 'main';
const branch = (feature.branchName as string | undefined) ?? 'main';
counts[branch] = (counts[branch] || 0) + 1;
}
return counts;

View File

@@ -174,7 +174,7 @@ export function useGraphNodes({
type: 'dependency',
animated: enableEdgeAnimations && (isRunning || runningTaskIds.has(depId)),
data: {
sourceStatus: sourceFeature.status,
sourceStatus: sourceFeature.status as Feature['status'],
targetStatus: feature.status,
isHighlighted: edgeIsHighlighted,
isDimmed: edgeIsDimmed,

View File

@@ -121,7 +121,7 @@ export function RecentActivityFeed({ activities, maxItems = 10 }: RecentActivity
async (activity: RecentActivity) => {
try {
// Get project path from the activity (projectId is actually the path in our data model)
const projectPath = activity.projectPath || activity.projectId;
const projectPath = (activity.projectPath as string | undefined) || activity.projectId;
const projectName = activity.projectName;
const initResult = await initializeProject(projectPath);

View File

@@ -168,7 +168,8 @@ export function ProjectBulkReplaceDialog({
currentEntry: PhaseModelEntry
) => {
const claudeAlias = getClaudeModelAlias(currentEntry);
const newEntry = findModelForClaudeAlias(selectedProviderConfig, claudeAlias, key);
const providerConfig: ClaudeCompatibleProvider | null = selectedProviderConfig ?? null;
const newEntry = findModelForClaudeAlias(providerConfig, claudeAlias, key);
// Get display names
const getCurrentDisplay = (): string => {

View File

@@ -19,6 +19,7 @@ import { useAppStore } from '@/store/app-store';
import {
useAvailableEditors,
useEffectiveDefaultEditor,
type EditorInfo,
} from '@/components/views/board-view/worktree-panel/hooks/use-available-editors';
import { getEditorIcon } from '@/components/icons/editor-icons';
@@ -36,7 +37,7 @@ export function AccountSection() {
// Normalize Select value: if saved editor isn't found, show 'auto'
const hasSavedEditor =
!!defaultEditorCommand && editors.some((e) => e.command === defaultEditorCommand);
!!defaultEditorCommand && editors.some((e: EditorInfo) => e.command === defaultEditorCommand);
const selectValue = hasSavedEditor ? defaultEditorCommand : 'auto';
// Get icon component for the effective editor
@@ -121,7 +122,7 @@ export function AccountSection() {
Auto-detect
</span>
</SelectItem>
{editors.map((editor) => {
{editors.map((editor: EditorInfo) => {
const Icon = getEditorIcon(editor.command);
return (
<SelectItem key={editor.command} value={editor.command}>

View File

@@ -89,6 +89,12 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C
setIsAuthenticating(true);
try {
const api = getElectronAPI();
if (!api.setup) {
toast.error('Authentication Failed', {
description: 'Setup API is not available',
});
return;
}
const result = await api.setup.authClaude();
if (result.success) {
@@ -114,7 +120,17 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C
setIsDeauthenticating(true);
try {
const api = getElectronAPI();
const result = await api.setup.deauthClaude();
// Check if deauthClaude method exists on the API
const deauthClaude = (api.setup as Record<string, unknown> | undefined)?.deauthClaude as
| (() => Promise<{ success: boolean; error?: string }>)
| undefined;
if (!deauthClaude) {
toast.error('Sign Out Failed', {
description: 'Claude sign out is not available',
});
return;
}
const result = await deauthClaude();
if (result.success) {
toast.success('Signed Out', {

View File

@@ -84,7 +84,17 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl
setIsAuthenticating(true);
try {
const api = getElectronAPI();
const result = await api.setup.authCodex();
// Check if authCodex method exists on the API
const authCodex = (api.setup as Record<string, unknown> | undefined)?.authCodex as
| (() => Promise<{ success: boolean; error?: string }>)
| undefined;
if (!authCodex) {
toast.error('Authentication Failed', {
description: 'Codex authentication is not available',
});
return;
}
const result = await authCodex();
if (result.success) {
toast.success('Signed In', {
@@ -109,7 +119,17 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl
setIsDeauthenticating(true);
try {
const api = getElectronAPI();
const result = await api.setup.deauthCodex();
// Check if deauthCodex method exists on the API
const deauthCodex = (api.setup as Record<string, unknown> | undefined)?.deauthCodex as
| (() => Promise<{ success: boolean; error?: string }>)
| undefined;
if (!deauthCodex) {
toast.error('Sign Out Failed', {
description: 'Codex sign out is not available',
});
return;
}
const result = await deauthCodex();
if (result.success) {
toast.success('Signed Out', {

View File

@@ -209,7 +209,17 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat
setIsAuthenticating(true);
try {
const api = getElectronAPI();
const result = await api.setup.authCursor();
// Check if authCursor method exists on the API
const authCursor = (api.setup as Record<string, unknown> | undefined)?.authCursor as
| (() => Promise<{ success: boolean; error?: string }>)
| undefined;
if (!authCursor) {
toast.error('Authentication Failed', {
description: 'Cursor authentication is not available',
});
return;
}
const result = await authCursor();
if (result.success) {
toast.success('Signed In', {
@@ -234,7 +244,17 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat
setIsDeauthenticating(true);
try {
const api = getElectronAPI();
const result = await api.setup.deauthCursor();
// Check if deauthCursor method exists on the API
const deauthCursor = (api.setup as Record<string, unknown> | undefined)?.deauthCursor as
| (() => Promise<{ success: boolean; error?: string }>)
| undefined;
if (!deauthCursor) {
toast.error('Sign Out Failed', {
description: 'Cursor sign out is not available',
});
return;
}
const result = await deauthCursor();
if (result.success) {
toast.success('Signed Out', {

View File

@@ -27,7 +27,7 @@ export function SecurityWarningDialog({
onOpenChange,
onConfirm,
serverType,
_serverName,
serverName: _serverName,
command,
args,
url,

View File

@@ -158,7 +158,7 @@ export function BulkReplaceDialog({ open, onOpenChange }: BulkReplaceDialogProps
currentEntry: PhaseModelEntry
) => {
const claudeAlias = getClaudeModelAlias(currentEntry);
const newEntry = findModelForClaudeAlias(selectedProviderConfig, claudeAlias, key);
const newEntry = findModelForClaudeAlias(selectedProviderConfig ?? null, claudeAlias, key);
// Get display names
const getCurrentDisplay = (): string => {

View File

@@ -54,9 +54,25 @@ export function CodexSettingsTab() {
useEffect(() => {
const checkCodexStatus = async () => {
const api = getElectronAPI();
if (api?.setup?.getCodexStatus) {
// Check if getCodexStatus method exists on the API (may not be implemented yet)
const getCodexStatus = (api?.setup as Record<string, unknown> | undefined)?.getCodexStatus as
| (() => Promise<{
success: boolean;
installed: boolean;
version?: string;
path?: string;
recommendation?: string;
installCommands?: { npm?: string; macos?: string; windows?: string };
auth?: {
authenticated: boolean;
method: string;
hasApiKey?: boolean;
};
}>)
| undefined;
if (getCodexStatus) {
try {
const result = await api.setup.getCodexStatus();
const result = await getCodexStatus();
setDisplayCliStatus({
success: result.success,
status: result.installed ? 'installed' : 'not_installed',
@@ -68,8 +84,8 @@ export function CodexSettingsTab() {
});
setCodexCliStatus({
installed: result.installed,
version: result.version,
path: result.path,
version: result.version ?? null,
path: result.path ?? null,
method: result.auth?.method || 'none',
});
if (result.auth) {
@@ -96,8 +112,24 @@ export function CodexSettingsTab() {
setIsCheckingCodexCli(true);
try {
const api = getElectronAPI();
if (api?.setup?.getCodexStatus) {
const result = await api.setup.getCodexStatus();
// Check if getCodexStatus method exists on the API (may not be implemented yet)
const getCodexStatus = (api?.setup as Record<string, unknown> | undefined)?.getCodexStatus as
| (() => Promise<{
success: boolean;
installed: boolean;
version?: string;
path?: string;
recommendation?: string;
installCommands?: { npm?: string; macos?: string; windows?: string };
auth?: {
authenticated: boolean;
method: string;
hasApiKey?: boolean;
};
}>)
| undefined;
if (getCodexStatus) {
const result = await getCodexStatus();
setDisplayCliStatus({
success: result.success,
status: result.installed ? 'installed' : 'not_installed',
@@ -109,8 +141,8 @@ export function CodexSettingsTab() {
});
setCodexCliStatus({
installed: result.installed,
version: result.version,
path: result.path,
version: result.version ?? null,
path: result.path ?? null,
method: result.auth?.method || 'none',
});
if (result.auth) {

View File

@@ -40,7 +40,7 @@ export function CopilotSettingsTab() {
// Server sends installCommand (singular), transform to expected format
installCommands: cliStatusData.installCommand
? { npm: cliStatusData.installCommand }
: cliStatusData.installCommands,
: undefined,
};
}, [cliStatusData]);

View File

@@ -16,12 +16,9 @@ interface CursorPermissionsSectionProps {
isSavingPermissions: boolean;
copiedConfig: boolean;
currentProject?: { path: string } | null;
onApplyProfile: (
profileId: 'strict' | 'development',
scope: 'global' | 'project'
) => Promise<void>;
onCopyConfig: (profileId: 'strict' | 'development') => Promise<void>;
onLoadPermissions: () => Promise<void>;
onApplyProfile: (profileId: 'strict' | 'development', scope: 'global' | 'project') => void;
onCopyConfig: (profileId: 'strict' | 'development') => void;
onLoadPermissions: () => void;
}
export function CursorPermissionsSection({

View File

@@ -54,13 +54,15 @@ export function OpencodeSettingsTab() {
// Transform auth status to the expected format
const authStatus = useMemo((): OpencodeAuthStatus | null => {
if (!cliStatusData?.auth) return null;
// Cast auth to include optional error field for type compatibility
const auth = cliStatusData.auth as typeof cliStatusData.auth & { error?: string };
return {
authenticated: cliStatusData.auth.authenticated,
method: (cliStatusData.auth.method as OpencodeAuthStatus['method']) || 'none',
hasApiKey: cliStatusData.auth.hasApiKey,
hasEnvApiKey: cliStatusData.auth.hasEnvApiKey,
hasOAuthToken: cliStatusData.auth.hasOAuthToken,
error: cliStatusData.auth.error,
authenticated: auth.authenticated,
method: (auth.method as OpencodeAuthStatus['method']) || 'none',
hasApiKey: auth.hasApiKey,
hasEnvApiKey: auth.hasEnvApiKey,
hasOAuthToken: auth.hasOAuthToken,
error: auth.error,
};
}, [cliStatusData]);

View File

@@ -5,7 +5,7 @@ import type { CliStatus, ClaudeAuthStatus, CodexAuthStatus } from '@/store/setup
interface CliStatusApiResponse {
success: boolean;
status?: 'installed' | 'not_installed';
status?: string;
installed?: boolean;
method?: string;
version?: string;
@@ -14,12 +14,16 @@ interface CliStatusApiResponse {
authenticated: boolean;
method: string;
hasCredentialsFile?: boolean;
hasToken?: boolean;
hasStoredOAuthToken?: boolean;
hasStoredApiKey?: boolean;
hasEnvApiKey?: boolean;
hasEnvOAuthToken?: boolean;
hasCliAuth?: boolean;
hasRecentActivity?: boolean;
hasAuthFile?: boolean;
hasApiKey?: boolean;
hasOAuthToken?: boolean;
};
error?: string;
}

View File

@@ -55,13 +55,18 @@ export function OpencodeSetupStep({ onNext, onBack, onSkip }: OpencodeSetupStepP
}
const result = await api.setup.getOpencodeStatus();
if (result.success) {
// Derive install command from platform-specific options or use npm fallback
const installCommand =
result.installCommands?.npm ||
result.installCommands?.macos ||
result.installCommands?.linux;
const status: OpencodeCliStatus = {
installed: result.installed ?? false,
version: result.version,
path: result.path,
version: result.version ?? null,
path: result.path ?? null,
auth: result.auth,
installCommand: result.installCommand,
loginCommand: result.loginCommand,
installCommand,
loginCommand: 'opencode auth login',
};
setOpencodeCliStatus(status);

View File

@@ -133,8 +133,8 @@ function ClaudeContent() {
if (result.success) {
setClaudeCliStatus({
installed: result.installed ?? false,
version: result.version,
path: result.path,
version: result.version ?? null,
path: result.path ?? null,
method: 'none',
});
@@ -707,14 +707,21 @@ function CodexContent() {
if (result.success) {
setCodexCliStatus({
installed: result.installed ?? false,
version: result.version,
path: result.path,
version: result.version ?? null,
path: result.path ?? null,
method: 'none',
});
if (result.auth?.authenticated) {
const validMethods = ['api_key_env', 'api_key', 'cli_authenticated', 'none'] as const;
type CodexAuthMethod = (typeof validMethods)[number];
const method: CodexAuthMethod = validMethods.includes(
result.auth.method as CodexAuthMethod
)
? (result.auth.method as CodexAuthMethod)
: 'cli_authenticated';
setCodexAuthStatus({
authenticated: true,
method: result.auth.method || 'cli_authenticated',
method,
});
toast.success('Codex CLI is ready!');
}
@@ -997,13 +1004,18 @@ function OpencodeContent() {
if (!api.setup?.getOpencodeStatus) return;
const result = await api.setup.getOpencodeStatus();
if (result.success) {
// Derive install command from platform-specific options or use npm fallback
const installCommand =
result.installCommands?.npm ||
result.installCommands?.macos ||
result.installCommands?.linux;
setOpencodeCliStatus({
installed: result.installed ?? false,
version: result.version,
path: result.path,
version: result.version ?? null,
path: result.path ?? null,
auth: result.auth,
installCommand: result.installCommand,
loginCommand: result.loginCommand,
installCommand,
loginCommand: 'opencode auth login',
});
if (result.auth?.authenticated) {
toast.success('OpenCode CLI is ready!');
@@ -1807,8 +1819,8 @@ export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps)
if (result.success) {
setClaudeCliStatus({
installed: result.installed ?? false,
version: result.version,
path: result.path,
version: result.version ?? null,
path: result.path ?? null,
method: 'none',
});
// Note: Auth verification is handled by ClaudeContent component to avoid duplicate calls
@@ -1846,14 +1858,21 @@ export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps)
if (result.success) {
setCodexCliStatus({
installed: result.installed ?? false,
version: result.version,
path: result.path,
version: result.version ?? null,
path: result.path ?? null,
method: 'none',
});
if (result.auth?.authenticated) {
const validMethods = ['api_key_env', 'api_key', 'cli_authenticated', 'none'] as const;
type CodexAuthMethodType = (typeof validMethods)[number];
const method: CodexAuthMethodType = validMethods.includes(
result.auth.method as CodexAuthMethodType
)
? (result.auth.method as CodexAuthMethodType)
: 'cli_authenticated';
setCodexAuthStatus({
authenticated: true,
method: result.auth.method || 'cli_authenticated',
method,
});
}
}
@@ -1868,13 +1887,18 @@ export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps)
if (!api.setup?.getOpencodeStatus) return;
const result = await api.setup.getOpencodeStatus();
if (result.success) {
// Derive install command from platform-specific options or use npm fallback
const installCommand =
result.installCommands?.npm ||
result.installCommands?.macos ||
result.installCommands?.linux;
setOpencodeCliStatus({
installed: result.installed ?? false,
version: result.version,
path: result.path,
version: result.version ?? null,
path: result.path ?? null,
auth: result.auth,
installCommand: result.installCommand,
loginCommand: result.loginCommand,
installCommand,
loginCommand: 'opencode auth login',
});
}
} catch {

View File

@@ -310,9 +310,10 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
if (!node) return;
if (node.type === 'terminal') {
sessionIds.push(node.sessionId);
} else {
} else if (node.type === 'split') {
node.panels.forEach(collectFromLayout);
}
// testRunner type has sessionId but we only collect terminal sessions
};
terminalState.tabs.forEach((tab) => collectFromLayout(tab.layout));
return sessionIds;
@@ -620,7 +621,7 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
description: data.error || 'Unknown error',
});
// Reset the handled ref so the same cwd can be retried
initialCwdHandledRef.current = undefined;
initialCwdHandledRef.current = null;
}
} catch (err) {
logger.error('Create terminal with cwd error:', err);
@@ -628,7 +629,7 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
description: 'Could not connect to server',
});
// Reset the handled ref so the same cwd can be retried
initialCwdHandledRef.current = undefined;
initialCwdHandledRef.current = null;
}
};
@@ -791,6 +792,11 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
};
}
// Handle testRunner type - skip for now as we don't persist test runner sessions
if (persisted.type === 'testRunner') {
return null;
}
// It's a split - rebuild all child panels
const childPanels: TerminalPanelContent[] = [];
for (const childPersisted of persisted.panels) {
@@ -1094,7 +1100,8 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
const collectSessionIds = (node: TerminalPanelContent | null): string[] => {
if (!node) return [];
if (node.type === 'terminal') return [node.sessionId];
return node.panels.flatMap(collectSessionIds);
if (node.type === 'split') return node.panels.flatMap(collectSessionIds);
return []; // testRunner type
};
const sessionIds = collectSessionIds(tab.layout);
@@ -1132,7 +1139,10 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
if (panel.type === 'terminal') {
return [panel.sessionId];
}
return panel.panels.flatMap(getTerminalIds);
if (panel.type === 'split') {
return panel.panels.flatMap(getTerminalIds);
}
return []; // testRunner type
};
// Get a STABLE key for a panel - uses the stable id for splits
@@ -1141,8 +1151,12 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
if (panel.type === 'terminal') {
return panel.sessionId;
}
// Use the stable id for split nodes
return panel.id;
if (panel.type === 'split') {
// Use the stable id for split nodes
return panel.id;
}
// testRunner - use sessionId
return panel.sessionId;
};
const findTerminalFontSize = useCallback(
@@ -1154,6 +1168,7 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
}
return null;
}
if (panel.type !== 'split') return null; // testRunner type
for (const child of panel.panels) {
const found = findInPanel(child);
if (found !== null) return found;
@@ -1208,7 +1223,8 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
// Helper to get all terminal IDs from a layout subtree
const getAllTerminals = (node: TerminalPanelContent): string[] => {
if (node.type === 'terminal') return [node.sessionId];
return node.panels.flatMap(getAllTerminals);
if (node.type === 'split') return node.panels.flatMap(getAllTerminals);
return []; // testRunner type
};
// Helper to find terminal and its path in the tree
@@ -1225,6 +1241,7 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
if (node.type === 'terminal') {
return node.sessionId === target ? path : null;
}
if (node.type !== 'split') return null; // testRunner type
for (let i = 0; i < node.panels.length; i++) {
const result = findPath(node.panels[i], target, [
...path,
@@ -1354,6 +1371,11 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
);
}
// Handle testRunner type - return null for now
if (content.type === 'testRunner') {
return null;
}
const isHorizontal = content.direction === 'horizontal';
const defaultSizePerPanel = 100 / content.panels.length;
@@ -1365,7 +1387,7 @@ export function TerminalView({ initialCwd, initialBranch, initialMode, nonce }:
return (
<PanelGroup direction={content.direction} onLayout={handleLayoutChange}>
{content.panels.map((panel, index) => {
{content.panels.map((panel: TerminalPanelContent, index: number) => {
const panelSize =
panel.type === 'terminal' && panel.size ? panel.size : defaultSizePerPanel;

View File

@@ -36,6 +36,7 @@ export function useStartFeature(projectPath: string) {
worktreePath?: string;
}) => {
const api = getElectronAPI();
if (!api.autoMode) throw new Error('AutoMode API not available');
const result = await api.autoMode.runFeature(
projectPath,
featureId,
@@ -77,6 +78,7 @@ export function useResumeFeature(projectPath: string) {
useWorktrees?: boolean;
}) => {
const api = getElectronAPI();
if (!api.autoMode) throw new Error('AutoMode API not available');
const result = await api.autoMode.resumeFeature(projectPath, featureId, useWorktrees);
if (!result.success) {
throw new Error(result.error || 'Failed to resume feature');
@@ -116,6 +118,7 @@ export function useStopFeature() {
mutationFn: async (input: string | { featureId: string; projectPath?: string }) => {
const featureId = typeof input === 'string' ? input : input.featureId;
const api = getElectronAPI();
if (!api.autoMode) throw new Error('AutoMode API not available');
const result = await api.autoMode.stopFeature(featureId);
if (!result.success) {
throw new Error(result.error || 'Failed to stop feature');
@@ -151,6 +154,7 @@ export function useVerifyFeature(projectPath: string) {
return useMutation({
mutationFn: async (featureId: string) => {
const api = getElectronAPI();
if (!api.autoMode) throw new Error('AutoMode API not available');
const result = await api.autoMode.verifyFeature(projectPath, featureId);
if (!result.success) {
throw new Error(result.error || 'Failed to verify feature');
@@ -196,6 +200,7 @@ export function useApprovePlan(projectPath: string) {
feedback?: string;
}) => {
const api = getElectronAPI();
if (!api.autoMode) throw new Error('AutoMode API not available');
const result = await api.autoMode.approvePlan(
projectPath,
featureId,
@@ -246,6 +251,7 @@ export function useFollowUpFeature(projectPath: string) {
useWorktrees?: boolean;
}) => {
const api = getElectronAPI();
if (!api.autoMode) throw new Error('AutoMode API not available');
const result = await api.autoMode.followUpFeature(
projectPath,
featureId,
@@ -282,6 +288,7 @@ export function useCommitFeature(projectPath: string) {
return useMutation({
mutationFn: async (featureId: string) => {
const api = getElectronAPI();
if (!api.autoMode) throw new Error('AutoMode API not available');
const result = await api.autoMode.commitFeature(projectPath, featureId);
if (!result.success) {
throw new Error(result.error || 'Failed to commit changes');
@@ -310,6 +317,7 @@ export function useAnalyzeProject() {
return useMutation({
mutationFn: async (projectPath: string) => {
const api = getElectronAPI();
if (!api.autoMode) throw new Error('AutoMode API not available');
const result = await api.autoMode.analyzeProject(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to analyze project');
@@ -339,7 +347,8 @@ export function useStartAutoMode(projectPath: string) {
return useMutation({
mutationFn: async (maxConcurrency?: number) => {
const api = getElectronAPI();
const result = await api.autoMode.start(projectPath, maxConcurrency);
if (!api.autoMode) throw new Error('AutoMode API not available');
const result = await api.autoMode.start(projectPath, String(maxConcurrency ?? ''));
if (!result.success) {
throw new Error(result.error || 'Failed to start auto mode');
}
@@ -369,6 +378,7 @@ export function useStopAutoMode(projectPath: string) {
return useMutation({
mutationFn: async () => {
const api = getElectronAPI();
if (!api.autoMode) throw new Error('AutoMode API not available');
const result = await api.autoMode.stop(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to stop auto mode');

View File

@@ -8,7 +8,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { getElectronAPI, GitHubIssue, GitHubComment } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { toast } from 'sonner';
import type { LinkedPRInfo, ModelId } from '@automaker/types';
import type { LinkedPRInfo, ModelId, ThinkingLevel, ReasoningEffort } from '@automaker/types';
import { resolveModelString } from '@automaker/model-resolver';
/**
@@ -17,8 +17,8 @@ import { resolveModelString } from '@automaker/model-resolver';
interface ValidateIssueInput {
issue: GitHubIssue;
model?: ModelId;
thinkingLevel?: number;
reasoningEffort?: string;
thinkingLevel?: ThinkingLevel;
reasoningEffort?: ReasoningEffort;
comments?: GitHubComment[];
linkedPRs?: LinkedPRInfo[];
}

View File

@@ -22,6 +22,7 @@ export function useCreateWorktree(projectPath: string) {
return useMutation({
mutationFn: async ({ branchName, baseBranch }: { branchName: string; baseBranch?: string }) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.create(projectPath, branchName, baseBranch);
if (!result.success) {
throw new Error(result.error || 'Failed to create worktree');
@@ -58,6 +59,7 @@ export function useDeleteWorktree(projectPath: string) {
deleteBranch?: boolean;
}) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.delete(projectPath, worktreePath, deleteBranch);
if (!result.success) {
throw new Error(result.error || 'Failed to delete worktree');
@@ -87,6 +89,7 @@ export function useCommitWorktree() {
return useMutation({
mutationFn: async ({ worktreePath, message }: { worktreePath: string; message: string }) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.commit(worktreePath, message);
if (!result.success) {
throw new Error(result.error || 'Failed to commit changes');
@@ -117,6 +120,7 @@ export function usePushWorktree() {
return useMutation({
mutationFn: async ({ worktreePath, force }: { worktreePath: string; force?: boolean }) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.push(worktreePath, force);
if (!result.success) {
throw new Error(result.error || 'Failed to push changes');
@@ -146,6 +150,7 @@ export function usePullWorktree() {
return useMutation({
mutationFn: async (worktreePath: string) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.pull(worktreePath);
if (!result.success) {
throw new Error(result.error || 'Failed to pull changes');
@@ -188,6 +193,7 @@ export function useCreatePullRequest() {
};
}) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.createPR(worktreePath, options);
if (!result.success) {
throw new Error(result.error || 'Failed to create pull request');
@@ -243,10 +249,12 @@ export function useMergeWorktree(projectPath: string) {
};
}) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.mergeFeature(
projectPath,
branchName,
worktreePath,
undefined, // targetBranch - use default (main)
options
);
if (!result.success) {
@@ -284,6 +292,7 @@ export function useSwitchBranch() {
branchName: string;
}) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.switchBranch(worktreePath, branchName);
if (!result.success) {
throw new Error(result.error || 'Failed to switch branch');
@@ -319,6 +328,7 @@ export function useCheckoutBranch() {
branchName: string;
}) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.checkoutBranch(worktreePath, branchName);
if (!result.success) {
throw new Error(result.error || 'Failed to checkout branch');
@@ -346,6 +356,7 @@ export function useGenerateCommitMessage() {
return useMutation({
mutationFn: async (worktreePath: string) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.generateCommitMessage(worktreePath);
if (!result.success) {
throw new Error(result.error || 'Failed to generate commit message');
@@ -375,6 +386,7 @@ export function useOpenInEditor() {
editorCommand?: string;
}) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.openInEditor(worktreePath, editorCommand);
if (!result.success) {
throw new Error(result.error || 'Failed to open in editor');
@@ -400,6 +412,7 @@ export function useInitGit() {
return useMutation({
mutationFn: async (projectPath: string) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.initGit(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to initialize git');
@@ -431,6 +444,7 @@ export function useSetInitScript(projectPath: string) {
return useMutation({
mutationFn: async (content: string) => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.setInitScript(projectPath, content);
if (!result.success) {
throw new Error(result.error || 'Failed to save init script');
@@ -461,6 +475,7 @@ export function useDeleteInitScript(projectPath: string) {
return useMutation({
mutationFn: async () => {
const api = getElectronAPI();
if (!api.worktree) throw new Error('Worktree API not available');
const result = await api.worktree.deleteInitScript(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to delete init script');

View File

@@ -60,14 +60,13 @@ export {
// CLI Status
export {
useClaudeCliStatus,
useCursorCliStatus,
useCodexCliStatus,
useOpencodeCliStatus,
useGeminiCliStatus,
useCopilotCliStatus,
useGitHubCliStatus,
useApiKeysStatus,
usePlatformInfo,
useCursorCliStatus,
useCopilotCliStatus,
useGeminiCliStatus,
useOpencodeCliStatus,
} from './use-cli-status';
// Ideation

View File

@@ -1,7 +1,7 @@
/**
* CLI Status Query Hooks
*
* React Query hooks for fetching CLI tool status (Claude, Cursor, Codex, etc.)
* React Query hooks for fetching CLI tool status (Claude, GitHub CLI, etc.)
*/
import { useQuery } from '@tanstack/react-query';
@@ -19,6 +19,9 @@ export function useClaudeCliStatus() {
queryKey: queryKeys.cli.claude(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.setup) {
throw new Error('Setup API not available');
}
const result = await api.setup.getClaudeStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Claude status');
@@ -29,106 +32,6 @@ export function useClaudeCliStatus() {
});
}
/**
* Fetch Cursor CLI status
*
* @returns Query result with Cursor CLI status
*/
export function useCursorCliStatus() {
return useQuery({
queryKey: queryKeys.cli.cursor(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.setup.getCursorStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Cursor status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch Codex CLI status
*
* @returns Query result with Codex CLI status
*/
export function useCodexCliStatus() {
return useQuery({
queryKey: queryKeys.cli.codex(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.setup.getCodexStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Codex status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch OpenCode CLI status
*
* @returns Query result with OpenCode CLI status
*/
export function useOpencodeCliStatus() {
return useQuery({
queryKey: queryKeys.cli.opencode(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.setup.getOpencodeStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch OpenCode status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch Gemini CLI status
*
* @returns Query result with Gemini CLI status
*/
export function useGeminiCliStatus() {
return useQuery({
queryKey: queryKeys.cli.gemini(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.setup.getGeminiStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Gemini status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch Copilot SDK status
*
* @returns Query result with Copilot SDK status
*/
export function useCopilotCliStatus() {
return useQuery({
queryKey: queryKeys.cli.copilot(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.setup.getCopilotStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Copilot status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch GitHub CLI status
*
@@ -139,6 +42,9 @@ export function useGitHubCliStatus() {
queryKey: queryKeys.cli.github(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.setup?.getGhStatus) {
throw new Error('GitHub CLI status API not available');
}
const result = await api.setup.getGhStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch GitHub CLI status');
@@ -159,6 +65,9 @@ export function useApiKeysStatus() {
queryKey: queryKeys.cli.apiKeys(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.setup) {
throw new Error('Setup API not available');
}
const result = await api.setup.getApiKeys();
return result;
},
@@ -176,6 +85,9 @@ export function usePlatformInfo() {
queryKey: queryKeys.cli.platform(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.setup) {
throw new Error('Setup API not available');
}
const result = await api.setup.getPlatform();
if (!result.success) {
throw new Error('Failed to fetch platform info');
@@ -185,3 +97,95 @@ export function usePlatformInfo() {
staleTime: Infinity, // Platform info never changes
});
}
/**
* Fetch Cursor CLI status
*
* @returns Query result with Cursor CLI status
*/
export function useCursorCliStatus() {
return useQuery({
queryKey: queryKeys.cli.cursor(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.setup?.getCursorStatus) {
throw new Error('Cursor CLI status API not available');
}
const result = await api.setup.getCursorStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Cursor CLI status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch Copilot CLI status
*
* @returns Query result with Copilot CLI status
*/
export function useCopilotCliStatus() {
return useQuery({
queryKey: queryKeys.cli.copilot(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.setup?.getCopilotStatus) {
throw new Error('Copilot CLI status API not available');
}
const result = await api.setup.getCopilotStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Copilot CLI status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch Gemini CLI status
*
* @returns Query result with Gemini CLI status
*/
export function useGeminiCliStatus() {
return useQuery({
queryKey: queryKeys.cli.gemini(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.setup?.getGeminiStatus) {
throw new Error('Gemini CLI status API not available');
}
const result = await api.setup.getGeminiStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Gemini CLI status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch OpenCode CLI status
*
* @returns Query result with OpenCode CLI status
*/
export function useOpencodeCliStatus() {
return useQuery({
queryKey: queryKeys.cli.opencode(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.setup?.getOpencodeStatus) {
throw new Error('OpenCode CLI status API not available');
}
const result = await api.setup.getOpencodeStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch OpenCode CLI status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}

View File

@@ -22,6 +22,9 @@ export function useGitDiffs(projectPath: string | undefined, enabled = true) {
queryFn: async () => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
if (!api.git) {
throw new Error('Git API not available');
}
const result = await api.git.getDiffs(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch diffs');

View File

@@ -8,7 +8,7 @@ import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import type { GitHubIssue, GitHubPR, GitHubComment, IssueValidation } from '@/lib/electron';
import type { GitHubIssue, GitHubPR, GitHubComment, StoredValidation } from '@/lib/electron';
interface GitHubIssuesResult {
openIssues: GitHubIssue[];
@@ -38,6 +38,9 @@ export function useGitHubIssues(projectPath: string | undefined) {
queryFn: async (): Promise<GitHubIssuesResult> => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
if (!api.github) {
throw new Error('GitHub API not available');
}
const result = await api.github.listIssues(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch issues');
@@ -64,6 +67,9 @@ export function useGitHubPRs(projectPath: string | undefined) {
queryFn: async (): Promise<GitHubPRsResult> => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
if (!api.github) {
throw new Error('GitHub API not available');
}
const result = await api.github.listPRs(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch PRs');
@@ -90,9 +96,12 @@ export function useGitHubValidations(projectPath: string | undefined, issueNumbe
queryKey: issueNumber
? queryKeys.github.validation(projectPath ?? '', issueNumber)
: queryKeys.github.validations(projectPath ?? ''),
queryFn: async (): Promise<IssueValidation[]> => {
queryFn: async (): Promise<StoredValidation[]> => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
if (!api.github) {
throw new Error('GitHub API not available');
}
const result = await api.github.getValidations(projectPath, issueNumber);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch validations');
@@ -116,15 +125,18 @@ export function useGitHubRemote(projectPath: string | undefined) {
queryFn: async () => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
if (!api.github) {
throw new Error('GitHub API not available');
}
const result = await api.github.checkRemote(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to check remote');
}
return {
hasRemote: result.hasRemote ?? false,
hasRemote: result.hasGitHubRemote ?? false,
owner: result.owner,
repo: result.repo,
url: result.url,
url: result.remoteUrl,
};
},
enabled: !!projectPath,
@@ -165,6 +177,9 @@ export function useGitHubIssueComments(
queryFn: async ({ pageParam }: { pageParam: string | undefined }) => {
if (!projectPath || !issueNumber) throw new Error('Missing project path or issue number');
const api = getElectronAPI();
if (!api.github) {
throw new Error('GitHub API not available');
}
const result = await api.github.getIssueComments(projectPath, issueNumber, pageParam);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch comments');

View File

@@ -8,6 +8,7 @@ import { useQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import type { ModelDefinition } from '@automaker/types';
interface CodexModel {
id: string;
@@ -19,18 +20,6 @@ interface CodexModel {
isDefault: boolean;
}
interface OpencodeModel {
id: string;
name: string;
modelString: string;
provider: string;
description: string;
supportsTools: boolean;
supportsVision: boolean;
tier: string;
default?: boolean;
}
/**
* Fetch available models
*
@@ -41,6 +30,9 @@ export function useAvailableModels() {
queryKey: queryKeys.models.available(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.model) {
throw new Error('Model API not available');
}
const result = await api.model.getAvailable();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch available models');
@@ -62,6 +54,9 @@ export function useCodexModels(refresh = false) {
queryKey: queryKeys.models.codex(),
queryFn: async (): Promise<CodexModel[]> => {
const api = getElectronAPI();
if (!api.codex) {
throw new Error('Codex API not available');
}
const result = await api.codex.getModels(refresh);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Codex models');
@@ -81,13 +76,16 @@ export function useCodexModels(refresh = false) {
export function useOpencodeModels(refresh = false) {
return useQuery({
queryKey: queryKeys.models.opencode(),
queryFn: async (): Promise<OpencodeModel[]> => {
queryFn: async (): Promise<ModelDefinition[]> => {
const api = getElectronAPI();
if (!api.setup?.getOpencodeModels) {
throw new Error('OpenCode models API not available');
}
const result = await api.setup.getOpencodeModels(refresh);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch OpenCode models');
}
return (result.models ?? []) as OpencodeModel[];
return (result.models ?? []) as ModelDefinition[];
},
staleTime: STALE_TIMES.MODELS,
});
@@ -103,6 +101,9 @@ export function useOpencodeProviders() {
queryKey: queryKeys.models.opencodeProviders(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.setup?.getOpencodeProviders) {
throw new Error('OpenCode providers API not available');
}
const result = await api.setup.getOpencodeProviders();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch OpenCode providers');
@@ -123,6 +124,9 @@ export function useModelProviders() {
queryKey: queryKeys.models.providers(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.model) {
throw new Error('Model API not available');
}
const result = await api.model.checkProviders();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch providers');

View File

@@ -8,7 +8,7 @@ import { useQuery } from '@tanstack/react-query';
import { getHttpApiClient } from '@/lib/http-api-client';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import type { PipelineConfig } from '@/store/app-store';
import type { PipelineConfig } from '@automaker/types';
/**
* Fetch pipeline config for a project

View File

@@ -34,6 +34,9 @@ export function useRunningAgents() {
queryKey: queryKeys.runningAgents.all(),
queryFn: async (): Promise<RunningAgentsResult> => {
const api = getElectronAPI();
if (!api.runningAgents) {
throw new Error('Running agents API not available');
}
const result = await api.runningAgents.getAll();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch running agents');

View File

@@ -26,6 +26,9 @@ export function useSessions(includeArchived = false) {
queryKey: queryKeys.sessions.all(includeArchived),
queryFn: async (): Promise<SessionListItem[]> => {
const api = getElectronAPI();
if (!api.sessions) {
throw new Error('Sessions API not available');
}
const result = await api.sessions.list(includeArchived);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch sessions');
@@ -48,6 +51,9 @@ export function useSessionHistory(sessionId: string | undefined) {
queryFn: async () => {
if (!sessionId) throw new Error('No session ID');
const api = getElectronAPI();
if (!api.agent) {
throw new Error('Agent API not available');
}
const result = await api.agent.getHistory(sessionId);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch session history');
@@ -74,6 +80,9 @@ export function useSessionQueue(sessionId: string | undefined) {
queryFn: async () => {
if (!sessionId) throw new Error('No session ID');
const api = getElectronAPI();
if (!api.agent) {
throw new Error('Agent API not available');
}
const result = await api.agent.queueList(sessionId);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch queue');

View File

@@ -25,11 +25,14 @@ export function useGlobalSettings() {
queryKey: queryKeys.settings.global(),
queryFn: async (): Promise<GlobalSettings> => {
const api = getElectronAPI();
if (!api.settings) {
throw new Error('Settings API not available');
}
const result = await api.settings.getGlobal();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch global settings');
}
return result.settings as GlobalSettings;
return result.settings as unknown as GlobalSettings;
},
staleTime: STALE_TIMES.SETTINGS,
});
@@ -47,11 +50,14 @@ export function useProjectSettings(projectPath: string | undefined) {
queryFn: async (): Promise<ProjectSettings> => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
if (!api.settings) {
throw new Error('Settings API not available');
}
const result = await api.settings.getProject(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch project settings');
}
return result.settings as ProjectSettings;
return result.settings as unknown as ProjectSettings;
},
enabled: !!projectPath,
staleTime: STALE_TIMES.SETTINGS,
@@ -68,6 +74,9 @@ export function useSettingsStatus() {
queryKey: queryKeys.settings.status(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.settings) {
throw new Error('Settings API not available');
}
const result = await api.settings.getStatus();
return result;
},
@@ -85,6 +94,9 @@ export function useCredentials() {
queryKey: queryKeys.settings.credentials(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.settings) {
throw new Error('Settings API not available');
}
const result = await api.settings.getCredentials();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch credentials');
@@ -111,6 +123,9 @@ export function useDiscoveredAgents(
queryKey: queryKeys.settings.agents(projectPath ?? '', sources),
queryFn: async () => {
const api = getElectronAPI();
if (!api.settings) {
throw new Error('Settings API not available');
}
const result = await api.settings.discoverAgents(projectPath, sources);
if (!result.success) {
throw new Error(result.error || 'Failed to discover agents');

View File

@@ -32,6 +32,9 @@ export function useClaudeUsage(enabled = true) {
queryKey: queryKeys.usage.claude(),
queryFn: async (): Promise<ClaudeUsage> => {
const api = getElectronAPI();
if (!api.claude) {
throw new Error('Claude API not available');
}
const result = await api.claude.getUsage();
// Check if result is an error response
if ('error' in result) {
@@ -65,6 +68,9 @@ export function useCodexUsage(enabled = true) {
queryKey: queryKeys.usage.codex(),
queryFn: async (): Promise<CodexUsage> => {
const api = getElectronAPI();
if (!api.codex) {
throw new Error('Codex API not available');
}
const result = await api.codex.getUsage();
// Check if result is an error response
if ('error' in result) {

View File

@@ -51,6 +51,9 @@ export function useWorktrees(projectPath: string | undefined, includeDetails = t
queryFn: async (): Promise<WorktreesResult> => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
if (!api.worktree) {
throw new Error('Worktree API not available');
}
const result = await api.worktree.listAll(projectPath, includeDetails);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch worktrees');
@@ -80,6 +83,9 @@ export function useWorktreeInfo(projectPath: string | undefined, featureId: stri
queryFn: async () => {
if (!projectPath || !featureId) throw new Error('Missing project path or feature ID');
const api = getElectronAPI();
if (!api.worktree) {
throw new Error('Worktree API not available');
}
const result = await api.worktree.getInfo(projectPath, featureId);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch worktree info');
@@ -106,6 +112,9 @@ export function useWorktreeStatus(projectPath: string | undefined, featureId: st
queryFn: async () => {
if (!projectPath || !featureId) throw new Error('Missing project path or feature ID');
const api = getElectronAPI();
if (!api.worktree) {
throw new Error('Worktree API not available');
}
const result = await api.worktree.getStatus(projectPath, featureId);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch worktree status');
@@ -132,6 +141,9 @@ export function useWorktreeDiffs(projectPath: string | undefined, featureId: str
queryFn: async () => {
if (!projectPath || !featureId) throw new Error('Missing project path or feature ID');
const api = getElectronAPI();
if (!api.worktree) {
throw new Error('Worktree API not available');
}
const result = await api.worktree.getDiffs(projectPath, featureId);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch diffs');
@@ -180,6 +192,9 @@ export function useWorktreeBranches(worktreePath: string | undefined, includeRem
queryFn: async (): Promise<BranchesResult> => {
if (!worktreePath) throw new Error('No worktree path');
const api = getElectronAPI();
if (!api.worktree) {
throw new Error('Worktree API not available');
}
const result = await api.worktree.listBranches(worktreePath, includeRemote);
// Handle special git status codes
@@ -239,6 +254,9 @@ export function useWorktreeInitScript(projectPath: string | undefined) {
queryFn: async () => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
if (!api.worktree) {
throw new Error('Worktree API not available');
}
const result = await api.worktree.getInitScript(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch init script');
@@ -265,11 +283,14 @@ export function useAvailableEditors() {
queryKey: queryKeys.worktrees.editors(),
queryFn: async () => {
const api = getElectronAPI();
if (!api.worktree) {
throw new Error('Worktree API not available');
}
const result = await api.worktree.getAvailableEditors();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch editors');
}
return result.editors ?? [];
return result.result?.editors ?? [];
},
staleTime: STALE_TIMES.CLI_STATUS,
refetchOnWindowFocus: WORKTREE_REFETCH_ON_FOCUS,

View File

@@ -99,7 +99,7 @@ export function useProjectSettingsLoader() {
// These are stored directly on the project, so we need to update both
// currentProject AND the projects array to keep them in sync
// Type assertion needed because API returns Record<string, unknown>
const settingsWithExtras = settings as Record<string, unknown>;
const settingsWithExtras = settings as unknown as Record<string, unknown>;
const activeClaudeApiProfileId = settingsWithExtras.activeClaudeApiProfileId as
| string
| null

View File

@@ -9,7 +9,7 @@ import { useEffect, useRef } from 'react';
import { useQueryClient, QueryClient } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import type { AutoModeEvent, SpecRegenerationEvent } from '@/types/electron';
import type { AutoModeEvent, SpecRegenerationEvent, StreamEvent } from '@/types/electron';
import type { IssueValidationEvent } from '@automaker/types';
import { debounce, type DebouncedFunction } from '@automaker/utils/debounce';
import { useEventRecencyStore } from './use-event-recency';
@@ -165,6 +165,7 @@ export function useAutoModeQueryInvalidation(projectPath: string | undefined) {
}
const api = getElectronAPI();
if (!api.autoMode) return;
const unsubscribe = api.autoMode.onEvent((event: AutoModeEvent) => {
// Record that we received a WebSocket event (for event recency tracking)
// This allows polling to be disabled when WebSocket events are flowing
@@ -241,6 +242,7 @@ export function useSpecRegenerationQueryInvalidation(projectPath: string | undef
if (!projectPath) return;
const api = getElectronAPI();
if (!api.specRegeneration) return;
const unsubscribe = api.specRegeneration.onEvent((event: SpecRegenerationEvent) => {
// Only handle events for the current project
if (event.projectPath !== projectPath) return;
@@ -288,14 +290,14 @@ export function useGitHubValidationQueryInvalidation(projectPath: string | undef
// Record that we received a WebSocket event
recordGlobalEvent();
if (event.type === 'validation_complete' || event.type === 'validation_error') {
if (event.type === 'issue_validation_complete' || event.type === 'issue_validation_error') {
// Invalidate all validations for this project
queryClient.invalidateQueries({
queryKey: queryKeys.github.validations(projectPath),
});
// Also invalidate specific issue validation if we have the issue number
if ('issueNumber' in event && event.issueNumber) {
if (event.issueNumber) {
queryClient.invalidateQueries({
queryKey: queryKeys.github.validation(projectPath, event.issueNumber),
});
@@ -320,7 +322,9 @@ export function useSessionQueryInvalidation(sessionId: string | undefined) {
if (!sessionId) return;
const api = getElectronAPI();
const unsubscribe = api.agent.onStream((event) => {
if (!api.agent) return;
const unsubscribe = api.agent.onStream((data: unknown) => {
const event = data as StreamEvent;
// Only handle events for the current session
if ('sessionId' in event && event.sessionId !== sessionId) return;

View File

@@ -668,8 +668,9 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
maxConcurrency: number;
}
> = {};
if ((settings as Record<string, unknown>).autoModeByWorktree) {
const persistedSettings = (settings as Record<string, unknown>).autoModeByWorktree as Record<
if ((settings as unknown as Record<string, unknown>).autoModeByWorktree) {
const persistedSettings = (settings as unknown as Record<string, unknown>)
.autoModeByWorktree as Record<
string,
{ maxConcurrency?: number; branchName?: string | null }
>;

View File

@@ -26,7 +26,6 @@ import {
DEFAULT_MAX_CONCURRENCY,
getAllOpencodeModelIds,
getAllCursorModelIds,
getAllCodexModelIds,
getAllGeminiModelIds,
getAllCopilotModelIds,
migrateCursorModelIds,
@@ -34,7 +33,6 @@ import {
migratePhaseModelEntry,
type GlobalSettings,
type CursorModelId,
type CodexModelId,
type GeminiModelId,
type CopilotModelId,
} from '@automaker/types';
@@ -76,8 +74,6 @@ const SETTINGS_FIELDS_TO_SYNC = [
'cursorDefaultModel',
'enabledOpencodeModels',
'opencodeDefaultModel',
'enabledCodexModels',
'codexDefaultModel',
'enabledGeminiModels',
'geminiDefaultModel',
'enabledCopilotModels',
@@ -585,22 +581,6 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
sanitizedEnabledOpencodeModels.push(sanitizedOpencodeDefaultModel);
}
// Sanitize Codex models
const validCodexModelIds = new Set(getAllCodexModelIds());
const DEFAULT_CODEX_MODEL: CodexModelId = 'codex-gpt-5.2-codex';
const sanitizedEnabledCodexModels = (serverSettings.enabledCodexModels ?? []).filter(
(id): id is CodexModelId => validCodexModelIds.has(id as CodexModelId)
);
const sanitizedCodexDefaultModel = validCodexModelIds.has(
serverSettings.codexDefaultModel as CodexModelId
)
? (serverSettings.codexDefaultModel as CodexModelId)
: DEFAULT_CODEX_MODEL;
if (!sanitizedEnabledCodexModels.includes(sanitizedCodexDefaultModel)) {
sanitizedEnabledCodexModels.push(sanitizedCodexDefaultModel);
}
// Sanitize Gemini models
const validGeminiModelIds = new Set(getAllGeminiModelIds());
const sanitizedEnabledGeminiModels = (serverSettings.enabledGeminiModels ?? []).filter(
@@ -726,8 +706,6 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
cursorDefaultModel: sanitizedCursorDefault,
enabledOpencodeModels: sanitizedEnabledOpencodeModels,
opencodeDefaultModel: sanitizedOpencodeDefaultModel,
enabledCodexModels: sanitizedEnabledCodexModels,
codexDefaultModel: sanitizedCodexDefaultModel,
enabledGeminiModels: sanitizedEnabledGeminiModels,
geminiDefaultModel: sanitizedGeminiDefaultModel,
enabledCopilotModels: sanitizedEnabledCopilotModels,

View File

@@ -32,7 +32,6 @@ import type {
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';
@@ -234,6 +233,7 @@ export interface RunningAgent {
isAutoMode: boolean;
title?: string;
description?: string;
branchName?: string;
}
export interface RunningAgentsResult {
@@ -785,6 +785,18 @@ export interface ElectronAPI {
}>;
stop: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
clear: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
queueList: (sessionId: string) => Promise<{
success: boolean;
queue?: Array<{
id: string;
message: string;
imagePaths?: string[];
model?: string;
thinkingLevel?: string;
addedAt: string;
}>;
error?: string;
}>;
onStream: (callback: (data: unknown) => void) => () => void;
};
sessions?: {
@@ -936,12 +948,16 @@ export interface ElectronAPI {
// Do not redeclare here to avoid type conflicts
// Mock data for web development
const mockFeatures = [
const mockFeatures: Feature[] = [
{
id: 'mock-feature-1',
title: 'Sample Feature',
category: 'Core',
description: 'Sample Feature',
status: 'backlog',
steps: ['Step 1', 'Step 2'],
passes: false,
createdAt: new Date().toISOString(),
},
];
@@ -1351,6 +1367,13 @@ const _getMockElectronAPI = (): ElectronAPI => {
};
};
// Install progress event type used by useCliInstallation hook
interface InstallProgressEvent {
cli?: string;
data?: string;
type?: string;
}
// Setup API interface
interface SetupAPI {
getClaudeStatus: () => Promise<{
@@ -1389,7 +1412,15 @@ interface SetupAPI {
message?: string;
output?: string;
}>;
deauthClaude?: () => Promise<{
success: boolean;
requiresManualDeauth?: boolean;
command?: string;
message?: string;
error?: string;
}>;
storeApiKey: (provider: string, apiKey: string) => Promise<{ success: boolean; error?: string }>;
saveApiKey?: (provider: string, apiKey: string) => Promise<{ success: boolean; error?: string }>;
getApiKeys: () => Promise<{
success: boolean;
hasAnthropicKey: boolean;
@@ -1422,12 +1453,252 @@ interface SetupAPI {
user: string | null;
error?: string;
}>;
onInstallProgress?: (callback: (progress: InstallProgress) => void) => () => void;
onAuthProgress?: (callback: (progress: InstallProgress) => void) => () => void;
// Cursor CLI methods
getCursorStatus?: () => Promise<{
success: boolean;
installed?: boolean;
version?: string | null;
path?: string | null;
auth?: {
authenticated: boolean;
method: string;
};
installCommand?: string;
loginCommand?: string;
error?: string;
}>;
authCursor?: () => Promise<{
success: boolean;
token?: string;
requiresManualAuth?: boolean;
terminalOpened?: boolean;
command?: string;
message?: string;
output?: string;
}>;
deauthCursor?: () => Promise<{
success: boolean;
requiresManualDeauth?: boolean;
command?: string;
message?: string;
error?: string;
}>;
// Codex CLI methods
getCodexStatus?: () => Promise<{
success: boolean;
status?: string;
installed?: boolean;
method?: string;
version?: string;
path?: string;
auth?: {
authenticated: boolean;
method: string;
hasAuthFile?: boolean;
hasOAuthToken?: boolean;
hasApiKey?: boolean;
hasStoredApiKey?: boolean;
hasEnvApiKey?: boolean;
};
error?: string;
}>;
installCodex?: () => Promise<{
success: boolean;
message?: string;
error?: string;
}>;
authCodex?: () => Promise<{
success: boolean;
token?: string;
requiresManualAuth?: boolean;
terminalOpened?: boolean;
command?: string;
error?: string;
message?: string;
output?: string;
}>;
deauthCodex?: () => Promise<{
success: boolean;
requiresManualDeauth?: boolean;
command?: string;
message?: string;
error?: string;
}>;
verifyCodexAuth?: (
authMethod: 'cli' | 'api_key',
apiKey?: string
) => Promise<{
success: boolean;
authenticated: boolean;
error?: string;
}>;
// OpenCode CLI methods
getOpencodeStatus?: () => Promise<{
success: boolean;
status?: string;
installed?: boolean;
method?: string;
version?: string;
path?: string;
recommendation?: string;
installCommands?: {
macos?: string;
linux?: string;
npm?: string;
};
auth?: {
authenticated: boolean;
method: string;
hasAuthFile?: boolean;
hasOAuthToken?: boolean;
hasApiKey?: boolean;
hasStoredApiKey?: boolean;
hasEnvApiKey?: boolean;
};
error?: string;
}>;
authOpencode?: () => Promise<{
success: boolean;
token?: string;
requiresManualAuth?: boolean;
terminalOpened?: boolean;
command?: string;
message?: string;
output?: string;
}>;
deauthOpencode?: () => Promise<{
success: boolean;
requiresManualDeauth?: boolean;
command?: string;
message?: string;
error?: string;
}>;
getOpencodeModels?: (refresh?: boolean) => Promise<{
success: boolean;
models?: Array<{
id: string;
name: string;
modelString: string;
provider: string;
description: string;
supportsTools: boolean;
supportsVision: boolean;
tier: string;
default?: boolean;
}>;
count?: number;
cached?: boolean;
error?: string;
}>;
refreshOpencodeModels?: () => Promise<{
success: boolean;
models?: Array<{
id: string;
name: string;
modelString: string;
provider: string;
description: string;
supportsTools: boolean;
supportsVision: boolean;
tier: string;
default?: boolean;
}>;
count?: number;
error?: string;
}>;
getOpencodeProviders?: () => Promise<{
success: boolean;
providers?: Array<{
id: string;
name: string;
authenticated: boolean;
authMethod?: 'oauth' | 'api_key';
}>;
authenticated?: Array<{
id: string;
name: string;
authenticated: boolean;
authMethod?: 'oauth' | 'api_key';
}>;
error?: string;
}>;
clearOpencodeCache?: () => Promise<{
success: boolean;
message?: string;
error?: string;
}>;
// Gemini CLI methods
getGeminiStatus?: () => Promise<{
success: boolean;
status?: string;
installed?: boolean;
method?: string;
version?: string;
path?: string;
recommendation?: string;
installCommands?: {
macos?: string;
linux?: string;
npm?: string;
};
auth?: {
authenticated: boolean;
method: string;
hasApiKey?: boolean;
hasEnvApiKey?: boolean;
error?: string;
};
loginCommand?: string;
installCommand?: string;
error?: string;
}>;
authGemini?: () => Promise<{
success: boolean;
requiresManualAuth?: boolean;
command?: string;
message?: string;
error?: string;
}>;
deauthGemini?: () => Promise<{
success: boolean;
requiresManualDeauth?: boolean;
command?: string;
message?: string;
error?: string;
}>;
// Copilot SDK methods
getCopilotStatus?: () => Promise<{
success: boolean;
status?: string;
installed?: boolean;
method?: string;
version?: string;
path?: string;
recommendation?: string;
auth?: {
authenticated: boolean;
method: string;
login?: string;
host?: string;
error?: string;
};
loginCommand?: string;
installCommand?: string;
error?: string;
}>;
onInstallProgress?: (
callback: (progress: InstallProgressEvent) => void
) => (() => void) | undefined;
onAuthProgress?: (callback: (progress: InstallProgressEvent) => void) => (() => void) | undefined;
}
// Mock Setup API implementation
function createMockSetupAPI(): SetupAPI {
const mockStoreApiKey = async (provider: string, _apiKey: string) => {
console.log('[Mock] Storing API key for:', provider);
return { success: true };
};
return {
getClaudeStatus: async () => {
console.log('[Mock] Getting Claude status');
@@ -1466,12 +1737,18 @@ function createMockSetupAPI(): SetupAPI {
};
},
storeApiKey: async (provider: string, _apiKey: string) => {
console.log('[Mock] Storing API key for:', provider);
// In mock mode, we just pretend to store it (it's already in the app store)
return { success: true };
deauthClaude: async () => {
console.log('[Mock] Deauth Claude CLI');
return {
success: true,
requiresManualDeauth: true,
command: 'claude logout',
};
},
storeApiKey: mockStoreApiKey,
saveApiKey: mockStoreApiKey,
getApiKeys: async () => {
console.log('[Mock] Getting API keys');
return {
@@ -1521,6 +1798,187 @@ function createMockSetupAPI(): SetupAPI {
};
},
// Cursor CLI mock methods
getCursorStatus: async () => {
console.log('[Mock] Getting Cursor status');
return {
success: true,
installed: false,
version: null,
path: null,
auth: { authenticated: false, method: 'none' },
};
},
authCursor: async () => {
console.log('[Mock] Auth Cursor CLI');
return {
success: true,
requiresManualAuth: true,
command: 'cursor --login',
};
},
deauthCursor: async () => {
console.log('[Mock] Deauth Cursor CLI');
return {
success: true,
requiresManualDeauth: true,
command: 'cursor --logout',
};
},
// Codex CLI mock methods
getCodexStatus: async () => {
console.log('[Mock] Getting Codex status');
return {
success: true,
status: 'not_installed',
installed: false,
auth: { authenticated: false, method: 'none' },
};
},
installCodex: async () => {
console.log('[Mock] Installing Codex CLI');
return {
success: false,
error: 'CLI installation is only available in the Electron app.',
};
},
authCodex: async () => {
console.log('[Mock] Auth Codex CLI');
return {
success: true,
requiresManualAuth: true,
command: 'codex login',
};
},
deauthCodex: async () => {
console.log('[Mock] Deauth Codex CLI');
return {
success: true,
requiresManualDeauth: true,
command: 'codex logout',
};
},
verifyCodexAuth: async (authMethod: 'cli' | 'api_key') => {
console.log('[Mock] Verifying Codex auth with method:', authMethod);
return {
success: true,
authenticated: false,
error: 'Mock environment - authentication not available',
};
},
// OpenCode CLI mock methods
getOpencodeStatus: async () => {
console.log('[Mock] Getting OpenCode status');
return {
success: true,
status: 'not_installed',
installed: false,
auth: { authenticated: false, method: 'none' },
};
},
authOpencode: async () => {
console.log('[Mock] Auth OpenCode CLI');
return {
success: true,
requiresManualAuth: true,
command: 'opencode auth login',
};
},
deauthOpencode: async () => {
console.log('[Mock] Deauth OpenCode CLI');
return {
success: true,
requiresManualDeauth: true,
command: 'opencode auth logout',
};
},
getOpencodeModels: async () => {
console.log('[Mock] Getting OpenCode models');
return {
success: true,
models: [],
count: 0,
cached: false,
};
},
refreshOpencodeModels: async () => {
console.log('[Mock] Refreshing OpenCode models');
return {
success: true,
models: [],
count: 0,
};
},
getOpencodeProviders: async () => {
console.log('[Mock] Getting OpenCode providers');
return {
success: true,
providers: [],
authenticated: [],
};
},
clearOpencodeCache: async () => {
console.log('[Mock] Clearing OpenCode cache');
return {
success: true,
message: 'Cache cleared',
};
},
// Gemini CLI mock methods
getGeminiStatus: async () => {
console.log('[Mock] Getting Gemini status');
return {
success: true,
status: 'not_installed',
installed: false,
auth: { authenticated: false, method: 'none' },
};
},
authGemini: async () => {
console.log('[Mock] Auth Gemini CLI');
return {
success: true,
requiresManualAuth: true,
command: 'gemini auth login',
};
},
deauthGemini: async () => {
console.log('[Mock] Deauth Gemini CLI');
return {
success: true,
requiresManualDeauth: true,
command: 'gemini auth logout',
};
},
// Copilot SDK mock methods
getCopilotStatus: async () => {
console.log('[Mock] Getting Copilot status');
return {
success: true,
status: 'not_installed',
installed: false,
auth: { authenticated: false, method: 'none' },
};
},
onInstallProgress: (_callback) => {
// Mock progress events
return () => {};
@@ -1793,6 +2251,19 @@ function createMockWorktreeAPI(): WorktreeAPI {
};
},
addRemote: async (worktreePath: string, remoteName: string, remoteUrl: string) => {
console.log('[Mock] Adding remote:', { worktreePath, remoteName, remoteUrl });
return {
success: true,
result: {
remoteName,
remoteUrl,
fetched: true,
message: `Added remote '${remoteName}' (${remoteUrl})`,
},
};
},
openInEditor: async (worktreePath: string, editorCommand?: string) => {
const ANTIGRAVITY_EDITOR_COMMAND = 'antigravity';
const ANTIGRAVITY_LEGACY_COMMAND = 'agy';
@@ -2122,14 +2593,14 @@ let mockAutoModeTimeouts = new Map<string, NodeJS.Timeout>(); // Track timeouts
function createMockAutoModeAPI(): AutoModeAPI {
return {
start: async (projectPath: string, maxConcurrency?: number) => {
start: async (projectPath: string, branchName?: string | null, maxConcurrency?: number) => {
if (mockAutoModeRunning) {
return { success: false, error: 'Auto mode is already running' };
}
mockAutoModeRunning = true;
console.log(
`[Mock] Auto mode started with maxConcurrency: ${maxConcurrency || DEFAULT_MAX_CONCURRENCY}`
`[Mock] Auto mode started with branchName: ${branchName}, maxConcurrency: ${maxConcurrency || DEFAULT_MAX_CONCURRENCY}`
);
const featureId = 'auto-mode-0';
mockRunningFeatures.add(featureId);
@@ -2140,7 +2611,7 @@ function createMockAutoModeAPI(): AutoModeAPI {
return { success: true };
},
stop: async (_projectPath: string) => {
stop: async (_projectPath: string, _branchName?: string | null) => {
mockAutoModeRunning = false;
const runningCount = mockRunningFeatures.size;
mockRunningFeatures.clear();

View File

@@ -41,9 +41,9 @@ import type {
Notification,
} from '@automaker/types';
import type { Message, SessionListItem } from '@/types/electron';
import type { Feature, ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store';
import type { ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store';
import type { WorktreeAPI, GitAPI, ModelDefinition, ProviderStatus } from '@/types/electron';
import type { ModelId, ThinkingLevel, ReasoningEffort } from '@automaker/types';
import type { ModelId, ThinkingLevel, ReasoningEffort, Feature } from '@automaker/types';
import { getGlobalFileBrowser } from '@/contexts/file-browser-context';
const logger = createLogger('HttpClient');
@@ -161,7 +161,7 @@ const getServerUrl = (): string => {
// In web mode (not Electron), use relative URL to leverage Vite proxy
// This avoids CORS issues since requests appear same-origin
if (!window.electron) {
if (!window.Electron) {
return '';
}
}
@@ -1723,12 +1723,16 @@ export class HttpApiClient implements ElectronAPI {
error?: string;
}> => this.get('/api/setup/copilot-status'),
onInstallProgress: (callback: (progress: unknown) => void) => {
return this.subscribeToEvent('agent:stream', callback);
onInstallProgress: (
callback: (progress: { cli?: string; data?: string; type?: string }) => void
) => {
return this.subscribeToEvent('agent:stream', callback as EventCallback);
},
onAuthProgress: (callback: (progress: unknown) => void) => {
return this.subscribeToEvent('agent:stream', callback);
onAuthProgress: (
callback: (progress: { cli?: string; data?: string; type?: string }) => void
) => {
return this.subscribeToEvent('agent:stream', callback as EventCallback);
},
};

View File

@@ -17,6 +17,7 @@ import type {
ModelAlias,
PlanningMode,
ThinkingLevel,
ReasoningEffort,
ModelProvider,
CursorModelId,
CodexModelId,
@@ -63,6 +64,7 @@ export type {
ModelAlias,
PlanningMode,
ThinkingLevel,
ReasoningEffort,
ModelProvider,
ServerLogLevel,
FeatureTextFilePath,
@@ -460,7 +462,17 @@ export type ClaudeModel = 'opus' | 'sonnet' | 'haiku';
export interface Feature extends Omit<
BaseFeature,
'steps' | 'imagePaths' | 'textFilePaths' | 'status' | 'planSpec'
| 'steps'
| 'imagePaths'
| 'textFilePaths'
| 'status'
| 'planSpec'
| 'dependencies'
| 'model'
| 'branchName'
| 'thinkingLevel'
| 'reasoningEffort'
| 'summary'
> {
id: string;
title?: string;
@@ -475,6 +487,12 @@ export interface Feature extends Omit<
justFinishedAt?: string; // UI-specific: ISO timestamp when agent just finished
prUrl?: string; // UI-specific: Pull request URL
planSpec?: PlanSpec; // Explicit planSpec type to override BaseFeature's index signature
dependencies?: string[]; // Explicit type to override BaseFeature's index signature
model?: string; // Explicit type to override BaseFeature's index signature
branchName?: string; // Explicit type to override BaseFeature's index signature
thinkingLevel?: ThinkingLevel; // Explicit type to override BaseFeature's index signature
reasoningEffort?: ReasoningEffort; // Explicit type to override BaseFeature's index signature
summary?: string; // Explicit type to override BaseFeature's index signature
}
// ParsedTask and PlanSpec types are now imported from @automaker/types
@@ -665,6 +683,8 @@ export interface AppState {
path: string;
branch: string;
isMain: boolean;
isCurrent: boolean;
hasWorktree: boolean;
hasChanges?: boolean;
changedFilesCount?: number;
}>
@@ -1156,6 +1176,8 @@ export interface AppActions {
path: string;
branch: string;
isMain: boolean;
isCurrent: boolean;
hasWorktree: boolean;
hasChanges?: boolean;
changedFilesCount?: number;
}>
@@ -1165,6 +1187,8 @@ export interface AppActions {
path: string;
branch: string;
isMain: boolean;
isCurrent: boolean;
hasWorktree: boolean;
hasChanges?: boolean;
changedFilesCount?: number;
}>;
@@ -4109,7 +4133,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
try {
const api = getElectronAPI();
if (!api.setup) {
if (!api.setup?.getOpencodeModels) {
throw new Error('Setup API not available');
}
@@ -4120,7 +4144,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
}
set({
dynamicOpencodeModels: result.models || [],
dynamicOpencodeModels: (result.models || []) as ModelDefinition[],
opencodeModelsLastFetched: Date.now(),
opencodeModelsLoading: false,
opencodeModelsError: null,

View File

@@ -1416,10 +1416,15 @@ export interface ModelDefinition {
id: string;
name: string;
modelString: string;
provider: 'claude';
description?: string;
tier?: 'basic' | 'standard' | 'premium';
provider: string;
description: string;
contextWindow?: number;
maxOutputTokens?: number;
supportsVision?: boolean;
supportsTools?: boolean;
tier?: 'basic' | 'standard' | 'premium' | string;
default?: boolean;
hasReasoning?: boolean;
}
// Provider status type