agent-ecosystem/src/renderer/components/settings/SettingsView.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

172 lines
5.4 KiB
TypeScript

/**
* SettingsView - Main settings panel with all app configuration options.
* Provides UI for managing notifications, display settings, and advanced options.
*/
import { useEffect, useState } from 'react';
import { useStore } from '@renderer/store';
import { Loader2 } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import { useSettingsConfig, useSettingsHandlers } from './hooks';
import {
AdvancedSection,
ConnectionSection,
GeneralSection,
NotificationsSection,
} from './sections';
import { type SettingsSection, SettingsTabs } from './SettingsTabs';
export const SettingsView = (): React.JSX.Element | null => {
const [activeSection, setActiveSection] = useState<SettingsSection>('general');
const { pendingSettingsSection, clearPendingSettingsSection } = useStore(
useShallow((s) => ({
pendingSettingsSection: s.pendingSettingsSection,
clearPendingSettingsSection: s.clearPendingSettingsSection,
}))
);
// Consume pending section (avoid setState during render)
useEffect(() => {
if (pendingSettingsSection) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional sync on prop change
setActiveSection(pendingSettingsSection as SettingsSection);
clearPendingSettingsSection();
}
}, [pendingSettingsSection, clearPendingSettingsSection]);
const {
config,
safeConfig,
loading,
saving,
error,
setError,
setSaving,
setConfig,
setOptimisticConfig,
updateConfig,
ignoredRepositoryItems,
excludedRepositoryIds,
isSnoozed,
} = useSettingsConfig();
const handlers = useSettingsHandlers({
config,
setSaving,
setError,
setConfig,
setOptimisticConfig,
updateConfig,
});
// Loading state
if (loading) {
return (
<div
className="flex flex-1 items-center justify-center"
style={{ backgroundColor: 'var(--color-surface)' }}
>
<div className="flex items-center gap-3" style={{ color: 'var(--color-text-muted)' }}>
<Loader2 className="size-5 animate-spin" />
<span>Loading settings...</span>
</div>
</div>
);
}
// Error state
if (error && !config) {
return (
<div
className="flex flex-1 items-center justify-center"
style={{ backgroundColor: 'var(--color-surface)' }}
>
<div className="text-center">
<p className="mb-4 text-red-400">{error}</p>
<button
onClick={() => window.location.reload()}
className="rounded-md px-4 py-2 transition-colors"
style={{
backgroundColor: 'var(--color-surface-raised)',
color: 'var(--color-text-secondary)',
}}
>
Retry
</button>
</div>
</div>
);
}
if (!config) return null;
return (
<div className="flex-1 overflow-auto" style={{ backgroundColor: 'var(--color-surface)' }}>
<div className="mx-auto max-w-2xl px-6 py-8">
{/* Header */}
<div className="mb-6">
<h1 className="text-lg font-medium" style={{ color: 'var(--color-text)' }}>
Settings
</h1>
<p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>
Manage your app preferences
</p>
{error && (
<div className="mt-4 rounded-md border border-red-500/20 bg-red-500/10 px-3 py-2">
<p className="text-sm text-red-400">{error}</p>
</div>
)}
</div>
{/* Tabs */}
<SettingsTabs activeSection={activeSection} onSectionChange={setActiveSection} />
{/* Content */}
<div className="mt-4">
{activeSection === 'general' && (
<GeneralSection
safeConfig={safeConfig}
saving={saving}
onGeneralToggle={handlers.handleGeneralToggle}
onThemeChange={handlers.handleThemeChange}
onLanguageChange={handlers.handleLanguageChange}
/>
)}
{activeSection === 'connection' && <ConnectionSection />}
{activeSection === 'notifications' && (
<NotificationsSection
safeConfig={safeConfig}
saving={saving}
isSnoozed={isSnoozed}
ignoredRepositoryItems={ignoredRepositoryItems}
excludedRepositoryIds={excludedRepositoryIds}
onNotificationToggle={handlers.handleNotificationToggle}
onSnooze={handlers.handleSnooze}
onClearSnooze={handlers.handleClearSnooze}
onAddIgnoredRepository={handlers.handleAddIgnoredRepository}
onRemoveIgnoredRepository={handlers.handleRemoveIgnoredRepository}
onAddTrigger={handlers.handleAddTrigger}
onUpdateTrigger={handlers.handleUpdateTrigger}
onRemoveTrigger={handlers.handleRemoveTrigger}
onStatusChangeStatusesUpdate={handlers.handleStatusChangeStatusesUpdate}
/>
)}
{activeSection === 'advanced' && (
<AdvancedSection
saving={saving}
onResetToDefaults={handlers.handleResetToDefaults}
onExportConfig={handlers.handleExportConfig}
onImportConfig={handlers.handleImportConfig}
onOpenInEditor={handlers.handleOpenInEditor}
/>
)}
</div>
</div>
</div>
);
};