Unify ANSI rendering with sequence parser

This commit is contained in:
Shantur Rathore
2026-01-02 19:42:26 +00:00
parent 4571a1dcf9
commit eebfcb5628
3 changed files with 112 additions and 258 deletions

View File

@@ -2,8 +2,7 @@ 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"
import { createAnsiStreamRenderer, hasAnsi } from "../lib/ansi"
interface BackgroundProcessOutputDialogProps {
open: boolean
@@ -18,6 +17,7 @@ export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDial
const [ansiEnabled, setAnsiEnabled] = createSignal(false)
const [truncated, setTruncated] = createSignal(false)
const [loading, setLoading] = createSignal(false)
let ansiRenderer = createAnsiStreamRenderer()
createEffect(() => {
const process = props.process
@@ -29,7 +29,6 @@ export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDial
let active = true
let rawOutput = ""
let sgrState = createAnsiSgrState()
const setRawOutput = (next: string) => {
rawOutput = next
@@ -44,6 +43,7 @@ export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDial
setAnsiEnabled(false)
setOutputHtml("")
setRawOutput("")
ansiRenderer.reset()
setLoading(true)
serverApi
@@ -57,12 +57,12 @@ export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDial
const detectedAnsi = hasAnsi(response.content)
if (detectedAnsi) {
setAnsiEnabled(true)
setOutputHtml(ansiToHtml(response.content))
sgrState = computeAnsiSgrState(response.content)
ansiRenderer.reset()
setOutputHtml(ansiRenderer.render(response.content))
} else {
setAnsiEnabled(false)
setOutputHtml("")
sgrState = createAnsiSgrState()
ansiRenderer.reset()
}
})
.catch(() => {
@@ -88,29 +88,20 @@ export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDial
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
ansiRenderer.reset()
setOutputHtml(ansiRenderer.render(rawOutput))
}
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
}
const htmlChunk = ansiRenderer.render(chunk)
setOutputHtml((prev) => `${prev}${htmlChunk}`)
} catch {
// ignore parse errors
}