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:
can1357
2026-06-07 05:13:58 +02:00
parent d18cc3c717
commit 1ebe2c7484
8 changed files with 160 additions and 12 deletions
@@ -82,7 +82,7 @@ export class SetupWizardComponent implements Component {
}
invalidate(): void {
this.#activeScene?.invalidate();
this.#activeScene?.invalidate?.();
}
handleInput(data: string): void {
+29 -5
View File
@@ -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(),
};
}
+5 -1
View File
@@ -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;