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 { createAnsiStreamRenderer, hasAnsi } from "../lib/ansi" import { useI18n } from "../lib/i18n" interface BackgroundProcessOutputDialogProps { open: boolean instanceId: string process: BackgroundProcess | null onClose: () => void } export function BackgroundProcessOutputDialog(props: BackgroundProcessOutputDialogProps) { const { t } = useI18n() const [output, setOutput] = createSignal("") const [outputHtml, setOutputHtml] = createSignal("") const [ansiEnabled, setAnsiEnabled] = createSignal(false) const [truncated, setTruncated] = createSignal(false) const [loading, setLoading] = createSignal(false) let ansiRenderer = createAnsiStreamRenderer() createEffect(() => { const process = props.process if (!props.open || !process) { return } let eventSource: EventSource | null = null let active = true let rawOutput = "" const setRawOutput = (next: string) => { rawOutput = next setOutput(next) } const appendRawOutput = (chunk: string) => { rawOutput += chunk setOutput(rawOutput) } setAnsiEnabled(false) setOutputHtml("") setRawOutput("") ansiRenderer.reset() setLoading(true) serverApi .fetchBackgroundProcessOutput(props.instanceId, process.id, { method: "full", maxBytes: undefined }) .then((response) => { if (!active) return setRawOutput(response.content) setTruncated(response.truncated) const detectedAnsi = hasAnsi(response.content) if (detectedAnsi) { setAnsiEnabled(true) ansiRenderer.reset() setOutputHtml(ansiRenderer.render(response.content)) } else { setAnsiEnabled(false) setOutputHtml("") ansiRenderer.reset() } }) .catch(() => { if (!active) return setRawOutput(t("backgroundProcessOutputDialog.loadErrorFallback")) setAnsiEnabled(false) setOutputHtml("") }) .finally(() => { if (!active) return setLoading(false) }) eventSource = new EventSource(buildBackgroundProcessStreamUrl(props.instanceId, process.id), { withCredentials: true } as any) eventSource.onmessage = (event) => { try { const payload = JSON.parse(event.data) as { type?: string; content?: string } if (payload?.type !== "chunk" || typeof payload.content !== "string") { return } const chunk = payload.content const wasAnsiEnabled = ansiEnabled() if (!wasAnsiEnabled) { appendRawOutput(chunk) if (hasAnsi(chunk)) { setAnsiEnabled(true) ansiRenderer.reset() setOutputHtml(ansiRenderer.render(rawOutput)) } return } appendRawOutput(chunk) const htmlChunk = ansiRenderer.render(chunk) setOutputHtml((prev) => `${prev}${htmlChunk}`) } catch { // ignore parse errors } } onCleanup(() => { active = false eventSource?.close() }) }) return ( !open && props.onClose()} modal>
{t("backgroundProcessOutputDialog.title")} {props.process?.title} ยท {props.process?.id} {props.process?.command}

{t("backgroundProcessOutputDialog.loading")}

{t("backgroundProcessOutputDialog.truncatedNotice")}

{output()} } >
                
              
            
) }