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
6 changes: 5 additions & 1 deletion .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@

**Learning:** Recreating `CanvasGradient` objects (like `createRadialGradient`) inside a 60fps render loop causes unnecessary garbage collection pressure and CPU overhead from parsing color strings (`addColorStop`).

**Action:** Cache gradients and dimension measurements at the `resize` event level rather than the `draw` level, as they only change when the window bounds change.
**Action:** Cache gradients and dimension measurements at the `resize` event level rather than the `draw` level, as they only change when the window bounds change.
## 2024-05-18 - App-wide Re-renders from Root Timers

**Learning:** When a React state that updates frequently (e.g., `now` updating every second) is placed at the top level of the app (`App.tsx`), it causes the entire component tree to re-render. If this state is only needed by a deeply nested component (e.g., a countdown timer), it introduces massive unnecessary overhead.
**Action:** Push frequently updating state down the component tree as close as possible to where it is used. Avoid placing intervals in root components.
16 changes: 1 addition & 15 deletions extension/webview/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export function App() {
const [projectReport, setProjectReport] = useState<MonadProjectReport>();
const [attestation, setAttestation] = useState<Attestation>();
const [notice, setNotice] = useState<{ tone: "error" | "ok"; message: string }>();
const [now, setNow] = useState(Date.now());
const send = (message: unknown) => vscode.postMessage(message);

useEffect(() => {
Expand Down Expand Up @@ -116,12 +115,6 @@ export function App() {
return () => window.removeEventListener("message", listener);
}, []);

useEffect(() => {
if (state?.rep.phase !== "active") return;
const timer = window.setInterval(() => setNow(Date.now()), 1000);
return () => window.clearInterval(timer);
}, [state?.rep.phase]);

useEffect(() => {
if (!notice) return;
const timeout = window.setTimeout(() => setNotice(undefined), 4500);
Expand All @@ -140,13 +133,6 @@ export function App() {
setMentorResult(undefined);
send({ type: "mentor", mode, reasoning });
};
const remaining = useMemo(() => {
if (!state?.rep.startedAt) return "00:00";
const end = state.rep.startedAt + state.rep.durationMinutes * 60_000;
const seconds = Math.max(0, Math.ceil((end - now) / 1000));
return `${String(Math.floor(seconds / 60)).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")}`;
}, [state?.rep.startedAt, state?.rep.durationMinutes, now]);

if (!state) return <LoadingScreen />;

return (
Expand Down Expand Up @@ -188,7 +174,7 @@ export function App() {
send={send}
/>
)}
{route === "focus" && <FocusDock state={state} remaining={remaining} send={send} />}
{route === "focus" && <FocusDock state={state} send={send} />}
{route === "monad" && <MonadDock state={state} loading={monadLoading} inspection={inspection} report={projectReport} attestation={attestation} send={send} />}
</main>

Expand Down
26 changes: 21 additions & 5 deletions extension/webview/FocusDock.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { FormEvent, useState } from "react";
import { FormEvent, useEffect, useMemo, useState } from "react";
import type { ClientState, Ownership } from "../src/types";
import { FocusIcon, TestIcon } from "./SidebarIcons";

interface Props {
state: ClientState;
remaining: string;
send(message: unknown): void;
}

export function FocusDock({ state, remaining, send }: Props) {
export function FocusDock({ state, send }: Props) {
const { rep } = state;
if (rep.phase === "idle") return <StartFocus aiExtensions={state.aiExtensions} send={send} />;
if (rep.phase === "active") return <ActiveFocus state={state} remaining={remaining} send={send} />;
if (rep.phase === "active") return <ActiveFocus state={state} send={send} />;
return <ReviewFocus state={state} send={send} />;
}

Expand Down Expand Up @@ -89,11 +88,28 @@ function StartFocus({ aiExtensions, send }: { aiExtensions: string[]; send(messa
);
}

function ActiveFocus({ state, remaining, send }: Props) {
function ActiveFocus({ state, send }: Props) {
const [hypothesis, setHypothesis] = useState("");
const [finishing, setFinishing] = useState(false);
const [outcome, setOutcome] = useState("");
const [ownership, setOwnership] = useState<Ownership>(2);
const [now, setNow] = useState(Date.now());

// ⚑ Bolt: Moved 1-second interval timer and `remaining` state down from App.tsx into ActiveFocus.
// Expected impact: Eliminates forced 1-second application-wide re-renders in App.tsx when Focus Rep is active.

useEffect(() => {
if (state?.rep.phase !== "active") return;
const timer = window.setInterval(() => setNow(Date.now()), 1000);
return () => window.clearInterval(timer);
}, [state?.rep.phase]);

const remaining = useMemo(() => {
if (!state?.rep.startedAt) return "00:00";
const end = state.rep.startedAt + state.rep.durationMinutes * 60_000;
const seconds = Math.max(0, Math.ceil((end - now) / 1000));
return `${String(Math.floor(seconds / 60)).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")}`;
}, [state?.rep.startedAt, state?.rep.durationMinutes, now]);
const add = (event: FormEvent) => {
event.preventDefault();
send({ type: "addHypothesis", text: hypothesis });
Expand Down
Loading