Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,7 @@
## 2024-05-09 - Avoid O(N) array allocation in loops
**Learning:** Using `[...updated].reverse().findIndex(...)` in frequent event callbacks (like stream_text in React) forces O(N) memory allocation and O(N) forward searching each tick, causing major garbage collection pauses. `findLastIndex` is not supported on ES2022 output targets.
**Action:** Replace functional backwards searches that depend on clones with traditional reverse `for` loops to find items instantly in O(1) time without allocations.

## 2026-05-19 - Top-Level Interval Anti-Pattern (SpinnerVerb)
**Learning:** Storing a slow interval state like `spinnerVerb` (every 2.4s) at the top-level `App` component still forces a full application tree O(N) cascade re-render every tick. Even if it is not as fast as a 250ms timer, it causes noticeable micro-stuttering during UI interactions and when large lists are rendered because it triggers diffs across the entire application including Heavy `ChatPanel` and `TabbedRightPanel`.
**Action:** Always extract even slow interval-driven decorative states into isolated leaf components (like `SpinnerVerbDisplay`) wrapped in `React.memo()`. Removing the prop-drilling entirely isolates the re-renders specifically to the UI span rendering the changing string.
24 changes: 1 addition & 23 deletions packages/desktop/src/renderer/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { useState, useCallback, useEffect } from 'react';
import { PanelLeft, PanelRight, Settings, Command } from 'lucide-react';
import type { HostedAgentEvent, ModeState } from '../preload/index';
import ChatPanel from './components/ChatPanel';
Expand Down Expand Up @@ -27,13 +27,6 @@ function uid(): string {
return Date.now().toString(36) + Math.random().toString(36).slice(2);
}

const SPINNER_VERBS = [
'Thinking', 'Analyzing', 'Processing', 'Writing code', 'Reading files',
'Running commands', 'Searching', 'Debugging', 'Building', 'Testing',
'Reviewing', 'Planning', 'Implementing', 'Refactoring', 'Optimizing',
'Generating', 'Compiling', 'Deploying', 'Cooking', 'Locked in',
];

