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.
230 lines
7.3 KiB
TypeScript
230 lines
7.3 KiB
TypeScript
/**
|
|
* UpdateDialog - Modal dialog shown when a new version is available.
|
|
*
|
|
* Prompts the user to download the update or dismiss it.
|
|
* Release notes (markdown from GitHub) are rendered with ReactMarkdown.
|
|
* Shows "Restart now" when the update has already been downloaded.
|
|
*/
|
|
|
|
import { useEffect, useRef } from 'react';
|
|
import ReactMarkdown from 'react-markdown';
|
|
|
|
import { isElectronMode } from '@renderer/api';
|
|
import { markdownComponents } from '@renderer/components/chat/markdownComponents';
|
|
import { useStore } from '@renderer/store';
|
|
import { REHYPE_PLUGINS } from '@renderer/utils/markdownPlugins';
|
|
import { ExternalLink, X } from 'lucide-react';
|
|
import remarkGfm from 'remark-gfm';
|
|
import { useShallow } from 'zustand/react/shallow';
|
|
|
|
export const UpdateDialog = (): React.JSX.Element | null => {
|
|
const {
|
|
showUpdateDialog,
|
|
updateStatus,
|
|
availableVersion,
|
|
releaseNotes,
|
|
downloadUpdate,
|
|
installUpdate,
|
|
dismissUpdateDialog,
|
|
} = useStore(
|
|
useShallow((s) => ({
|
|
showUpdateDialog: s.showUpdateDialog,
|
|
updateStatus: s.updateStatus,
|
|
availableVersion: s.availableVersion,
|
|
releaseNotes: s.releaseNotes,
|
|
downloadUpdate: s.downloadUpdate,
|
|
installUpdate: s.installUpdate,
|
|
dismissUpdateDialog: s.dismissUpdateDialog,
|
|
}))
|
|
);
|
|
|
|
const dialogRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Handle ESC key to close dialog
|
|
useEffect(() => {
|
|
if (!showUpdateDialog) return;
|
|
|
|
const handleEscape = (e: KeyboardEvent): void => {
|
|
if (e.key === 'Escape') {
|
|
dismissUpdateDialog();
|
|
}
|
|
};
|
|
|
|
document.addEventListener('keydown', handleEscape);
|
|
return () => document.removeEventListener('keydown', handleEscape);
|
|
}, [showUpdateDialog, dismissUpdateDialog]);
|
|
|
|
// Focus trap: keep focus within dialog
|
|
useEffect(() => {
|
|
if (!showUpdateDialog || !dialogRef.current) return;
|
|
|
|
const dialog = dialogRef.current;
|
|
const focusableElements = dialog.querySelectorAll<HTMLElement>(
|
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
);
|
|
const firstElement = focusableElements[0];
|
|
const lastElement = focusableElements[focusableElements.length - 1];
|
|
|
|
// Focus first element when dialog opens
|
|
firstElement?.focus();
|
|
|
|
const handleTab = (e: KeyboardEvent): void => {
|
|
if (e.key !== 'Tab') return;
|
|
|
|
if (e.shiftKey) {
|
|
// Shift+Tab: if on first element, go to last
|
|
if (document.activeElement === firstElement) {
|
|
e.preventDefault();
|
|
lastElement?.focus();
|
|
}
|
|
} else {
|
|
// Tab: if on last element, go to first
|
|
if (document.activeElement === lastElement) {
|
|
e.preventDefault();
|
|
firstElement?.focus();
|
|
}
|
|
}
|
|
};
|
|
|
|
dialog.addEventListener('keydown', handleTab);
|
|
return () => dialog.removeEventListener('keydown', handleTab);
|
|
}, [showUpdateDialog]);
|
|
|
|
if (!showUpdateDialog) return null;
|
|
|
|
const isDownloaded = updateStatus === 'downloaded';
|
|
|
|
// Strip "Downloads" section (and everything after it) from release notes
|
|
const filteredNotes = releaseNotes
|
|
? releaseNotes.replace(/\n#{1,3}\s+Downloads[\s\S]*$/i, '').trimEnd()
|
|
: releaseNotes;
|
|
|
|
const releaseUrl = availableVersion
|
|
? `https://github.com/777genius/claude_agent_teams_ui/releases/tag/v${availableVersion}`
|
|
: null;
|
|
|
|
const openReleaseOnGitHub = (): void => {
|
|
if (!releaseUrl) return;
|
|
if (isElectronMode()) {
|
|
void window.electronAPI.openExternal(releaseUrl);
|
|
} else {
|
|
window.open(releaseUrl, '_blank');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
{/* Backdrop */}
|
|
<button
|
|
className="absolute inset-0 cursor-default"
|
|
style={{ backgroundColor: 'rgba(0, 0, 0, 0.6)' }}
|
|
onClick={dismissUpdateDialog}
|
|
aria-label="Close dialog"
|
|
tabIndex={-1}
|
|
/>
|
|
<div
|
|
ref={dialogRef}
|
|
className="relative mx-4 w-full max-w-2xl rounded-md border p-5 shadow-lg"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label="Update available"
|
|
style={{
|
|
backgroundColor: 'var(--color-surface-overlay)',
|
|
borderColor: 'var(--color-border-emphasis)',
|
|
}}
|
|
>
|
|
{/* Close button */}
|
|
<button
|
|
onClick={dismissUpdateDialog}
|
|
className="absolute right-3 top-3 rounded p-1 transition-colors hover:bg-white/10"
|
|
style={{ color: 'var(--color-text-muted)' }}
|
|
>
|
|
<X className="size-4" />
|
|
</button>
|
|
|
|
<div className="mb-3 pr-8">
|
|
<h2 className="text-base font-semibold" style={{ color: 'var(--color-text)' }}>
|
|
{isDownloaded ? 'Update Ready' : 'Update Available'}
|
|
</h2>
|
|
{availableVersion && (
|
|
<div
|
|
className="mt-1.5 inline-block rounded-full px-2.5 py-0.5 text-xs font-medium"
|
|
style={{
|
|
backgroundColor: isDownloaded
|
|
? 'rgba(34, 197, 94, 0.15)'
|
|
: 'rgba(59, 130, 246, 0.15)',
|
|
color: isDownloaded ? '#4ade80' : '#60a5fa',
|
|
}}
|
|
>
|
|
v{availableVersion}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Release notes */}
|
|
<div
|
|
className="prose prose-sm prose-invert mb-4 max-h-[60vh] max-w-none overflow-y-auto rounded border p-3 text-xs"
|
|
style={{
|
|
backgroundColor: 'var(--color-surface)',
|
|
borderColor: 'var(--color-border)',
|
|
color: 'var(--color-text-secondary)',
|
|
}}
|
|
>
|
|
{filteredNotes ? (
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
rehypePlugins={REHYPE_PLUGINS}
|
|
components={markdownComponents}
|
|
>
|
|
{filteredNotes}
|
|
</ReactMarkdown>
|
|
) : (
|
|
<p className="italic" style={{ color: 'var(--color-text-muted)' }}>
|
|
No release notes available.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center gap-2">
|
|
{releaseUrl && (
|
|
<button
|
|
onClick={openReleaseOnGitHub}
|
|
className="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs transition-colors hover:bg-white/5"
|
|
style={{ color: 'var(--color-text-muted)' }}
|
|
>
|
|
<ExternalLink className="size-3" />
|
|
View on GitHub
|
|
</button>
|
|
)}
|
|
<div className="flex-1" />
|
|
<button
|
|
onClick={dismissUpdateDialog}
|
|
className="rounded-md border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-white/5"
|
|
style={{
|
|
borderColor: 'var(--color-border)',
|
|
color: 'var(--color-text-secondary)',
|
|
}}
|
|
>
|
|
Later
|
|
</button>
|
|
{isDownloaded ? (
|
|
<button
|
|
onClick={installUpdate}
|
|
className="rounded-md bg-green-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-green-500"
|
|
>
|
|
Restart now
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={downloadUpdate}
|
|
className="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-500"
|
|
>
|
|
Download
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|