mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 16:21:50 +03:00
refactor(tui): clarify conversation hierarchy
This commit is contained in:
+42
-5
@@ -235,7 +235,7 @@ describe("NanobotTui layout", () => {
|
|||||||
if (width >= 30 && height >= 9) {
|
if (width >= 30 && height >= 9) {
|
||||||
expect(occurrences(frame, "Ask nanobot anything")).toBe(1)
|
expect(occurrences(frame, "Ask nanobot anything")).toBe(1)
|
||||||
}
|
}
|
||||||
expect(occurrences(frame, "nanobot · test/model")).toBe(height >= 12 ? 1 : 0)
|
expect(occurrences(frame, "nanobot · test/model")).toBe(height >= 14 ? 1 : 0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -282,7 +282,6 @@ describe("NanobotTui layout", () => {
|
|||||||
shell: { backgroundColor: { intent: string } }
|
shell: { backgroundColor: { intent: string } }
|
||||||
composer: { backgroundColor: { intent: string }; textColor: { toInts(): number[] } }
|
composer: { backgroundColor: { intent: string }; textColor: { toInts(): number[] } }
|
||||||
transcript: {
|
transcript: {
|
||||||
frames: Set<{ borderColor: { toInts(): number[] } }>
|
|
||||||
markdown: Set<{ syntaxStyle: object }>
|
markdown: Set<{ syntaxStyle: object }>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,12 +299,50 @@ describe("NanobotTui layout", () => {
|
|||||||
expect(internals.shell.backgroundColor.intent).toBe("default")
|
expect(internals.shell.backgroundColor.intent).toBe("default")
|
||||||
expect(internals.composer.backgroundColor.intent).toBe("default")
|
expect(internals.composer.backgroundColor.intent).toBe("default")
|
||||||
expect(internals.composer.textColor.toInts().slice(0, 3)).toEqual([24, 24, 27])
|
expect(internals.composer.textColor.toInts().slice(0, 3)).toEqual([24, 24, 27])
|
||||||
expect([...internals.transcript.frames][0]?.borderColor.toInts().slice(0, 3)).toEqual([
|
|
||||||
212, 212, 216,
|
|
||||||
])
|
|
||||||
expect(markdown?.syntaxStyle).not.toBe(darkSyntax)
|
expect(markdown?.syntaxStyle).not.toBe(darkSyntax)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("uses asymmetric roles instead of chat bubbles", async () => {
|
||||||
|
setup = await createRenderer({ width: 72, height: 24, screenMode: "alternate-screen" })
|
||||||
|
const app = mount(setup)
|
||||||
|
app.accept({ event: "attached", chat_id: "chat" })
|
||||||
|
app.accept({ event: "delta", chat_id: "chat", text: "Agent **answer**" })
|
||||||
|
app.accept({ event: "stream_end", chat_id: "chat" })
|
||||||
|
app.accept({ event: "turn_end", chat_id: "chat" })
|
||||||
|
;(app as unknown as { transcript: { user(content: string): void } }).transcript.user("User question")
|
||||||
|
await setup.flush()
|
||||||
|
|
||||||
|
const frame = setup.captureCharFrame()
|
||||||
|
const userLine = frame.split("\n").find((line) => line.includes("User question")) || ""
|
||||||
|
const agentLine = frame.split("\n").find((line) => line.includes("Agent **answer**")) || ""
|
||||||
|
const headerLine = frame.split("\n").find((line) => line.includes(">_ nanobot")) || ""
|
||||||
|
|
||||||
|
expect(userLine).toContain("› User question")
|
||||||
|
expect(agentLine).toContain("• Agent **answer**")
|
||||||
|
expect(headerLine).not.toMatch(/[╭╮│]/u)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps footer status and shortcuts visually separated", async () => {
|
||||||
|
setup = await createRenderer({ width: 88, height: 24, screenMode: "alternate-screen" })
|
||||||
|
const app = mount(setup)
|
||||||
|
app.accept({ event: "attached", chat_id: "chat" })
|
||||||
|
app.accept({ event: "turn_end", chat_id: "chat", latency_ms: 1700 })
|
||||||
|
await setup.flush()
|
||||||
|
|
||||||
|
const footer = setup.captureCharFrame().split("\n").find((line) => line.includes("Ready · 1.7s")) || ""
|
||||||
|
expect(footer).toContain("Ready · 1.7s")
|
||||||
|
expect(footer).toContain("enter send")
|
||||||
|
expect(footer).not.toContain("1.7senter")
|
||||||
|
|
||||||
|
app.accept({ event: "reasoning_delta", chat_id: "chat", text: "hidden" })
|
||||||
|
await Bun.sleep(130)
|
||||||
|
await setup.renderOnce()
|
||||||
|
const activeFooter = setup.captureCharFrame().split("\n").find((line) => line.includes("Thinking")) || ""
|
||||||
|
expect(activeFooter).toContain("ctrl+c stop")
|
||||||
|
expect(activeFooter).not.toContain("enter send")
|
||||||
|
app.accept({ event: "turn_end", chat_id: "chat" })
|
||||||
|
})
|
||||||
|
|
||||||
test("keeps an explicit theme stable when the terminal reports another mode", async () => {
|
test("keeps an explicit theme stable when the terminal reports another mode", async () => {
|
||||||
setup = await createRenderer({ width: 72, height: 20, screenMode: "alternate-screen" })
|
setup = await createRenderer({ width: 72, height: 20, screenMode: "alternate-screen" })
|
||||||
const app = NanobotTui.mount(
|
const app = NanobotTui.mount(
|
||||||
|
|||||||
+32
-8
@@ -116,7 +116,7 @@ function transcriptTheme(palette: Palette): TranscriptTheme {
|
|||||||
muted: palette.muted,
|
muted: palette.muted,
|
||||||
error: palette.error,
|
error: palette.error,
|
||||||
user: palette.user,
|
user: palette.user,
|
||||||
border: palette.border,
|
assistant: palette.accent,
|
||||||
syntax: syntaxStyle(palette),
|
syntax: syntaxStyle(palette),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,6 +176,7 @@ export class NanobotTui {
|
|||||||
private readonly promptHistory: string[] = []
|
private readonly promptHistory: string[] = []
|
||||||
private historyCursor = 0
|
private historyCursor = 0
|
||||||
private historyDraft = ""
|
private historyDraft = ""
|
||||||
|
private modelName: string
|
||||||
private quitting = false
|
private quitting = false
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
@@ -185,6 +186,7 @@ export class NanobotTui {
|
|||||||
treeSitterClient = getTreeSitterClient(),
|
treeSitterClient = getTreeSitterClient(),
|
||||||
) {
|
) {
|
||||||
this.renderer = renderer
|
this.renderer = renderer
|
||||||
|
this.modelName = options.model
|
||||||
this.activeThemeMode = this.resolveThemeMode(renderer.themeMode)
|
this.activeThemeMode = this.resolveThemeMode(renderer.themeMode)
|
||||||
this.palette = this.activeThemeMode === "light" ? LIGHT : DARK
|
this.palette = this.activeThemeMode === "light" ? LIGHT : DARK
|
||||||
this.transcript = new Transcript(renderer, transcriptTheme(this.palette), treeSitterClient)
|
this.transcript = new Transcript(renderer, transcriptTheme(this.palette), treeSitterClient)
|
||||||
@@ -210,7 +212,7 @@ export class NanobotTui {
|
|||||||
})
|
})
|
||||||
this.title = new TextRenderable(renderer, {
|
this.title = new TextRenderable(renderer, {
|
||||||
id: "nanobot-tui-title",
|
id: "nanobot-tui-title",
|
||||||
content: `nanobot · ${options.model}`,
|
content: `nanobot · ${this.modelName}`,
|
||||||
height: 1,
|
height: 1,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
fg: this.palette.muted,
|
fg: this.palette.muted,
|
||||||
@@ -258,13 +260,18 @@ export class NanobotTui {
|
|||||||
content: "Connecting…",
|
content: "Connecting…",
|
||||||
fg: this.palette.muted,
|
fg: this.palette.muted,
|
||||||
height: 1,
|
height: 1,
|
||||||
|
width: "auto",
|
||||||
|
minWidth: 0,
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
})
|
})
|
||||||
this.meta = new TextRenderable(renderer, {
|
this.meta = new TextRenderable(renderer, {
|
||||||
id: "nanobot-tui-meta",
|
id: "nanobot-tui-meta",
|
||||||
content: "enter send · alt+enter newline · ctrl+c stop",
|
content: "enter send · alt+enter newline · ctrl+c stop",
|
||||||
fg: this.palette.faint,
|
fg: this.palette.faint,
|
||||||
height: 1,
|
height: 1,
|
||||||
|
width: "auto",
|
||||||
|
flexShrink: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
const statusRow = new BoxRenderable(renderer, {
|
const statusRow = new BoxRenderable(renderer, {
|
||||||
@@ -274,6 +281,7 @@ export class NanobotTui {
|
|||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
|
gap: 2,
|
||||||
})
|
})
|
||||||
this.composerFrame.add(this.composer)
|
this.composerFrame.add(this.composer)
|
||||||
statusRow.add(this.status)
|
statusRow.add(this.status)
|
||||||
@@ -465,10 +473,10 @@ export class NanobotTui {
|
|||||||
case "goal_state":
|
case "goal_state":
|
||||||
return
|
return
|
||||||
case "turn_model_updated":
|
case "turn_model_updated":
|
||||||
this.title.content = `nanobot · ${event.model_name}`
|
this.setModel(event.model_name)
|
||||||
return
|
return
|
||||||
case "runtime_model_updated":
|
case "runtime_model_updated":
|
||||||
this.title.content = `nanobot · ${event.model_name}`
|
this.setModel(event.model_name)
|
||||||
return
|
return
|
||||||
case "error":
|
case "error":
|
||||||
this.transcript.finishStream(this.turnHadAnswer ? "" : this.finalMessage)
|
this.transcript.finishStream(this.turnHadAnswer ? "" : this.finalMessage)
|
||||||
@@ -484,7 +492,7 @@ export class NanobotTui {
|
|||||||
try {
|
try {
|
||||||
if (restoring) {
|
if (restoring) {
|
||||||
this.transcript.reset({
|
this.transcript.reset({
|
||||||
model: this.options.model,
|
model: this.modelName,
|
||||||
workspace: this.options.workspace,
|
workspace: this.options.workspace,
|
||||||
version: this.options.version,
|
version: this.options.version,
|
||||||
access: this.options.access,
|
access: this.options.access,
|
||||||
@@ -544,6 +552,7 @@ export class NanobotTui {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.activeTurn = active
|
this.activeTurn = active
|
||||||
|
this.updateMeta()
|
||||||
if (active) {
|
if (active) {
|
||||||
this.activeStartedAt = startedAt ?? Date.now()
|
this.activeStartedAt = startedAt ?? Date.now()
|
||||||
this.shimmerFrame = 0
|
this.shimmerFrame = 0
|
||||||
@@ -664,14 +673,29 @@ export class NanobotTui {
|
|||||||
|
|
||||||
private handleResize = (): void => {
|
private handleResize = (): void => {
|
||||||
this.resizeComposer()
|
this.resizeComposer()
|
||||||
this.title.visible = this.renderer.height >= 12
|
this.title.visible = this.renderer.height >= 14
|
||||||
this.meta.content = this.renderer.width >= 72
|
this.updateMeta()
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateMeta(): void {
|
||||||
|
if (this.activeTurn) {
|
||||||
|
this.meta.content = this.renderer.width >= 48 ? "ctrl+c stop" : ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.meta.content = this.renderer.width >= 112
|
||||||
? "enter send · alt+enter newline · pgup/pgdn scroll · ctrl+o tools · ctrl+c stop"
|
? "enter send · alt+enter newline · pgup/pgdn scroll · ctrl+o tools · ctrl+c stop"
|
||||||
: this.renderer.width >= 48
|
: this.renderer.width >= 72
|
||||||
|
? "enter send · alt+enter newline · ctrl+c stop"
|
||||||
|
: this.renderer.width >= 48
|
||||||
? "enter send · alt+enter newline"
|
? "enter send · alt+enter newline"
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setModel(model: string): void {
|
||||||
|
this.modelName = model
|
||||||
|
this.title.content = `nanobot · ${model}`
|
||||||
|
}
|
||||||
|
|
||||||
private resizeComposer(): void {
|
private resizeComposer(): void {
|
||||||
const maxHeight = Math.max(1, Math.min(12, Math.floor(this.renderer.height / 3)))
|
const maxHeight = Math.max(1, Math.min(12, Math.floor(this.renderer.height / 3)))
|
||||||
this.composer.maxHeight = maxHeight
|
this.composer.maxHeight = maxHeight
|
||||||
|
|||||||
+55
-36
@@ -16,7 +16,7 @@ export interface TranscriptTheme {
|
|||||||
muted: string
|
muted: string
|
||||||
error: string
|
error: string
|
||||||
user: string
|
user: string
|
||||||
border: string
|
assistant: string
|
||||||
syntax: SyntaxStyle
|
syntax: SyntaxStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,10 +43,9 @@ export class Transcript {
|
|||||||
private activity: Activity | null = null
|
private activity: Activity | null = null
|
||||||
private readonly styledText: Array<{
|
private readonly styledText: Array<{
|
||||||
renderable: TextRenderable
|
renderable: TextRenderable
|
||||||
tone: "text" | "muted" | "error" | "user"
|
tone: "text" | "muted" | "error" | "user" | "assistant"
|
||||||
}> = []
|
}> = []
|
||||||
private readonly markdown = new Set<MarkdownRenderable>()
|
private readonly markdown = new Set<MarkdownRenderable>()
|
||||||
private readonly frames = new Set<BoxRenderable>()
|
|
||||||
private readonly activities = new Set<Activity>()
|
private readonly activities = new Set<Activity>()
|
||||||
private wrote = false
|
private wrote = false
|
||||||
private nextId = 0
|
private nextId = 0
|
||||||
@@ -85,7 +84,6 @@ export class Transcript {
|
|||||||
this.theme = theme
|
this.theme = theme
|
||||||
for (const { renderable, tone } of this.styledText) renderable.fg = theme[tone]
|
for (const { renderable, tone } of this.styledText) renderable.fg = theme[tone]
|
||||||
for (const renderable of this.markdown) renderable.syntaxStyle = theme.syntax
|
for (const renderable of this.markdown) renderable.syntaxStyle = theme.syntax
|
||||||
for (const frame of this.frames) frame.borderColor = theme.border
|
|
||||||
// Markdown may still be rendering this frame. Release the prior native
|
// Markdown may still be rendering this frame. Release the prior native
|
||||||
// style only after the renderer reaches idle, matching OpenCode's retained
|
// style only after the renderer reaches idle, matching OpenCode's retained
|
||||||
// theme lifecycle and avoiding both leaks and use-after-free transitions.
|
// theme lifecycle and avoiding both leaks and use-after-free transitions.
|
||||||
@@ -93,11 +91,16 @@ export class Transcript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header(options: TranscriptHeader): void {
|
header(options: TranscriptHeader): void {
|
||||||
this.writeText([
|
const row = this.createRow("header")
|
||||||
`>_ nanobot v${options.version}`,
|
const title = this.createText(`>_ nanobot v${options.version}`, "text", true)
|
||||||
|
const context = this.createText([
|
||||||
`${options.model} · ${options.access}`,
|
`${options.model} · ${options.access}`,
|
||||||
options.workspace,
|
options.workspace,
|
||||||
].join("\n"), "text", true, true)
|
].join("\n"), "muted")
|
||||||
|
row.add(title)
|
||||||
|
row.add(context)
|
||||||
|
this.root.add(row)
|
||||||
|
this.wrote = true
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(header: TranscriptHeader): void {
|
reset(header: TranscriptHeader): void {
|
||||||
@@ -109,7 +112,6 @@ export class Transcript {
|
|||||||
this.activity = null
|
this.activity = null
|
||||||
this.styledText.length = 0
|
this.styledText.length = 0
|
||||||
this.markdown.clear()
|
this.markdown.clear()
|
||||||
this.frames.clear()
|
|
||||||
this.activities.clear()
|
this.activities.clear()
|
||||||
this.wrote = false
|
this.wrote = false
|
||||||
this.nextId = 0
|
this.nextId = 0
|
||||||
@@ -128,7 +130,7 @@ export class Transcript {
|
|||||||
|
|
||||||
user(content: string): void {
|
user(content: string): void {
|
||||||
this.finishActivity()
|
this.finishActivity()
|
||||||
this.writeText(`› ${content}`, "user", true)
|
this.writeRole("›", content, "user")
|
||||||
}
|
}
|
||||||
|
|
||||||
assistant(content: string): void {
|
assistant(content: string): void {
|
||||||
@@ -139,19 +141,16 @@ export class Transcript {
|
|||||||
|
|
||||||
notice(content: string, error = false): void {
|
notice(content: string, error = false): void {
|
||||||
this.finishActivity()
|
this.finishActivity()
|
||||||
this.writeText(content, error ? "error" : "muted")
|
this.writeRole(error ? "×" : "·", content, error ? "error" : "muted")
|
||||||
}
|
}
|
||||||
|
|
||||||
stream(delta: string): void {
|
stream(delta: string): void {
|
||||||
if (!delta) return
|
if (!delta) return
|
||||||
if (!this.live) {
|
if (!this.live) {
|
||||||
this.finishActivity()
|
this.finishActivity()
|
||||||
const row = this.createRow()
|
|
||||||
const markdown = this.createMarkdown("", true, "assistant-stream")
|
const markdown = this.createMarkdown("", true, "assistant-stream")
|
||||||
row.add(markdown)
|
const row = this.writeAssistant(markdown)
|
||||||
this.root.add(row)
|
|
||||||
this.live = { row, markdown, content: "" }
|
this.live = { row, markdown, content: "" }
|
||||||
this.wrote = true
|
|
||||||
}
|
}
|
||||||
this.live.content += delta
|
this.live.content += delta
|
||||||
this.live.markdown.content = this.live.content
|
this.live.markdown.content = this.live.content
|
||||||
@@ -238,24 +237,17 @@ export class Transcript {
|
|||||||
return `${prefix}-${this.nextId}`
|
return `${prefix}-${this.nextId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
private createRow(framed = false): BoxRenderable {
|
private createRow(kind = "row", direction: "column" | "row" = "column"): BoxRenderable {
|
||||||
const row = new BoxRenderable(this.renderer, {
|
return new BoxRenderable(this.renderer, {
|
||||||
id: this.id(framed ? "text-frame" : "text-row"),
|
id: this.id(`${kind}-row`),
|
||||||
width: "100%",
|
width: "100%",
|
||||||
marginTop: this.wrote ? 1 : 0,
|
marginTop: this.wrote ? 1 : 0,
|
||||||
border: framed,
|
flexDirection: direction,
|
||||||
borderStyle: "rounded",
|
|
||||||
borderColor: this.theme.border,
|
|
||||||
paddingLeft: 1,
|
|
||||||
paddingRight: 1,
|
|
||||||
flexDirection: "column",
|
|
||||||
})
|
})
|
||||||
if (framed) this.frames.add(row)
|
|
||||||
return row
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createActivity(): Activity {
|
private createActivity(): Activity {
|
||||||
const row = this.createRow()
|
const row = this.createRow("activity")
|
||||||
const text = new TextRenderable(this.renderer, {
|
const text = new TextRenderable(this.renderer, {
|
||||||
id: this.id("agent-activity"),
|
id: this.id("agent-activity"),
|
||||||
content: "",
|
content: "",
|
||||||
@@ -282,24 +274,40 @@ export class Transcript {
|
|||||||
activity.text.content = [` … ${hidden} earlier steps · Ctrl+O expand`, ...visible].join("\n")
|
activity.text.content = [` … ${hidden} earlier steps · Ctrl+O expand`, ...visible].join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
private writeText(
|
private createText(
|
||||||
content: string,
|
content: string,
|
||||||
tone: "text" | "muted" | "error" | "user",
|
tone: "text" | "muted" | "error" | "user" | "assistant",
|
||||||
bold = false,
|
bold = false,
|
||||||
framed = false,
|
id = "text",
|
||||||
): void {
|
): TextRenderable {
|
||||||
const row = this.createRow(framed)
|
|
||||||
const text = new TextRenderable(this.renderer, {
|
const text = new TextRenderable(this.renderer, {
|
||||||
id: this.id("text"),
|
id: this.id(id),
|
||||||
content,
|
content,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
wrapMode: "word",
|
wrapMode: "word",
|
||||||
fg: this.theme[tone],
|
fg: this.theme[tone],
|
||||||
attributes: bold ? TextAttributes.BOLD : 0,
|
attributes: bold ? TextAttributes.BOLD : 0,
|
||||||
})
|
})
|
||||||
|
this.styledText.push({ renderable: text, tone })
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeRole(
|
||||||
|
marker: string,
|
||||||
|
content: string,
|
||||||
|
tone: "muted" | "error" | "user",
|
||||||
|
): void {
|
||||||
|
const row = this.createRow(tone === "user" ? "user" : "notice", "row")
|
||||||
|
const prefix = this.createText(marker, tone, true, "role-marker")
|
||||||
|
prefix.width = 2
|
||||||
|
prefix.flexShrink = 0
|
||||||
|
const text = this.createText(content, tone === "user" ? "text" : tone, false, "role-content")
|
||||||
|
text.width = "auto"
|
||||||
|
text.minWidth = 0
|
||||||
|
text.flexGrow = 1
|
||||||
|
row.add(prefix)
|
||||||
row.add(text)
|
row.add(text)
|
||||||
this.root.add(row)
|
this.root.add(row)
|
||||||
this.styledText.push({ renderable: text, tone })
|
|
||||||
this.wrote = true
|
this.wrote = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +315,9 @@ export class Transcript {
|
|||||||
const markdown = new MarkdownRenderable(this.renderer, {
|
const markdown = new MarkdownRenderable(this.renderer, {
|
||||||
id: this.id(id),
|
id: this.id(id),
|
||||||
content,
|
content,
|
||||||
width: "100%",
|
width: "auto",
|
||||||
|
minWidth: 0,
|
||||||
|
flexGrow: 1,
|
||||||
syntaxStyle: this.theme.syntax,
|
syntaxStyle: this.theme.syntax,
|
||||||
streaming,
|
streaming,
|
||||||
internalBlockMode: "top-level",
|
internalBlockMode: "top-level",
|
||||||
@@ -318,10 +328,19 @@ export class Transcript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private writeMarkdown(content: string, streaming: boolean): void {
|
private writeMarkdown(content: string, streaming: boolean): void {
|
||||||
const row = this.createRow()
|
this.writeAssistant(this.createMarkdown(content, streaming))
|
||||||
row.add(this.createMarkdown(content, streaming))
|
}
|
||||||
|
|
||||||
|
private writeAssistant(markdown: MarkdownRenderable): BoxRenderable {
|
||||||
|
const row = this.createRow("assistant", "row")
|
||||||
|
const prefix = this.createText("•", "assistant", false, "role-marker")
|
||||||
|
prefix.width = 2
|
||||||
|
prefix.flexShrink = 0
|
||||||
|
row.add(prefix)
|
||||||
|
row.add(markdown)
|
||||||
this.root.add(row)
|
this.root.add(row)
|
||||||
this.wrote = true
|
this.wrote = true
|
||||||
|
return row
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user