Files
automaker/apps/ui/src/renderer.tsx
gsxdsm c81ea768a7 Feature: Add PR review comments and resolution, improve AI prompt handling (#790)
* feat: Add PR review comments and resolution endpoints, improve prompt handling

* Feature: File Editor (#789)

* feat: Add file management feature

* feat: Add auto-save functionality to file editor

* fix: Replace HardDriveDownload icon with Save icon for consistency

* fix: Prevent recursive copy/move and improve shell injection prevention

* refactor: Extract editor settings form into separate component

* ```
fix: Improve error handling and stabilize async operations

- Add error event handlers to GraphQL process spawns to prevent unhandled rejections
- Replace execAsync with execFile for safer command execution and better control
- Fix timeout cleanup in withTimeout generator to prevent memory leaks
- Improve outdated comment detection logic by removing redundant condition
- Use resolveModelString for consistent model string handling
- Replace || with ?? for proper falsy value handling in dialog initialization
- Add comments clarifying branch name resolution logic for local branches with slashes
- Add catch handler for project selection to handle async errors gracefully
```

* refactor: Extract PR review comments logic to dedicated service

* fix: Improve robustness and UX for PR review and file operations

* fix: Consolidate exec utilities and improve type safety

* refactor: Replace ScrollArea with div and improve file tree layout
2026-02-20 21:34:40 -08:00

263 lines
12 KiB
TypeScript

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './app';
import { AppErrorBoundary } from './components/ui/app-error-boundary';
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)
// 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')) {
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 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,
});
}
});
// 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">&times;</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();
});
}
/**
* 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 warmAssetCache(registration: ServiceWorkerRegistration): void {
const idleCallback =
typeof requestIdleCallback !== 'undefined'
? requestIdleCallback
: (cb: () => void) => setTimeout(cb, 2000);
// 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[] = [];
// 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 && 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);
});
document.querySelectorAll('script[src*="/assets/"]').forEach((script) => {
const src = (script as HTMLScriptElement).src;
if (src) assetUrls.push(src);
});
// 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: assetUrls,
});
}
};
idleCallback(doWarm);
}
// Render the app - prioritize First Contentful Paint
// AppErrorBoundary catches uncaught React errors and shows a friendly error screen
// instead of TanStack Router's default "Something went wrong!" overlay.
createRoot(document.getElementById('app')!).render(
<StrictMode>
<AppErrorBoundary>
<App />
</AppErrorBoundary>
</StrictMode>
);