Skip to content

Commit 2e262b8

Browse files
authored
Merge pull request #352 from codextde/c/desktop-perf
Make the desktop app faster: lazy screenshots, fewer refetches and re-renders, React Compiler
2 parents ca94f97 + 07afa1d commit 2e262b8

95 files changed

Lines changed: 1044 additions & 546 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/desktop/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"@types/react": "^19.3.0",
7575
"@types/react-dom": "^19.3.0",
7676
"@vitejs/plugin-react": "^6.1.2",
77+
"oxc-transform-react": "0.156.0",
7778
"tailwindcss": "^4.3.3",
7879
"typescript": "^7.0.2",
7980
"vite": "^8.3.5"

‎apps/desktop/src/App.tsx‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { lazy, Suspense, useCallback, useEffect, useRef, useState, type ReactNode } from "react";
1+
import { lazy, Suspense, useCallback, useEffect, useLayoutEffect, useRef, useState, type ReactNode } from "react";
22
import { Navigate, Route, Routes, useLocation, useNavigate, type Location } from "react-router";
33
import { useQuery, useQueryClient } from "@tanstack/react-query";
44
import { CloudErrorCode, type Bootstrap } from "@godmode/shared";
@@ -46,16 +46,23 @@ const SettingsDialog = lazy(() => import("@/pages/settings/settings-page"));
4646

4747
const isSettingsPath = (pathname: string) => pathname === "/settings" || pathname.startsWith("/settings/");
4848

