From 7e4bcfb8c2f124b0cb1ca5f34134c356df1f1a9e Mon Sep 17 00:00:00 2001 From: Wes Date: Wed, 8 Apr 2026 11:13:14 -0700 Subject: [PATCH] [codex] Reduce desktop release startup work (#269) --- desktop/scripts/build-release-config.mjs | 6 + desktop/src-tauri/build.rs | 16 +++ desktop/src-tauri/src/lib.rs | 8 +- desktop/src-tauri/tauri.conf.json | 5 - desktop/src/app/AppShell.tsx | 80 ++++++++----- desktop/src/app/routes/ChannelRouteScreen.tsx | 96 ++++++++++++++++ .../src/app/routes/WorkflowsRouteScreen.tsx | 27 +++++ .../channels.$channelId.posts.$postId.tsx | 24 ++-- .../src/app/routes/channels.$channelId.tsx | 105 +++--------------- .../src/app/routes/workflows.$workflowId.tsx | 16 ++- desktop/src/app/routes/workflows.tsx | 38 +++---- desktop/src/app/useWebviewZoomShortcuts.ts | 2 +- desktop/src/features/home/ui/HomeView.tsx | 80 +++++++------ desktop/tests/e2e/integration.spec.ts | 4 + desktop/tests/e2e/profile.spec.ts | 3 + desktop/tests/e2e/stream.spec.ts | 2 + 16 files changed, 315 insertions(+), 197 deletions(-) create mode 100644 desktop/src/app/routes/ChannelRouteScreen.tsx create mode 100644 desktop/src/app/routes/WorkflowsRouteScreen.tsx diff --git a/desktop/scripts/build-release-config.mjs b/desktop/scripts/build-release-config.mjs index 0ffd7a6b8..3de486517 100644 --- a/desktop/scripts/build-release-config.mjs +++ b/desktop/scripts/build-release-config.mjs @@ -32,6 +32,12 @@ if (publicKey && endpoint) { const missing = []; if (!publicKey) missing.push("SPROUT_UPDATER_PUBLIC_KEY"); if (!endpoint) missing.push("SPROUT_UPDATER_ENDPOINT"); + if (releaseConfig.plugins) { + delete releaseConfig.plugins.updater; + if (Object.keys(releaseConfig.plugins).length === 0) { + delete releaseConfig.plugins; + } + } console.log(`Updater config skipped (missing: ${missing.join(", ")})`); } diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs index cb6618e1c..e148451a3 100644 --- a/desktop/src-tauri/build.rs +++ b/desktop/src-tauri/build.rs @@ -1,6 +1,9 @@ fn main() { println!("cargo:rerun-if-env-changed=SPROUT_RELAY_URL"); println!("cargo:rerun-if-env-changed=SPROUT_RELAY_HTTP"); + println!("cargo:rerun-if-env-changed=SPROUT_UPDATER_PUBLIC_KEY"); + println!("cargo:rerun-if-env-changed=SPROUT_UPDATER_ENDPOINT"); + println!("cargo:rustc-check-cfg=cfg(sprout_updater_enabled)"); if let Ok(relay_url) = std::env::var("SPROUT_RELAY_URL") { println!("cargo:rustc-env=SPROUT_DESKTOP_BUILD_RELAY_URL={relay_url}"); @@ -10,5 +13,18 @@ fn main() { println!("cargo:rustc-env=SPROUT_DESKTOP_BUILD_RELAY_HTTP={relay_http}"); } + let updater_public_key = std::env::var("SPROUT_UPDATER_PUBLIC_KEY") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()); + let updater_endpoint = std::env::var("SPROUT_UPDATER_ENDPOINT") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()); + + if updater_public_key.is_some() && updater_endpoint.is_some() { + println!("cargo:rustc-cfg=sprout_updater_enabled"); + } + tauri_build::build() } diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 9560e7115..5aa89028b 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -288,13 +288,19 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_process::init()); - // The updater config is only generated for signed release builds. + // Only register the updater in release builds that were compiled with a + // real updater configuration. Local unsigned builds omit that config and + // should still launch for debugging. + #[cfg(sprout_updater_enabled)] let builder = if cfg!(debug_assertions) { builder } else { builder.plugin(tauri_plugin_updater::Builder::new().build()) }; + #[cfg(not(sprout_updater_enabled))] + let builder = builder; + let shutdown_started = Arc::new(AtomicBool::new(false)); let restore_shutdown_started = Arc::clone(&shutdown_started); let app = builder diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 4b12536d3..5bdfc39f1 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -30,11 +30,6 @@ "csp": null } }, - "plugins": { - "updater": { - "endpoints": [] - } - }, "bundle": { "active": true, "targets": "all", diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 4e579cee6..7123b3d99 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -29,7 +29,6 @@ import { import { usePresenceSession } from "@/features/presence/hooks"; import { useProfileQuery } from "@/features/profile/hooks"; import type { SettingsSection } from "@/features/settings/ui/SettingsPanels"; -import { SettingsScreen } from "@/features/settings/ui/SettingsScreen"; import { AppSidebar } from "@/features/sidebar/ui/AppSidebar"; import { relayClient } from "@/shared/api/relayClient"; import { useIdentityQuery } from "@/shared/api/hooks"; @@ -46,6 +45,11 @@ import { type AppView = "home" | "channel" | "agents" | "workflows"; const DEFAULT_SETTINGS_SECTION: SettingsSection = "profile"; +const LazySettingsScreen = React.lazy(async () => { + const module = await import("@/features/settings/ui/SettingsScreen"); + return { default: module.SettingsScreen }; +}); + function toSearchHit(target: DesktopNotificationTarget): SearchHit | null { if (!target.eventId) { return null; @@ -250,14 +254,32 @@ export function AppShell() { React.useEffect(() => { let isCancelled = false; - void relayClient.preconnect().catch((error) => { - if (!isCancelled) { - console.error("Failed to preconnect to relay", error); + const startPreconnect = () => { + if (isCancelled) { + return; } - }); + void relayClient.preconnect().catch((error) => { + if (!isCancelled) { + console.error("Failed to preconnect to relay", error); + } + }); + }; + + if ("requestIdleCallback" in window) { + const idleId = window.requestIdleCallback(startPreconnect, { + timeout: 1_500, + }); + return () => { + isCancelled = true; + window.cancelIdleCallback(idleId); + }; + } + + const timeoutId = globalThis.setTimeout(startPreconnect, 250); return () => { isCancelled = true; + globalThis.clearTimeout(timeoutId); }; }, []); @@ -486,29 +508,31 @@ export function AppShell() { /> {settingsOpen ? ( - + + + ) : null} diff --git a/desktop/src/app/routes/ChannelRouteScreen.tsx b/desktop/src/app/routes/ChannelRouteScreen.tsx new file mode 100644 index 000000000..08a6df924 --- /dev/null +++ b/desktop/src/app/routes/ChannelRouteScreen.tsx @@ -0,0 +1,96 @@ +import * as React from "react"; + +import { getCachedSearchHitEvent } from "@/app/navigation/searchHitEventCache"; +import { useAppNavigation } from "@/app/navigation/useAppNavigation"; +import { useChannelsQuery } from "@/features/channels/hooks"; +import { ChannelScreen } from "@/features/channels/ui/ChannelScreen"; +import { useProfileQuery } from "@/features/profile/hooks"; +import { useIdentityQuery } from "@/shared/api/hooks"; +import { getEventById } from "@/shared/api/tauri"; +import type { RelayEvent } from "@/shared/api/types"; +import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback"; + +type ChannelRouteScreenProps = { + channelId: string; + selectedPostId: string | null; + targetMessageId: string | null; + targetReplyId: string | null; +}; + +export function ChannelRouteScreen({ + channelId, + selectedPostId, + targetMessageId, + targetReplyId, +}: ChannelRouteScreenProps) { + const { closeForumPost, goForumPost } = useAppNavigation(); + const channelsQuery = useChannelsQuery(); + const identityQuery = useIdentityQuery(); + const profileQuery = useProfileQuery(); + const channels = channelsQuery.data ?? []; + const activeChannel = + channels.find((channel) => channel.id === channelId) ?? null; + const [targetMessageEvent, setTargetMessageEvent] = + React.useState(() => + getCachedSearchHitEvent(targetMessageId), + ); + + React.useEffect(() => { + let isCancelled = false; + + if (!targetMessageId || selectedPostId) { + setTargetMessageEvent(null); + return () => { + isCancelled = true; + }; + } + + setTargetMessageEvent(getCachedSearchHitEvent(targetMessageId)); + void getEventById(targetMessageId) + .then((event) => { + if (!isCancelled) { + setTargetMessageEvent(event); + } + }) + .catch((error) => { + if (!isCancelled) { + console.error( + "Failed to load route target event", + targetMessageId, + error, + ); + } + }); + + return () => { + isCancelled = true; + }; + }, [selectedPostId, targetMessageId]); + + if (channelsQuery.isPending && !activeChannel) { + return ( + + ); + } + + return ( + { + void closeForumPost(channelId); + }} + onSelectForumPost={(postId) => { + void goForumPost(channelId, postId); + }} + selectedForumPostId={selectedPostId} + targetForumReplyId={targetReplyId} + targetMessageEvent={targetMessageEvent} + targetMessageId={targetMessageId} + /> + ); +} diff --git a/desktop/src/app/routes/WorkflowsRouteScreen.tsx b/desktop/src/app/routes/WorkflowsRouteScreen.tsx new file mode 100644 index 000000000..0a0a4dfb3 --- /dev/null +++ b/desktop/src/app/routes/WorkflowsRouteScreen.tsx @@ -0,0 +1,27 @@ +import { useAppNavigation } from "@/app/navigation/useAppNavigation"; +import { useChannelsQuery } from "@/features/channels/hooks"; +import { WorkflowsScreen } from "@/features/workflows/ui/WorkflowsScreen"; + +type WorkflowsRouteScreenProps = { + selectedWorkflowId: string | null; +}; + +export function WorkflowsRouteScreen({ + selectedWorkflowId, +}: WorkflowsRouteScreenProps) { + const { closeWorkflowDetail, goWorkflow } = useAppNavigation(); + const channelsQuery = useChannelsQuery(); + const channels = channelsQuery.data ?? []; + const memberChannels = channels.filter((channel) => channel.isMember); + + return ( + { + void goWorkflow(workflowId); + }} + selectedWorkflowId={selectedWorkflowId} + /> + ); +} diff --git a/desktop/src/app/routes/channels.$channelId.posts.$postId.tsx b/desktop/src/app/routes/channels.$channelId.posts.$postId.tsx index 48f57c483..94f153d5e 100644 --- a/desktop/src/app/routes/channels.$channelId.posts.$postId.tsx +++ b/desktop/src/app/routes/channels.$channelId.posts.$postId.tsx @@ -1,6 +1,7 @@ +import * as React from "react"; import { createFileRoute } from "@tanstack/react-router"; -import { ChannelRouteScreen } from "@/app/routes/channels.$channelId"; +import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback"; type ForumPostRouteSearch = { replyId?: string; @@ -22,16 +23,25 @@ export const Route = createFileRoute("/channels/$channelId/posts/$postId")({ component: ForumPostRouteComponent, }); +const ChannelRouteScreen = React.lazy(async () => { + const module = await import("./ChannelRouteScreen"); + return { default: module.ChannelRouteScreen }; +}); + function ForumPostRouteComponent() { const { channelId, postId } = Route.useParams(); const search = Route.useSearch(); return ( - + } + > + + ); } diff --git a/desktop/src/app/routes/channels.$channelId.tsx b/desktop/src/app/routes/channels.$channelId.tsx index d359dcfe4..2394d5fb0 100644 --- a/desktop/src/app/routes/channels.$channelId.tsx +++ b/desktop/src/app/routes/channels.$channelId.tsx @@ -1,14 +1,6 @@ import * as React from "react"; import { createFileRoute } from "@tanstack/react-router"; -import { getCachedSearchHitEvent } from "@/app/navigation/searchHitEventCache"; -import { useAppNavigation } from "@/app/navigation/useAppNavigation"; -import { useChannelsQuery } from "@/features/channels/hooks"; -import { ChannelScreen } from "@/features/channels/ui/ChannelScreen"; -import { useProfileQuery } from "@/features/profile/hooks"; -import { useIdentityQuery } from "@/shared/api/hooks"; -import { getEventById } from "@/shared/api/tauri"; -import type { RelayEvent } from "@/shared/api/types"; import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback"; type ChannelRouteSearch = { @@ -31,94 +23,25 @@ export const Route = createFileRoute("/channels/$channelId")({ component: ChannelRouteComponent, }); -export function ChannelRouteScreen({ - channelId, - selectedPostId, - targetMessageId, - targetReplyId, -}: { - channelId: string; - selectedPostId: string | null; - targetMessageId: string | null; - targetReplyId: string | null; -}) { - const { closeForumPost, goForumPost } = useAppNavigation(); - const channelsQuery = useChannelsQuery(); - const identityQuery = useIdentityQuery(); - const profileQuery = useProfileQuery(); - const channels = channelsQuery.data ?? []; - const activeChannel = - channels.find((channel) => channel.id === channelId) ?? null; - const [targetMessageEvent, setTargetMessageEvent] = - React.useState(() => - getCachedSearchHitEvent(targetMessageId), - ); - - React.useEffect(() => { - let isCancelled = false; - - if (!targetMessageId || selectedPostId) { - setTargetMessageEvent(null); - return () => { - isCancelled = true; - }; - } - - setTargetMessageEvent(getCachedSearchHitEvent(targetMessageId)); - void getEventById(targetMessageId) - .then((event) => { - if (!isCancelled) { - setTargetMessageEvent(event); - } - }) - .catch((error) => { - if (!isCancelled) { - console.error( - "Failed to load route target event", - targetMessageId, - error, - ); - } - }); - - return () => { - isCancelled = true; - }; - }, [selectedPostId, targetMessageId]); - - if (channelsQuery.isPending && !activeChannel) { - return ; - } - - return ( - { - void closeForumPost(channelId); - }} - onSelectForumPost={(postId) => { - void goForumPost(channelId, postId); - }} - selectedForumPostId={selectedPostId} - targetForumReplyId={targetReplyId} - targetMessageEvent={targetMessageEvent} - targetMessageId={targetMessageId} - /> - ); -} +const ChannelRouteScreen = React.lazy(async () => { + const module = await import("./ChannelRouteScreen"); + return { default: module.ChannelRouteScreen }; +}); function ChannelRouteComponent() { const { channelId } = Route.useParams(); const search = Route.useSearch(); return ( - + } + > + + ); } diff --git a/desktop/src/app/routes/workflows.$workflowId.tsx b/desktop/src/app/routes/workflows.$workflowId.tsx index 83e068469..2639f8646 100644 --- a/desktop/src/app/routes/workflows.$workflowId.tsx +++ b/desktop/src/app/routes/workflows.$workflowId.tsx @@ -1,13 +1,25 @@ +import * as React from "react"; import { createFileRoute } from "@tanstack/react-router"; -import { WorkflowsRouteScreen } from "@/app/routes/workflows"; +import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback"; export const Route = createFileRoute("/workflows/$workflowId")({ component: WorkflowDetailRouteComponent, }); +const WorkflowsRouteScreen = React.lazy(async () => { + const module = await import("./WorkflowsRouteScreen"); + return { default: module.WorkflowsRouteScreen }; +}); + function WorkflowDetailRouteComponent() { const { workflowId } = Route.useParams(); - return ; + return ( + } + > + + + ); } diff --git a/desktop/src/app/routes/workflows.tsx b/desktop/src/app/routes/workflows.tsx index e00fc7630..00d9a26d7 100644 --- a/desktop/src/app/routes/workflows.tsx +++ b/desktop/src/app/routes/workflows.tsx @@ -1,35 +1,23 @@ +import * as React from "react"; import { createFileRoute } from "@tanstack/react-router"; -import { useAppNavigation } from "@/app/navigation/useAppNavigation"; -import { useChannelsQuery } from "@/features/channels/hooks"; -import { WorkflowsScreen } from "@/features/workflows/ui/WorkflowsScreen"; +import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback"; export const Route = createFileRoute("/workflows")({ component: WorkflowsRouteComponent, }); -export function WorkflowsRouteScreen({ - selectedWorkflowId, -}: { - selectedWorkflowId: string | null; -}) { - const { closeWorkflowDetail, goWorkflow } = useAppNavigation(); - const channelsQuery = useChannelsQuery(); - const channels = channelsQuery.data ?? []; - const memberChannels = channels.filter((channel) => channel.isMember); - - return ( - { - void goWorkflow(workflowId); - }} - selectedWorkflowId={selectedWorkflowId} - /> - ); -} +const WorkflowsRouteScreen = React.lazy(async () => { + const module = await import("./WorkflowsRouteScreen"); + return { default: module.WorkflowsRouteScreen }; +}); function WorkflowsRouteComponent() { - return ; + return ( + } + > + + + ); } diff --git a/desktop/src/app/useWebviewZoomShortcuts.ts b/desktop/src/app/useWebviewZoomShortcuts.ts index 1c4c4d71d..182927742 100644 --- a/desktop/src/app/useWebviewZoomShortcuts.ts +++ b/desktop/src/app/useWebviewZoomShortcuts.ts @@ -56,7 +56,7 @@ function getNextZoomFactor(action: ZoomAction, zoomFactor: number) { export function useWebviewZoomShortcuts() { const zoomFactorRef = React.useRef(DEFAULT_ZOOM_FACTOR); - React.useEffect(() => { + React.useLayoutEffect(() => { const webview = getCurrentWebview(); function handleKeyDown(event: KeyboardEvent) { diff --git a/desktop/src/features/home/ui/HomeView.tsx b/desktop/src/features/home/ui/HomeView.tsx index c038510b0..9b31f8195 100644 --- a/desktop/src/features/home/ui/HomeView.tsx +++ b/desktop/src/features/home/ui/HomeView.tsx @@ -6,7 +6,11 @@ import { useUsersBatchQuery } from "@/features/profile/hooks"; import type { HomeFeedResponse } from "@/shared/api/types"; import { Button } from "@/shared/ui/button"; import { Skeleton } from "@/shared/ui/skeleton"; -import { FeedSection } from "./FeedSection"; + +const FeedSection = React.lazy(async () => { + const module = await import("./FeedSection"); + return { default: module.FeedSection }; +}); type FeedFilter = "all" | "mention" | "needs_action"; @@ -117,42 +121,44 @@ export function HomeView({ ))} -
- {showMentions ? ( - - ) : null} - {showNeedsAction ? ( - - ) : null} -
+ +
+ {showMentions ? ( + + ) : null} + {showNeedsAction ? ( + + ) : null} +
+
); diff --git a/desktop/tests/e2e/integration.spec.ts b/desktop/tests/e2e/integration.spec.ts index 2c6a88434..2e3b73df0 100644 --- a/desktop/tests/e2e/integration.spec.ts +++ b/desktop/tests/e2e/integration.spec.ts @@ -4,6 +4,9 @@ import { installRelayBridge, TEST_IDENTITIES } from "../helpers/bridge"; import { openSettings } from "../helpers/settings"; import { assertRelaySeeded } from "../helpers/seed"; +const isCi = Boolean(process.env.CI); +const relaySeedHookTimeoutMs = isCi ? 90_000 : 30_000; + async function createStream( page: import("@playwright/test").Page, channelName: string, @@ -156,6 +159,7 @@ async function getLoggedNotificationCount( } test.beforeAll(async () => { + test.setTimeout(relaySeedHookTimeoutMs); await assertRelaySeeded(); }); diff --git a/desktop/tests/e2e/profile.spec.ts b/desktop/tests/e2e/profile.spec.ts index 8904f0b11..67a670b42 100644 --- a/desktop/tests/e2e/profile.spec.ts +++ b/desktop/tests/e2e/profile.spec.ts @@ -280,12 +280,14 @@ test("opens settings with the keyboard shortcut and updates theme", async ({ page, }) => { await page.goto("/"); + await expect(page.getByTestId("chat-title")).toHaveText("Home"); await page.keyboard.press( process.platform === "darwin" ? "Meta+," : "Control+,", ); await expect(page.getByTestId("settings-view")).toBeVisible(); + await expect(page.getByTestId("settings-nav-appearance")).toBeVisible(); await page.getByTestId("settings-nav-appearance").click(); // Default theme is catppuccin-macchiato (dark) @@ -347,6 +349,7 @@ test("opens settings with the keyboard shortcut and updates theme", async ({ test("supports webview zoom keyboard shortcuts", async ({ page }) => { await page.goto("/"); + await expect(page.getByTestId("chat-title")).toHaveText("Home"); await page.keyboard.press( process.platform === "darwin" ? "Meta+Shift+Equal" : "Control+Shift+Equal", diff --git a/desktop/tests/e2e/stream.spec.ts b/desktop/tests/e2e/stream.spec.ts index f149b7a1c..97083fb9c 100644 --- a/desktop/tests/e2e/stream.spec.ts +++ b/desktop/tests/e2e/stream.spec.ts @@ -5,6 +5,7 @@ import { assertRelaySeeded } from "../helpers/seed"; const isCi = Boolean(process.env.CI); const relayDeliveryTimeoutMs = isCi ? 15_000 : 5_000; +const relaySeedHookTimeoutMs = isCi ? 90_000 : 30_000; async function expectTimelineToContain(page: Page, text: string) { await expect(page.getByTestId("message-timeline")).toContainText(text, { @@ -94,6 +95,7 @@ async function scrollTimelineAwayFromBottom(page: Page, minDistance = 160) { } test.beforeAll(async () => { + test.setTimeout(relaySeedHookTimeoutMs); await assertRelaySeeded(); });