mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-20 11:03:08 +00:00
Improve pull request flow, add branch selection for worktree creation, fix auto-mode concurrency count (#787)
* Changes from fix/fetch-before-pull-fetch * feat: Improve pull request flow, add branch selection for worktree creation, fix for automode concurrency count * feat: Add validation for remote names and improve error handling * Address PR comments and mobile layout fixes * ``` refactor: Extract PR target resolution logic into dedicated service ``` * feat: Add app shell UI and improve service imports. Address PR comments * fix: Improve security validation and cache handling in git operations * feat: Add GET /list endpoint and improve parameter handling * chore: Improve validation, accessibility, and error handling across apps * chore: Format vite server port configuration * fix: Add error handling for gh pr list command and improve offline fallbacks * fix: Preserve existing PR creation time and improve remote handling
This commit is contained in:
@@ -1,133 +1,252 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './app';
|
||||
import { isMobileDevice } from './lib/mobile-detect';
|
||||
import { isMobileDevice, isPwaStandalone } from './lib/mobile-detect';
|
||||
|
||||
// Defensive fallback: index.html's inline script already applies data-pwa="standalone"
|
||||
// before first paint. This re-applies it in case the inline script failed (e.g.
|
||||
// CSP restrictions or unexpected errors). setAttribute is a no-op if already set.
|
||||
if (isPwaStandalone) {
|
||||
document.documentElement.setAttribute('data-pwa', 'standalone');
|
||||
}
|
||||
|
||||
// Register service worker for PWA support (web mode only)
|
||||
// Uses optimized registration strategy for faster mobile loading:
|
||||
// - Registers after load event to avoid competing with critical resources
|
||||
// - Handles updates gracefully with skipWaiting support
|
||||
// - Triggers cache cleanup on activation
|
||||
// - Prefetches likely-needed route chunks during idle time
|
||||
// - Enables mobile-specific API caching when on a mobile device
|
||||
// Registers immediately (not deferred to load event) so the SW can intercept
|
||||
// and cache JS/CSS bundle requests during the current page load. When the SW is
|
||||
// registered inside a 'load' listener, all assets have already downloaded before
|
||||
// the SW installs, so they can't be cached until warmAssetCache runs later.
|
||||
// Registering early allows the SW to serve bundles from cache on the NEXT visit.
|
||||
//
|
||||
// Note: The SW itself does NOT call skipWaiting() on install, so a newly
|
||||
// registered SW won't disrupt a live page — it waits for SKIP_WAITING from the
|
||||
// main thread or for all old-SW tabs to close before activating.
|
||||
if ('serviceWorker' in navigator && !window.location.protocol.startsWith('file')) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker
|
||||
.register('/sw.js', {
|
||||
// Check for updates on every page load for PWA freshness
|
||||
updateViaCache: 'none',
|
||||
})
|
||||
.then((registration) => {
|
||||
// Check for service worker updates periodically
|
||||
// Mobile: every 60 minutes (saves battery/bandwidth)
|
||||
// Desktop: every 30 minutes
|
||||
const updateInterval = isMobileDevice ? 60 * 60 * 1000 : 30 * 60 * 1000;
|
||||
setInterval(() => {
|
||||
registration.update().catch(() => {
|
||||
// Update check failed silently - will try again next interval
|
||||
});
|
||||
}, updateInterval);
|
||||
|
||||
// When a new service worker takes over, trigger cache cleanup
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration.installing;
|
||||
if (newWorker) {
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'activated') {
|
||||
// New service worker is active - clean up old immutable cache entries
|
||||
newWorker.postMessage({ type: 'CACHE_CLEANUP' });
|
||||
}
|
||||
});
|
||||
}
|
||||
navigator.serviceWorker
|
||||
.register('/sw.js', {
|
||||
// Check for updates on every page load for PWA freshness
|
||||
updateViaCache: 'none',
|
||||
})
|
||||
.then((registration) => {
|
||||
// Check for service worker updates periodically
|
||||
// Mobile: every 60 minutes (saves battery/bandwidth)
|
||||
// Desktop: every 30 minutes
|
||||
const updateInterval = isMobileDevice ? 60 * 60 * 1000 : 30 * 60 * 1000;
|
||||
setInterval(() => {
|
||||
registration.update().catch(() => {
|
||||
// Update check failed silently - will try again next interval
|
||||
});
|
||||
}, updateInterval);
|
||||
|
||||
// Notify the service worker about mobile mode.
|
||||
// This enables stale-while-revalidate caching for API responses,
|
||||
// preventing blank screens caused by failed/slow API fetches on mobile.
|
||||
if (isMobileDevice && registration.active) {
|
||||
registration.active.postMessage({
|
||||
// When a new service worker is found, DON'T activate it immediately.
|
||||
// Instead, wait until the user navigates away or refreshes. This prevents
|
||||
// the brief reload/flash that occurs when skipWaiting() + clients.claim()
|
||||
// swaps the SW under a live page (especially noticeable when switching back
|
||||
// to the PWA on mobile).
|
||||
//
|
||||
// The new SW will naturally activate when all tabs using the old SW are closed.
|
||||
// For urgent updates, we send SKIP_WAITING on fresh page loads (see below).
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration.installing;
|
||||
if (newWorker) {
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
// A new SW is waiting — show an update banner so long-lived
|
||||
// sessions can immediately pick up new deployments.
|
||||
console.debug('[SW] New service worker installed and waiting to activate');
|
||||
showUpdateNotification(registration);
|
||||
}
|
||||
if (newWorker.state === 'activated') {
|
||||
// New service worker is active - clean up old immutable cache entries
|
||||
newWorker.postMessage({ type: 'CACHE_CLEANUP' });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// On fresh page loads (not tab-switch-back), if there's a waiting SW,
|
||||
// tell it to activate now. This is safe because the page is freshly loaded
|
||||
// and won't flash. This ensures updates are picked up within one page visit.
|
||||
if (registration.waiting) {
|
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
}
|
||||
|
||||
// Notify the service worker about mobile mode.
|
||||
// This enables stale-while-revalidate caching for API responses,
|
||||
// preventing blank screens caused by failed/slow API fetches on mobile.
|
||||
if (isMobileDevice && registration.active) {
|
||||
registration.active.postMessage({
|
||||
type: 'SET_MOBILE_MODE',
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Also listen for the SW becoming active (in case it wasn't ready above)
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
if (isMobileDevice && navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'SET_MOBILE_MODE',
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Also listen for the SW becoming active (in case it wasn't ready above)
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
if (isMobileDevice && navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'SET_MOBILE_MODE',
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Prefetch likely-needed route chunks during idle time.
|
||||
// On mobile, this means subsequent navigations are instant from cache
|
||||
// instead of requiring network round-trips over slow cellular connections.
|
||||
prefetchRouteChunks(registration);
|
||||
})
|
||||
.catch(() => {
|
||||
// Service worker registration failed; app still works without it
|
||||
});
|
||||
|
||||
// Warm the SW's immutable cache with all page assets during idle time.
|
||||
// This ensures that when the PWA is evicted from memory and reopened,
|
||||
// all JS/CSS can be served instantly from SW cache instead of re-downloading.
|
||||
warmAssetCache(registration);
|
||||
|
||||
// Request persistent storage to protect caches from OS eviction.
|
||||
// Without this, iOS Safari can purge Cache Storage under memory pressure
|
||||
// or after 7 days of inactivity, forcing a full network reload on next visit.
|
||||
// This is a best-effort request — the browser may deny it, but it's a no-op
|
||||
// on browsers that don't support it (no error thrown).
|
||||
if (navigator.storage?.persist) {
|
||||
navigator.storage
|
||||
.persist()
|
||||
.then((granted) => {
|
||||
if (granted) {
|
||||
console.debug('[SW] Persistent storage granted — caches protected from eviction');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently ignore — persistent storage is a nice-to-have
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Service worker registration failed; app still works without it
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a user-visible notification when a new service worker version is detected.
|
||||
* The notification offers a "Reload" action that sends SKIP_WAITING to the waiting
|
||||
* SW and reloads the page once the new SW activates. This ensures long-lived
|
||||
* sessions can immediately pick up new deployments.
|
||||
*/
|
||||
function showUpdateNotification(registration: ServiceWorkerRegistration): void {
|
||||
// Create a simple DOM-based notification (avoids depending on React rendering)
|
||||
const banner = document.createElement('div');
|
||||
banner.setAttribute('role', 'alert');
|
||||
|
||||
// Read theme-aware colors from CSS custom properties with sensible fallbacks
|
||||
// so the banner matches the current dark/light theme.
|
||||
const rootStyle = getComputedStyle(document.documentElement);
|
||||
const bgColor = rootStyle.getPropertyValue('--background').trim() || '#1a1a2e';
|
||||
const fgColor = rootStyle.getPropertyValue('--foreground').trim() || '#e0e0e0';
|
||||
const accentColor = rootStyle.getPropertyValue('--primary').trim() || '#6366f1';
|
||||
const mutedColor = rootStyle.getPropertyValue('--muted-foreground').trim() || '#888';
|
||||
|
||||
banner.style.cssText =
|
||||
'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:99999;' +
|
||||
`background:hsl(${bgColor});color:hsl(${fgColor});padding:12px 20px;border-radius:10px;` +
|
||||
'display:flex;align-items:center;gap:12px;font-size:14px;' +
|
||||
'box-shadow:0 4px 24px rgba(0,0,0,0.3);font-family:system-ui,sans-serif;';
|
||||
banner.innerHTML =
|
||||
'<span>A new version is available.</span>' +
|
||||
`<button id="sw-update-btn" style="background:hsl(${accentColor});color:hsl(${bgColor});border:none;` +
|
||||
'padding:6px 14px;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500;">Reload</button>' +
|
||||
`<button id="sw-dismiss-btn" style="background:transparent;color:hsl(${mutedColor});border:none;` +
|
||||
'padding:4px 8px;cursor:pointer;font-size:18px;line-height:1;" aria-label="Dismiss">×</button>';
|
||||
document.body.appendChild(banner);
|
||||
|
||||
// Listen for controllerchange to reload after the new SW activates.
|
||||
// Named handler so it can be cleaned up when the banner is dismissed or after reload.
|
||||
let reloading = false;
|
||||
const onControllerChange = () => {
|
||||
if (!reloading) {
|
||||
reloading = true;
|
||||
navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
|
||||
|
||||
banner.querySelector('#sw-update-btn')?.addEventListener('click', () => {
|
||||
// Send SKIP_WAITING to the waiting SW — it will call skipWaiting() and
|
||||
// the controllerchange listener above will reload the page.
|
||||
registration.waiting?.postMessage({ type: 'SKIP_WAITING' });
|
||||
const btn = banner.querySelector('#sw-update-btn') as HTMLButtonElement | null;
|
||||
if (btn) {
|
||||
btn.textContent = 'Updating…';
|
||||
btn.disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
banner.querySelector('#sw-dismiss-btn')?.addEventListener('click', () => {
|
||||
navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
|
||||
banner.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch route JS chunks that the user is likely to navigate to.
|
||||
* Uses requestIdleCallback to avoid competing with the initial render,
|
||||
* and sends URLs to the service worker for background caching.
|
||||
* This is especially impactful on mobile where network latency is high.
|
||||
* Warm the service worker's immutable cache with all critical page assets.
|
||||
* Collects URLs from modulepreload, prefetch, stylesheet, and script tags
|
||||
* and sends them to the SW via PRECACHE_ASSETS for background caching.
|
||||
*
|
||||
* This is critical for PWA cold-start performance: when iOS/Android evicts
|
||||
* the PWA from memory, reopening it needs assets from SW cache. The SW's
|
||||
* fetch handler caches assets on first access, but on the very first visit
|
||||
* the SW isn't active yet when assets load. This function bridges that gap
|
||||
* by explicitly telling the SW "cache these URLs" after registration.
|
||||
*/
|
||||
function prefetchRouteChunks(registration: ServiceWorkerRegistration): void {
|
||||
function warmAssetCache(registration: ServiceWorkerRegistration): void {
|
||||
const idleCallback =
|
||||
typeof requestIdleCallback !== 'undefined'
|
||||
? requestIdleCallback
|
||||
: (cb: () => void) => setTimeout(cb, 2000);
|
||||
|
||||
// On mobile, wait a bit longer before prefetching to let the critical path complete.
|
||||
// Mobile connections are often slower and we don't want to compete with initial data fetches.
|
||||
const prefetchDelay = isMobileDevice ? 4000 : 0;
|
||||
// CRITICAL_ASSETS are precached at SW install time (see sw.js install handler).
|
||||
// This function is a complementary backup: it collects asset URLs from the live
|
||||
// DOM (which includes any assets the browser already fetched on this visit) and
|
||||
// sends them to the SW for caching via PRECACHE_ASSETS. This covers:
|
||||
// - Assets that were fetched before the SW was active on the first visit
|
||||
// - Any assets the install-time precaching missed due to transient failures
|
||||
//
|
||||
// No delay needed — warmAssetCache is called after the SW registers (which is
|
||||
// already deferred until renderer.tsx module evaluation, post-parse). Asset URLs
|
||||
// are already in the DOM at that point and the SW processes PRECACHE_ASSETS
|
||||
// asynchronously without blocking the render path.
|
||||
const doWarm = () => {
|
||||
const assetUrls: string[] = [];
|
||||
|
||||
const doPrefetch = () => {
|
||||
// Find all modulepreload links already in the document (Vite injects these)
|
||||
// and any route chunks that might be linked
|
||||
const existingPreloads = new Set(
|
||||
Array.from(document.querySelectorAll('link[rel="modulepreload"]')).map(
|
||||
(link) => (link as HTMLLinkElement).href
|
||||
)
|
||||
);
|
||||
|
||||
// Also collect prefetch links (Vite mobile optimization converts some to prefetch)
|
||||
Array.from(document.querySelectorAll('link[rel="prefetch"]')).forEach((link) => {
|
||||
// Collect ALL asset URLs from the page that should be in the SW cache:
|
||||
// 1. modulepreload links (critical vendor chunks: react, tanstack, radix, state)
|
||||
// 2. prefetch links (deferred chunks: icons, reactflow, xterm, codemirror, markdown)
|
||||
// 3. stylesheet links (main CSS bundle)
|
||||
// 4. script tags with asset paths (entry point JS)
|
||||
document.querySelectorAll('link[rel="modulepreload"], link[rel="prefetch"]').forEach((link) => {
|
||||
const href = (link as HTMLLinkElement).href;
|
||||
if (href) existingPreloads.add(href);
|
||||
if (href && href.includes('/assets/')) assetUrls.push(href);
|
||||
});
|
||||
|
||||
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
|
||||
const href = (link as HTMLLinkElement).href;
|
||||
if (href && href.includes('/assets/')) assetUrls.push(href);
|
||||
});
|
||||
|
||||
// Discover route chunk URLs from the document's script tags
|
||||
// These are the code-split route bundles that TanStack Router will lazy-load
|
||||
const routeChunkUrls: string[] = [];
|
||||
document.querySelectorAll('script[src*="/assets/"]').forEach((script) => {
|
||||
const src = (script as HTMLScriptElement).src;
|
||||
if (src && !existingPreloads.has(src)) {
|
||||
routeChunkUrls.push(src);
|
||||
}
|
||||
if (src) assetUrls.push(src);
|
||||
});
|
||||
|
||||
// Send URLs to service worker for background caching
|
||||
if (routeChunkUrls.length > 0 && registration.active) {
|
||||
registration.active.postMessage({
|
||||
// Send all discovered URLs to the SW for background caching.
|
||||
// The SW's PRECACHE_ASSETS handler checks cache.match() first, so URLs
|
||||
// already in the immutable cache won't be re-fetched.
|
||||
//
|
||||
// Target the active SW if available; otherwise fall back to the installing/waiting
|
||||
// SW. On first visit, the SW may still be in 'installing' state when this runs,
|
||||
// but it can still receive messages and process them once it activates.
|
||||
const target = registration.active || registration.waiting || registration.installing;
|
||||
if (assetUrls.length > 0 && target) {
|
||||
target.postMessage({
|
||||
type: 'PRECACHE_ASSETS',
|
||||
urls: routeChunkUrls,
|
||||
urls: assetUrls,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for idle time after the app is interactive
|
||||
if (prefetchDelay > 0) {
|
||||
setTimeout(() => idleCallback(doPrefetch), prefetchDelay);
|
||||
} else {
|
||||
idleCallback(doPrefetch);
|
||||
}
|
||||
idleCallback(doWarm);
|
||||
}
|
||||
|
||||
// Render the app - prioritize First Contentful Paint
|
||||
|
||||
Reference in New Issue
Block a user