49+
const listenTauri = <T,>(event: string, handler: (e: { payload: T }) => void) => import("@tauri-apps/api/event").then(({ listen }) => listen<T>(event, handler));
50+
51+
const invokeTauri = (cmd: string, args?: Record<string, unknown>) => import("@tauri-apps/api/core").then(({ invoke }) => invoke(cmd, args));
52+
4953
export function App() {
5054
const qc = useQueryClient();
5155
const navigate = useNavigate();
5256
// The page on screen as the router sees it (through Godmode Cloud the address bar has a /d/<device> prefix).
5357
const location = useLocation();
5458
const onScreen = useRef("");
55-
onScreen.current = `${location.pathname}${location.search}`;
59+
const screenPath = `${location.pathname}${location.search}`;
60+
useLayoutEffect(() => {
61+
onScreen.current = screenPath;
62+
}, [screenPath]);
5663
const settingsOpen = isSettingsPath(location.pathname);
57-
const underSettings = useRef<Location | null>(null);
58-
if (!settingsOpen) underSettings.current = location;
64+
const [underSettings, setUnderSettings] = useState<Location | null>(settingsOpen ? null : location);
65+
if (!settingsOpen && underSettings !== location) setUnderSettings(location);
5966
const [coreReady, setCoreReady] = useState(false);
6067
const [coreError, setCoreError] = useState<string | null>(null);
6168
const [cloudIssue, setCloudIssue] = useState<CloudIssue | null>(null);
@@ -72,8 +79,7 @@ export function App() {
7279
if (!isTauri) return;
7380
let stopped = false;
7481
let unlisten: (() => void) | undefined;
75-
void import("@tauri-apps/api/event").then(async ({ listen }) => {
76-
const off = await listen<string>("navigate", (e) => navigate(e.payload));
82+
void listenTauri<string>("navigate", (e) => navigate(e.payload)).then((off) => {
7783
if (stopped) off();
7884
else unlisten = off;
7985
});
@@ -124,7 +130,7 @@ export function App() {
124130
const minimizeToTray = boot.data?.settings.general.minimizeToTray;
125131
useEffect(() => {
126132
if (!isTauri || minimizeToTray === undefined) return;
127-
void import("@tauri-apps/api/core").then(({ invoke }) => invoke("set_close_to_tray", { enabled: minimizeToTray })).catch(() => {});
133+
void invokeTauri("set_close_to_tray", { enabled: minimizeToTray }).catch(() => {});
128134
}, [minimizeToTray]);
129135

130136
// Realtime connection once authenticated
@@ -213,7 +219,7 @@ export function App() {
213219
return (
214220
<AppShell>
215221
<Suspense fallback={<div className="p-8" />}>
216-
<Routes location={settingsOpen ? (underSettings.current ?? "/") : location}>
222+
<Routes location={settingsOpen ? (underSettings ?? "/") : location}>
217223
<Route path="/" element={<ChatHome />} />
218224
<Route path="/chat/:conversationId" element={<ChatConversation />} />
219225
<Route path="/archived" element={<ArchivedPage />} />
@@ -252,7 +258,7 @@ export function App() {
252258
</Suspense>
253259
{settingsOpen && (
254260
<Suspense fallback={null}>
255-
<SettingsDialog closeTo={underSettings.current ?? "/"} />
261+
<SettingsDialog closeTo={underSettings ?? "/"} />
256262
</Suspense>
257263
)}
258264
</AppShell>

‎apps/desktop/src/components/activity/activity-overview.tsx‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { useSearchParams } from "react-router";
33
import { AnimatePresence, motion } from "motion/react";
44
import { format, isToday, isYesterday } from "date-fns";
55
import { useTranslation } from "react-i18next";
6+
import type { TFunction } from "i18next";
67
import { Activity, RefreshCw, Search } from "lucide-react";
78
import type { Agent, Run, RunStatus, RunTrigger } from "@godmode/shared";
89
import { useAllAgents, useRuns } from "@/lib/hooks";
9-
import { dateLocale, t as translate } from "@/i18n";
10+
import { dateLocale } from "@/i18n";
1011
import { errorMessage } from "@/lib/api";
11-
import { useLive, type LiveRun } from "@/stores/live";
12+
import { useLiveRunsMap, type LiveRun } from "@/stores/live";
1213
import { cn } from "@/lib/utils";
1314
import { AgentAvatar, EmptyState } from "@/components/common";
1415
import { Orb } from "@/components/aicss/Orb";
@@ -26,16 +27,16 @@ const STATUSES: ("all" | RunStatus)[] = ["all", "running", "paused", "succeeded"
2627

2728
const TRIGGERS: RunTrigger[] = ["chat", "routine", "check", "dream", "delegation", "task", "followup", "heartbeat", "manual", "api"];
2829

29-
function dayLabel(d: Date) {
30-
if (isToday(d)) return translate("common:words.today");
31-
if (isYesterday(d)) return translate("common:words.yesterday");
32-
return format(d, translate("activity:dayFormat"), { locale: dateLocale() });
30+
function dayLabel(d: Date, t: TFunction<["activity", "runs", "common"]>) {
31+
if (isToday(d)) return t("common:words.today");
32+
if (isYesterday(d)) return t("common:words.yesterday");
33+
return format(d, t("activity:dayFormat"), { locale: dateLocale() });
3334
}
3435

3536
/** Every run of every agent, filtered by agent, trigger and outcome; `?run=` opens one. */
3637
export function ActivityOverview() {
3738
const reduce = useReduceAnimations();
38-
const { t, i18n } = useTranslation(["activity", "runs", "common"]);
39+
const { t } = useTranslation(["activity", "runs", "common"]);
3940
const [params, setParams] = useSearchParams();
4041
const agentFilter = params.get("agent") ?? "all";
4142
const statusFilter = params.get("status") ?? "all";
@@ -46,7 +47,7 @@ export function ActivityOverview() {
4647
const runsQ = useRuns(agentFilter, statusFilter);
4748
const { data: agents = [] } = useAllAgents();
4849
const agentById = useMemo(() => new Map(agents.map((a) => [a.id, a])), [agents]);
49-
const liveRuns = useLive((s) => s.runs);
50+
const liveRuns = useLiveRunsMap();
5051
const liveList = useMemo(
5152
// Working right now; a queued run shows in the list below as queued.
5253
() => Object.values(liveRuns).filter((r) => r.status === "running" && (agentFilter === "all" || r.agentId === agentFilter)),
@@ -75,13 +76,13 @@ export function ActivityOverview() {
7576
const groups = useMemo(() => {
7677
const out: { label: string; runs: Run[] }[] = [];
7778
for (const r of runs) {
78-
const label = dayLabel(new Date(r.startedAt ?? r.createdAt));
79+
const label = dayLabel(new Date(r.startedAt ?? r.createdAt), t);
7980
const last = out[out.length - 1];
8081
if (last?.label === label) last.runs.push(r);
8182
else out.push({ label, runs: [r] });
8283
}
8384
return out;
84-
}, [runs, i18n.language]);
85+
}, [runs, t]);
8586

8687
const successRate = useMemo(() => {
8788
const finished = runs.filter((r) => r.status === "succeeded" || r.status === "failed");

‎apps/desktop/src/components/agents/agent-actions.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { qk } from "@/lib/queryKeys";
1212
import { useMissingLogins, useScopeWorkspace } from "@/lib/hooks";
1313
import { modKey } from "@/lib/desktop";
1414
import { clearDraft, useDraft } from "@/lib/drafts";
15-
import { useLive, type LiveRun } from "@/stores/live";
15+
import { useLive, useLiveRunsMap, type LiveRun } from "@/stores/live";
1616
import { AgentAvatar, Kbd } from "@/components/common";
1717
import { liveMood } from "@/components/chat/conversation-mood";
1818
import { LiveDot } from "@/components/aicss/Motion";
@@ -52,7 +52,7 @@ export function useAgentLiveRuns(agentId: string | undefined): LiveRun[] {
5252
.join(",")
5353
: "",
5454
);
55-
const runs = useLive((s) => s.runs);
55+
const runs = useLiveRunsMap();
5656
return useMemo(() => (ids ? ids.split(",").flatMap((id) => (runs[id] ? [runs[id]] : [])) : []), [ids, runs]);
5757
}
5858

‎apps/desktop/src/components/agents/agent-dnd.tsx‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -324,14 +324,14 @@ function Dock({
324324

325325
function DockTarget({ workspace, here }: { workspace: Workspace | null; here: boolean }) {
326326
const { t } = useTranslation("agents");
327-
const drop = useAgentDrop({ kind: "workspace", workspaceId: workspace?.id ?? null }, `dock:${workspace?.id ?? "global"}`);
327+
const { ref: dropRef, state: dropState } = useAgentDrop({ kind: "workspace", workspaceId: workspace?.id ?? null }, `dock:${workspace?.id ?? "global"}`);
328328
return (
329329
<div
330-
ref={drop.ref}
330+
ref={dropRef}
331331
className={cn(
332332
"flex shrink-0 items-center gap-1.5 rounded-xl border border-transparent px-2 py-1.5 text-sm transition-[background-color,border-color,transform] duration-150",
333333
here ? "text-muted-foreground opacity-50" : "text-foreground",
334-
drop.state === "valid" && "scale-[1.04] border-brand/50 bg-brand/10",
334+
dropState === "valid" && "scale-[1.04] border-brand/50 bg-brand/10",
335335
)}
336336
>
337337
<GroupIcon workspace={workspace} isGlobal={!workspace} className="size-5" />
@@ -342,7 +342,8 @@ function DockTarget({ workspace, here }: { workspace: Workspace | null; here: bo
342342
}
343343

344344
/** Makes an agent draggable; the built-in agent stays put. */
345-
export function useAgentDrag(agent: Agent, id = `agent:${agent.id}`) {
345+
export function useAgentDrag(agent: Agent, dragId?: string) {
346+
const id = dragId ?? `agent:${agent.id}`;
346347
const { setNodeRef, listeners, isDragging } = useDraggable({ id, data: { agent }, disabled: agent.isDefault });
347348
return {
348349
ref: setNodeRef,
@@ -381,9 +382,9 @@ export function DroppableSection({ target, dropId, ...props }: ComponentProps<ty
381382

382383
/** Wraps a card or row so it can be picked up. */
383384
export function DraggableAgent({ agent, className, children }: { agent: Agent; className?: string; children: ReactNode }) {
384-
const drag = useAgentDrag(agent);
385+
const { ref: dragRef, props: dragProps, isDragging } = useAgentDrag(agent);
385386
return (
386-
<div ref={drag.ref} {...drag.props} className={cn("h-full transition-opacity", drag.isDragging && "opacity-35", className)}>
387+
<div ref={dragRef} {...dragProps} className={cn("h-full transition-opacity", isDragging && "opacity-35", className)}>
387388
{children}
388389
</div>
389390
);

‎apps/desktop/src/components/agents/agent-form.tsx‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useMemo, useRef, useState, type ComponentProps, type ReactNode } from "react";
1+
import { useEffect, useEffectEvent, useMemo, useState, type ComponentProps, type ReactNode } from "react";
22
import { useQuery } from "@tanstack/react-query";
33
import { Link, useLocation } from "react-router";
44
import { Trans, useTranslation } from "react-i18next";
@@ -332,15 +332,14 @@ export function AgentForm({
332332
};
333333

334334
// Save shortcut
335-
const submitRef = useRef(submit);
336-
submitRef.current = submit;
335+
const onSaveShortcut = useEffectEvent(() => submit());
337336
useEffect(() => {
338337
const onKey = (e: KeyboardEvent) => {
339338
const mod = isMac ? e.metaKey : e.ctrlKey;
340339
if (mod && e.key.toLowerCase() === "s") {
341340
e.preventDefault();
342341
if (document.querySelector('[role="dialog"], [role="alertdialog"]')) return;
343-
submitRef.current();
342+
onSaveShortcut();
344343
}
345344
};
346345
window.addEventListener("keydown", onKey);
@@ -1315,14 +1314,17 @@ function TeamField({
13151314
const [resetNote, setResetNote] = useState<string | null>(null);
13161315

13171316
// A new workspace can put the chosen lead out of bounds: back to the built-in agent, and say why.
1318-
useEffect(() => {
1317+
const checkLead = useEffectEvent(() => {
13191318
if (!current) return;
13201319
if (leadProblem(self, current, team) === "workspace") {
13211320
setResetNote(t("agentForm.team.resetNote", { lead: builtin?.name ?? "Godmode", name: current.name }));
13221321
onChange(null);
13231322
}
1324-
// eslint-disable-next-line react-hooks/exhaustive-deps
1325-
}, [workspaceId, current?.id]);
1323+
});
1324+
const currentId = current?.id;
1325+
useEffect(() => {
1326+
checkLead();
1327+
}, [workspaceId, currentId]);
13261328

13271329
if (isDefault) {
13281330
return (

‎apps/desktop/src/components/agents/agent-list.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,16 @@ function AgentRow({
6060
const chat = useStartAgentChat();
6161
const chatting = chat.isPending && chat.variables?.agent.id === agent.id;
6262
const lead = leadOf(agent, all);
63-
const drag = useAgentDrag(agent);
63+
const { ref: dragRef, props: dragProps, isDragging } = useAgentDrag(agent);
6464
return (
6565
<li
66-
ref={drag.ref}
67-
{...drag.props}
66+
ref={dragRef}
67+
{...dragProps}
6868
className={cn(
6969
"group relative px-3 py-2 transition-[background-color,opacity] hover:bg-foreground/[0.025]",
7070
COLUMNS,
7171
(!!live || agent.status === "running") && "bg-brand/[0.04]",
72-
drag.isDragging && "opacity-35",
72+
isDragging && "opacity-35",
7373
)}
7474
>
7575
<div className={cn("flex min-w-0 items-center gap-3", !agent.enabled && "opacity-60")}>

‎apps/desktop/src/components/agents/character-studio.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ export function PersonalityPicker({ value, onChange }: { value: string; onChange
162162
const isCustom = !!value.trim() && !personalityPreset(value);
163163
const [custom, setCustom] = useState(isCustom);
164164
const lastCustom = useRef(isCustom ? value : "");
165-
if (isCustom) lastCustom.current = value;
165+
useEffect(() => {
166+
if (isCustom) lastCustom.current = value;
167+
}, [isCustom, value]);
166168
const customOn = custom || isCustom;
167169

168170
const card = (selected: boolean) =>

‎apps/desktop/src/components/agents/detail/history-tab.tsx‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useMemo } from "react";
22
import { useQuery } from "@tanstack/react-query";
33
import { motion } from "motion/react";
4-
import { format, formatDistanceToNowStrict, isToday, isYesterday } from "date-fns";
4+
import { format, formatDistanceToNowStrict, isToday, isYesterday, type Locale } from "date-fns";
55
import { GitCommitHorizontal, History } from "lucide-react";
66
import { useTranslation } from "react-i18next";
77
import type { Agent, GitCommit } from "@godmode/shared";
@@ -11,7 +11,7 @@ import { EmptyState } from "@/components/common";
1111
import { CopyButton } from "@/components/chat/copy-button";
1212
import { Skeleton } from "@/components/ui/skeleton";
1313
import { useReduceAnimations } from "@/lib/motion";
14-
import { dateLocale, t } from "@/i18n";
14+
import { t } from "@/i18n";
1515
import { useLocale } from "@/i18n/language";
1616

1717
/** isomorphic-git reports seconds; accept ISO strings and ms too. */
@@ -23,10 +23,10 @@ function commitDate(ts: string): Date {
2323
return new Date(ts);
2424
}
2525

26-
function dayLabel(d: Date) {
26+
function dayLabel(d: Date, locale: Locale) {
2727
if (isToday(d)) return t("common:words.today");
2828
if (isYesterday(d)) return t("common:words.yesterday");
29-
return format(d, t("agents:historyTab.dayFormat"), { locale: dateLocale() });
29+
return format(d, t("agents:historyTab.dayFormat"), { locale });
3030
}
3131

3232
export function HistoryTab({ agent }: { agent: Agent }) {
@@ -36,13 +36,15 @@ export function HistoryTab({ agent }: { agent: Agent }) {
3636
const q = useQuery({ queryKey: qk.agentCommits(agent.id), queryFn: () => api.agents.commits(agent.id) });
3737

3838
const groups = useMemo(() => {
39-
const out: { label: string; commits: (GitCommit & { date: Date })[] }[] = [];
39+
const out: { label: string; start: number; commits: (GitCommit & { date: Date })[] }[] = [];
40+
let count = 0;
4041
for (const c of q.data ?? []) {
4142
const date = commitDate(c.timestamp);
42-
const label = Number.isNaN(date.getTime()) ? t("historyTab.unknownDate") : dayLabel(date);
43+
const label = Number.isNaN(date.getTime()) ? t("historyTab.unknownDate") : dayLabel(date, dateLocale);
4344
const last = out[out.length - 1];
4445
if (last?.label === label) last.commits.push({ ...c, date });
45-
else out.push({ label, commits: [{ ...c, date }] });
46+
else out.push({ label, start: count, commits: [{ ...c, date }] });
47+
count = count + 1;
4648
}
4749
return out;
4850
}, [q.data, t, dateLocale]);
@@ -67,7 +69,6 @@ export function HistoryTab({ agent }: { agent: Agent }) {
6769
);
6870
}
6971

70-
let index = 0;
7172
return (
7273
<div className="space-y-8">
7374
<p className="text-sm text-muted-foreground">
@@ -78,10 +79,10 @@ export function HistoryTab({ agent }: { agent: Agent }) {
7879
<section key={g.label}>
7980
<h3 className="eyebrow mb-3">{g.label}</h3>
8081
<ol className="relative space-y-1 border-l pl-6">
81-
{g.commits.map((c) => {
82+
{g.commits.map((c, j) => {
8283
const [title, ...rest] = c.message.trim().split("\n");
8384
const body = rest.join("\n").trim();
84-
const i = index++;
85+
const i = g.start + j;
8586
return (
8687
<motion.li
8788
key={c.oid}

‎apps/desktop/src/components/agents/org-chart.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,17 +179,17 @@ function Hub({ agent, dim, reports }: { agent: Agent; dim: boolean; reports: Age
179179
const workspaces = new Set(reports.flatMap((a) => (a.workspaceId ? [a.workspaceId] : []))).size;
180180
const where = global ? t("orgChart.hub.whereGlobalAnd", { count: workspaces }) : t("orgChart.hub.whereWorkspaces", { count: workspaces });
181181
const label = t("orgChart.hub.ariaLabel", { name: agent.name, role: agent.role || t("orgChart.noRoleLower"), status: agentPresenceLabel(presence) });
182-
const drop = useAgentDrop({ kind: "hub", agent }, "hub");
182+
const { ref: dropRef, state: dropState } = useAgentDrop({ kind: "hub", agent }, "hub");
183183
return (
184184
<div className="flex flex-col items-start gap-3 @2xl:flex-row @2xl:items-center">
185185
<div
186-
ref={drop.ref}
186+
ref={dropRef}
187187
className={cn(
188188
"group relative flex w-full max-w-sm items-center gap-3 rounded-xl border bg-card p-3 pr-4 shadow-card transition-[box-shadow,border-color,opacity] hover:border-foreground/15 hover:shadow-float",
189189
presence.state === "working" && "glow-border",
190190
dim && "opacity-40",
191-
drop.state === "available" && "border-brand/30",
192-
drop.state === "valid" && "border-brand/60 ring-4 ring-brand/15",
191+
dropState === "available" && "border-brand/30",
192+
dropState === "valid" && "border-brand/60 ring-4 ring-brand/15",
193193
)}
194194
>
195195
<AgentAvatar agent={agent} size="lg" mood={mood} still={dim} />

0 commit comments

Comments
 (0)