Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e57549c
feat(ui): add React Query foundation and provider setup
Shironex Jan 15, 2026
2bc931a
feat(ui): add React Query hooks for data fetching
Shironex Jan 15, 2026
8456741
feat(ui): add React Query mutation hooks
Shironex Jan 15, 2026
d81997d
feat(ui): add WebSocket event to React Query cache bridge
Shironex Jan 15, 2026
d08ef47
feat(ui): add shared skeleton component and update CLI status
Shironex Jan 15, 2026
3411256
refactor(ui): migrate board view to React Query
Shironex Jan 15, 2026
d1219a2
refactor(ui): migrate worktree panel to React Query
Shironex Jan 15, 2026
c4e0a7c
refactor(ui): migrate GitHub views to React Query
Shironex Jan 15, 2026
20caa42
refactor(ui): migrate settings view to React Query
Shironex Jan 15, 2026
5fe7bcd
refactor(ui): migrate usage popovers and running agents to React Query
Shironex Jan 15, 2026
c2fed78
refactor(ui): migrate remaining components to React Query
Shironex Jan 15, 2026
9dbec72
fix: package lock file
Shironex Jan 15, 2026
3170e22
fix(ui): add missing cache invalidation for React Query
Shironex Jan 15, 2026
361cb06
fix(ui): improve React Query hooks and fix edge cases
Shironex Jan 15, 2026
f987fc1
Merge branch 'v0.13.0rc' into feat/react-query
Shironex Jan 19, 2026
9bb52f1
perf(ui): smooth large lists and graphs
DhanushSantosh Jan 19, 2026
2fac2ca
Fix opencode auth error mapping and perf containment
DhanushSantosh Jan 19, 2026
cf60f84
Merge remote-tracking branch 'upstream/v0.13.0rc' into feat/react-query
DhanushSantosh Jan 20, 2026
a863dcc
fix(ui): handle review feedback
DhanushSantosh Jan 20, 2026
8c356d7
fix(ui): sync updated feature query
DhanushSantosh Jan 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

- Setting the default model does not seem like it works.

# Performance (completed)

- [x] Graph performance mode for large graphs (compact nodes/edges + visible-only rendering)
- [x] Render containment on heavy scroll regions (kanban columns, chat history)
- [x] Reduce blur/shadow effects when lists get large
- [x] React Query tuning for heavy datasets (less refetch on focus/reconnect)
- [x] DnD/list rendering optimizations (virtualized kanban + memoized card sections)

# UX

- Consolidate all models to a single place in the settings instead of having AI profiles and all this other stuff
Expand Down
3 changes: 2 additions & 1 deletion apps/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@
"@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-tooltip": "1.2.8",
"@tanstack/react-query": "5.90.12",
"@tanstack/react-query": "^5.90.17",
"@tanstack/react-query-devtools": "^5.91.2",
"@tanstack/react-router": "1.141.6",
"@uiw/react-codemirror": "4.25.4",
"@xterm/addon-fit": "0.10.0",
Expand Down
146 changes: 30 additions & 116 deletions apps/ui/src/components/claude-usage-popover.tsx
Original file line number Diff line number Diff line change
@@ -1,115 +1,40 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
/**
* Claude Usage Popover
*
* Displays Claude API usage statistics using React Query for data fetching.
*/

import { useState, useMemo } from 'react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { RefreshCw, AlertTriangle, CheckCircle, XCircle, Clock, ExternalLink } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
import { getElectronAPI } from '@/lib/electron';
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';

// Error codes for distinguishing failure modes
const ERROR_CODES = {
API_BRIDGE_UNAVAILABLE: 'API_BRIDGE_UNAVAILABLE',
AUTH_ERROR: 'AUTH_ERROR',
TRUST_PROMPT: 'TRUST_PROMPT',
UNKNOWN: 'UNKNOWN',
} as const;

type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];

type UsageError = {
code: ErrorCode;
message: string;
};

// Fixed refresh interval (45 seconds)
const REFRESH_INTERVAL_SECONDS = 45;
import { useClaudeUsage } from '@/hooks/queries';