export default function App() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [modeState, setModeState] = useState<ModeState>({
Expand All @@ -52,12 +45,10 @@ export default function App() {
const [needsSetup, setNeedsSetup] = useState(false);
const [showSetup, setShowSetup] = useState(false);
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
const [spinnerVerb, setSpinnerVerb] = useState('');
const [leftPanelOpen, setLeftPanelOpen] = useState(true);
const [rightPanelOpen, setRightPanelOpen] = useState(false);
const [costMode, setCostMode] = useState('normal');
const [activeSessionId, setActiveSessionId] = useState<string | undefined>();
const spinnerIndexRef = useRef(0);

useEffect(() => {
(async () => {
Expand All @@ -83,17 +74,6 @@ export default function App() {
})();
}, []);

useEffect(() => {
if (!isRunning) return;
spinnerIndexRef.current = Math.floor(Math.random() * SPINNER_VERBS.length);
setSpinnerVerb(SPINNER_VERBS[spinnerIndexRef.current]);
const id = setInterval(() => {
spinnerIndexRef.current = (spinnerIndexRef.current + 1) % SPINNER_VERBS.length;
setSpinnerVerb(SPINNER_VERBS[spinnerIndexRef.current]);
}, 2400);
return () => clearInterval(id);
}, [isRunning]);

const handleAgentEvents = useCallback((batch: HostedAgentEvent[]) => {
setMessages((prev) => {
const updated = [...prev];
Expand Down Expand Up @@ -419,7 +399,6 @@ export default function App() {
onSendMessage={handleSendMessage}
onCommand={handleCommand}
isRunning={isRunning}
spinnerVerb={spinnerVerb}
activeModel={activeModel}
activeProvider={activeProvider}
wireFormat={wireFormat}
Expand Down Expand Up @@ -451,7 +430,6 @@ export default function App() {
mode={modeState.current}
workingDir={workingDir}
isRunning={isRunning}
spinnerVerb={spinnerVerb}
activeModel={activeModel}
activeProvider={activeProvider}
wireFormat={wireFormat}
Expand Down
6 changes: 3 additions & 3 deletions packages/desktop/src/renderer/components/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import MessageBubble from './MessageBubble';
import ToolCallCard from './ToolCallCard';
import { cn } from '../lib/utils';
import { useRunElapsed } from '../hooks/useRunElapsed';
import SpinnerVerbDisplay from './SpinnerVerbDisplay';

const ChatPanelTimer = memo(function ChatPanelTimer({ isRunning }: { isRunning: boolean }) {
const runElapsed = useRunElapsed(isRunning);
Expand Down Expand Up @@ -41,7 +42,6 @@ interface ChatPanelProps {
onSendMessage: (content: string) => void;
onCommand: (cmd: string, arg?: string) => void;
isRunning: boolean;
spinnerVerb: string;
activeModel: string;
activeProvider: string;
wireFormat: string;
Expand All @@ -54,7 +54,7 @@ interface ChatPanelProps {
}

export default function ChatPanel({
messages, onSendMessage, onCommand, isRunning, spinnerVerb,
messages, onSendMessage, onCommand, isRunning,
activeModel, activeProvider, wireFormat, appVersion, needsSetup, onOpenSetup, onOpenCommandPalette,
modeState, onModeSwitch,
}: ChatPanelProps) {
Expand Down Expand Up @@ -177,7 +177,7 @@ export default function ChatPanel({
<span className="inline-block h-1.5 w-1.5 rounded-full bg-xibe-text-dim/60 animate-bounce" style={{ animationDelay: '150ms' }} />
<span className="inline-block h-1.5 w-1.5 rounded-full bg-xibe-text-dim/60 animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
<span className="ml-1">{spinnerVerb}</span>
<SpinnerVerbDisplay isRunning={isRunning} className="ml-1" />
<ChatPanelTimer isRunning={isRunning} />
</div>
)}
Expand Down
36 changes: 36 additions & 0 deletions packages/desktop/src/renderer/components/SpinnerVerbDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState, useEffect, useRef, memo } from 'react';
import { cn } from '../lib/utils';

const SPINNER_VERBS = [
'Thinking', 'Analyzing', 'Processing', 'Writing code', 'Reading files',
'Running commands', 'Searching', 'Debugging', 'Building', 'Testing',
'Reviewing', 'Planning', 'Implementing', 'Refactoring', 'Optimizing',
'Generating', 'Compiling', 'Deploying', 'Cooking', 'Locked in',
];

interface SpinnerVerbDisplayProps {
isRunning: boolean;
className?: string;
}

const SpinnerVerbDisplay = memo(function SpinnerVerbDisplay({ isRunning, className }: SpinnerVerbDisplayProps) {
const [spinnerVerb, setSpinnerVerb] = useState('');
const spinnerIndexRef = useRef(0);

useEffect(() => {
if (!isRunning) return;
spinnerIndexRef.current = Math.floor(Math.random() * SPINNER_VERBS.length);
setSpinnerVerb(SPINNER_VERBS[spinnerIndexRef.current]);
Comment on lines +17 to +23
const id = setInterval(() => {
spinnerIndexRef.current = (spinnerIndexRef.current + 1) % SPINNER_VERBS.length;
setSpinnerVerb(SPINNER_VERBS[spinnerIndexRef.current]);
Comment on lines +22 to +26
}, 2400);
return () => clearInterval(id);
}, [isRunning]);

if (!isRunning) return null;

return <span className={cn(className)}>{spinnerVerb}</span>;
});

export default SpinnerVerbDisplay;
6 changes: 3 additions & 3 deletions packages/desktop/src/renderer/components/StatusBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { memo } from 'react';
import { useRunElapsed } from '../hooks/useRunElapsed';
import SpinnerVerbDisplay from './SpinnerVerbDisplay';

const StatusBarTimer = memo(function StatusBarTimer({ isRunning }: { isRunning: boolean }) {
const runElapsed = useRunElapsed(isRunning);
Expand All @@ -16,7 +17,6 @@ interface Props {
mode: string;
workingDir: string;
isRunning: boolean;
spinnerVerb: string;
activeModel: string;
activeProvider: string;
wireFormat: string;
Expand All @@ -33,7 +33,7 @@ const MODE_COLORS: Record<string, string> = {
engineer: 'bg-green-400', data: 'bg-cyan-400', researcher: 'bg-pink-300',
};

export default function StatusBar({ mode, workingDir, isRunning, spinnerVerb, activeModel, activeProvider, wireFormat, costMode, onToggleSidebar, onTogglePreview }: Props) {
export default function StatusBar({ mode, workingDir, isRunning, activeModel, activeProvider, wireFormat, costMode, onToggleSidebar, onTogglePreview }: Props) {
const dot = MODE_COLORS[mode] ?? 'bg-zinc-400';

return (
Expand All @@ -55,7 +55,7 @@ export default function StatusBar({ mode, workingDir, isRunning, spinnerVerb, ac
{isRunning && (
<>
<span className="text-xibe-border">|</span>
<span className="text-xibe-brand-blue">{spinnerVerb}</span>
<SpinnerVerbDisplay isRunning={isRunning} className="text-xibe-brand-blue" />
<StatusBarTimer isRunning={isRunning} />
</>
)}
Expand Down
Loading