mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-17 10:03:08 +00:00
fix: Remove unused vars and improve type safety. Improve task recovery
This commit is contained in:
@@ -340,7 +340,7 @@ export function UsagePopover() {
|
||||
// Calculate max percentage for header button
|
||||
const claudeSessionPercentage = claudeUsage?.sessionPercentage || 0;
|
||||
|
||||
const codexMaxPercentage = codexUsage?.rateLimits
|
||||
const _codexMaxPercentage = codexUsage?.rateLimits
|
||||
? Math.max(
|
||||
codexUsage.rateLimits.primary?.usedPercent || 0,
|
||||
codexUsage.rateLimits.secondary?.usedPercent || 0
|
||||
@@ -369,7 +369,7 @@ export function UsagePopover() {
|
||||
codexSecondaryWindowMinutes && codexPrimaryWindowMinutes
|
||||
? Math.min(codexPrimaryWindowMinutes, codexSecondaryWindowMinutes)
|
||||
: (codexSecondaryWindowMinutes ?? codexPrimaryWindowMinutes);
|
||||
const codexWindowLabel = codexWindowMinutes
|
||||
const _codexWindowLabel = codexWindowMinutes
|
||||
? getCodexWindowLabel(codexWindowMinutes).title
|
||||
: 'Window';
|
||||
const codexWindowUsage =
|
||||
@@ -408,16 +408,16 @@ export function UsagePopover() {
|
||||
}
|
||||
: null;
|
||||
|
||||
const statusColor = getStatusInfo(indicatorInfo.percentage).color;
|
||||
const ProviderIcon = indicatorInfo.icon;
|
||||
const statusColor = indicatorInfo ? getStatusInfo(indicatorInfo.percentage).color : '';
|
||||
const ProviderIcon = indicatorInfo?.icon;
|
||||
|
||||
const trigger = (
|
||||
<Button variant="ghost" size="sm" className="h-9 gap-2 bg-secondary border border-border px-3">
|
||||
{(claudeUsage || codexUsage || zaiUsage || geminiUsage) && (
|
||||
{(claudeUsage || codexUsage || zaiUsage || geminiUsage) && ProviderIcon && (
|
||||
<ProviderIcon className={cn('w-4 h-4', statusColor)} />
|
||||
)}
|
||||
<span className="text-sm font-medium">Usage</span>
|
||||
{(claudeUsage || codexUsage || zaiUsage || geminiUsage) && (
|
||||
{(claudeUsage || codexUsage || zaiUsage || geminiUsage) && indicatorInfo && (
|
||||
<div
|
||||
title={indicatorInfo.title}
|
||||
className={cn(
|
||||
|
||||
@@ -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() });
|
||||
|
||||
@@ -172,7 +172,10 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
(backendIsRunning &&
|
||||
Array.isArray(backendRunningFeatures) &&
|
||||
backendRunningFeatures.length > 0 &&
|
||||
!arraysEqual(backendRunningFeatures, runningAutoTasks));
|
||||
!arraysEqual(backendRunningFeatures, runningAutoTasks)) ||
|
||||
// Also sync when UI has stale running tasks but backend has none
|
||||
// (handles server restart where features were reconciled to backlog/ready)
|
||||
(!backendIsRunning && runningAutoTasks.length > 0 && backendRunningFeatures.length === 0);
|
||||
|
||||
if (needsSync) {
|
||||
const worktreeDesc = branchName ? `worktree ${branchName}` : 'main worktree';
|
||||
|
||||
@@ -108,22 +108,41 @@ export function useProviderAuthInit() {
|
||||
try {
|
||||
const result = await api.zai.getStatus();
|
||||
if (result.success || result.available !== undefined) {
|
||||
const available = !!result.available;
|
||||
const hasApiKey = !!(result.hasApiKey ?? result.available);
|
||||
const hasEnvApiKey = !!(result.hasEnvApiKey ?? false);
|
||||
|
||||
let method: ZaiAuthMethod = 'none';
|
||||
if (result.hasEnvApiKey) {
|
||||
if (hasEnvApiKey) {
|
||||
method = 'api_key_env';
|
||||
} else if (result.hasApiKey || result.available) {
|
||||
} else if (hasApiKey || available) {
|
||||
method = 'api_key';
|
||||
}
|
||||
|
||||
setZaiAuthStatus({
|
||||
authenticated: result.available,
|
||||
authenticated: available,
|
||||
method,
|
||||
hasApiKey: result.hasApiKey ?? result.available,
|
||||
hasEnvApiKey: result.hasEnvApiKey ?? false,
|
||||
hasApiKey,
|
||||
hasEnvApiKey,
|
||||
});
|
||||
} else {
|
||||
// Non-success path - set default unauthenticated status
|
||||
setZaiAuthStatus({
|
||||
authenticated: false,
|
||||
method: 'none',
|
||||
hasApiKey: false,
|
||||
hasEnvApiKey: false,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to init z.ai auth status:', error);
|
||||
// Set default status on error to prevent stale state
|
||||
setZaiAuthStatus({
|
||||
authenticated: false,
|
||||
method: 'none',
|
||||
hasApiKey: false,
|
||||
hasEnvApiKey: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Gemini Auth Status
|
||||
@@ -134,7 +153,7 @@ export function useProviderAuthInit() {
|
||||
setGeminiCliStatus({
|
||||
installed: result.installed ?? false,
|
||||
version: result.version,
|
||||
path: result.status,
|
||||
path: result.path,
|
||||
});
|
||||
|
||||
// Set Auth status - always set a status to mark initialization as complete
|
||||
|
||||
@@ -41,7 +41,12 @@ import type {
|
||||
Notification,
|
||||
} from '@automaker/types';
|
||||
import type { Message, SessionListItem } from '@/types/electron';
|
||||
import type { ClaudeUsageResponse, CodexUsageResponse, GeminiUsage } from '@/store/app-store';
|
||||
import type {
|
||||
ClaudeUsageResponse,
|
||||
CodexUsageResponse,
|
||||
GeminiUsage,
|
||||
ZaiUsageResponse,
|
||||
} from '@/store/app-store';
|
||||
import type { WorktreeAPI, GitAPI, ModelDefinition, ProviderStatus } from '@/types/electron';
|
||||
import type { ModelId, ThinkingLevel, ReasoningEffort, Feature } from '@automaker/types';
|
||||
import { getGlobalFileBrowser } from '@/contexts/file-browser-context';
|
||||
@@ -1748,35 +1753,7 @@ export class HttpApiClient implements ElectronAPI {
|
||||
error?: string;
|
||||
}> => this.get('/api/zai/status'),
|
||||
|
||||
getUsage: (): Promise<{
|
||||
quotaLimits?: {
|
||||
tokens?: {
|
||||
limitType: string;
|
||||
limit: number;
|
||||
used: number;
|
||||
remaining: number;
|
||||
usedPercent: number;
|
||||
nextResetTime: number;
|
||||
};
|
||||
time?: {
|
||||
limitType: string;
|
||||
limit: number;
|
||||
used: number;
|
||||
remaining: number;
|
||||
usedPercent: number;
|
||||
nextResetTime: number;
|
||||
};
|
||||
planType: string;
|
||||
} | null;
|
||||
usageDetails?: Array<{
|
||||
modelId: string;
|
||||
used: number;
|
||||
limit: number;
|
||||
}>;
|
||||
lastUpdated: string;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}> => this.get('/api/zai/usage'),
|
||||
getUsage: (): Promise<ZaiUsageResponse> => this.get('/api/zai/usage'),
|
||||
|
||||
configure: (
|
||||
apiToken?: string,
|
||||
|
||||
@@ -321,6 +321,8 @@ const initialState: AppState = {
|
||||
codexUsageLastUpdated: null,
|
||||
zaiUsage: null,
|
||||
zaiUsageLastUpdated: null,
|
||||
geminiUsage: null,
|
||||
geminiUsageLastUpdated: null,
|
||||
codexModels: [],
|
||||
codexModelsLoading: false,
|
||||
codexModelsError: null,
|
||||
@@ -2410,6 +2412,13 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
// z.ai Usage Tracking actions
|
||||
setZaiUsage: (usage) => set({ zaiUsage: usage, zaiUsageLastUpdated: usage ? Date.now() : null }),
|
||||
|
||||
// Gemini Usage Tracking actions
|
||||
setGeminiUsage: (usage, lastUpdated) =>
|
||||
set({
|
||||
geminiUsage: usage,
|
||||
geminiUsageLastUpdated: lastUpdated ?? (usage ? Date.now() : null),
|
||||
}),
|
||||
|
||||
// Codex Models actions
|
||||
fetchCodexModels: async (forceRefresh = false) => {
|
||||
const state = get();
|
||||
|
||||
@@ -34,7 +34,7 @@ import type { ApiKeys } from './settings-types';
|
||||
import type { ChatMessage, ChatSession } from './chat-types';
|
||||
import type { TerminalState, TerminalPanelContent, PersistedTerminalState } from './terminal-types';
|
||||
import type { Feature, ProjectAnalysis } from './project-types';
|
||||
import type { ClaudeUsage, CodexUsage, ZaiUsage } from './usage-types';
|
||||
import type { ClaudeUsage, CodexUsage, ZaiUsage, GeminiUsage } from './usage-types';
|
||||
|
||||
/** State for worktree init script execution */
|
||||
export interface InitScriptState {
|
||||
@@ -299,6 +299,10 @@ export interface AppState {
|
||||
zaiUsage: ZaiUsage | null;
|
||||
zaiUsageLastUpdated: number | null;
|
||||
|
||||
// Gemini Usage Tracking
|
||||
geminiUsage: GeminiUsage | null;
|
||||
geminiUsageLastUpdated: number | null;
|
||||
|
||||
// Codex Models (dynamically fetched)
|
||||
codexModels: Array<{
|
||||
id: string;
|
||||
@@ -769,6 +773,9 @@ export interface AppActions {
|
||||
// z.ai Usage Tracking actions
|
||||
setZaiUsage: (usage: ZaiUsage | null) => void;
|
||||
|
||||
// Gemini Usage Tracking actions
|
||||
setGeminiUsage: (usage: GeminiUsage | null, lastUpdated?: number) => void;
|
||||
|
||||
// Codex Models actions
|
||||
fetchCodexModels: (forceRefresh?: boolean) => Promise<void>;
|
||||
setCodexModels: (
|
||||
|
||||
Reference in New Issue
Block a user