export function ClaudeUsagePopover() {
const { claudeUsage, claudeUsageLastUpdated, setClaudeUsage } = useAppStore();
const claudeAuthStatus = useSetupStore((state) => state.claudeAuthStatus);
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<UsageError | null>(null);

// Check if CLI is verified/authenticated
const isCliVerified =
claudeAuthStatus?.authenticated && claudeAuthStatus?.method === 'cli_authenticated';

// Check if data is stale (older than 2 minutes) - recalculates when claudeUsageLastUpdated changes
const isStale = useMemo(() => {
return !claudeUsageLastUpdated || Date.now() - claudeUsageLastUpdated > 2 * 60 * 1000;
}, [claudeUsageLastUpdated]);

const fetchUsage = useCallback(
async (isAutoRefresh = false) => {
if (!isAutoRefresh) setLoading(true);
setError(null);
try {
const api = getElectronAPI();
if (!api.claude) {
setError({
code: ERROR_CODES.API_BRIDGE_UNAVAILABLE,
message: 'Claude API bridge not available',
});
return;
}
const data = await api.claude.getUsage();
if ('error' in data) {
// Detect trust prompt error
const isTrustPrompt =
data.error === 'Trust prompt pending' ||
(data.message && data.message.includes('folder permission'));
setError({
code: isTrustPrompt ? ERROR_CODES.TRUST_PROMPT : ERROR_CODES.AUTH_ERROR,
message: data.message || data.error,
});
return;
}
setClaudeUsage(data);
} catch (err) {
setError({
code: ERROR_CODES.UNKNOWN,
message: err instanceof Error ? err.message : 'Failed to fetch usage',
});
} finally {
if (!isAutoRefresh) setLoading(false);
}
},
[setClaudeUsage]
);

// Auto-fetch on mount if data is stale (only if CLI is verified)
useEffect(() => {
if (isStale && isCliVerified) {
fetchUsage(true);
}
}, [isStale, isCliVerified, fetchUsage]);

useEffect(() => {
// Skip if CLI is not verified
if (!isCliVerified) return;

// Initial fetch when opened
if (open) {
if (!claudeUsage || isStale) {
fetchUsage();
}
}
// Use React Query for usage data
const {
data: claudeUsage,
isLoading,
isFetching,
error,
dataUpdatedAt,
refetch,
} = useClaudeUsage(isCliVerified);
Comment on lines 20 to +32
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the authentication check patterns across Claude usage components
echo "=== claude-usage-section.tsx auth check ==="
rg -n "canFetchUsage|authenticated" apps/ui/src/components/views/settings-view/api-keys/claude-usage-section.tsx

echo ""
echo "=== claude-usage-popover.tsx auth check ==="
rg -n "isCliVerified|authenticated" apps/ui/src/components/claude-usage-popover.tsx

echo ""
echo "=== usage-popover.tsx auth check ==="
rg -n "isClaudeAuthenticated|authenticated" apps/ui/src/components/usage-popover.tsx

Repository: AutoMaker-Org/automaker

Length of output: 1183


Inconsistent authentication check across Claude usage components.

claude-usage-popover.tsx checks both authenticated and method === 'cli_authenticated' (lines 21-22), while claude-usage-section.tsx (line 80) and usage-popover.tsx (line 70) only check claudeAuthStatus?.authenticated. Verify if this stricter check in the popover is intentional; if not, align the authentication logic across all three components.

🤖 Prompt for AI Agents
In `@apps/ui/src/components/claude-usage-popover.tsx` around lines 20 - 32, The
popover's authentication check (variable isCliVerified in
claude-usage-popover.tsx) is stricter than the other components—it's checking
claudeAuthStatus?.authenticated && claudeAuthStatus?.method ===
'cli_authenticated' before calling useClaudeUsage; to align behavior, remove the
method check and set isCliVerified to claudeAuthStatus?.authenticated (or, if
the stricter policy is desired, update claude-usage-section.tsx and
usage-popover.tsx to include the method check); update the isCliVerified
definition and any callers of useClaudeUsage accordingly so all three components
use the same authentication logic.


// Auto-refresh interval (only when open)
let intervalId: NodeJS.Timeout | null = null;
if (open) {
intervalId = setInterval(() => {
fetchUsage(true);
}, REFRESH_INTERVAL_SECONDS * 1000);
}

return () => {
if (intervalId) clearInterval(intervalId);
};
}, [open, claudeUsage, isStale, isCliVerified, fetchUsage]);
// Check if data is stale (older than 2 minutes)
const isStale = useMemo(() => {
return !dataUpdatedAt || Date.now() - dataUpdatedAt > 2 * 60 * 1000;
}, [dataUpdatedAt]);

