fix ctrl-c/fix flickering

This commit is contained in:
Simon Klee
2026-03-29 19:22:52 +02:00
parent 7871920b56
commit 02a958b30c
3 changed files with 75 additions and 27 deletions

View File

@@ -26,6 +26,7 @@ export class RunFooter implements FooterApi {
private rows = TEXTAREA_MIN_ROWS
private state: Accessor<FooterState>
private setState: Setter<FooterState>
private settle = false
constructor(
private renderer: CliRenderer,
@@ -91,12 +92,14 @@ export class RunFooter implements FooterApi {
return
}
this.setState((state) => ({
phase: next.phase ?? state.phase,
status: typeof next.status === "string" ? next.status : state.status,
queue: typeof next.queue === "number" ? Math.max(0, next.queue) : state.queue,
model: typeof next.model === "string" ? next.model : state.model,
}))
const prev = this.state()
const state = {
phase: next.phase ?? prev.phase,
status: typeof next.status === "string" ? next.status : prev.status,
queue: typeof next.queue === "number" ? Math.max(0, next.queue) : prev.queue,
model: typeof next.model === "string" ? next.model : prev.model,
}
this.setState(state)
}
public append(kind: EntryKind, text: string): void {
@@ -109,6 +112,7 @@ export class RunFooter implements FooterApi {
}
this.renderer.writeToScrollback(entryWriter(kind, text, new Date()))
this.scheduleSettleRender()
}
public close(): void {
@@ -117,10 +121,6 @@ export class RunFooter implements FooterApi {
}
this.notifyClose()
if (!this.renderer.isDestroyed) {
this.renderer.destroy()
}
}
public destroy(): void {
@@ -208,4 +208,21 @@ export class RunFooter implements FooterApi {
private handleDestroy = (): void => {
this.notifyClose()
}
private scheduleSettleRender(): void {
if (this.settle || this.destroyed || this.renderer.isDestroyed) {
return
}
this.settle = true
void this.renderer.idle().then(() => {
this.settle = false
if (this.destroyed || this.renderer.isDestroyed || this.closed) {
return
}
this.renderer.requestRender()
})
}
}

View File

@@ -166,6 +166,7 @@ export function RunFooterView(props: RunFooterViewProps) {
let area: Area | undefined
let leader = false
let timeout: NodeJS.Timeout | undefined
let rowsTick = false
const clearLeader = () => {
leader = false
@@ -192,6 +193,18 @@ export function RunFooterView(props: RunFooterViewProps) {
props.onRows(clampRows(area.virtualLineCount || 1))
}
const scheduleRows = () => {
if (rowsTick) {
return
}
rowsTick = true
queueMicrotask(() => {
rowsTick = false
syncRows()
})
}
const push = (text: string) => {
if (!text) {
return
@@ -289,6 +302,12 @@ export function RunFooterView(props: RunFooterViewProps) {
}
const onKeyDown = (event: Key) => {
if (event.ctrl && event.name === "c") {
props.onExit()
event.preventDefault()
return
}
if (handleCycle(event)) {
return
}
@@ -329,36 +348,33 @@ export function RunFooterView(props: RunFooterViewProps) {
push(text)
area.setText("")
syncRows()
scheduleRows()
area.focus()
}
const onLineInfoChange = () => {
syncRows()
}
onMount(() => {
if (!area || area.isDestroyed) {
return
}
area.on("line-info-change", onLineInfoChange)
syncRows()
area.on("line-info-change", scheduleRows)
scheduleRows()
area.focus()
})
onCleanup(() => {
clearLeader()
if (!area || area.isDestroyed) {
return
}
area.off("line-info-change", onLineInfoChange)
area.off("line-info-change", scheduleRows)
})
createEffect(() => {
term().width
queueMicrotask(syncRows)
scheduleRows()
})
createEffect(() => {
@@ -443,7 +459,7 @@ export function RunFooterView(props: RunFooterViewProps) {
keyBindings={bindings()}
onSubmit={onSubmit}
onKeyDown={onKeyDown}
onContentChange={syncRows}
onContentChange={scheduleRows}
ref={(item) => {
area = item as Area
}}

View File

@@ -1,4 +1,4 @@
import { createCliRenderer } from "@opentui/core"
import { createCliRenderer, type CliRenderer } from "@opentui/core"
import { TuiConfig } from "../../../config/tui"
import { RunFooter } from "./footer"
import { formatUnknownError, runPromptTurn } from "./stream"
@@ -13,6 +13,24 @@ const DEFAULT_KEYBINDS: FooterKeybinds = {
inputNewline: "shift+return,ctrl+return,alt+return,ctrl+j",
}
function shutdown(renderer: CliRenderer): void {
if (renderer.isDestroyed) {
return
}
if (renderer.externalOutputMode === "capture-stdout") {
renderer.externalOutputMode = "passthrough"
}
if (renderer.screenMode === "split-footer") {
renderer.screenMode = "main-screen"
}
if (!renderer.isDestroyed) {
renderer.destroy()
}
}
function formatModelLabel(model: NonNullable<RunInput["model"]>, variant: string | undefined): string {
const variantLabel = variant ? ` · ${variant}` : ""
return `${model.modelID} · ${model.providerID}${variantLabel}`
@@ -218,16 +236,15 @@ export async function runInteractiveMode(input: RunInput): Promise<void> {
useMouse: false,
autoFocus: false,
openConsoleOnError: false,
exitOnCtrlC: true,
exitOnCtrlC: false,
useKittyKeyboard: { events: process.platform === "win32" },
screenMode: "split-footer",
footerHeight: FOOTER_HEIGHT,
externalOutputMode: "capture-stdout",
consoleMode: "disabled",
clearOnShutdown: false,
})
renderer.start()
const footer = new RunFooter(renderer, {
...footerLabels({
agent: input.agent,
@@ -278,8 +295,6 @@ export async function runInteractiveMode(input: RunInput): Promise<void> {
})
} finally {
footer.destroy()
if (!renderer.isDestroyed) {
renderer.destroy()
}
shutdown(renderer)
}
}