feat(packages/tui-and-shared-tooling-primitives): added TUI terminal support
- Made `Component.invalidate` optional in `packages/tui/src/tui.ts` and callsites. - Added `iconOverride` support to status-line rendering in `status-line.ts`. - Added `borderColor` and `framedBlock()` primitives in `output-block.ts` for framed output styling. - Adjusted `renderOutputBlock()` to draw a continuous border when no header text is present. - Added terminal checkpoint-reconciliation tests in `submit-checkpoint-reconcile.test.ts` for pinned-tail behavior.
This commit is contained in:
@@ -82,7 +82,7 @@ export class SetupWizardComponent implements Component {
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.#activeScene?.invalidate();
|
||||
this.#activeScene?.invalidate?.();
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { ImageProtocol, padding, TERMINAL, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
||||
import type { Theme } from "../modes/theme/theme";
|
||||
import type { Theme, ThemeColor } from "../modes/theme/theme";
|
||||
import { getSixelLineMask } from "../utils/sixel";
|
||||
import type { State } from "./types";
|
||||
import type { RenderCache } from "./utils";
|
||||
@@ -18,6 +18,9 @@ export interface OutputBlockOptions {
|
||||
applyBg?: boolean;
|
||||
/** Animate the border with a sweeping dark segment (pending/running state). */
|
||||
animate?: boolean;
|
||||
/** Override the state-derived border color. Used for muted "legacy" tool
|
||||
* frames that should not visually compete with framed-output tools. */
|
||||
borderColor?: ThemeColor;
|
||||
}
|
||||
|
||||
const FRAMED_BLOCK_COMPONENT = Symbol("framedBlockComponent");
|
||||
@@ -99,14 +102,15 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
||||
const cap = h.repeat(3);
|
||||
const lineWidth = Math.max(0, width);
|
||||
// Border colors: running/pending use accent, success uses dim (gray), error/warning keep their colors
|
||||
const borderColor: "error" | "warning" | "accent" | "dim" =
|
||||
state === "error"
|
||||
const borderColor: ThemeColor =
|
||||
options.borderColor ??
|
||||
(state === "error"
|
||||
? "error"
|
||||
: state === "warning"
|
||||
? "warning"
|
||||
: state === "running" || state === "pending"
|
||||
? "accent"
|
||||
: "dim";
|
||||
: "dim");
|
||||
const border = (text: string) => theme.fg(borderColor, text);
|
||||
const bgFn = (() => {
|
||||
if (!state || !applyBg) return undefined;
|
||||
@@ -202,7 +206,12 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
||||
const rightGlyph = row.rightChar;
|
||||
if (lineWidth <= 0) return border(leftGlyphs) + border(rightGlyph);
|
||||
const labelText = [row.label, row.meta].filter(Boolean).join(theme.sep.dot);
|
||||
const rawLabel = labelText ? ` ${labelText} ` : " ";
|
||||
if (!labelText) {
|
||||
// No header: draw a clean, continuous top/separator bar (no 1-col gap).
|
||||
const fillCount = Math.max(0, lineWidth - visibleWidth(leftGlyphs) - visibleWidth(rightGlyph));
|
||||
return `${border(leftGlyphs)}${border(h.repeat(fillCount))}${border(rightGlyph)}`;
|
||||
}
|
||||
const rawLabel = ` ${labelText} `;
|
||||
const leftWidth = visibleWidth(leftGlyphs);
|
||||
const rightWidth = visibleWidth(rightGlyph);
|
||||
const maxLabelWidth = Math.max(0, lineWidth - leftWidth - rightWidth);
|
||||
@@ -272,6 +281,7 @@ export class CachedOutputBlock {
|
||||
h.optional(options.header);
|
||||
h.optional(options.headerMeta);
|
||||
h.optional(options.state);
|
||||
h.optional(options.borderColor);
|
||||
h.bool(options.applyBg ?? true);
|
||||
h.bool(options.animate ?? false);
|
||||
if (options.animate) h.u32(borderShimmerTick());
|
||||
@@ -286,3 +296,17 @@ export class CachedOutputBlock {
|
||||
return h.digest();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a self-framing tool component backed by a cached output block. The
|
||||
* `build` callback returns the block options for a given width; the cache
|
||||
* dedupes re-renders. Pass `borderColor: "borderMuted"` for the dim "legacy"
|
||||
* look that does not compete with the state-colored framed tools.
|
||||
*/
|
||||
export function framedBlock(theme: Theme, build: (width: number) => OutputBlockOptions): Component {
|
||||
const block = new CachedOutputBlock();
|
||||
return {
|
||||
render: (width: number): string[] => block.render(build(width), theme),
|
||||
invalidate: () => block.invalidate(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ import { formatStatusIcon } from "../tools/render-utils";
|
||||
|
||||
export interface StatusLineOptions {
|
||||
icon?: ToolUIStatus;
|
||||
/** Pre-rendered glyph that replaces the status icon (e.g. a magnifier for
|
||||
* search-family tools). Takes precedence over `icon`. */
|
||||
iconOverride?: string;
|
||||
spinnerFrame?: number;
|
||||
title: string;
|
||||
titleColor?: ThemeColor;
|
||||
@@ -27,7 +30,8 @@ function flattenForHeader(text: string): string {
|
||||
}
|
||||
|
||||
export function renderStatusLine(options: StatusLineOptions, theme: Theme): string {
|
||||
const icon = options.icon ? formatStatusIcon(options.icon, theme, options.spinnerFrame) : "";
|
||||
const icon =
|
||||
options.iconOverride ?? (options.icon ? formatStatusIcon(options.icon, theme, options.spinnerFrame) : "");
|
||||
const titleColor = options.titleColor ?? "accent";
|
||||
const title = theme.fg(titleColor, flattenForHeader(options.title));
|
||||
let line = icon ? `${icon} ${title}` : title;
|
||||
|
||||
Reference in New Issue
Block a user