mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-17 22:13:08 +00:00
fix: Remove unused vars and improve type safety. Improve task recovery
This commit is contained in:
@@ -122,83 +122,130 @@ export const CardActions = memo(function CardActions({
|
||||
(feature.status === 'in_progress' ||
|
||||
(typeof feature.status === 'string' && feature.status.startsWith('pipeline_'))) && (
|
||||
<>
|
||||
{/* Approve Plan button - shows when plan is generated and waiting for approval */}
|
||||
{feature.planSpec?.status === 'generated' && onApprovePlan && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-purple-600 hover:bg-purple-700 text-white animate-pulse"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onApprovePlan();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`approve-plan-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
Approve Plan
|
||||
</Button>
|
||||
)}
|
||||
{feature.skipTests && onManualVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onManualVerify();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`manual-verify-${feature.id}`}
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
Verify
|
||||
</Button>
|
||||
) : onResume ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onResume();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`resume-feature-${feature.id}`}
|
||||
>
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
Resume
|
||||
</Button>
|
||||
) : onVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onVerify();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`verify-feature-${feature.id}`}
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
Verify
|
||||
</Button>
|
||||
) : null}
|
||||
{onViewOutput && !feature.skipTests && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-7 text-[11px] px-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewOutput();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`view-output-inprogress-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
</Button>
|
||||
{/* When feature is in_progress with no error and onForceStop is available,
|
||||
it means the agent is starting/running but hasn't been added to runningAutoTasks yet.
|
||||
Show Stop button instead of Verify/Resume to avoid confusing UI during this race window. */}
|
||||
{!feature.error && onForceStop ? (
|
||||
<>
|
||||
{onViewOutput && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewOutput();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`view-output-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1 shrink-0" />
|
||||
<span className="truncate">Logs</span>
|
||||
{shortcutKey && (
|
||||
<span
|
||||
className="ml-1.5 px-1 py-0.5 text-[9px] font-mono rounded bg-foreground/10"
|
||||
data-testid={`shortcut-key-${feature.id}`}
|
||||
>
|
||||
{shortcutKey}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-7 text-[11px] px-2 shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onForceStop();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`force-stop-${feature.id}`}
|
||||
>
|
||||
<StopCircle className="w-3 h-3" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Approve Plan button - shows when plan is generated and waiting for approval */}
|
||||
{feature.planSpec?.status === 'generated' && onApprovePlan && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-purple-600 hover:bg-purple-700 text-white animate-pulse"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onApprovePlan();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`approve-plan-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
Approve Plan
|
||||
</Button>
|
||||
)}
|
||||
{feature.skipTests && onManualVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onManualVerify();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`manual-verify-${feature.id}`}
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
Verify
|
||||
</Button>
|
||||
) : onResume ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onResume();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`resume-feature-${feature.id}`}
|
||||
>
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
Resume
|
||||
</Button>
|
||||
) : onVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onVerify();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`verify-feature-${feature.id}`}
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
Verify
|
||||
</Button>
|
||||
) : null}
|
||||
{onViewOutput && !feature.skipTests && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-7 text-[11px] px-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewOutput();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`view-output-inprogress-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -112,9 +112,15 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
currentProject: state.currentProject,
|
||||
}))
|
||||
);
|
||||
// A card in waiting_approval should not display as "actively running" even if
|
||||
// it's still in the runningAutoTasks list. The waiting_approval UI takes precedence.
|
||||
const isActivelyRunning = !!isCurrentAutoTask && feature.status !== 'waiting_approval';
|
||||
// A card should only display as "actively running" if it's both in the
|
||||
// runningAutoTasks list AND in an execution-compatible status. Cards in resting
|
||||
// states (backlog, ready, waiting_approval, verified, completed) should never
|
||||
// show running controls, even if they appear in runningAutoTasks due to stale
|
||||
// state (e.g., after a server restart that reconciled features back to backlog).
|
||||
const isInExecutionState =
|
||||
feature.status === 'in_progress' ||
|
||||
(typeof feature.status === 'string' && feature.status.startsWith('pipeline_'));
|
||||
const isActivelyRunning = !!isCurrentAutoTask && isInExecutionState;
|
||||
const [isLifted, setIsLifted] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
||||
@@ -209,9 +209,15 @@ export const ListRow = memo(function ListRow({
|
||||
blockingDependencies = [],
|
||||
className,
|
||||
}: ListRowProps) {
|
||||
// A card in waiting_approval should not display as "actively running" even if
|
||||
// it's still in the runningAutoTasks list. The waiting_approval UI takes precedence.
|
||||
const isActivelyRunning = isCurrentAutoTask && feature.status !== 'waiting_approval';
|
||||
// A row should only display as "actively running" if it's both in the
|
||||
// runningAutoTasks list AND in an execution-compatible status. Features in resting
|
||||
// states (backlog, ready, waiting_approval, verified, completed) should never
|
||||
// show running controls, even if they appear in runningAutoTasks due to stale
|
||||
// state (e.g., after a server restart that reconciled features back to backlog).
|
||||
const isInExecutionState =
|
||||
feature.status === 'in_progress' ||
|
||||
(typeof feature.status === 'string' && feature.status.startsWith('pipeline_'));
|
||||
const isActivelyRunning = isCurrentAutoTask && isInExecutionState;
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
|
||||
@@ -143,6 +143,17 @@ function getPrimaryAction(
|
||||
};
|
||||
}
|
||||
|
||||
// In progress with no error - agent is starting/running but not yet in runningAutoTasks.
|
||||
// Show Stop button immediately instead of Verify/Resume during this race window.
|
||||
if (feature.status === 'in_progress' && !feature.error && handlers.onForceStop) {
|
||||
return {
|
||||
icon: StopCircle,
|
||||
label: 'Stop',
|
||||
onClick: handlers.onForceStop,
|
||||
variant: 'destructive',
|
||||
};
|
||||
}
|
||||
|
||||
// In progress with plan approval pending
|
||||
if (
|
||||
feature.status === 'in_progress' &&
|
||||
@@ -446,81 +457,126 @@ export const RowActions = memo(function RowActions({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* In Progress actions */}
|
||||
{!isCurrentAutoTask && feature.status === 'in_progress' && (
|
||||
<>
|
||||
{handlers.onViewOutput && (
|
||||
<MenuItem
|
||||
icon={FileText}
|
||||
label="View Logs"
|
||||
onClick={withClose(handlers.onViewOutput)}
|
||||
/>
|
||||
)}
|
||||
{feature.planSpec?.status === 'generated' && handlers.onApprovePlan && (
|
||||
<MenuItem
|
||||
icon={FileText}
|
||||
label="Approve Plan"
|
||||
onClick={withClose(handlers.onApprovePlan)}
|
||||
variant="warning"
|
||||
/>
|
||||
)}
|
||||
{feature.skipTests && handlers.onManualVerify ? (
|
||||
<MenuItem
|
||||
icon={CheckCircle2}
|
||||
label="Verify"
|
||||
onClick={withClose(handlers.onManualVerify)}
|
||||
variant="success"
|
||||
/>
|
||||
) : handlers.onResume ? (
|
||||
<MenuItem
|
||||
icon={RotateCcw}
|
||||
label="Resume"
|
||||
onClick={withClose(handlers.onResume)}
|
||||
variant="success"
|
||||
/>
|
||||
) : null}
|
||||
<DropdownMenuSeparator />
|
||||
<MenuItem icon={Edit} label="Edit" onClick={withClose(handlers.onEdit)} />
|
||||
{handlers.onSpawnTask && (
|
||||
<MenuItem
|
||||
icon={GitFork}
|
||||
label="Spawn Sub-Task"
|
||||
onClick={withClose(handlers.onSpawnTask)}
|
||||
/>
|
||||
)}
|
||||
{handlers.onDuplicate && (
|
||||
<DropdownMenuSub>
|
||||
<div className="flex items-center">
|
||||
<DropdownMenuItem
|
||||
onClick={withClose(handlers.onDuplicate)}
|
||||
className="flex-1 pr-0 rounded-r-none"
|
||||
>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Duplicate
|
||||
</DropdownMenuItem>
|
||||
{/* In Progress actions - starting/running (no error, force stop available) - mirrors running task actions */}
|
||||
{!isCurrentAutoTask &&
|
||||
feature.status === 'in_progress' &&
|
||||
!feature.error &&
|
||||
handlers.onForceStop && (
|
||||
<>
|
||||
{handlers.onViewOutput && (
|
||||
<MenuItem
|
||||
icon={FileText}
|
||||
label="View Logs"
|
||||
onClick={withClose(handlers.onViewOutput)}
|
||||
/>
|
||||
)}
|
||||
{feature.planSpec?.status === 'generated' && handlers.onApprovePlan && (
|
||||
<MenuItem
|
||||
icon={FileText}
|
||||
label="Approve Plan"
|
||||
onClick={withClose(handlers.onApprovePlan)}
|
||||
variant="warning"
|
||||
/>
|
||||
)}
|
||||
<MenuItem icon={Edit} label="Edit" onClick={withClose(handlers.onEdit)} />
|
||||
{handlers.onSpawnTask && (
|
||||
<MenuItem
|
||||
icon={GitFork}
|
||||
label="Spawn Sub-Task"
|
||||
onClick={withClose(handlers.onSpawnTask)}
|
||||
/>
|
||||
)}
|
||||
{handlers.onForceStop && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<MenuItem
|
||||
icon={StopCircle}
|
||||
label="Force Stop"
|
||||
onClick={withClose(handlers.onForceStop)}
|
||||
variant="destructive"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* In Progress actions - interrupted/error state */}
|
||||
{!isCurrentAutoTask &&
|
||||
feature.status === 'in_progress' &&
|
||||
!(!feature.error && handlers.onForceStop) && (
|
||||
<>
|
||||
{handlers.onViewOutput && (
|
||||
<MenuItem
|
||||
icon={FileText}
|
||||
label="View Logs"
|
||||
onClick={withClose(handlers.onViewOutput)}
|
||||
/>
|
||||
)}
|
||||
{feature.planSpec?.status === 'generated' && handlers.onApprovePlan && (
|
||||
<MenuItem
|
||||
icon={FileText}
|
||||
label="Approve Plan"
|
||||
onClick={withClose(handlers.onApprovePlan)}
|
||||
variant="warning"
|
||||
/>
|
||||
)}
|
||||
{feature.skipTests && handlers.onManualVerify ? (
|
||||
<MenuItem
|
||||
icon={CheckCircle2}
|
||||
label="Verify"
|
||||
onClick={withClose(handlers.onManualVerify)}
|
||||
variant="success"
|
||||
/>
|
||||
) : handlers.onResume ? (
|
||||
<MenuItem
|
||||
icon={RotateCcw}
|
||||
label="Resume"
|
||||
onClick={withClose(handlers.onResume)}
|
||||
variant="success"
|
||||
/>
|
||||
) : null}
|
||||
<DropdownMenuSeparator />
|
||||
<MenuItem icon={Edit} label="Edit" onClick={withClose(handlers.onEdit)} />
|
||||
{handlers.onSpawnTask && (
|
||||
<MenuItem
|
||||
icon={GitFork}
|
||||
label="Spawn Sub-Task"
|
||||
onClick={withClose(handlers.onSpawnTask)}
|
||||
/>
|
||||
)}
|
||||
{handlers.onDuplicate && (
|
||||
<DropdownMenuSub>
|
||||
<div className="flex items-center">
|
||||
<DropdownMenuItem
|
||||
onClick={withClose(handlers.onDuplicate)}
|
||||
className="flex-1 pr-0 rounded-r-none"
|
||||
>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Duplicate
|
||||
</DropdownMenuItem>
|
||||
{handlers.onDuplicateAsChild && (
|
||||
<DropdownMenuSubTrigger className="px-1 rounded-l-none border-l border-border/30 h-8" />
|
||||
)}
|
||||
</div>
|
||||
{handlers.onDuplicateAsChild && (
|
||||
<DropdownMenuSubTrigger className="px-1 rounded-l-none border-l border-border/30 h-8" />
|
||||
<DropdownMenuSubContent>
|
||||
<MenuItem
|
||||
icon={GitFork}
|
||||
label="Duplicate as Child"
|
||||
onClick={withClose(handlers.onDuplicateAsChild)}
|
||||
/>
|
||||
</DropdownMenuSubContent>
|
||||
)}
|
||||
</div>
|
||||
{handlers.onDuplicateAsChild && (
|
||||
<DropdownMenuSubContent>
|
||||
<MenuItem
|
||||
icon={GitFork}
|
||||
label="Duplicate as Child"
|
||||
onClick={withClose(handlers.onDuplicateAsChild)}
|
||||
/>
|
||||
</DropdownMenuSubContent>
|
||||
)}
|
||||
</DropdownMenuSub>
|
||||
)}
|
||||
<MenuItem
|
||||
icon={Trash2}
|
||||
label="Delete"
|
||||
onClick={withClose(handlers.onDelete)}
|
||||
variant="destructive"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuSub>
|
||||
)}
|
||||
<MenuItem
|
||||
icon={Trash2}
|
||||
label="Delete"
|
||||
onClick={withClose(handlers.onDelete)}
|
||||
variant="destructive"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Waiting Approval actions */}
|
||||
{!isCurrentAutoTask && feature.status === 'waiting_approval' && (
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Spinner } from '@/components/ui/spinner';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { AnthropicIcon, OpenAIIcon, ZaiIcon, GeminiIcon } from '@/components/ui/provider-icon';
|
||||
import type { GeminiUsage } from '@/store/app-store';
|
||||
import { getExpectedWeeklyPacePercentage, getPaceStatusLabel } from '@/store/utils/usage-utils';
|
||||
|
||||
interface MobileUsageBarProps {
|
||||
@@ -42,6 +41,11 @@ function formatResetTime(unixTimestamp: number, isMilliseconds = false): string
|
||||
const now = new Date();
|
||||
const diff = date.getTime() - now.getTime();
|
||||
|
||||
// Handle past timestamps (negative diff)
|
||||
if (diff <= 0) {
|
||||
return 'Resetting soon';
|
||||
}
|
||||
|
||||
if (diff < 3600000) {
|
||||
const mins = Math.ceil(diff / 60000);
|
||||
return `Resets in ${mins}m`;
|
||||
@@ -184,12 +188,11 @@ export function MobileUsageBar({
|
||||
const { claudeUsage, claudeUsageLastUpdated, setClaudeUsage } = useAppStore();
|
||||
const { codexUsage, codexUsageLastUpdated, setCodexUsage } = useAppStore();
|
||||
const { zaiUsage, zaiUsageLastUpdated, setZaiUsage } = useAppStore();
|
||||
const { geminiUsage, geminiUsageLastUpdated, setGeminiUsage } = useAppStore();
|
||||
const [isClaudeLoading, setIsClaudeLoading] = useState(false);
|
||||
const [isCodexLoading, setIsCodexLoading] = useState(false);
|
||||
const [isZaiLoading, setIsZaiLoading] = useState(false);
|
||||
const [isGeminiLoading, setIsGeminiLoading] = useState(false);
|
||||
const [geminiUsage, setGeminiUsage] = useState<GeminiUsage | null>(null);
|
||||
const [geminiUsageLastUpdated, setGeminiUsageLastUpdated] = useState<number | null>(null);
|
||||
|
||||
// Check if data is stale (older than 2 minutes)
|
||||
const isClaudeStale =
|
||||
@@ -254,15 +257,14 @@ export function MobileUsageBar({
|
||||
if (!api.gemini) return;
|
||||
const data = await api.gemini.getUsage();
|
||||
if (!('error' in data)) {
|
||||
setGeminiUsage(data);
|
||||
setGeminiUsageLastUpdated(Date.now());
|
||||
setGeminiUsage(data, Date.now());
|
||||
}
|
||||
} catch {
|
||||
// Silently fail - usage display is optional
|
||||
} finally {
|
||||
setIsGeminiLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [setGeminiUsage]);
|
||||
|
||||
const getCodexWindowLabel = (durationMins: number) => {
|
||||
if (durationMins < 60) return `${durationMins}m Window`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @ts-nocheck - API key management state with validation and persistence
|
||||
// API key management state with validation and persistence
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
@@ -23,20 +23,44 @@ interface ApiKeyStatus {
|
||||
hasZaiKey: boolean;
|
||||
}
|
||||
|
||||
/** Shape of the configure API response */
|
||||
interface ConfigureResponse {
|
||||
success?: boolean;
|
||||
isAvailable?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Shape of a verify API response */
|
||||
interface VerifyResponse {
|
||||
success?: boolean;
|
||||
authenticated?: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Shape of an API key status response from the env check */
|
||||
interface ApiKeyStatusResponse {
|
||||
success: boolean;
|
||||
hasAnthropicKey: boolean;
|
||||
hasGoogleKey: boolean;
|
||||
hasOpenaiKey: boolean;
|
||||
hasZaiKey?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing API key state and operations
|
||||
* Handles input values, visibility toggles, connection testing, and saving
|
||||
*/
|
||||
export function useApiKeyManagement() {
|
||||
const { apiKeys, setApiKeys } = useAppStore();
|
||||
const { setZaiAuthStatus } = useSetupStore();
|
||||
const { setZaiAuthStatus, zaiAuthStatus } = useSetupStore();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// API key values
|
||||
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
|
||||
const [googleKey, setGoogleKey] = useState(apiKeys.google);
|
||||
const [openaiKey, setOpenaiKey] = useState(apiKeys.openai);
|
||||
const [zaiKey, setZaiKey] = useState(apiKeys.zai);
|
||||
const [anthropicKey, setAnthropicKey] = useState<string>(apiKeys.anthropic);
|
||||
const [googleKey, setGoogleKey] = useState<string>(apiKeys.google);
|
||||
const [openaiKey, setOpenaiKey] = useState<string>(apiKeys.openai);
|
||||
const [zaiKey, setZaiKey] = useState<string>(apiKeys.zai);
|
||||
|
||||
// Visibility toggles
|
||||
const [showAnthropicKey, setShowAnthropicKey] = useState(false);
|
||||
@@ -74,7 +98,7 @@ export function useApiKeyManagement() {
|
||||
const api = getElectronAPI();
|
||||
if (api?.setup?.getApiKeys) {
|
||||
try {
|
||||
const status = await api.setup.getApiKeys();
|
||||
const status: ApiKeyStatusResponse = await api.setup.getApiKeys();
|
||||
if (status.success) {
|
||||
setApiKeyStatus({
|
||||
hasAnthropicKey: status.hasAnthropicKey,
|
||||
@@ -92,7 +116,7 @@ export function useApiKeyManagement() {
|
||||
}, []);
|
||||
|
||||
// Test Anthropic/Claude connection
|
||||
const handleTestAnthropicConnection = async () => {
|
||||
const handleTestAnthropicConnection = async (): Promise<void> => {
|
||||
// Validate input first
|
||||
if (!anthropicKey || anthropicKey.trim().length === 0) {
|
||||
setTestResult({
|
||||
@@ -106,7 +130,7 @@ export function useApiKeyManagement() {
|
||||
setTestResult(null);
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const api = getHttpApiClient();
|
||||
// Pass the current input value to test unsaved keys
|
||||
const data = await api.setup.verifyClaudeAuth('api_key', anthropicKey);
|
||||
|
||||
@@ -133,7 +157,7 @@ export function useApiKeyManagement() {
|
||||
|
||||
// Test Google/Gemini connection
|
||||
// TODO: Add backend endpoint for Gemini API key verification
|
||||
const handleTestGeminiConnection = async () => {
|
||||
const handleTestGeminiConnection = async (): Promise<void> => {
|
||||
setTestingGeminiConnection(true);
|
||||
setGeminiTestResult(null);
|
||||
|
||||
@@ -157,12 +181,12 @@ export function useApiKeyManagement() {
|
||||
};
|
||||
|
||||
// Test OpenAI/Codex connection
|
||||
const handleTestOpenaiConnection = async () => {
|
||||
const handleTestOpenaiConnection = async (): Promise<void> => {
|
||||
setTestingOpenaiConnection(true);
|
||||
setOpenaiTestResult(null);
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const api = getHttpApiClient();
|
||||
const data = await api.setup.verifyCodexAuth('api_key', openaiKey);
|
||||
|
||||
if (data.success && data.authenticated) {
|
||||
@@ -187,7 +211,7 @@ export function useApiKeyManagement() {
|
||||
};
|
||||
|
||||
// Test z.ai connection
|
||||
const handleTestZaiConnection = async () => {
|
||||
const handleTestZaiConnection = async (): Promise<void> => {
|
||||
setTestingZaiConnection(true);
|
||||
setZaiTestResult(null);
|
||||
|
||||
@@ -204,7 +228,7 @@ export function useApiKeyManagement() {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
// Use the verify endpoint to test the key without storing it
|
||||
const response = await api.zai?.verify(zaiKey);
|
||||
const response: VerifyResponse | undefined = await api.zai?.verify(zaiKey);
|
||||
|
||||
if (response?.success && response?.authenticated) {
|
||||
setZaiTestResult({
|
||||
@@ -228,42 +252,70 @@ export function useApiKeyManagement() {
|
||||
};
|
||||
|
||||
// Save API keys
|
||||
const handleSave = async () => {
|
||||
setApiKeys({
|
||||
anthropic: anthropicKey,
|
||||
google: googleKey,
|
||||
openai: openaiKey,
|
||||
zai: zaiKey,
|
||||
});
|
||||
|
||||
const handleSave = async (): Promise<void> => {
|
||||
// Configure z.ai service on the server with the new key
|
||||
if (zaiKey && zaiKey.trim().length > 0) {
|
||||
try {
|
||||
const api = getHttpApiClient();
|
||||
const result = await api.zai.configure(zaiKey.trim());
|
||||
const result: ConfigureResponse = await api.zai.configure(zaiKey.trim());
|
||||
|
||||
if (result.success) {
|
||||
// Only persist to local store after server confirms success
|
||||
setApiKeys({
|
||||
anthropic: anthropicKey,
|
||||
google: googleKey,
|
||||
openai: openaiKey,
|
||||
zai: zaiKey,
|
||||
});
|
||||
|
||||
// Preserve the existing hasEnvApiKey flag from current auth status
|
||||
const currentHasEnvApiKey = zaiAuthStatus?.hasEnvApiKey ?? false;
|
||||
|
||||
if (result.success || result.isAvailable) {
|
||||
// Update z.ai auth status in the store
|
||||
setZaiAuthStatus({
|
||||
authenticated: true,
|
||||
method: 'api_key' as ZaiAuthMethod,
|
||||
hasApiKey: true,
|
||||
hasEnvApiKey: false,
|
||||
hasEnvApiKey: currentHasEnvApiKey,
|
||||
});
|
||||
// Invalidate the z.ai usage query so it refetches with the new key
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.usage.zai() });
|
||||
logger.info('z.ai API key configured successfully');
|
||||
} else {
|
||||
// Server config failed - still save other keys but log the issue
|
||||
logger.error('z.ai API key configuration failed on server');
|
||||
setApiKeys({
|
||||
anthropic: anthropicKey,
|
||||
google: googleKey,
|
||||
openai: openaiKey,
|
||||
zai: zaiKey,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to configure z.ai API key:', error);
|
||||
// Still save other keys even if z.ai config fails
|
||||
setApiKeys({
|
||||
anthropic: anthropicKey,
|
||||
google: googleKey,
|
||||
openai: openaiKey,
|
||||
zai: zaiKey,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Save keys (z.ai key is empty/removed)
|
||||
setApiKeys({
|
||||
anthropic: anthropicKey,
|
||||
google: googleKey,
|
||||
openai: openaiKey,
|
||||
zai: zaiKey,
|
||||
});
|
||||
|
||||
// Clear z.ai auth status if key is removed
|
||||
setZaiAuthStatus({
|
||||
authenticated: false,
|
||||
method: 'none' as ZaiAuthMethod,
|
||||
hasApiKey: false,
|
||||
hasEnvApiKey: false,
|
||||
hasEnvApiKey: zaiAuthStatus?.hasEnvApiKey ?? false,
|
||||
});
|
||||
// Invalidate the query to clear any cached data
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.usage.zai() });
|
||||
|
||||
Reference in New Issue
Block a user