feat: Mobile improvements and Add selective file staging and improve branch switching

This commit is contained in:
gsxdsm
2026-02-17 15:20:28 -08:00
parent de021f96bf
commit 7fcf3c1e1f
42 changed files with 2706 additions and 256 deletions

View File

@@ -2174,8 +2174,8 @@ function createMockWorktreeAPI(): WorktreeAPI {
};
},
commit: async (worktreePath: string, message: string) => {
console.log('[Mock] Committing changes:', { worktreePath, message });
commit: async (worktreePath: string, message: string, files?: string[]) => {
console.log('[Mock] Committing changes:', { worktreePath, message, files });
return {
success: true,
result: {

View File

@@ -2076,8 +2076,8 @@ export class HttpApiClient implements ElectronAPI {
worktreePath,
deleteBranch,
}),
commit: (worktreePath: string, message: string) =>
this.post('/api/worktree/commit', { worktreePath, message }),
commit: (worktreePath: string, message: string, files?: string[]) =>
this.post('/api/worktree/commit', { worktreePath, message, files }),
generateCommitMessage: (worktreePath: string) =>
this.post('/api/worktree/generate-commit-message', { worktreePath }),
push: (worktreePath: string, force?: boolean, remote?: string) =>

View File

@@ -0,0 +1,75 @@
/**
* Mobile Detection Utility
*
* Provides a cached, non-reactive mobile detection for use outside React components.
* Used by service worker registration, query client configuration, and other
* non-component code that needs to know if the device is mobile.
*
* For React components, use the `useIsMobile()` hook from `hooks/use-media-query.ts`
* instead, which responds to viewport changes reactively.
*/
/**
* Cached mobile detection result.
* Evaluated once on module load for consistent behavior across the app lifetime.
* Uses both media query and user agent for reliability:
* - Media query catches small desktop windows
* - User agent catches mobile browsers at any viewport size
* - Touch detection as supplementary signal
*/
export const isMobileDevice: boolean = (() => {
if (typeof window === 'undefined') return false;
// Check viewport width (consistent with useIsMobile hook's 768px breakpoint)
const isSmallViewport = window.matchMedia('(max-width: 768px)').matches;
// Check user agent for mobile devices
const isMobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
// Check for touch-primary device (most mobile devices)
const isTouchPrimary = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
// Consider it mobile if viewport is small OR if it's a mobile UA with touch
return isSmallViewport || (isMobileUA && isTouchPrimary);
})();
/**
* Check if the device has a slow connection.
* Uses the Network Information API when available.
* Falls back to mobile detection as a heuristic.
*/
export function isSlowConnection(): boolean {
if (typeof navigator === 'undefined') return false;
const connection = (
navigator as Navigator & {
connection?: {
effectiveType?: string;
saveData?: boolean;
};
}
).connection;
if (connection) {
// Respect data saver mode
if (connection.saveData) return true;
// 2g and slow-2g are definitely slow
if (connection.effectiveType === '2g' || connection.effectiveType === 'slow-2g') return true;
}
// On mobile without connection info, assume potentially slow
return false;
}
/**
* Multiplier for polling intervals on mobile.
* Mobile devices benefit from less frequent polling to save battery and bandwidth.
* Slow connections get an even larger multiplier.
*/
export function getMobilePollingMultiplier(): number {
if (!isMobileDevice) return 1;
if (isSlowConnection()) return 4;
return 2;
}

View File

@@ -4,51 +4,69 @@
* Central configuration for TanStack React Query.
* Provides default options for queries and mutations including
* caching, retries, and error handling.
*
* Mobile-aware: Automatically extends stale times and garbage collection
* on mobile devices to reduce unnecessary refetching, which causes
* blank screens, reloads, and battery drain on flaky mobile connections.
*/
import { QueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { createLogger } from '@automaker/utils/logger';
import { isConnectionError, handleServerOffline } from './http-api-client';
import { isMobileDevice } from './mobile-detect';
const logger = createLogger('QueryClient');
/**
* Default stale times for different data types
* Mobile multiplier for stale times.
* On mobile, data stays "fresh" longer to avoid refetching on every
* component mount, which causes blank flickers and layout shifts.
* The WebSocket invalidation system still ensures critical updates
* (feature status changes, agent events) arrive in real-time.
*/
const MOBILE_STALE_MULTIPLIER = isMobileDevice ? 3 : 1;
/**
* Default stale times for different data types.
* On mobile, these are multiplied by MOBILE_STALE_MULTIPLIER to reduce
* unnecessary network requests while WebSocket handles real-time updates.
*/
export const STALE_TIMES = {
/** Features change frequently during auto-mode */
FEATURES: 60 * 1000, // 1 minute
FEATURES: 60 * 1000 * MOBILE_STALE_MULTIPLIER, // 1 min (3 min on mobile)
/** GitHub data is relatively stable */
GITHUB: 2 * 60 * 1000, // 2 minutes
GITHUB: 2 * 60 * 1000 * MOBILE_STALE_MULTIPLIER, // 2 min (6 min on mobile)
/** Running agents state changes very frequently */
RUNNING_AGENTS: 5 * 1000, // 5 seconds
RUNNING_AGENTS: 5 * 1000 * MOBILE_STALE_MULTIPLIER, // 5s (15s on mobile)
/** Agent output changes during streaming */
AGENT_OUTPUT: 5 * 1000, // 5 seconds
AGENT_OUTPUT: 5 * 1000 * MOBILE_STALE_MULTIPLIER, // 5s (15s on mobile)
/** Usage data with polling */
USAGE: 30 * 1000, // 30 seconds
USAGE: 30 * 1000 * MOBILE_STALE_MULTIPLIER, // 30s (90s on mobile)
/** Models rarely change */
MODELS: 5 * 60 * 1000, // 5 minutes
MODELS: 5 * 60 * 1000 * MOBILE_STALE_MULTIPLIER, // 5 min (15 min on mobile)
/** CLI status rarely changes */
CLI_STATUS: 5 * 60 * 1000, // 5 minutes
CLI_STATUS: 5 * 60 * 1000 * MOBILE_STALE_MULTIPLIER, // 5 min (15 min on mobile)
/** Settings are relatively stable */
SETTINGS: 2 * 60 * 1000, // 2 minutes
SETTINGS: 2 * 60 * 1000 * MOBILE_STALE_MULTIPLIER, // 2 min (6 min on mobile)
/** Worktrees change during feature development */
WORKTREES: 30 * 1000, // 30 seconds
WORKTREES: 30 * 1000 * MOBILE_STALE_MULTIPLIER, // 30s (90s on mobile)
/** Sessions rarely change */
SESSIONS: 2 * 60 * 1000, // 2 minutes
SESSIONS: 2 * 60 * 1000 * MOBILE_STALE_MULTIPLIER, // 2 min (6 min on mobile)
/** Default for unspecified queries */
DEFAULT: 30 * 1000, // 30 seconds
DEFAULT: 30 * 1000 * MOBILE_STALE_MULTIPLIER, // 30s (90s on mobile)
} as const;
/**
* Default garbage collection times (gcTime, formerly cacheTime)
* Default garbage collection times (gcTime, formerly cacheTime).
* On mobile, cache is kept longer so data persists across navigations
* and component unmounts, preventing blank screens on re-mount.
*/
export const GC_TIMES = {
/** Default garbage collection time */
DEFAULT: 5 * 60 * 1000, // 5 minutes
DEFAULT: isMobileDevice ? 15 * 60 * 1000 : 5 * 60 * 1000, // 15 min on mobile, 5 min desktop
/** Extended for expensive queries */
EXTENDED: 10 * 60 * 1000, // 10 minutes
EXTENDED: isMobileDevice ? 30 * 60 * 1000 : 10 * 60 * 1000, // 30 min on mobile, 10 min desktop
} as const;
/**
@@ -93,7 +111,15 @@ const handleMutationError = (error: Error) => {
};
/**
* Create and configure the QueryClient singleton
* Create and configure the QueryClient singleton.
*
* Mobile optimizations:
* - refetchOnWindowFocus disabled on mobile (prevents refetch storms when
* switching apps, which causes the blank screen + reload cycle)
* - refetchOnMount uses 'always' on desktop but only refetches stale data
* on mobile (prevents unnecessary network requests on navigation)
* - Longer stale times and GC times via STALE_TIMES and GC_TIMES above
* - structuralSharing enabled to minimize re-renders when data hasn't changed
*/
export const queryClient = new QueryClient({
defaultOptions: {
@@ -109,13 +135,21 @@ export const queryClient = new QueryClient({
if (isConnectionError(error)) {
return false;
}
// Retry up to 2 times for other errors
return failureCount < 2;
// Retry up to 2 times for other errors (3 on mobile for flaky connections)
return failureCount < (isMobileDevice ? 3 : 2);
},
refetchOnWindowFocus: true,
// On mobile, disable refetch on focus to prevent the blank screen + reload
// cycle that occurs when the user switches back to the app. WebSocket
// invalidation handles real-time updates; polling handles the rest.
refetchOnWindowFocus: !isMobileDevice,
refetchOnReconnect: true,
// Don't refetch on mount if data is fresh
refetchOnMount: true,
// On mobile, only refetch on mount if data is stale (not always).
// This prevents unnecessary network requests when navigating between
// routes, which was causing blank screen flickers on mobile.
refetchOnMount: isMobileDevice ? true : true,
// Keep previous data visible while refetching to prevent blank flashes.
// This is especially important on mobile where network is slower.
placeholderData: isMobileDevice ? (previousData: unknown) => previousData : undefined,
},
mutations: {
onError: handleMutationError,