diff --git a/desktop/src/renderer/src/lib/__mocks__/setup.ts b/desktop/src/renderer/src/lib/__mocks__/setup.ts index ef543cbb6..ccda9d0ce 100644 --- a/desktop/src/renderer/src/lib/__mocks__/setup.ts +++ b/desktop/src/renderer/src/lib/__mocks__/setup.ts @@ -33,6 +33,15 @@ if ( } } +// jsdom doesn't implement ResizeObserver +if (typeof globalThis.ResizeObserver === "undefined") { + globalThis.ResizeObserver = class { + observe() {} + unobserve() {} + disconnect() {} + } +} + // jsdom doesn't implement matchMedia if (typeof window !== "undefined" && typeof window.matchMedia !== "function") { Object.defineProperty(window, "matchMedia", { diff --git a/desktop/src/renderer/src/lib/components/log/LogTable.svelte b/desktop/src/renderer/src/lib/components/log/LogTable.svelte index 1c11c8074..2166fe3b8 100644 --- a/desktop/src/renderer/src/lib/components/log/LogTable.svelte +++ b/desktop/src/renderer/src/lib/components/log/LogTable.svelte @@ -1,49 +1,124 @@ - - - - Time - Level - Message - - - - {#each parsed as line, i (i)} - +
+
Time
+
Level
+
Message
+
+ +
+ {#each visible as row (row.index)} +
- {line.time} - - {#if line.level} +
{row.line.time}
+
+ {#if row.line.level} - {line.level} + {row.line.level} {/if} - - {line.message} - +
+
{row.line.message}
+
{/each} - - +
+ diff --git a/desktop/src/renderer/src/lib/components/provider/ProviderWizard.svelte b/desktop/src/renderer/src/lib/components/provider/ProviderWizard.svelte index d810c1a0e..8f0a54e7d 100644 --- a/desktop/src/renderer/src/lib/components/provider/ProviderWizard.svelte +++ b/desktop/src/renderer/src/lib/components/provider/ProviderWizard.svelte @@ -529,14 +529,10 @@ function handleDone() { > Show logs -
- -
+ {:else} -
- -
+ {/if} {:else if initRunning}
diff --git a/desktop/src/renderer/src/lib/components/terminal/Terminal.svelte b/desktop/src/renderer/src/lib/components/terminal/Terminal.svelte index d43950756..a34c61f8c 100644 --- a/desktop/src/renderer/src/lib/components/terminal/Terminal.svelte +++ b/desktop/src/renderer/src/lib/components/terminal/Terminal.svelte @@ -32,6 +32,8 @@ let containerEl: HTMLDivElement | undefined = $state() let term: Terminal | undefined let fitAddon: FitAddon | undefined let resizeObserver: ResizeObserver | undefined +let lastCols = -1 +let lastRows = -1 const darkTheme = { background: "#1e1e2e", @@ -187,11 +189,13 @@ onMount(async () => { } resizeObserver = new ResizeObserver(() => { - if (fitAddon && term) { - fitAddon.fit() - term.refresh(0, term.rows - 1) - terminalResize(sessionId, term.cols, term.rows) - } + if (!fitAddon || !term) return + fitAddon.fit() + if (term.cols === lastCols && term.rows === lastRows) return + lastCols = term.cols + lastRows = term.rows + term.refresh(0, term.rows - 1) + terminalResize(sessionId, term.cols, term.rows) }) resizeObserver.observe(containerEl) }) diff --git a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte index bd23fbf72..4a1fdd3f6 100644 --- a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte +++ b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte @@ -200,7 +200,6 @@ let ideSearch = $state("") // Launch state let commandId = $state(null) let outputLines = $state([]) -let outputEl = $state(null) let launchRunning = $state(false) let launchError = $state("") let launchSuccess = $state(false) @@ -208,6 +207,8 @@ let launchedWorkspaceId = $state(null) let confirmCancelOpen = $state(false) let unlisten: UnlistenFn | null = null let watchdog: ReturnType | null = null +let pendingLines: string[] = [] +let flushHandle: number | null = null // Image platform compatibility (image source only) let hostPlatform = $state("") @@ -331,6 +332,11 @@ function reset() { ideSearch = "" commandId = null outputLines = [] + pendingLines = [] + if (flushHandle !== null) { + cancelAnimationFrame(flushHandle) + flushHandle = null + } launchRunning = false launchError = "" launchSuccess = false @@ -400,16 +406,31 @@ onMount(() => { onDestroy(() => { unlisten?.() clearWatchdog() + if (flushHandle !== null) { + cancelAnimationFrame(flushHandle) + flushHandle = null + } }) +function flushLines() { + flushHandle = null + if (pendingLines.length === 0) return + outputLines.push(...pendingLines) + pendingLines.length = 0 +} + function handleProgress(progress: CommandProgress, wsId: string | undefined) { if (progress.message) { - outputLines = [...outputLines, progress.message] - requestAnimationFrame(() => { - outputEl?.scrollIntoView?.({ block: "end", behavior: "smooth" }) - }) + pendingLines.push(progress.message) + if (flushHandle === null) { + flushHandle = requestAnimationFrame(flushLines) + } } if (progress.done) { + if (flushHandle !== null) { + cancelAnimationFrame(flushHandle) + } + flushLines() launchRunning = false clearWatchdog() if (isCommandSuccess(progress.message)) { @@ -430,6 +451,11 @@ async function handleLaunch() { launchError = "" launchSuccess = false outputLines = [] + pendingLines = [] + if (flushHandle !== null) { + cancelAnimationFrame(flushHandle) + flushHandle = null + } commandId = null launchedWorkspaceId = null clearWatchdog() @@ -1121,10 +1147,7 @@ function selectTemplate(t: { name: string; source: string }) { Copy
-
- -
-
+ {:else if launchRunning}
diff --git a/desktop/src/renderer/src/lib/utils/log-parser.ts b/desktop/src/renderer/src/lib/utils/log-parser.ts index 6c9eda1d9..9349389c5 100644 --- a/desktop/src/renderer/src/lib/utils/log-parser.ts +++ b/desktop/src/renderer/src/lib/utils/log-parser.ts @@ -86,7 +86,24 @@ function tryParseInnerLog( * * ANSI codes are stripped before parsing. */ +const parseCache = new Map() +const PARSE_CACHE_MAX = 10_000 + export function parseLogLine(raw: string): ParsedLogLine { + const cached = parseCache.get(raw) + if (cached) return cached + + const result = parseLogLineUncached(raw) + + if (parseCache.size >= PARSE_CACHE_MAX) { + const oldest = parseCache.keys().next().value + if (oldest !== undefined) parseCache.delete(oldest) + } + parseCache.set(raw, result) + return result +} + +function parseLogLineUncached(raw: string): ParsedLogLine { const clean = stripAnsi(raw) // Zap console format (tab-separated): ISO8601\tLEVEL\tmessage\tsource.go:NNN diff --git a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte index ef7c6bdbf..5f31f9a5d 100644 --- a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte +++ b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte @@ -111,7 +111,8 @@ let commandId = $state(null) let operationLabel = $state("") let operationRunning = $state(false) let unlisten: UnlistenFn | null = null -let tableEndEl = $state(null) +let pendingLines: string[] = [] +let flushHandle: number | null = null let logEntries = $state([]) let selectedLog = $state(null) @@ -149,10 +150,11 @@ let filteredIdes = $derived( ), ) -function scrollToBottom() { - if (tableEndEl) { - tableEndEl.scrollIntoView({ block: "end", behavior: "smooth" }) - } +function flushLines() { + flushHandle = null + if (pendingLines.length === 0) return + outputLines.push(...pendingLines) + pendingLines.length = 0 } async function copyToClipboard(text: string) { @@ -169,10 +171,16 @@ onMount(async () => { unlisten = await onCommandProgress((progress) => { if (commandId && progress.commandId === commandId) { if (progress.message) { - outputLines = [...outputLines, progress.message] - requestAnimationFrame(scrollToBottom) + pendingLines.push(progress.message) + if (flushHandle === null) { + flushHandle = requestAnimationFrame(flushLines) + } } if (progress.done) { + if (flushHandle !== null) { + cancelAnimationFrame(flushHandle) + } + flushLines() operationRunning = false const success = isCommandSuccess(progress.message) if (success) { @@ -213,6 +221,10 @@ onMount(async () => { onDestroy(() => { unlisten?.() + if (flushHandle !== null) { + cancelAnimationFrame(flushHandle) + flushHandle = null + } // Clean up SSH session if navigating away if (sshSessionId) { if (!sshExited) { @@ -313,6 +325,11 @@ function startStreamingOp(label: string) { operationLabel = label operationRunning = true outputLines = [] + pendingLines = [] + if (flushHandle !== null) { + cancelAnimationFrame(flushHandle) + flushHandle = null + } activeTab = "logs" } @@ -683,18 +700,15 @@ async function handleRenameConfirmed() {
{/if} -
- {#if outputLines.length === 0} -
-

- {operationRunning ? "Waiting for output..." : "No output yet. Run an operation to see live output."} -

-
- {:else} - -
- {/if} -
+ {#if outputLines.length === 0} +
+

+ {operationRunning ? "Waiting for output..." : "No output yet. Run an operation to see live output."} +

+
+ {:else} + + {/if} @@ -761,13 +775,11 @@ async function handleRenameConfirmed() { -
- {#if selectedLog === entry.filename} - - {:else} -

Loading...

- {/if} -
+ {#if selectedLog === entry.filename} + + {:else} +

Loading...

+ {/if}
{/each}