fix: resize input (#6)
This commit is contained in:
@@ -44,6 +44,9 @@ import {
|
||||
import { safeJsonParseOr } from "./helpers/safeJsonParse";
|
||||
import { type ApprovalRequest, drainStream } from "./helpers/stream";
|
||||
import { getRandomThinkingMessage } from "./helpers/thinkingMessages";
|
||||
import { useTerminalWidth } from "./hooks/useTerminalWidth";
|
||||
|
||||
const CLEAR_SCREEN_AND_HOME = "\u001B[2J\u001B[H";
|
||||
|
||||
// tiny helper for unique ids (avoid overwriting prior user lines)
|
||||
function uid(prefix: string) {
|
||||
@@ -61,7 +64,7 @@ function getPlanModeReminder(): string {
|
||||
return PLAN_MODE_REMINDER;
|
||||
}
|
||||
|
||||
// items that we push into <Static> that are not part of the live transcript
|
||||
// Items that have finished rendering and no longer change
|
||||
type StaticItem =
|
||||
| {
|
||||
kind: "welcome";
|
||||
@@ -134,7 +137,26 @@ export default function App({
|
||||
// Guard to append welcome snapshot only once
|
||||
const welcomeCommittedRef = useRef(false);
|
||||
|
||||
// Commit immutable/finished lines into <Static>
|
||||
// Track terminal shrink events to refresh static output (prevents wrapped leftovers)
|
||||
const columns = useTerminalWidth();
|
||||
const prevColumnsRef = useRef(columns);
|
||||
const [staticRenderEpoch, setStaticRenderEpoch] = useState(0);
|
||||
useEffect(() => {
|
||||
const prev = prevColumnsRef.current;
|
||||
if (
|
||||
columns < prev &&
|
||||
typeof process !== "undefined" &&
|
||||
process.stdout &&
|
||||
"write" in process.stdout &&
|
||||
process.stdout.isTTY
|
||||
) {
|
||||
process.stdout.write(CLEAR_SCREEN_AND_HOME);
|
||||
setStaticRenderEpoch((epoch) => epoch + 1);
|
||||
}
|
||||
prevColumnsRef.current = columns;
|
||||
}, [columns]);
|
||||
|
||||
// Commit immutable/finished lines into the historical log
|
||||
const commitEligibleLines = useCallback((b: Buffers) => {
|
||||
const newlyCommitted: StaticItem[] = [];
|
||||
// console.log(`[COMMIT] Checking ${b.order.length} lines for commit eligibility`);
|
||||
@@ -254,7 +276,7 @@ export default function App({
|
||||
) {
|
||||
// Set flag FIRST to prevent double-execution in strict mode
|
||||
hasBackfilledRef.current = true;
|
||||
// Append welcome snapshot FIRST so it appears above history in <Static>
|
||||
// Append welcome snapshot FIRST so it appears above history
|
||||
if (!welcomeCommittedRef.current) {
|
||||
welcomeCommittedRef.current = true;
|
||||
setStaticItems((prev) => [
|
||||
@@ -973,7 +995,11 @@ export default function App({
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Static items={staticItems} style={{ flexDirection: "column" }}>
|
||||
<Static
|
||||
key={staticRenderEpoch}
|
||||
items={staticItems}
|
||||
style={{ flexDirection: "column" }}
|
||||
>
|
||||
{(item: StaticItem, index: number) => (
|
||||
<Box key={item.id} marginTop={index > 0 ? 1 : 0}>
|
||||
{item.kind === "welcome" ? (
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type AdvancedDiffSuccess,
|
||||
computeAdvancedDiff,
|
||||
} from "../helpers/diff";
|
||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
import { colors } from "./colors";
|
||||
import { EditRenderer, MultiEditRenderer, WriteRenderer } from "./DiffRenderer";
|
||||
|
||||
@@ -196,6 +197,9 @@ function Line({
|
||||
export function AdvancedDiffRenderer(
|
||||
props: Props & { precomputed?: AdvancedDiffSuccess },
|
||||
) {
|
||||
// Must call hooks at top level before any early returns
|
||||
const columns = useTerminalWidth();
|
||||
|
||||
const result = useMemo(() => {
|
||||
if (props.precomputed) return props.precomputed;
|
||||
if (props.kind === "write") {
|
||||
@@ -350,12 +354,6 @@ export function AdvancedDiffRenderer(
|
||||
: `Updated ${relative}`;
|
||||
|
||||
// Best-effort width clamp for rendering inside approval panel (border + padding + indent ~ 8 cols)
|
||||
const columns =
|
||||
typeof process !== "undefined" &&
|
||||
process.stdout &&
|
||||
"columns" in process.stdout
|
||||
? (process.stdout as NodeJS.WriteStream & { columns: number }).columns
|
||||
: 80;
|
||||
const panelInnerWidth = Math.max(20, columns - 8); // keep a reasonable minimum
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, Text } from "ink";
|
||||
import { memo } from "react";
|
||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
import { MarkdownDisplay } from "./MarkdownDisplay.js";
|
||||
|
||||
// Helper function to normalize text - copied from old codebase
|
||||
@@ -28,12 +29,7 @@ type AssistantLine = {
|
||||
* - Support for markdown rendering (when MarkdownDisplay is available)
|
||||
*/
|
||||
export const AssistantMessage = memo(({ line }: { line: AssistantLine }) => {
|
||||
const columns =
|
||||
typeof process !== "undefined" &&
|
||||
process.stdout &&
|
||||
"columns" in process.stdout
|
||||
? ((process.stdout as { columns?: number }).columns ?? 80)
|
||||
: 80;
|
||||
const columns = useTerminalWidth();
|
||||
const contentWidth = Math.max(0, columns - 2);
|
||||
|
||||
const normalizedText = normalize(line.text);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, Text } from "ink";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
import { colors } from "./colors.js";
|
||||
import { MarkdownDisplay } from "./MarkdownDisplay.js";
|
||||
|
||||
@@ -34,13 +35,7 @@ const BlinkDot: React.FC<{ color?: string }> = ({ color = "yellow" }) => {
|
||||
* - Consistent symbols (● for command, ⎿ for result)
|
||||
*/
|
||||
export const CommandMessage = memo(({ line }: { line: CommandLine }) => {
|
||||
const columns =
|
||||
typeof process !== "undefined" &&
|
||||
process.stdout &&
|
||||
"columns" in process.stdout
|
||||
? ((process.stdout as { columns?: number }).columns ?? 80)
|
||||
: 80;
|
||||
|
||||
const columns = useTerminalWidth();
|
||||
const rightWidth = Math.max(0, columns - 2); // gutter is 2 cols
|
||||
|
||||
// Determine dot state based on phase and success
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { ComponentType } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { PermissionMode } from "../../permissions/mode";
|
||||
import { permissionMode } from "../../permissions/mode";
|
||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
import { CommandPreview } from "./CommandPreview";
|
||||
import { colors } from "./colors";
|
||||
import { PasteAwareTextInput } from "./PasteAwareTextInput";
|
||||
@@ -55,13 +56,8 @@ export function Input({
|
||||
// Shimmer animation state
|
||||
const [shimmerOffset, setShimmerOffset] = useState(-3);
|
||||
|
||||
// Get terminal width for proper column sizing
|
||||
const columns =
|
||||
typeof process !== "undefined" &&
|
||||
process.stdout &&
|
||||
"columns" in process.stdout
|
||||
? ((process.stdout as { columns?: number }).columns ?? 80)
|
||||
: 80;
|
||||
// Terminal width (reactive to window resizing)
|
||||
const columns = useTerminalWidth();
|
||||
const contentWidth = Math.max(0, columns - 2);
|
||||
|
||||
// Handle escape key for double-escape-to-clear
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, Text } from "ink";
|
||||
import { memo } from "react";
|
||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
import { MarkdownDisplay } from "./MarkdownDisplay.js";
|
||||
|
||||
// Helper function to normalize text - copied from old codebase
|
||||
@@ -28,12 +29,7 @@ type ReasoningLine = {
|
||||
* - Proper text normalization
|
||||
*/
|
||||
export const ReasoningMessage = memo(({ line }: { line: ReasoningLine }) => {
|
||||
const columns =
|
||||
typeof process !== "undefined" &&
|
||||
process.stdout &&
|
||||
"columns" in process.stdout
|
||||
? ((process.stdout as { columns?: number }).columns ?? 80)
|
||||
: 80;
|
||||
const columns = useTerminalWidth();
|
||||
const contentWidth = Math.max(0, columns - 2);
|
||||
|
||||
const normalizedText = normalize(line.text);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Box, Text } from "ink";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { clipToolReturn } from "../../tools/manager.js";
|
||||
import { formatArgsDisplay } from "../helpers/formatArgsDisplay.js";
|
||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
import { colors } from "./colors.js";
|
||||
import { MarkdownDisplay } from "./MarkdownDisplay.js";
|
||||
import { TodoRenderer } from "./TodoRenderer.js";
|
||||
@@ -41,12 +42,7 @@ const BlinkDot: React.FC<{ color?: string }> = ({
|
||||
* - Result shown with ⎿ prefix underneath
|
||||
*/
|
||||
export const ToolCallMessage = memo(({ line }: { line: ToolCallLine }) => {
|
||||
const columns =
|
||||
typeof process !== "undefined" &&
|
||||
process.stdout &&
|
||||
"columns" in process.stdout
|
||||
? ((process.stdout as { columns?: number }).columns ?? 80)
|
||||
: 80;
|
||||
const columns = useTerminalWidth();
|
||||
|
||||
// Parse and format the tool call
|
||||
const rawName = line.name ?? "?";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, Text } from "ink";
|
||||
import { memo } from "react";
|
||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
import { MarkdownDisplay } from "./MarkdownDisplay.js";
|
||||
|
||||
type UserLine = {
|
||||
@@ -18,12 +19,7 @@ type UserLine = {
|
||||
* - Full markdown rendering support
|
||||
*/
|
||||
export const UserMessage = memo(({ line }: { line: UserLine }) => {
|
||||
const columns =
|
||||
typeof process !== "undefined" &&
|
||||
process.stdout &&
|
||||
"columns" in process.stdout
|
||||
? ((process.stdout as { columns?: number }).columns ?? 80)
|
||||
: 80;
|
||||
const columns = useTerminalWidth();
|
||||
const contentWidth = Math.max(0, columns - 2);
|
||||
|
||||
return (
|
||||
|
||||
65
src/cli/hooks/useTerminalWidth.ts
Normal file
65
src/cli/hooks/useTerminalWidth.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const getStdout = () => {
|
||||
if (typeof process === "undefined") return undefined;
|
||||
const stdout = process.stdout as NodeJS.WriteStream | undefined;
|
||||
return stdout && typeof stdout.on === "function" ? stdout : undefined;
|
||||
};
|
||||
|
||||
const getTerminalWidth = () => getStdout()?.columns ?? 80;
|
||||
|
||||
type Listener = (columns: number) => void;
|
||||
|
||||
const listeners = new Set<Listener>();
|
||||
let resizeHandlerRegistered = false;
|
||||
let trackedColumns = getTerminalWidth();
|
||||
|
||||
const resizeHandler = () => {
|
||||
const nextColumns = getTerminalWidth();
|
||||
if (nextColumns === trackedColumns) {
|
||||
return;
|
||||
}
|
||||
trackedColumns = nextColumns;
|
||||
for (const listener of listeners) {
|
||||
listener(nextColumns);
|
||||
}
|
||||
};
|
||||
|
||||
const ensureResizeHandler = () => {
|
||||
if (resizeHandlerRegistered) return;
|
||||
const stdout = getStdout();
|
||||
if (!stdout) return;
|
||||
stdout.on("resize", resizeHandler);
|
||||
resizeHandlerRegistered = true;
|
||||
};
|
||||
|
||||
const removeResizeHandlerIfIdle = () => {
|
||||
if (!resizeHandlerRegistered || listeners.size > 0) return;
|
||||
const stdout = getStdout();
|
||||
if (!stdout) return;
|
||||
stdout.off("resize", resizeHandler);
|
||||
resizeHandlerRegistered = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get terminal width and reactively update on resize
|
||||
* Uses a shared resize listener to avoid exceeding WriteStream listener limits.
|
||||
*/
|
||||
export function useTerminalWidth(): number {
|
||||
const [columns, setColumns] = useState(trackedColumns);
|
||||
|
||||
useEffect(() => {
|
||||
ensureResizeHandler();
|
||||
const listener: Listener = (value) => {
|
||||
setColumns(value);
|
||||
};
|
||||
listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
removeResizeHandlerIfIdle();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return columns;
|
||||
}
|
||||
Reference in New Issue
Block a user