From 54dd7d0f1aeec1e9e57841ba29f763621914798e Mon Sep 17 00:00:00 2001 From: Ben Holmes Date: Sat, 11 Jul 2026 20:17:37 -0400 Subject: [PATCH 1/6] react compiler initial pass --- apps/desktop/electron.vite.config.ts | 6 +- apps/desktop/package.json | 1 + apps/desktop/src/App.tsx | 128 ++-- apps/desktop/src/components/TerminalPanel.tsx | 124 ++-- .../src/components/useSidebarKeyboardNav.ts | 61 +- apps/desktop/src/editor/IframeView.tsx | 4 +- apps/desktop/src/store/actions.test.ts | 37 ++ apps/desktop/src/store/actions.ts | 3 + apps/desktop/src/store/history.ts | 2 - apps/desktop/vite.config.ts | 6 +- apps/www/package.json | 1 + apps/www/src/screens/ConnectScreen.tsx | 2 +- apps/www/src/screens/OpenWorkspaceScreen.tsx | 9 +- apps/www/src/shell/AppShell.tsx | 57 +- apps/www/vite.config.ts | 6 +- packages/ui/package.json | 2 + .../ui/src/components/GlobalSearchPalette.tsx | 47 +- packages/ui/src/components/Sidebar.tsx | 589 ++++++++---------- .../src/components/useSidebarKeyboardNav.ts | 113 ++-- packages/ui/src/components/useSidebarTree.ts | 80 +-- packages/ui/src/editor/EditorView.tsx | 48 +- packages/ui/src/editor/FindBar.tsx | 72 +-- packages/ui/src/editor/FormatCommandMenu.tsx | 64 +- packages/ui/src/editor/LinkPopover.tsx | 415 ++++++------ .../ui/src/editor/MarkdownSourceEditor.tsx | 19 +- .../src/editor/SelectionFormattingToolbar.tsx | 11 +- packages/ui/vite.config.ts | 6 + pnpm-lock.yaml | 66 +- 28 files changed, 972 insertions(+), 1007 deletions(-) diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index e608483b..ead9fd29 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -30,7 +30,11 @@ export default defineConfig({ renderer: { root: ".", plugins: [ - react(), + react({ + babel: { + plugins: ["babel-plugin-react-compiler"], + }, + }), icons({ compiler: "jsx", jsx: "react", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index d0030806..0ea36980 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -68,6 +68,7 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", + "babel-plugin-react-compiler": "^1.0.0", "chokidar": "^4.0.3", "electron": "^42.3.1", "electron-builder": "^26.8.1", diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 3654f74e..ab78b671 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -11,7 +11,7 @@ import { } from "@hubble.md/ui"; import { useStoreValue } from "@simplestack/store/react"; import { keymatch } from "keymatch"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; import MingcutePencilLine from "~icons/mingcute/pencil-line"; import { HtmlAppEmptyState } from "./components/HtmlAppEmptyState"; @@ -103,6 +103,18 @@ async function revealPath(path: string | null) { } } +async function openFilePicker() { + const currentPath = viewerStore.get().currentPath; + const defaultPath = + (isChangelogPath(currentPath) ? null : currentPath) ?? + workspaceStore.get().workspacePath ?? + undefined; + const selected = await desktopApi.openFilePicker({ defaultPath }); + if (typeof selected === "string") { + await loadPath(selected); + } +} + let nextSearchRequestId = 0; /** @@ -141,15 +153,11 @@ function App() { const [dismissedVersion, setDismissedVersion] = useState(null); const [searchOpen, setSearchOpen] = useState(false); const workspaceFiles = useStoreValue(workspaceStore).files; - const paletteFiles: PaletteFile[] = useMemo( - () => - workspaceFiles.map((file) => ({ - path: file.path, - relativePath: relativeWorkspacePath(file.path, workspacePath ?? null), - modifiedAt: file.modified_at, - })), - [workspaceFiles, workspacePath], - ); + const paletteFiles: PaletteFile[] = workspaceFiles.map((file) => ({ + path: file.path, + relativePath: relativeWorkspacePath(file.path, workspacePath ?? null), + modifiedAt: file.modified_at, + })); const lastSeenVersion = useStoreValue(lastSeenVersionStore); const readyVersion = @@ -168,9 +176,9 @@ function App() { lastSeenVersion !== currentVersion ? currentVersion : null; - const markWhatsNewSeen = useCallback(() => { + const markWhatsNewSeen = () => { if (currentVersion) setLastSeenVersion(currentVersion); - }, [currentVersion]); + }; useEffect(() => { // First install has no update to announce; just record the version. @@ -179,15 +187,12 @@ function App() { } }, [currentVersion, lastSeenVersion]); - const openSettings = useCallback(() => { - setSettingsOpen(true); - }, []); - const openWhatsNew = useCallback(() => { + const openWhatsNew = () => { setSettingsOpen(false); void openChangelog(); - }, []); + }; - const installUpdate = useCallback(async () => { + const installUpdate = async () => { try { await desktopApi.installUpdate(); } catch (error) { @@ -195,16 +200,16 @@ function App() { description: error instanceof Error ? error.message : String(error), }); } - }, []); + }; - const triggerPrimaryUpdateAction = useCallback(async () => { + const triggerPrimaryUpdateAction = async () => { if (!updateState?.isSupported) return; if (updateState.status === "ready") { await installUpdate(); return; } await desktopApi.checkForUpdates(); - }, [installUpdate, updateState]); + }; useEffect(() => { const currentPath = state.currentPath; @@ -247,18 +252,6 @@ function App() { }; }, [state.currentPath]); - const openFilePicker = useCallback(async () => { - const currentPath = viewerStore.get().currentPath; - const defaultPath = - (isChangelogPath(currentPath) ? null : currentPath) ?? - workspaceStore.get().workspacePath ?? - undefined; - const selected = await desktopApi.openFilePicker({ defaultPath }); - if (typeof selected === "string") { - await loadPath(selected); - } - }, []); - useEffect(() => { const currentPath = state.currentPath; void desktopApi.setMenuState({ @@ -296,7 +289,7 @@ function App() { await createMarkdownFile(); } else if (keymatch(event, "CmdOrCtrl+,")) { event.preventDefault(); - openSettings(); + setSettingsOpen(true); } else if (keymatch(event, "CmdOrCtrl+Shift+O")) { if (!workspaceStore.get().workspacePath) return; event.preventDefault(); @@ -341,7 +334,7 @@ function App() { }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); - }, [focusedSidebarPath, openFilePicker, openSettings]); + }, [focusedSidebarPath]); useEffect(() => { let active = true; @@ -372,8 +365,11 @@ function App() { desktopApi.onMenuCreateHtmlFile(() => void createHtmlFile()), desktopApi.onMenuOpenFile(() => void openFilePicker()), desktopApi.onMenuOpenFolder(() => void openWorkspaceWithSidebar()), - desktopApi.onMenuOpenSettings(() => openSettings()), - desktopApi.onMenuOpenChangelog(openWhatsNew), + desktopApi.onMenuOpenSettings(() => setSettingsOpen(true)), + desktopApi.onMenuOpenChangelog(() => { + setSettingsOpen(false); + void openChangelog(); + }), desktopApi.onMenuCopyAsMarkdown(() => setCopyAsMarkdownRequest((request) => request + 1), ), @@ -399,7 +395,7 @@ function App() { return () => { for (const dispose of disposers) dispose(); }; - }, [openFilePicker, openSettings, openWhatsNew]); + }, []); useEffect(() => { // Window focus can fire in bursts when switching apps, so debounce the @@ -731,40 +727,34 @@ function MarkdownEditor({ title: wikiDisplayNameForTarget(target), }; }); - const openExternalLink = useCallback( - async (href: string) => { - if (classifyHref(href) === "external") { - await desktopApi.openExternalUrl(href); + const openExternalLink = async (href: string) => { + if (classifyHref(href) === "external") { + await desktopApi.openExternalUrl(href); + return; + } + const resolved = resolveRelativeLinkPath({ + href, + currentFilePath: path, + workspacePath: workspace.workspacePath, + }); + try { + const result = await desktopApi.openPathFromLink(resolved); + if (result.kind === "markdown") await loadPath(result.path); + } catch (error) { + if (error instanceof Error && error.message.includes("Open cancelled")) { return; } - const resolved = resolveRelativeLinkPath({ - href, - currentFilePath: path, - workspacePath: workspace.workspacePath, - }); - try { - const result = await desktopApi.openPathFromLink(resolved); - if (result.kind === "markdown") await loadPath(result.path); - } catch (error) { - if ( - error instanceof Error && - error.message.includes("Open cancelled") - ) { - return; - } - if ( - hasMarkdownExtension(resolved) && - error instanceof Error && - error.message.includes("FILE_NOT_FOUND") - ) { - toast.error(`File not found: ${href.split("#", 1)[0] ?? href}`); - return; - } - throw error; + if ( + hasMarkdownExtension(resolved) && + error instanceof Error && + error.message.includes("FILE_NOT_FOUND") + ) { + toast.error(`File not found: ${href.split("#", 1)[0] ?? href}`); + return; } - }, - [path, workspace.workspacePath], - ); + throw error; + } + }; return ( void, +) { + while (true) { + const requestedWorkspacePath = workspacePathStore.get(); + if (!requestedWorkspacePath) return; + const notePath = viewerStore.get().currentPath ?? undefined; + const sessionId = await desktopApi.terminalStart(requestedWorkspacePath, { + ...(notePath ? { notePath } : {}), + ...options, + }); + if (workspacePathStore.get() === requestedWorkspacePath) { + onStart({ id: sessionId, title }); + return; + } + // The workspace changed while this shell booted. Adopting it would + // show a shell cwd'd to the old workspace, so drop it and retry. + void desktopApi.terminalStop(sessionId); + if (!terminalOpenStore.get()) return; + } +} + const LIGHT_THEME = { black: "#000000", red: "#cd3131", @@ -256,61 +282,15 @@ export function TerminalPanel() { ); }, [sessions, workspacePath]); - // Start sessions while the panel is open: a pending chat command gets its - // own tab; otherwise an empty panel boots a plain shell. sessions.length - // retries a chat launch that arrived while another session was starting. - // biome-ignore lint/correctness/useExhaustiveDependencies: startSession is render-local - useEffect(() => { - if (!isOpen || !workspacePath || isInitializingRef.current) return; - if (!pendingTerminalCommand && sessions.length > 0) return; - - isInitializingRef.current = true; - void (async () => { - try { - if (pendingTerminalCommand) { - await startSession( - pendingTerminalCommand.split(/\s+/, 1)[0] || "chat", - { initialCommand: pendingTerminalCommand }, - ); - clearPendingTerminalCommand(); - } else { - await startSession("bash"); - } - } finally { - isInitializingRef.current = false; - } - })(); - }, [isOpen, pendingTerminalCommand, workspacePath, sessions.length]); - const startSession = async ( title: string, - options: { initialCommand?: string } = {}, + options: StartSessionOptions = {}, ) => { setStartError(null); try { - while (true) { - const requestedWorkspacePath = workspacePathStore.get(); - if (!requestedWorkspacePath) return; - const notePath = viewerStore.get().currentPath ?? undefined; - const sessionId = await desktopApi.terminalStart( - requestedWorkspacePath, - { - ...(notePath ? { notePath } : {}), - ...options, - }, - ); - if (workspacePathStore.get() === requestedWorkspacePath) { - dispatch({ - type: "add", - session: { id: sessionId, title }, - }); - return; - } - // The workspace changed while this shell booted. Adopting it would - // show a shell cwd'd to the old workspace, so drop it and retry. - void desktopApi.terminalStop(sessionId); - if (!terminalOpenStore.get()) return; - } + await startTerminalSession(title, options, (session) => { + dispatch({ type: "add", session }); + }); } catch (error) { console.error("Failed to start terminal session:", error); setStartError( @@ -319,6 +299,31 @@ export function TerminalPanel() { } }; + // Start sessions while the panel is open: a pending chat command gets its + // own tab; otherwise an empty panel boots a plain shell. sessions.length + // retries a chat launch that arrived while another session was starting. + // biome-ignore lint/correctness/useExhaustiveDependencies: startSession is render-local + useEffect(() => { + if (!isOpen || !workspacePath || isInitializingRef.current) return; + if (!pendingTerminalCommand && sessions.length > 0) return; + + isInitializingRef.current = true; + const sessionPromise = pendingTerminalCommand + ? startSession(pendingTerminalCommand.split(/\s+/, 1)[0] || "chat", { + initialCommand: pendingTerminalCommand, + }) + : startSession("bash"); + void sessionPromise.then( + () => { + if (pendingTerminalCommand) clearPendingTerminalCommand(); + isInitializingRef.current = false; + }, + () => { + isInitializingRef.current = false; + }, + ); + }, [isOpen, pendingTerminalCommand, workspacePath, sessions.length]); + const handleCloseSession = async (sessionId: string) => { await desktopApi.terminalStop(sessionId); dispatch({ type: "remove", sessionId }); @@ -567,9 +572,11 @@ function TerminalInstance({ const containerRef = useRef(null); const termRef = useRef(null); const fitAddonRef = useRef(null); - // Read via ref so an unstable callback prop can never remount xterm. + // Read via ref so callback changes never remount xterm. const onExitRef = useRef(onExit); - onExitRef.current = onExit; + useEffect(() => { + onExitRef.current = onExit; + }, [onExit]); // biome-ignore lint/correctness/useExhaustiveDependencies: intentional useEffect(() => { @@ -676,11 +683,18 @@ function TerminalInstance({ fitAddonRef.current && containerRef.current?.offsetParent !== null ) { + const fitAddon = fitAddonRef.current; + const term = termRef.current; + try { + fitAddon.fit(); + } catch { + // Layout may disappear while the debounced fit is running. + } + if (!term) return; try { - fitAddonRef.current.fit(); - termRef.current?.focus(); + term.focus(); } catch { - // + // The terminal may be disposed during panel teardown. } } }, [isActive]); diff --git a/apps/desktop/src/components/useSidebarKeyboardNav.ts b/apps/desktop/src/components/useSidebarKeyboardNav.ts index 8c124c88..38a3208d 100644 --- a/apps/desktop/src/components/useSidebarKeyboardNav.ts +++ b/apps/desktop/src/components/useSidebarKeyboardNav.ts @@ -1,4 +1,4 @@ -import { type RefObject, useCallback, useEffect, useState } from "react"; +import { type RefObject, useEffect, useState } from "react"; import { EDITOR_INPUT_SELECTOR } from "../selectors"; export function useSidebarKeyboardNav({ @@ -13,10 +13,6 @@ export function useSidebarKeyboardNav({ activeIndex?: number; }) { const [focusedIndex, setFocusedIndex] = useState(null); - const getActionIndex = useCallback( - () => focusedIndex ?? (activeIndex >= 0 ? activeIndex : null), - [activeIndex, focusedIndex], - ); useEffect(() => { if (focusedIndex === null) return; @@ -25,39 +21,36 @@ export function useSidebarKeyboardNav({ ?.scrollIntoView({ block: "nearest" }); }, [focusedIndex, navRef]); - const onKeyDown = useCallback( - (event: React.KeyboardEvent) => { - if (items.length === 0) return; + const onKeyDown = (event: React.KeyboardEvent) => { + if (items.length === 0) return; - switch (event.key) { - case "ArrowDown": - case "ArrowUp": { - event.preventDefault(); - const delta = event.key === "ArrowDown" ? 1 : -1; - setFocusedIndex((prev) => { - const start = prev ?? (activeIndex >= 0 ? activeIndex : -1); - return Math.max(0, Math.min(start + delta, items.length - 1)); - }); - break; - } - case "Enter": { - const idx = getActionIndex(); - if (idx !== null && items[idx]) { - event.preventDefault(); - onSelect(items[idx]); - } - break; - } - case "Escape": { + switch (event.key) { + case "ArrowDown": + case "ArrowUp": { + event.preventDefault(); + const delta = event.key === "ArrowDown" ? 1 : -1; + setFocusedIndex((prev) => { + const start = prev ?? (activeIndex >= 0 ? activeIndex : -1); + return Math.max(0, Math.min(start + delta, items.length - 1)); + }); + break; + } + case "Enter": { + const idx = focusedIndex ?? (activeIndex >= 0 ? activeIndex : null); + if (idx !== null && items[idx]) { event.preventDefault(); - setFocusedIndex(null); - document.querySelector(EDITOR_INPUT_SELECTOR)?.focus(); - break; + onSelect(items[idx]); } + break; + } + case "Escape": { + event.preventDefault(); + setFocusedIndex(null); + document.querySelector(EDITOR_INPUT_SELECTOR)?.focus(); + break; } - }, - [items, onSelect, activeIndex, getActionIndex], - ); + } + }; return { focusedIndex, setFocusedIndex, onKeyDown }; } diff --git a/apps/desktop/src/editor/IframeView.tsx b/apps/desktop/src/editor/IframeView.tsx index d65a5bdf..00742b1d 100644 --- a/apps/desktop/src/editor/IframeView.tsx +++ b/apps/desktop/src/editor/IframeView.tsx @@ -87,7 +87,7 @@ export function IframeView({ onHeightChange, }: IframeViewProps) { const iframeRef = useRef(null); - const tokenRef = useRef(crypto.randomUUID()); + const [token] = useState(() => crypto.randomUUID()); const [error, setError] = useState(null); useEffect(() => { @@ -144,7 +144,7 @@ export function IframeView({