feat(tui): removed cursor positioning and refactored TUI rendering with explicit env defaults

- Removed getCursorPosition() method from Component interface and implementations, eliminating hardware cursor positioning support.
- Changed PI_HARDWARE_CURSOR environment variable default from implicit true to explicit '1' with fallback pattern.
- Changed PI_CLEAR_ON_SHRINK environment variable default from implicit false to explicit '0' with fallback pattern.
- Added screen clearing on TUI startup to prevent shell prompts from bleeding into first rendered frame.
- Refactored full-render logic into reusable fullRender() helper function.
This commit is contained in:
can1357
2026-02-06 04:35:16 +01:00
parent 3240318338
commit 7222bc1eae
23 changed files with 282 additions and 398 deletions
+8 -6
View File
@@ -21,12 +21,11 @@ export interface Args {
appendSystemPrompt?: string;
thinking?: ThinkingLevel;
continue?: boolean;
resume?: boolean;
resume?: string | true;
help?: boolean;
version?: boolean;
mode?: Mode;
noSession?: boolean;
session?: string;
sessionDir?: string;
models?: string[];
tools?: string[];
@@ -76,8 +75,13 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
}
} else if (arg === "--continue" || arg === "-c") {
result.continue = true;
} else if (arg === "--resume" || arg === "-r") {
result.resume = true;
} else if (arg === "--resume" || arg === "-r" || arg === "--session") {
const next = args[i + 1];
if (next && !next.startsWith("-")) {
result.resume = args[++i];
} else {
result.resume = true;
}
} else if (arg === "--provider" && i + 1 < args.length) {
result.provider = args[++i];
} else if (arg === "--model" && i + 1 < args.length) {
@@ -96,8 +100,6 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
result.appendSystemPrompt = args[++i];
} else if (arg === "--no-session") {
result.noSession = true;
} else if (arg === "--session" && i + 1 < args.length) {
result.session = args[++i];
} else if (arg === "--session-dir" && i + 1 < args.length) {
result.sessionDir = args[++i];
} else if (arg === "--models" && i + 1 < args.length) {
@@ -58,12 +58,9 @@ export default class Index extends Command {
char: "c",
description: "Continue previous session",
}),
resume: Flags.boolean({
resume: Flags.string({
char: "r",
description: "Select a session to resume",
}),
session: Flags.string({
description: "Use specific session file",
description: "Resume a session (by ID prefix, path, or picker if omitted)",
}),
"session-dir": Flags.string({
description: "Directory for session storage and lookup",
-8
View File
@@ -40,9 +40,6 @@ export type {
export { CustomToolLoader, discoverAndLoadCustomTools, loadCustomTools } from "./extensibility/custom-tools";
export type {
AppAction,
BashToolCallEvent,
CustomToolCallEvent,
EditToolCallEvent,
Extension,
ExtensionActions,
ExtensionAPI,
@@ -58,15 +55,12 @@ export type {
ExtensionShortcut,
ExtensionUIContext,
ExtensionUIDialogOptions,
FindToolCallEvent,
GrepToolCallEvent,
InputEvent,
InputEventResult,
KeybindingsManager,
LoadExtensionsResult,
MessageRenderer,
MessageRenderOptions,
ReadToolCallEvent,
RegisteredCommand,
ToolCallEvent,
ToolResultEvent,
@@ -76,14 +70,12 @@ export type {
UserBashEventResult,
UserPythonEvent,
UserPythonEventResult,
WriteToolCallEvent,
} from "./extensibility/extensions";
// Extension types and utilities
export {
discoverAndLoadExtensions,
ExtensionRunner,
ExtensionRuntime,
isToolCallEventType,
} from "./extensibility/extensions";
// Hook system types (legacy re-export)
export type * from "./extensibility/hooks";
+5 -5
View File
@@ -235,8 +235,8 @@ async function createSessionManager(parsed: Args, cwd: string): Promise<SessionM
if (parsed.noSession) {
return SessionManager.inMemory();
}
if (parsed.session) {
const sessionArg = parsed.session;
if (typeof parsed.resume === "string") {
const sessionArg = parsed.resume;
if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
return await SessionManager.open(sessionArg, parsed.sessionDir);
}
@@ -258,7 +258,7 @@ async function createSessionManager(parsed: Args, cwd: string): Promise<SessionM
if (parsed.continue) {
return await SessionManager.continueRecent(cwd, parsed.sessionDir);
}
// --resume is handled separately (needs picker UI)
// --resume without value is handled separately (needs picker UI)
// If --session-dir provided without --continue/--resume, create new session there
if (parsed.sessionDir) {
return SessionManager.create(cwd, parsed.sessionDir);
@@ -572,8 +572,8 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
debugStartup("main:createSessionManager");
time("createSessionManager");
// Handle --resume: show session picker
if (parsedArgs.resume) {
// Handle --resume (no value): show session picker
if (parsedArgs.resume === true) {
const sessions = await SessionManager.list(cwd, parsedArgs.sessionDir);
time("SessionManager.list");
if (sessions.length === 0) {
@@ -1,5 +1,6 @@
import * as Diff from "diff";
import { theme } from "../../modes/theme/theme";
import { replaceTabs } from "../../tools/render-utils";
/**
* Parse diff line to extract prefix, line number, and content.
@@ -11,13 +12,6 @@ function parseDiffLine(line: string): { prefix: string; lineNum: string; content
return { prefix: match[1], lineNum: match[2] ?? "", content: match[3] };
}
/**
* Replace tabs with spaces for consistent rendering.
*/
function replaceTabs(text: string): string {
return text.replace(/\t/g, " ");
}
/**
* Compute word-level diff and render with inverse on changed parts.
* Uses diffWords which groups whitespace with adjacent words for cleaner highlighting.
@@ -84,7 +84,7 @@ export class SelectorController {
},
getStatusLinePreview: () => {
// Return the rendered status line for inline preview
const width = this.ctx.ui.getWidth();
const width = this.ctx.ui.terminal.columns;
return this.ctx.statusLine.getTopBorder(width).content;
},
onPluginsChanged: () => {
@@ -17,6 +17,7 @@ import {
} from "@oh-my-pi/pi-tui";
import { $env, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import { APP_NAME } from "../config";
import { KeybindingsManager } from "../config/keybindings";
import { renderPromptTemplate } from "../config/prompt-templates";
import { type Settings, settings } from "../config/settings";
@@ -391,7 +392,7 @@ export class InteractiveMode implements InteractiveModeContext {
}
updateEditorTopBorder(): void {
const width = this.ui.getWidth();
const width = this.ui.terminal.columns;
const topBorder = this.statusLine.getTopBorder(width);
this.editor.setTopBorder(topBorder);
}
@@ -726,14 +727,26 @@ export class InteractiveMode implements InteractiveModeContext {
await this.session.emitCustomToolSessionEvent("shutdown");
if (this.isInitialized) {
await this.ui.waitForRender();
this.ui.requestRender(true);
}
// Wait for any pending renders to complete
// requestRender() uses process.nextTick(), so we wait one tick
await new Promise(resolve => process.nextTick(resolve));
// Drain any in-flight Kitty key release events before stopping.
// This prevents escape sequences from leaking to the parent shell over slow SSH.
await this.ui.terminal.drainInput(1000);
this.stop();
// Print resumption hint if this is a persisted session
const sessionId = this.sessionManager.getSessionId();
const sessionFile = this.sessionManager.getSessionFile();
if (sessionId && sessionFile) {
process.stderr.write(`\n${chalk.dim(`Resume this session with ${APP_NAME} --resume ${sessionId}`)}\n`);
}
await postmortem.quit(0);
}
@@ -204,13 +204,6 @@ export class UiHelpers {
// Render tool call components
for (const content of message.content) {
if (content.type !== "toolCall") {
// Text/thinking blocks between tool calls break read grouping
if (
(content.type === "text" && content.text?.trim()) ||
(content.type === "thinking" && (content as any).thinking?.trim())
) {
readGroup = null;
}
continue;
}
@@ -1265,7 +1265,9 @@ export class SessionManager {
if (!this.persist || !this.sessionFile) return;
await this._queuePersistTask(async () => {
await this._closePersistWriterInternal();
const entries = await Promise.all(this.fileEntries.map(entry => prepareEntryForPersistence(entry, this.blobStore)));
const entries = await Promise.all(
this.fileEntries.map(entry => prepareEntryForPersistence(entry, this.blobStore)),
);
await this._writeEntriesAtomically(entries);
this.flushed = true;
});
@@ -8,7 +8,7 @@ import * as os from "node:os";
import { type Ellipsis, truncateToWidth } from "@oh-my-pi/pi-tui";
import type { Theme } from "../modes/theme/theme";
export { Ellipsis, truncateToWidth } from "@oh-my-pi/pi-tui";
export { Ellipsis, replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
// =============================================================================
// Standardized Display Constants
@@ -672,10 +672,6 @@ export function wrapBrackets(text: string, theme: Theme): string {
return `${theme.format.bracketLeft}${text}${theme.format.bracketRight}`;
}
export function replaceTabs(text: string): string {
return text.replace(/\t/g, " ");
}
function pluralize(label: string, count: number): string {
if (count === 1) return label;
if (/(?:ch|sh|s|x|z)$/i.test(label)) return `${label}es`;
+5 -5
View File
@@ -2,7 +2,7 @@
* Hierarchical tree list rendering helper.
*/
import type { Theme } from "../modes/theme/theme";
import { formatMoreItems } from "../tools/render-utils";
import { formatMoreItems, replaceTabs } from "../tools/render-utils";
import type { TreeContext } from "./types";
import { getTreeBranch, getTreeContinuePrefix } from "./utils";
@@ -35,14 +35,14 @@ export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): st
const rendered = renderItem(items[i], context);
if (Array.isArray(rendered)) {
if (rendered.length === 0) continue;
lines.push(`${prefix}${rendered[0]}`);
lines.push(`${prefix}${replaceTabs(rendered[0])}`);
for (let j = 1; j < rendered.length; j++) {
lines.push(`${continuePrefix}${rendered[j]}`);
lines.push(`${continuePrefix}${replaceTabs(rendered[j])}`);
}
} else {
lines.push(`${prefix}${rendered}`);
lines.push(`${prefix}${replaceTabs(rendered)}`);
}
}
}
if (!expanded && items.length > maxItems) {
const remaining = items.length - maxItems;