mini: monochrome rendered markdown only (#38656)
This commit is contained in:
parent
00f063b381
commit
4184149b90
7 changed files with 322 additions and 45 deletions
|
|
@ -81,8 +81,8 @@ function systemBody(raw: string, phase: StreamCommit["phase"]): RunEntryBody {
|
|||
}
|
||||
|
||||
function monoBody(body: RunEntryBody): RunEntryBody {
|
||||
if (body.type === "none" || body.type === "text") return body
|
||||
if (body.type === "code" || body.type === "markdown") return textBody(body.content)
|
||||
if (body.type === "none" || body.type === "text" || body.type === "markdown") return body
|
||||
if (body.type === "code") return textBody(body.content)
|
||||
const snapshot = body.snapshot
|
||||
if (snapshot.kind === "code") return textBody(`${snapshot.title}\n${snapshot.content}`)
|
||||
if (snapshot.kind === "diff") {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
import {
|
||||
BoxRenderable,
|
||||
CodeRenderable,
|
||||
MarkdownRenderable,
|
||||
RGBA,
|
||||
Renderable,
|
||||
StyledText,
|
||||
TextRenderable,
|
||||
TextTableRenderable,
|
||||
isStyledText,
|
||||
stringToStyledText,
|
||||
type BorderCharacters,
|
||||
type CliRendererExternalOutputEvent,
|
||||
type MarkdownOptions,
|
||||
type Renderable,
|
||||
type TreeSitterClient,
|
||||
} from "@opentui/core"
|
||||
|
||||
const prefixes: Record<number, string> = {
|
||||
|
|
@ -52,27 +59,129 @@ const asciiBorder: BorderCharacters = {
|
|||
cross: "+",
|
||||
}
|
||||
|
||||
const hooked = new WeakSet<Renderable>()
|
||||
|
||||
export const monoMarkdownTableOptions = {
|
||||
style: "columns" as const,
|
||||
widthMode: "content" as const,
|
||||
borders: false,
|
||||
}
|
||||
|
||||
export const monoMarkdownRenderNode: NonNullable<MarkdownOptions["renderNode"]> = (token, context) => {
|
||||
if (token.type !== "blockquote" && token.type !== "hr" && token.type !== "list") return
|
||||
const renderable = context.defaultRender()
|
||||
if (!renderable) return renderable
|
||||
monoBorders(renderable)
|
||||
return renderable
|
||||
export function monoMarkdownRenderable(renderable: MarkdownRenderable): void {
|
||||
monoRenderable(renderable)
|
||||
}
|
||||
|
||||
function monoBorders(renderable: Renderable): void {
|
||||
function monoRenderable(renderable: Renderable): void {
|
||||
if (hooked.has(renderable)) return
|
||||
hooked.add(renderable)
|
||||
// Markdown reconciles nested lists and tables without calling renderNode.
|
||||
// Hook the actual tree so future descendants are transformed before layout.
|
||||
const add = renderable.add.bind(renderable)
|
||||
renderable.add = (child, index) => {
|
||||
if (child instanceof Renderable) monoRenderable(child)
|
||||
return add(child, index)
|
||||
}
|
||||
|
||||
if (renderable instanceof BoxRenderable) renderable.customBorderChars = asciiBorder
|
||||
renderable.getChildren().forEach(monoBorders)
|
||||
if (renderable instanceof CodeRenderable) monoCode(renderable)
|
||||
if (renderable instanceof TextRenderable) renderable.content = monoStyledText(renderable.content)
|
||||
if (renderable instanceof TextTableRenderable) monoTable(renderable)
|
||||
renderable.getChildren().forEach(monoRenderable)
|
||||
}
|
||||
|
||||
export function monoMarkdown(value: string, mono: boolean): string {
|
||||
if (!mono) return value
|
||||
function monoCode(renderable: CodeRenderable): void {
|
||||
const onChunks = renderable.onChunks
|
||||
const prose = renderable.filetype === "markdown" && onChunks !== undefined
|
||||
renderable.onChunks = async (chunks, context) => monoChunks((await onChunks?.(chunks, context)) ?? chunks)
|
||||
renderable.treeSitterClient = monoTreeSitter(renderable.treeSitterClient)
|
||||
|
||||
const initialDescriptor = Object.getOwnPropertyDescriptor(CodeRenderable.prototype, "initialStyledText")
|
||||
const contentDescriptor = Object.getOwnPropertyDescriptor(CodeRenderable.prototype, "content")
|
||||
if (!initialDescriptor?.set || !contentDescriptor?.get || !contentDescriptor.set) return
|
||||
const initialSetter = initialDescriptor.set.bind(renderable)
|
||||
const contentGetter = contentDescriptor.get.bind(renderable)
|
||||
const contentSetter = contentDescriptor.set.bind(renderable)
|
||||
const initial = Reflect.get(renderable, "_initialStyledText")
|
||||
Object.defineProperty(renderable, "initialStyledText", {
|
||||
configurable: true,
|
||||
set(value: StyledText | undefined) {
|
||||
initialSetter(value ? monoStyledText(value) : value)
|
||||
},
|
||||
})
|
||||
Object.defineProperty(renderable, "content", {
|
||||
configurable: true,
|
||||
get: contentGetter,
|
||||
set(value: string) {
|
||||
if (!prose || !isStyledText(Reflect.get(renderable, "_initialStyledText"))) {
|
||||
renderable.drawUnstyledText = true
|
||||
renderable.initialStyledText = stringToStyledText(value)
|
||||
}
|
||||
contentSetter(value)
|
||||
},
|
||||
})
|
||||
|
||||
if (isStyledText(initial)) {
|
||||
renderable.initialStyledText = initial
|
||||
} else {
|
||||
renderable.drawUnstyledText = true
|
||||
renderable.initialStyledText = stringToStyledText(renderable.content)
|
||||
}
|
||||
if (!renderable.drawUnstyledText) return
|
||||
|
||||
// Refresh the eager buffer with the transformed initial text. Highlighted
|
||||
// chunks continue through onChunks without changing the Markdown source.
|
||||
const content = renderable.content
|
||||
renderable.content = ""
|
||||
renderable.content = content
|
||||
}
|
||||
|
||||
function monoTreeSitter(client: TreeSitterClient): TreeSitterClient {
|
||||
return new Proxy(client, {
|
||||
get(target, property) {
|
||||
if (property !== "highlightOnce") return Reflect.get(target, property, target)
|
||||
// Keep parser failures on the chunk path instead of OpenTUI's raw-text fallback.
|
||||
return (...args: Parameters<TreeSitterClient["highlightOnce"]>) =>
|
||||
target.highlightOnce(...args).catch(() => ({ highlights: [] }))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function monoTable(renderable: TextTableRenderable): void {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(TextTableRenderable.prototype, "content")
|
||||
if (!descriptor?.get || !descriptor.set) return
|
||||
const cells = new WeakMap<StyledText["chunks"], StyledText["chunks"]>()
|
||||
const content = renderable.content
|
||||
Object.defineProperty(renderable, "content", {
|
||||
configurable: true,
|
||||
get: () => descriptor.get!.call(renderable),
|
||||
set: (value: TextTableRenderable["content"]) => {
|
||||
descriptor.set!.call(
|
||||
renderable,
|
||||
value.map((row) =>
|
||||
row.map((cell) => {
|
||||
if (!cell) return cell
|
||||
const cached = cells.get(cell)
|
||||
if (cached) return cached
|
||||
const next = monoChunks(cell)
|
||||
cells.set(cell, next)
|
||||
return next
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
})
|
||||
renderable.content = content
|
||||
}
|
||||
|
||||
function monoStyledText(value: StyledText): StyledText {
|
||||
return new StyledText(monoChunks(value.chunks))
|
||||
}
|
||||
|
||||
function monoChunks(value: StyledText["chunks"]): StyledText["chunks"] {
|
||||
return value.map((chunk) => ({ ...chunk, text: monoText(chunk.text) }))
|
||||
}
|
||||
|
||||
function monoText(value: string): string {
|
||||
return value.replace(/[^\t\n\x20-\x7e]/gu, (char) => markdown[char.codePointAt(0)!] ?? "?")
|
||||
}
|
||||
|
||||
|
|
@ -80,7 +189,7 @@ export function monoSnapshot(event: CliRendererExternalOutputEvent): void {
|
|||
const buffers = event.snapshot.buffers
|
||||
const chars = buffers.char
|
||||
for (let index = 0; index < chars.length; index += 1) {
|
||||
const point = chars[index]!
|
||||
const point = chars[index]
|
||||
if (point <= 0x7f) continue
|
||||
const offset = index * 4
|
||||
event.snapshot.setCell(
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
type ScrollbackSurface,
|
||||
} from "@opentui/core"
|
||||
import { entryBody, entryCanStream, entryDone, entryFlags } from "./entry.body"
|
||||
import { monoMarkdown, monoMarkdownRenderNode, monoMarkdownTableOptions } from "./mono"
|
||||
import { monoMarkdownRenderable, monoMarkdownTableOptions } from "./mono"
|
||||
import { entryColor, entryLook, entrySyntax } from "./scrollback.shared"
|
||||
import { turnSummaryCommit } from "./turn-summary"
|
||||
import { entryWriter, sameEntryGroup, separatorRows, spacerWriter, turnSummaryWriter } from "./scrollback.writer"
|
||||
|
|
@ -181,11 +181,11 @@ export class RunScrollbackStream {
|
|||
streaming: true,
|
||||
internalBlockMode: "top-level",
|
||||
tableOptions: this.mono ? monoMarkdownTableOptions : { widthMode: "content" },
|
||||
renderNode: this.mono ? monoMarkdownRenderNode : undefined,
|
||||
fg: entryColor(commit, this.theme),
|
||||
treeSitterClient,
|
||||
})
|
||||
|
||||
if (this.mono && renderable instanceof MarkdownRenderable) monoMarkdownRenderable(renderable)
|
||||
surface.root.add(renderable)
|
||||
|
||||
const rows = separatorRows(this.rendered, commit, body)
|
||||
|
|
@ -283,7 +283,7 @@ export class RunScrollbackStream {
|
|||
}
|
||||
|
||||
const renderable = active.renderable
|
||||
renderable.content = monoMarkdown(active.content, this.mono)
|
||||
renderable.content = active.content
|
||||
renderable.streaming = !done
|
||||
await active.surface.settle()
|
||||
this.releasePendingThemes()
|
||||
|
|
@ -378,7 +378,7 @@ export class RunScrollbackStream {
|
|||
) {
|
||||
await this.writeStreaming(commit, body)
|
||||
if (entryDone(commit)) {
|
||||
this.markRendered(await this.finishActive(false))
|
||||
this.markRendered(await this.finishActive(entryFlags(commit).trailingNewline))
|
||||
}
|
||||
this.tail = commit
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import { createScrollbackWriter } from "@opentui/solid"
|
||||
import { TextRenderable, type ColorInput, type ScrollbackRenderContext, type ScrollbackWriter } from "@opentui/core"
|
||||
import {
|
||||
MarkdownRenderable,
|
||||
TextRenderable,
|
||||
type ColorInput,
|
||||
type ScrollbackRenderContext,
|
||||
type ScrollbackWriter,
|
||||
} from "@opentui/core"
|
||||
import { Match, Switch, createMemo } from "solid-js"
|
||||
import { entryBody, entryFlags } from "./entry.body"
|
||||
import { monoMarkdown, monoMarkdownRenderNode, monoMarkdownTableOptions } from "./mono"
|
||||
import { monoMarkdownRenderable, monoMarkdownTableOptions } from "./mono"
|
||||
import { entryColor, entryLook, entrySyntax } from "./scrollback.shared"
|
||||
import { toolFiletype, toolStructuredFinal } from "./tool"
|
||||
import { RUN_THEME_FALLBACK, transparent, type RunTheme } from "./theme"
|
||||
|
|
@ -237,13 +243,15 @@ export function RunEntryContent(props: {
|
|||
</Match>
|
||||
<Match when={markdown()}>
|
||||
<markdown
|
||||
ref={(renderable: MarkdownRenderable) => {
|
||||
if (props.opts?.mono) monoMarkdownRenderable(renderable)
|
||||
}}
|
||||
width="100%"
|
||||
syntaxStyle={syntax()}
|
||||
streaming={streaming()}
|
||||
content={monoMarkdown(markdown()!.content, props.opts?.mono === true)}
|
||||
content={markdown()!.content}
|
||||
fg={color()}
|
||||
tableOptions={props.opts?.mono ? monoMarkdownTableOptions : { widthMode: "content" }}
|
||||
renderNode={props.opts?.mono ? monoMarkdownRenderNode : undefined}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue