fix: Remove unused vars and improve type safety. Improve task recovery

This commit is contained in:
gsxdsm
2026-02-17 13:18:40 -08:00
parent 8bb10632b1
commit de021f96bf
68 changed files with 1028 additions and 534 deletions

View File

@@ -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(

View File

@@ -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>
)}
</>
)}
</>
)}

View File

@@ -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(() => {

View File

@@ -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) => {

View File

@@ -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' && (

View File

@@ -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`;

View File

@@ -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() });

View File

@@ -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';

View File

@@ -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

View File

@@ -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,

View File

@@ -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();

View File

@@ -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: (