+
+
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}