mini migrate to v2 (#35526)
This commit is contained in:
parent
fb75ea2cf6
commit
32cf36de9d
93 changed files with 2244 additions and 2333 deletions
351
packages/cli/src/mini/scrollback.writer.tsx
Normal file
351
packages/cli/src/mini/scrollback.writer.tsx
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
import { createScrollbackWriter } from "@opentui/solid"
|
||||
import { TextRenderable, type ColorInput, type ScrollbackRenderContext, type ScrollbackWriter } from "@opentui/core"
|
||||
import { Match, Switch, createMemo } from "solid-js"
|
||||
import { entryBody, entryFlags } from "./entry.body"
|
||||
import { entryColor, entryLook, entrySyntax } from "./scrollback.shared"
|
||||
import { toolFiletype, toolStructuredFinal } from "./tool"
|
||||
import { RUN_THEME_FALLBACK, transparent, type RunTheme } from "./theme"
|
||||
import type { EntryLayout, RunEntryBody, ScrollbackOptions, StreamCommit } from "./types"
|
||||
|
||||
function todoText(item: { status: string; content: string }): string {
|
||||
if (item.status === "completed") {
|
||||
return `[✓] ${item.content}`
|
||||
}
|
||||
|
||||
if (item.status === "cancelled") {
|
||||
return `~[ ] ${item.content}~`
|
||||
}
|
||||
|
||||
if (item.status === "in_progress") {
|
||||
return `[•] ${item.content}`
|
||||
}
|
||||
|
||||
return `[ ] ${item.content}`
|
||||
}
|
||||
|
||||
function todoColor(theme: RunTheme, status: string) {
|
||||
return status === "in_progress" ? theme.block.warning : theme.block.muted
|
||||
}
|
||||
|
||||
export function entryGroupKey(commit: StreamCommit): string | undefined {
|
||||
if (!commit.partID) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (toolStructuredFinal(commit)) {
|
||||
return `tool:${commit.partID}:final`
|
||||
}
|
||||
|
||||
return `${commit.kind}:${commit.partID}`
|
||||
}
|
||||
|
||||
export function sameEntryGroup(left: StreamCommit | undefined, right: StreamCommit): boolean {
|
||||
if (!left) {
|
||||
return false
|
||||
}
|
||||
|
||||
const current = entryGroupKey(left)
|
||||
const next = entryGroupKey(right)
|
||||
return Boolean(current && next && current === next)
|
||||
}
|
||||
|
||||
export function entryLayout(commit: StreamCommit, body: RunEntryBody = entryBody(commit)): EntryLayout {
|
||||
if (commit.kind === "tool") {
|
||||
if (body.type === "structured" || body.type === "markdown") {
|
||||
return "block"
|
||||
}
|
||||
|
||||
if (
|
||||
commit.phase === "progress" &&
|
||||
commit.toolState === "completed" &&
|
||||
body.type === "text" &&
|
||||
body.content.includes("\n")
|
||||
) {
|
||||
return "block"
|
||||
}
|
||||
|
||||
return "inline"
|
||||
}
|
||||
|
||||
if (commit.kind === "reasoning") {
|
||||
return "block"
|
||||
}
|
||||
|
||||
if (commit.kind === "error") {
|
||||
return "block"
|
||||
}
|
||||
|
||||
return "block"
|
||||
}
|
||||
|
||||
export function separatorRows(
|
||||
prev: StreamCommit | undefined,
|
||||
next: StreamCommit,
|
||||
body: RunEntryBody = entryBody(next),
|
||||
): number {
|
||||
if (!prev || sameEntryGroup(prev, next)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (entryLayout(prev) === "inline" && entryLayout(next, body) === "inline") {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
export function RunEntryContent(props: {
|
||||
commit: StreamCommit
|
||||
body?: RunEntryBody
|
||||
theme?: RunTheme
|
||||
opts?: ScrollbackOptions
|
||||
width?: number
|
||||
}) {
|
||||
const theme = createMemo(() => props.theme ?? RUN_THEME_FALLBACK)
|
||||
const body = createMemo(() => props.body ?? entryBody(props.commit))
|
||||
const style = createMemo(() => entryLook(props.commit, theme().entry))
|
||||
const syntax = createMemo(() => entrySyntax(props.commit, theme()))
|
||||
const color = createMemo(() => entryColor(props.commit, theme()))
|
||||
const suppressBackgrounds = createMemo(() => props.opts?.suppressBackgrounds === true)
|
||||
const diffBg = (color: ColorInput) => (suppressBackgrounds() ? transparent : color)
|
||||
const streaming = createMemo(() => props.commit.phase === "progress")
|
||||
const text = createMemo(() => {
|
||||
const next = body()
|
||||
return next.type === "text" ? next : undefined
|
||||
})
|
||||
const code = createMemo(() => {
|
||||
const next = body()
|
||||
return next.type === "code" ? next : undefined
|
||||
})
|
||||
const structured = createMemo(() => {
|
||||
const next = body()
|
||||
return next.type === "structured" ? next.snapshot : undefined
|
||||
})
|
||||
const markdown = createMemo(() => {
|
||||
const next = body()
|
||||
return next.type === "markdown" ? next : undefined
|
||||
})
|
||||
const code_snapshot = createMemo(() => {
|
||||
const next = structured()
|
||||
return next?.kind === "code" ? next : undefined
|
||||
})
|
||||
const diff_snapshot = createMemo(() => {
|
||||
const next = structured()
|
||||
return next?.kind === "diff" ? next : undefined
|
||||
})
|
||||
const task_snapshot = createMemo(() => {
|
||||
const next = structured()
|
||||
return next?.kind === "task" ? next : undefined
|
||||
})
|
||||
const todo_snapshot = createMemo(() => {
|
||||
const next = structured()
|
||||
return next?.kind === "todo" ? next : undefined
|
||||
})
|
||||
const question_snapshot = createMemo(() => {
|
||||
const next = structured()
|
||||
return next?.kind === "question" ? next : undefined
|
||||
})
|
||||
|
||||
return (
|
||||
<Switch fallback={null}>
|
||||
<Match when={text()}>
|
||||
<text width="100%" wrapMode="word" fg={style().fg} attributes={style().attrs}>
|
||||
{text()!.content}
|
||||
</text>
|
||||
</Match>
|
||||
<Match when={code()}>
|
||||
<code
|
||||
width="100%"
|
||||
wrapMode="word"
|
||||
filetype={code()!.filetype}
|
||||
drawUnstyledText={false}
|
||||
streaming={streaming()}
|
||||
syntaxStyle={syntax()}
|
||||
content={code()!.content}
|
||||
fg={color()}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={code_snapshot()}>
|
||||
<box width="100%" flexDirection="column" gap={1}>
|
||||
<text width="100%" wrapMode="word" fg={theme().block.muted}>
|
||||
{code_snapshot()!.title}
|
||||
</text>
|
||||
<box width="100%" paddingLeft={1}>
|
||||
<line_number width="100%" fg={theme().block.muted} minWidth={3} paddingRight={1}>
|
||||
<code
|
||||
width="100%"
|
||||
wrapMode="char"
|
||||
filetype={toolFiletype(code_snapshot()!.file)}
|
||||
streaming={false}
|
||||
syntaxStyle={syntax()}
|
||||
content={code_snapshot()!.content}
|
||||
fg={theme().block.text}
|
||||
/>
|
||||
</line_number>
|
||||
</box>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={diff_snapshot()}>
|
||||
<box width="100%" flexDirection="column" gap={1}>
|
||||
{diff_snapshot()!.items.map((item) => (
|
||||
<box width="100%" flexDirection="column" gap={1}>
|
||||
<text width="100%" wrapMode="word" fg={theme().block.muted}>
|
||||
{item.title}
|
||||
</text>
|
||||
{item.diff.trim() ? (
|
||||
<box width="100%" paddingLeft={1}>
|
||||
<diff
|
||||
diff={item.diff}
|
||||
view="unified"
|
||||
filetype={toolFiletype(item.file)}
|
||||
syntaxStyle={syntax()}
|
||||
showLineNumbers={true}
|
||||
width="100%"
|
||||
wrapMode="word"
|
||||
fg={theme().block.text}
|
||||
addedBg={diffBg(theme().block.diffAddedBg)}
|
||||
removedBg={diffBg(theme().block.diffRemovedBg)}
|
||||
contextBg={diffBg(theme().block.diffContextBg)}
|
||||
addedSignColor={theme().block.diffHighlightAdded}
|
||||
removedSignColor={theme().block.diffHighlightRemoved}
|
||||
lineNumberFg={theme().block.diffLineNumber}
|
||||
lineNumberBg={diffBg(theme().block.diffContextBg)}
|
||||
addedLineNumberBg={diffBg(theme().block.diffAddedLineNumberBg)}
|
||||
removedLineNumberBg={diffBg(theme().block.diffRemovedLineNumberBg)}
|
||||
/>
|
||||
</box>
|
||||
) : (
|
||||
<text width="100%" wrapMode="word" fg={theme().block.diffRemoved}>
|
||||
-{item.deletions ?? 0} line{item.deletions === 1 ? "" : "s"}
|
||||
</text>
|
||||
)}
|
||||
</box>
|
||||
))}
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={task_snapshot()}>
|
||||
<box width="100%" flexDirection="column" gap={1}>
|
||||
<text width="100%" wrapMode="word" fg={theme().block.muted}>
|
||||
{task_snapshot()!.title}
|
||||
</text>
|
||||
<box width="100%" flexDirection="column" gap={0} paddingLeft={1}>
|
||||
{task_snapshot()!.rows.map((row) => (
|
||||
<text width="100%" wrapMode="word" fg={theme().block.text}>
|
||||
{row}
|
||||
</text>
|
||||
))}
|
||||
{task_snapshot()!.tail ? (
|
||||
<text width="100%" wrapMode="word" fg={theme().block.muted}>
|
||||
{task_snapshot()!.tail}
|
||||
</text>
|
||||
) : null}
|
||||
</box>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={todo_snapshot()}>
|
||||
<box width="100%" flexDirection="column" gap={1}>
|
||||
<text width="100%" wrapMode="word" fg={theme().block.muted}>
|
||||
# Todos
|
||||
</text>
|
||||
<box width="100%" flexDirection="column" gap={0}>
|
||||
{todo_snapshot()!.items.map((item) => (
|
||||
<text width="100%" wrapMode="word" fg={todoColor(theme(), item.status)}>
|
||||
{todoText(item)}
|
||||
</text>
|
||||
))}
|
||||
{todo_snapshot()!.tail ? (
|
||||
<text width="100%" wrapMode="word" fg={theme().block.muted}>
|
||||
{todo_snapshot()!.tail}
|
||||
</text>
|
||||
) : null}
|
||||
</box>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={question_snapshot()}>
|
||||
<box width="100%" flexDirection="column" gap={1}>
|
||||
<text width="100%" wrapMode="word" fg={theme().block.muted}>
|
||||
# Questions
|
||||
</text>
|
||||
<box width="100%" flexDirection="column" gap={1}>
|
||||
{question_snapshot()!.items.map((item) => (
|
||||
<box width="100%" flexDirection="column" gap={0}>
|
||||
<text width="100%" wrapMode="word" fg={theme().block.muted}>
|
||||
{item.question}
|
||||
</text>
|
||||
<text width="100%" wrapMode="word" fg={theme().block.text}>
|
||||
{item.answer}
|
||||
</text>
|
||||
</box>
|
||||
))}
|
||||
{question_snapshot()!.tail ? (
|
||||
<text width="100%" wrapMode="word" fg={theme().block.muted}>
|
||||
{question_snapshot()!.tail}
|
||||
</text>
|
||||
) : null}
|
||||
</box>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={markdown()}>
|
||||
<markdown
|
||||
width="100%"
|
||||
syntaxStyle={syntax()}
|
||||
streaming={streaming()}
|
||||
content={markdown()!.content}
|
||||
fg={color()}
|
||||
tableOptions={{ widthMode: "content" }}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
export function entryWriter(input: {
|
||||
commit: StreamCommit
|
||||
body?: RunEntryBody
|
||||
theme?: RunTheme
|
||||
opts?: ScrollbackOptions
|
||||
}): ScrollbackWriter {
|
||||
return createScrollbackWriter(
|
||||
(ctx) => (
|
||||
<RunEntryContent
|
||||
commit={input.commit}
|
||||
body={input.body}
|
||||
theme={input.theme}
|
||||
opts={{ ...input.opts, suppressBackgrounds: true }}
|
||||
width={ctx.width}
|
||||
/>
|
||||
),
|
||||
entryFlags(input.commit),
|
||||
)
|
||||
}
|
||||
|
||||
export function spacerWriter(): ScrollbackWriter {
|
||||
return (ctx: ScrollbackRenderContext) => ({
|
||||
root: new TextRenderable(ctx.renderContext, {
|
||||
width: Math.max(1, Math.trunc(ctx.width)),
|
||||
height: 1,
|
||||
content: "",
|
||||
}),
|
||||
width: Math.max(1, Math.trunc(ctx.width)),
|
||||
height: 1,
|
||||
startOnNewLine: true,
|
||||
trailingNewline: true,
|
||||
})
|
||||
}
|
||||
|
||||
export function turnSummaryWriter(input: { agent: string; model: string; duration: string; theme: RunTheme }) {
|
||||
return createScrollbackWriter(
|
||||
() => (
|
||||
<box width="100%" height={1}>
|
||||
<text wrapMode="none" truncate>
|
||||
<span style={{ fg: input.theme.block.text }}>{input.agent}</span>
|
||||
<span style={{ fg: input.theme.block.muted }}>
|
||||
{" "}
|
||||
· {input.model} · {input.duration}
|
||||
</span>
|
||||
</text>
|
||||
</box>
|
||||
),
|
||||
{ startOnNewLine: true, trailingNewline: false },
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue