Render ANSI background output

This commit is contained in:
Shantur Rathore
2026-01-02 19:26:02 +00:00
parent a041e1c6c3
commit 4571a1dcf9
2 changed files with 317 additions and 7 deletions

View File

@@ -2,6 +2,8 @@ import { Dialog } from "@kobalte/core/dialog"
import { Show, createEffect, createSignal, onCleanup } from "solid-js"
import type { BackgroundProcess } from "../../../server/src/api-types"
import { buildBackgroundProcessStreamUrl, serverApi } from "../lib/api-client"
import { ansiChunkToHtml, ansiToHtml, computeAnsiSgrState, createAnsiSgrState, hasAnsi, isAnsiSgrStateEmpty } from "../lib/ansi"
import { escapeHtml } from "../lib/markdown"
interface BackgroundProcessOutputDialogProps {
open: boolean
@@ -12,6 +14,8 @@ interface BackgroundProcessOutputDialogProps {
export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDialogProps) {
const [output, setOutput] = createSignal("")
const [outputHtml, setOutputHtml] = createSignal("")
const [ansiEnabled, setAnsiEnabled] = createSignal(false)
const [truncated, setTruncated] = createSignal(false)
const [loading, setLoading] = createSignal(false)
@@ -24,17 +28,48 @@ export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDial
let eventSource: EventSource | null = null
let active = true
let rawOutput = ""
let sgrState = createAnsiSgrState()
const setRawOutput = (next: string) => {
rawOutput = next
setOutput(next)
}
const appendRawOutput = (chunk: string) => {
rawOutput += chunk
setOutput(rawOutput)
}
setAnsiEnabled(false)
setOutputHtml("")
setRawOutput("")
setLoading(true)
serverApi
.fetchBackgroundProcessOutput(props.instanceId, process.id, { method: "full", maxBytes: undefined })
.then((response) => {
if (!active) return
setOutput(response.content)
setRawOutput(response.content)
setTruncated(response.truncated)
const detectedAnsi = hasAnsi(response.content)
if (detectedAnsi) {
setAnsiEnabled(true)
setOutputHtml(ansiToHtml(response.content))
sgrState = computeAnsiSgrState(response.content)
} else {
setAnsiEnabled(false)
setOutputHtml("")
sgrState = createAnsiSgrState()
}
})
.catch(() => {
if (!active) return
setOutput("Failed to load output.")
setRawOutput("Failed to load output.")
setAnsiEnabled(false)
setOutputHtml("")
})
.finally(() => {
if (!active) return
@@ -45,8 +80,36 @@ export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDial
eventSource.onmessage = (event) => {
try {
const payload = JSON.parse(event.data) as { type?: string; content?: string }
if (payload?.type === "chunk" && typeof payload.content === "string") {
setOutput((prev) => `${prev}${payload.content}`)
if (payload?.type !== "chunk" || typeof payload.content !== "string") {
return
}
const chunk = payload.content
const wasAnsiEnabled = ansiEnabled()
if (!wasAnsiEnabled) {
const before = rawOutput
appendRawOutput(chunk)
if (hasAnsi(chunk)) {
setAnsiEnabled(true)
const initialHtml = escapeHtml(before)
const result = ansiChunkToHtml(chunk, createAnsiSgrState())
setOutputHtml(initialHtml + result.html)
sgrState = result.nextState
}
return
}
appendRawOutput(chunk)
const result = ansiChunkToHtml(chunk, sgrState)
setOutputHtml((prev) => `${prev}${result.html}`)
sgrState = result.nextState
if (isAnsiSgrStateEmpty(sgrState)) {
// keep streaming normally; state can legitimately be empty
}
} catch {
// ignore parse errors
@@ -90,9 +153,19 @@ export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDial
<Show when={truncated()}>
<p class="text-xs text-secondary mb-2">Output truncated for display.</p>
</Show>
<pre class="text-xs whitespace-pre-wrap break-all text-primary bg-surface-secondary border border-base rounded-md p-4 font-mono">
{output()}
</pre>
<Show
when={ansiEnabled()}
fallback={
<pre class="text-xs whitespace-pre-wrap break-all text-primary bg-surface-secondary border border-base rounded-md p-4 font-mono">
{output()}
</pre>
}
>
<pre
class="text-xs whitespace-pre-wrap break-all text-primary bg-surface-secondary border border-base rounded-md p-4 font-mono"
innerHTML={outputHtml()}
/>
</Show>
</Show>
</div>
</Dialog.Content>