feat(coding-agent): pinned turn-ending provider errors above editor
- Added a persistent error banner so stream errors survive transcript scroll. - Cleared the banner when the next turn starts. - Skipped pinning for aborts and normal stops.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
|
||||
import { getPreviewLines, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
||||
import { theme } from "../theme/theme";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
|
||||
/** Max lines of the error message shown in the pinned banner. */
|
||||
const MAX_BANNER_LINES = 3;
|
||||
|
||||
/**
|
||||
* A persistent error banner pinned above the editor. Unlike the transcript
|
||||
* "Error: …" line (which scrolls away as the conversation grows), this stays in
|
||||
* the fixed region directly above the input so a turn that ended on a provider
|
||||
* error — e.g. Anthropic's "Output blocked by content filtering policy" — cannot
|
||||
* be missed. It is cleared when the next turn starts.
|
||||
*/
|
||||
export class ErrorBannerComponent extends Container {
|
||||
constructor(message: string) {
|
||||
super();
|
||||
const lines = getPreviewLines(message, MAX_BANNER_LINES, TRUNCATE_LENGTHS.LINE);
|
||||
if (lines.length === 0) {
|
||||
lines.push("Unknown error");
|
||||
}
|
||||
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new DynamicBorder(str => theme.fg("error", str)));
|
||||
this.addChild(new Text(theme.bold(theme.fg("error", `${theme.status.error} ${lines[0]}`)), 1, 0));
|
||||
for (const line of lines.slice(1)) {
|
||||
this.addChild(new Text(theme.fg("error", ` ${line}`), 1, 0));
|
||||
}
|
||||
this.addChild(new Text(theme.fg("dim", "Dismissed when you send your next message."), 1, 0));
|
||||
this.addChild(new DynamicBorder(str => theme.fg("error", str)));
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@ export class EventController {
|
||||
this.#readToolCallAssistantComponents.clear();
|
||||
this.#assistantMessageStreaming = false;
|
||||
this.#lastAssistantComponent = undefined;
|
||||
this.ctx.clearPinnedError();
|
||||
if (this.ctx.retryEscapeHandler) {
|
||||
this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
|
||||
this.ctx.retryEscapeHandler = undefined;
|
||||
@@ -490,6 +491,16 @@ export class EventController {
|
||||
this.#lastAssistantComponent.markTranscriptBlockFinalized();
|
||||
this.ctx.streamingComponent = undefined;
|
||||
this.ctx.streamingMessage = undefined;
|
||||
// Pin a turn-ending provider error (e.g. Anthropic content-filter block)
|
||||
// above the editor so it survives transcript scroll. Cleared at the next
|
||||
// turn's agent_start.
|
||||
if (
|
||||
event.message.stopReason === "error" &&
|
||||
event.message.errorMessage &&
|
||||
!isSilentAbort(event.message.errorMessage)
|
||||
) {
|
||||
this.ctx.showPinnedError(event.message.errorMessage);
|
||||
}
|
||||
this.ctx.statusLine.invalidate();
|
||||
this.ctx.updateEditorTopBorder();
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ import type { AssistantMessageComponent } from "./components/assistant-message";
|
||||
import type { BashExecutionComponent } from "./components/bash-execution";
|
||||
import { CustomEditor } from "./components/custom-editor";
|
||||
import { DynamicBorder } from "./components/dynamic-border";
|
||||
import { ErrorBannerComponent } from "./components/error-banner";
|
||||
import type { EvalExecutionComponent } from "./components/eval-execution";
|
||||
import type { HookEditorComponent } from "./components/hook-editor";
|
||||
import type { HookInputComponent } from "./components/hook-input";
|
||||
@@ -264,6 +265,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
todoContainer: Container;
|
||||
btwContainer: Container;
|
||||
omfgContainer: Container;
|
||||
errorBannerContainer: Container;
|
||||
editor: CustomEditor;
|
||||
editorContainer: Container;
|
||||
hookWidgetContainerAbove: Container;
|
||||
@@ -405,6 +407,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
this.todoContainer = new Container();
|
||||
this.btwContainer = new Container();
|
||||
this.omfgContainer = new Container();
|
||||
this.errorBannerContainer = new Container();
|
||||
this.editor = new CustomEditor(getEditorTheme());
|
||||
this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
|
||||
this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
|
||||
@@ -566,6 +569,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
this.ui.addChild(this.todoContainer);
|
||||
this.ui.addChild(this.btwContainer);
|
||||
this.ui.addChild(this.omfgContainer);
|
||||
this.ui.addChild(this.errorBannerContainer);
|
||||
this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
|
||||
this.ui.addChild(this.hookWidgetContainerAbove);
|
||||
this.ui.addChild(this.editorContainer);
|
||||
@@ -2503,6 +2507,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
this.#uiHelpers.showError(message);
|
||||
}
|
||||
|
||||
showPinnedError(message: string): void {
|
||||
if (this.isBackgrounded) return;
|
||||
this.errorBannerContainer.clear();
|
||||
this.errorBannerContainer.addChild(new ErrorBannerComponent(message));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
clearPinnedError(): void {
|
||||
if (this.errorBannerContainer.children.length === 0) return;
|
||||
this.errorBannerContainer.clear();
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
showWarning(message: string): void {
|
||||
this.#uiHelpers.showWarning(message);
|
||||
}
|
||||
@@ -2723,6 +2740,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
this.#btwController.dispose();
|
||||
this.#omfgController.dispose();
|
||||
this.#extensionUiController.clearExtensionTerminalInputListeners();
|
||||
this.clearPinnedError();
|
||||
this.#planReviewContainer = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ export interface InteractiveModeContext {
|
||||
todoContainer: Container;
|
||||
btwContainer: Container;
|
||||
omfgContainer: Container;
|
||||
errorBannerContainer: Container;
|
||||
editor: CustomEditor;
|
||||
editorContainer: Container;
|
||||
hookWidgetContainerAbove: Container;
|
||||
@@ -159,6 +160,8 @@ export interface InteractiveModeContext {
|
||||
// UI helpers
|
||||
showStatus(message: string, options?: { dim?: boolean }): void;
|
||||
showError(message: string): void;
|
||||
showPinnedError(message: string): void;
|
||||
clearPinnedError(): void;
|
||||
showWarning(message: string): void;
|
||||
showNewVersionNotification(newVersion: string): void;
|
||||
clearEditor(): void;
|
||||
|
||||
Reference in New Issue
Block a user