@@ -1,15 +1,23 @@ |
| 1 | | -import { useMemo } from "react"; |
| 1 | +import { lazy, Suspense, useMemo } from "react"; |
| 2 | 2 | |
| 3 | 3 | import { ArchiveChatBanner } from "@/components/ArchiveChatBanner"; |
| 4 | 4 | import { ChatInput } from "@/components/ChatInput"; |
| 5 | 5 | import { MessageTimeline } from "@/components/MessageTimeline"; |
| 6 | | -import { TerminalPane } from "@/components/TerminalPane"; |
| 7 | 6 | import { TurnStatusBanner } from "@/components/TurnStatusBanner"; |
| 8 | 7 | import { PaneHeader } from "@/components/panes/PaneHeader"; |
| 9 | 8 | import { shortModel } from "@/lib/format"; |
| 10 | 9 | import { useSessionStore } from "@/lib/store/sessions"; |
| 11 | 10 | import type { SessionSummary } from "@/lib/ipc/types"; |
| 12 | 11 | |
| 12 | +// Code-split xterm. The terminal stack (xterm core + fit + webgl + |
| 13 | +// unicode11 + web-links) is ~230 KB gzipped — that's a third of the |
| 14 | +// total JS bundle. Lazy-loading keeps it out of initial paint so |
| 15 | +// the app starts faster, and users who never open a terminal |
| 16 | +// session never pay the cost at all. |
| 17 | +const TerminalPane = lazy(() => |
| 18 | + import("@/components/TerminalPane").then((m) => ({ default: m.TerminalPane })), |
| 19 | +); |
| 20 | + |
| 13 | 21 | export function ViewerPane() { |
| 14 | 22 | const detail = useSessionStore((s) => s.detail); |
| 15 | 23 | const loading = useSessionStore((s) => s.loading.detail); |
@@ -125,12 +133,18 @@ export function ViewerPane() { |
| 125 | 133 | // key forces a fresh mount per session — xterm state is |
| 126 | 134 | // bound to sessionId so switching sessions must teardown |
| 127 | 135 | // and re-open (the backend PTY keeps running regardless). |
| 128 | | - <TerminalPane |
| 129 | | - key={summary.id} |
| 130 | | - sessionId={summary.id} |
| 131 | | - cwd={summary.cwd} |
| 132 | | - claudeArgs={claudeArgs} |
| 133 | | - /> |
| 136 | + // |
| 137 | + // Suspense fallback renders while the lazy chunk downloads |
| 138 | + // on first use. Once loaded, the chunk is cached so |
| 139 | + // subsequent mounts are instant. |
| 140 | + <Suspense fallback={<TerminalLoading />}> |
| 141 | + <TerminalPane |
| 142 | + key={summary.id} |
| 143 | + sessionId={summary.id} |
| 144 | + cwd={summary.cwd} |
| 145 | + claudeArgs={claudeArgs} |
| 146 | + /> |
| 147 | + </Suspense> |
| 134 | 148 | ) : showingSkeleton ? ( |
| 135 | 149 | <CardsSkeleton /> |
| 136 | 150 | ) : detail ? ( |
@@ -152,6 +166,17 @@ export function ViewerPane() { |
| 152 | 166 | ); |
| 153 | 167 | } |
| 154 | 168 | |
| 169 | +/** Tiny placeholder for the lazy TerminalPane chunk. Only visible |
| 170 | + * on the first-ever terminal mount in a session — subsequent |
| 171 | + * mounts reuse the cached module and render instantly. */ |
| 172 | +function TerminalLoading() { |
| 173 | + return ( |
| 174 | + <div className="flex h-full items-center justify-center text-[11px] text-fg-3"> |
| 175 | + loading terminal… |
| 176 | + </div> |
| 177 | + ); |
| 178 | +} |
| 179 | + |
| 155 | 180 | /** Shimmer placeholder shown while a session detail is loading. |
| 156 | 181 | * Six ghost message cards, sized similarly to real messages, with |
| 157 | 182 | * a subtle animation. Keeps the viewer feeling alive instead of |