mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ?? ''),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -27,7 +27,7 @@ export function SecurityWarningDialog({
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
serverType,
|
||||
_serverName,
|
||||
serverName: _serverName,
|
||||
command,
|
||||
args,
|
||||
url,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user