Stream ANSI tool output rendering
This commit is contained in:
1643
package-lock.json
generated
1643
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ import type {
|
|||||||
import { getRelativePath, getToolIcon, getToolName, isToolStateCompleted, isToolStateError, isToolStateRunning, getDefaultToolAction } from "./tool-call/utils"
|
import { getRelativePath, getToolIcon, getToolName, isToolStateCompleted, isToolStateError, isToolStateRunning, getDefaultToolAction } from "./tool-call/utils"
|
||||||
import { resolveTitleForTool } from "./tool-call/tool-title"
|
import { resolveTitleForTool } from "./tool-call/tool-title"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { ansiToHtml, hasAnsi } from "../lib/ansi"
|
import { ansiToHtml, createAnsiStreamRenderer, hasAnsi } from "../lib/ansi"
|
||||||
import { escapeHtml } from "../lib/markdown"
|
import { escapeHtml } from "../lib/markdown"
|
||||||
|
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
@@ -262,6 +262,9 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
const versionKey = typeof props.partVersion === "number" ? String(props.partVersion) : "noversion"
|
const versionKey = typeof props.partVersion === "number" ? String(props.partVersion) : "noversion"
|
||||||
return `ansi-final:${versionKey}`
|
return `ansi-final:${versionKey}`
|
||||||
})
|
})
|
||||||
|
const runningAnsiRenderer = createAnsiStreamRenderer()
|
||||||
|
let runningAnsiSource = ""
|
||||||
|
|
||||||
const permissionState = createMemo(() => store().getPermissionState(props.messageId, toolCallIdentifier()))
|
const permissionState = createMemo(() => store().getPermissionState(props.messageId, toolCallIdentifier()))
|
||||||
const pendingPermission = createMemo(() => {
|
const pendingPermission = createMemo(() => {
|
||||||
const state = permissionState()
|
const state = permissionState()
|
||||||
@@ -647,35 +650,61 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
const messageClass = `message-text tool-call-markdown${size === "large" ? " tool-call-markdown-large" : ""}`
|
const messageClass = `message-text tool-call-markdown${size === "large" ? " tool-call-markdown-large" : ""}`
|
||||||
const cacheHandle = options.variant === "running" ? ansiRunningCache : ansiFinalCache
|
const cacheHandle = options.variant === "running" ? ansiRunningCache : ansiFinalCache
|
||||||
const cached = cacheHandle.get<AnsiRenderCache>()
|
const cached = cacheHandle.get<AnsiRenderCache>()
|
||||||
if (cached) {
|
const mode = typeof props.partVersion === "number" ? String(props.partVersion) : undefined
|
||||||
if (options.requireAnsi && !cached.hasAnsi) {
|
const isRunningVariant = options.variant === "running"
|
||||||
return null
|
|
||||||
|
let nextCache: AnsiRenderCache
|
||||||
|
|
||||||
|
if (isRunningVariant) {
|
||||||
|
const content = options.content
|
||||||
|
const resetStreaming = !cached || !cached.text || !content.startsWith(cached.text) || cached.text !== runningAnsiSource
|
||||||
|
|
||||||
|
if (resetStreaming) {
|
||||||
|
const detectedAnsi = hasAnsi(content)
|
||||||
|
if (detectedAnsi) {
|
||||||
|
runningAnsiRenderer.reset()
|
||||||
|
const html = runningAnsiRenderer.render(content)
|
||||||
|
nextCache = { text: content, html, mode, hasAnsi: true }
|
||||||
|
} else {
|
||||||
|
runningAnsiRenderer.reset()
|
||||||
|
nextCache = { text: content, html: escapeHtml(content), mode, hasAnsi: false }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const delta = content.slice(cached.text.length)
|
||||||
|
if (delta.length === 0) {
|
||||||
|
nextCache = { ...cached, mode }
|
||||||
|
} else if (!cached.hasAnsi && hasAnsi(delta)) {
|
||||||
|
runningAnsiRenderer.reset()
|
||||||
|
const html = runningAnsiRenderer.render(content)
|
||||||
|
nextCache = { text: content, html, mode, hasAnsi: true }
|
||||||
|
} else if (cached.hasAnsi) {
|
||||||
|
const htmlChunk = runningAnsiRenderer.render(delta)
|
||||||
|
nextCache = { text: content, html: `${cached.html}${htmlChunk}`, mode, hasAnsi: true }
|
||||||
|
} else {
|
||||||
|
nextCache = { text: content, html: `${cached.html}${escapeHtml(delta)}`, mode, hasAnsi: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runningAnsiSource = nextCache.text
|
||||||
|
cacheHandle.set(nextCache)
|
||||||
|
} else {
|
||||||
|
if (cached && cached.text === options.content) {
|
||||||
|
nextCache = { ...cached, mode }
|
||||||
|
} else {
|
||||||
|
const detectedAnsi = hasAnsi(options.content)
|
||||||
|
const html = detectedAnsi ? ansiToHtml(options.content) : escapeHtml(options.content)
|
||||||
|
nextCache = { text: options.content, html, mode, hasAnsi: detectedAnsi }
|
||||||
|
cacheHandle.set(nextCache)
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<div class={messageClass} ref={(element) => scrollHelpers.registerContainer(element)} onScroll={scrollHelpers.handleScroll}>
|
|
||||||
<pre class="tool-call-content tool-call-ansi" innerHTML={cached.html} />
|
|
||||||
{scrollHelpers.renderSentinel()}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const detectedAnsi = hasAnsi(options.content)
|
if (options.requireAnsi && !nextCache.hasAnsi) {
|
||||||
const html = detectedAnsi ? ansiToHtml(options.content) : escapeHtml(options.content)
|
|
||||||
const cacheEntry: AnsiRenderCache = {
|
|
||||||
text: "",
|
|
||||||
html,
|
|
||||||
mode: typeof props.partVersion === "number" ? String(props.partVersion) : undefined,
|
|
||||||
hasAnsi: detectedAnsi,
|
|
||||||
}
|
|
||||||
cacheHandle.set(cacheEntry)
|
|
||||||
|
|
||||||
if (options.requireAnsi && !detectedAnsi) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={messageClass} ref={(element) => scrollHelpers.registerContainer(element)} onScroll={scrollHelpers.handleScroll}>
|
<div class={messageClass} ref={(element) => scrollHelpers.registerContainer(element)} onScroll={scrollHelpers.handleScroll}>
|
||||||
<pre class="tool-call-content tool-call-ansi" innerHTML={html} />
|
<pre class="tool-call-content tool-call-ansi" innerHTML={nextCache.html} />
|
||||||
{scrollHelpers.renderSentinel()}
|
{scrollHelpers.renderSentinel()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user