agent-ecosystem/src/renderer/components/common/UpdateBanner.tsx
Artem Rootman da58917032
perf: offload heavy I/O to worker thread, reduce renderer re-renders
Main process — worker thread for team data:
- New team-data-worker thread handles getTeamData and findLogsForTask,
  isolating heavy file I/O (scanning 300+ subagent JSONL files) from
  Electron's main event loop. getTeamData dropped from ~2000ms on the
  main thread to ~110ms via the worker.
- Worker-side dedup and 10s result cache for findLogsForTask prevents
  redundant scans when the same task is queried multiple times.
- Discovery cache TTL raised from 5s to 30s — avoids re-scanning the
  entire project directory on every call.
- Message cap at 200 in TeamDataService to keep IPC payloads under 1MB
  (was sending 2200+ messages / ~3MB, stalling Chromium IPC serialization).
- IPC handlers fall back to main-thread execution if the worker is
  unavailable (graceful degradation).

Renderer — useShallow and memoization (55 files):
- Added useShallow to store selectors across 55 renderer files. Batched
  individual useStore() calls (e.g. 17 calls in ExtensionStoreView,
  10 in ConnectionSection) into single useShallow selectors, cutting
  unnecessary re-render checks on every store update.
- MemberLogsTab: three 5-second polling intervals now pause when the
  parent tab is hidden (display:none). Previously 5 hidden tabs × 3
  intervals = 15 polling timers firing continuously.
- KanbanColumn wrapped in React.memo to skip re-renders when props
  haven't changed.
- MemberList: memoized activeMembers/removedMembers/colorMap; replaced
  O(n×m) per-member task scan with a pre-computed reviewer map.
- Bounded timer Maps in store initialization to prevent unbounded growth
  of debounce/throttle tracking maps during long sessions.
2026-04-05 16:21:05 +00:00

102 lines
3.3 KiB
TypeScript

/**
* UpdateBanner - Slim top banner for download progress and restart prompt.
*
* Visible during download and after the update is ready to install.
*/
import { useStore } from '@renderer/store';
import { CheckCircle, Loader2, X } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
export const UpdateBanner = (): React.JSX.Element | null => {
const {
showUpdateBanner,
updateStatus,
downloadProgress,
availableVersion,
installUpdate,
dismissUpdateBanner,
} = useStore(
useShallow((s) => ({
showUpdateBanner: s.showUpdateBanner,
updateStatus: s.updateStatus,
downloadProgress: s.downloadProgress,
availableVersion: s.availableVersion,
installUpdate: s.installUpdate,
dismissUpdateBanner: s.dismissUpdateBanner,
}))
);
if (!showUpdateBanner || (updateStatus !== 'downloading' && updateStatus !== 'downloaded')) {
return null;
}
const isDownloading = updateStatus === 'downloading';
const percent = Math.round(downloadProgress);
const clampedPercent = Math.max(0, Math.min(percent, 100));
return (
<div
className="relative border-b px-4 py-2.5"
style={{
backgroundColor: 'var(--color-surface)',
borderColor: 'var(--color-border)',
}}
>
{isDownloading ? (
<div className="pr-8">
<div
className="mb-1.5 flex items-center gap-2 text-xs"
style={{ color: 'var(--color-text-secondary)' }}
>
<Loader2 className="size-3.5 shrink-0 animate-spin text-blue-600 dark:text-blue-400" />
<span>Updating app</span>
<span className="tabular-nums" style={{ color: 'var(--color-text-muted)' }}>
{clampedPercent}%
</span>
</div>
<div
className="h-1 w-full overflow-hidden rounded-full"
style={{ backgroundColor: 'var(--color-border)' }}
>
<div
className="h-full rounded-full bg-blue-600 transition-all duration-300 ease-out dark:bg-blue-500"
style={{ width: `${clampedPercent}%` }}
/>
</div>
</div>
) : (
<div className="flex items-center gap-2 pr-8">
<CheckCircle className="size-4 shrink-0 text-green-400" />
<span className="text-sm" style={{ color: 'var(--color-text-secondary)' }}>
Update ready
{availableVersion ? (
<span className="ml-1 text-xs" style={{ color: 'var(--color-text-muted)' }}>
v{availableVersion}
</span>
) : null}
</span>
<button
onClick={installUpdate}
className="ml-auto rounded-md border px-2.5 py-1 text-xs font-medium transition-colors hover:bg-white/5"
style={{
borderColor: 'var(--color-border-emphasis)',
color: 'var(--color-text)',
}}
>
Restart now
</button>
</div>
)}
{/* Dismiss */}
<button
onClick={dismissUpdateBanner}
className="absolute right-3 top-1/2 shrink-0 -translate-y-1/2 rounded p-0.5 transition-colors hover:bg-white/10"
style={{ color: 'var(--color-text-muted)' }}
>
<X className="size-3.5" />
</button>
</div>
);
};