// Derived status color/icon helper
const getStatusInfo = (percentage: number) => {
Expand Down Expand Up @@ -144,7 +69,6 @@ export function ClaudeUsagePopover() {
isPrimary?: boolean;
stale?: boolean;
}) => {
// Check if percentage is valid (not NaN, not undefined, is a finite number)
const isValidPercentage =
typeof percentage === 'number' && !isNaN(percentage) && isFinite(percentage);
const safePercentage = isValidPercentage ? percentage : 0;
Expand Down Expand Up @@ -245,10 +169,10 @@ export function ClaudeUsagePopover() {
<Button
variant="ghost"
size="icon"
className={cn('h-6 w-6', loading && 'opacity-80')}
onClick={() => !loading && fetchUsage(false)}
className={cn('h-6 w-6', isFetching && 'opacity-80')}
onClick={() => !isFetching && refetch()}
>
<RefreshCw className="w-3.5 h-3.5" />
<RefreshCw className={cn('w-3.5 h-3.5', isFetching && 'animate-spin')} />
</Button>
)}
</div>
Expand All @@ -259,26 +183,16 @@ export function ClaudeUsagePopover() {
<div className="flex flex-col items-center justify-center py-6 text-center space-y-3">
<AlertTriangle className="w-8 h-8 text-yellow-500/80" />
<div className="space-y-1 flex flex-col items-center">
<p className="text-sm font-medium">{error.message}</p>
<p className="text-sm font-medium">
{error instanceof Error ? error.message : 'Failed to fetch usage'}
</p>
<p className="text-xs text-muted-foreground">
{error.code === ERROR_CODES.API_BRIDGE_UNAVAILABLE ? (
'Ensure the Electron bridge is running or restart the app'
) : error.code === ERROR_CODES.TRUST_PROMPT ? (
<>
Run <code className="font-mono bg-muted px-1 rounded">claude</code> in your
terminal and approve access to continue
</>
) : (
<>
Make sure Claude CLI is installed and authenticated via{' '}
<code className="font-mono bg-muted px-1 rounded">claude login</code>
</>
)}
Make sure Claude CLI is installed and authenticated via{' '}
<code className="font-mono bg-muted px-1 rounded">claude login</code>
</p>
</div>
</div>
) : !claudeUsage ? (
// Loading state
) : isLoading || !claudeUsage ? (
<div className="flex flex-col items-center justify-center py-8 space-y-2">
<Spinner size="lg" />
<p className="text-xs text-muted-foreground">Loading usage data...</p>
Expand Down
116 changes: 28 additions & 88 deletions apps/ui/src/components/codex-usage-popover.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { useState, useMemo } from 'react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { RefreshCw, AlertTriangle, CheckCircle, XCircle, Clock, ExternalLink } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
import { getElectronAPI } from '@/lib/electron';
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
import { useCodexUsage } from '@/hooks/queries';

// Error codes for distinguishing failure modes
const ERROR_CODES = {
Expand All @@ -23,9 +22,6 @@ type UsageError = {
message: string;
};

// Fixed refresh interval (45 seconds)
const REFRESH_INTERVAL_SECONDS = 45;

// Helper to format reset time
function formatResetTime(unixTimestamp: number): string {
const date = new Date(unixTimestamp * 1000);
Expand Down Expand Up @@ -63,95 +59,39 @@ function getWindowLabel(durationMins: number): { title: string; subtitle: string
}

export function CodexUsagePopover() {
const { codexUsage, codexUsageLastUpdated, setCodexUsage } = useAppStore();
const codexAuthStatus = useSetupStore((state) => state.codexAuthStatus);
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<UsageError | null>(null);

// Check if Codex is authenticated
const isCodexAuthenticated = codexAuthStatus?.authenticated;

// Use React Query for data fetching with automatic polling
const {
data: codexUsage,
isLoading,
isFetching,
error: queryError,
dataUpdatedAt,
refetch,
} = useCodexUsage(isCodexAuthenticated);

// Check if data is stale (older than 2 minutes)
const isStale = useMemo(() => {
return !codexUsageLastUpdated || Date.now() - codexUsageLastUpdated > 2 * 60 * 1000;
}, [codexUsageLastUpdated]);

const fetchUsage = useCallback(
async (isAutoRefresh = false) => {
if (!isAutoRefresh) setLoading(true);
setError(null);
try {
const api = getElectronAPI();
if (!api.codex) {
setError({
code: ERROR_CODES.API_BRIDGE_UNAVAILABLE,
message: 'Codex API bridge not available',
});
return;
}
const data = await api.codex.getUsage();
if ('error' in data) {
// Check if it's the "not available" error
if (
data.message?.includes('not available') ||
data.message?.includes('does not provide')
) {
setError({
code: ERROR_CODES.NOT_AVAILABLE,
message: data.message || data.error,
});
} else {
setError({
code: ERROR_CODES.AUTH_ERROR,
message: data.message || data.error,
});
}
return;
}
setCodexUsage(data);
} catch (err) {
setError({
code: ERROR_CODES.UNKNOWN,
message: err instanceof Error ? err.message : 'Failed to fetch usage',
});
} finally {
if (!isAutoRefresh) setLoading(false);
}
},
[setCodexUsage]
);

// Auto-fetch on mount if data is stale (only if authenticated)
useEffect(() => {
if (isStale && isCodexAuthenticated) {
fetchUsage(true);
return !dataUpdatedAt || Date.now() - dataUpdatedAt > 2 * 60 * 1000;
}, [dataUpdatedAt]);

// Convert query error to UsageError format for backward compatibility
const error = useMemo((): UsageError | null => {
if (!queryError) return null;
const message = queryError instanceof Error ? queryError.message : String(queryError);
if (message.includes('not available') || message.includes('does not provide')) {
return { code: ERROR_CODES.NOT_AVAILABLE, message };
}
}, [isStale, isCodexAuthenticated, fetchUsage]);

useEffect(() => {
// Skip if not authenticated
if (!isCodexAuthenticated) return;

// Initial fetch when opened
if (open) {
if (!codexUsage || isStale) {
fetchUsage();
}
if (message.includes('bridge') || message.includes('API')) {
return { code: ERROR_CODES.API_BRIDGE_UNAVAILABLE, message };
}

// Auto-refresh interval (only when open)
let intervalId: NodeJS.Timeout | null = null;
if (open) {
intervalId = setInterval(() => {
fetchUsage(true);
}, REFRESH_INTERVAL_SECONDS * 1000);
}

return () => {
if (intervalId) clearInterval(intervalId);
};
}, [open, codexUsage, isStale, isCodexAuthenticated, fetchUsage]);
return { code: ERROR_CODES.AUTH_ERROR, message };
}, [queryError]);

// Derived status color/icon helper
const getStatusInfo = (percentage: number) => {
Expand Down Expand Up @@ -289,10 +229,10 @@ export function CodexUsagePopover() {
<Button
variant="ghost"
size="icon"
className={cn('h-6 w-6', loading && 'opacity-80')}
onClick={() => !loading && fetchUsage(false)}
className={cn('h-6 w-6', isFetching && 'opacity-80')}
onClick={() => !isFetching && refetch()}
>
<RefreshCw className="w-3.5 h-3.5" />
<RefreshCw className={cn('w-3.5 h-3.5', isFetching && 'animate-spin')} />
</Button>
)}
</div>
Expand Down
Loading