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:
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user