feat(ui): polish pickers, narrow-term reflow, multi-line indent

- ShellConfirm border yellow→red (highest-risk gate distinct from review)
- EditConfirm border cyan→yellow (decouple from PlanConfirm cyan)
- PromptInput: extract [Esc]-to-stop into separate dim row; narrow mode
  (≤90 cols) condenses prefix `you ›`→`›` with proportional indent
- EventLog: user/warning/error multi-line text now aligns under glyph
  via indentContinuationLines helper
- Slash/At/SlashArg suggestion overlays: marginTop separates from scrollback
This commit is contained in:
esengine committed 2026-04-25 06:15:33 -07:00
1 parent 9e3c62ed1e
commit d6bb4054dc
8 files changed
+104 -71

No files matched your search

+1 -1
View File
@@ -1018,7 +1018,7 @@ export function App({
if (!codeMode) return "not in code mode";
const blocks = pendingEdits.current;
if (blocks.length === 0) {
return "nothing pending — the assistant hasn't proposed edits since the last /apply or /discard.";
return "nothing pending — the model hasn't proposed edits since the last /apply or /discard.";
}
const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
const results = applyEditBlocks(blocks, codeMode.rootDir);
+2 -2
View File
@@ -30,7 +30,7 @@ export function AtMentionSuggestions({
if (matches === null) return null;
if (matches.length === 0) {
return (
<Box paddingX={1}>
<Box paddingX={1} marginTop={1}>
<Text color="yellow">no files match "@{query}"</Text>
<Text dimColor>
{" "}
@@ -47,7 +47,7 @@ export function AtMentionSuggestions({
const hiddenAbove = windowStart;
const hiddenBelow = total - windowStart - shown.length;
return (
<Box flexDirection="column" paddingX={1}>
<Box flexDirection="column" paddingX={1} marginTop={1}>
{hiddenAbove > 0 ? <Text dimColor> ↑ {hiddenAbove} more above</Text> : null}
{shown.map((path, i) => (
<FileRow key={path} path={path} isSelected={windowStart + i === selectedIndex} />
+3 -3
View File
@@ -134,14 +134,14 @@ export function EditConfirm({ block, onChoose }: EditConfirmProps) {
const showScrollHud = hiddenAbove + hiddenBelow > 0;
return (
<Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1} marginY={1}>
<Box flexDirection="column" borderStyle="round" borderColor="yellow" paddingX={1} marginY={1}>
<Box>
<Text bold color="cyan">
<Text bold color="yellow">
{"▸ model wants to edit a file"}
</Text>
</Box>
<Box>
<Text color="cyan" dimColor>
<Text color="yellow" dimColor>
{"──────────────────────────────────────────"}
</Text>
</Box>
+14 -3
View File
@@ -149,6 +149,17 @@ function RoleGlyph({
);
}
/**
* Pad continuation lines so wrapped multi-line text aligns under the
* glyph indent instead of jumping back to column 0. Header is `glyph
* (1 col) + " " (2 cols)`, so subsequent lines need 3 leading spaces
* to land under the body's first character.
*/
function indentContinuationLines(text: string): string {
if (!text.includes("\n")) return text;
return text.split("\n").join("\n ");
}
export const EventRow = React.memo(function EventRow({
event,
projectRoot,
@@ -162,7 +173,7 @@ export const EventRow = React.memo(function EventRow({
<RoleGlyph glyph={ROLE_GLYPH.user} color="cyan" />
<Text>
{" "}
{event.text}
{indentContinuationLines(event.text)}
</Text>
</Box>
);
@@ -249,7 +260,7 @@ export const EventRow = React.memo(function EventRow({
<RoleGlyph glyph={ROLE_GLYPH.error} color="red" />
<Text color="red">
{" "}
{event.text}
{indentContinuationLines(event.text)}
</Text>
</Box>
);
@@ -394,7 +405,7 @@ export const EventRow = React.memo(function EventRow({
<RoleGlyph glyph={ROLE_GLYPH.warning} color="yellow" />
<Text color="yellow">
{" "}
{event.text}
{indentContinuationLines(event.text)}
</Text>
</Box>
);
+76 -54
View File
@@ -1,4 +1,4 @@
import { Box, Text, useInput } from "ink";
import { Box, Text, useInput, useStdout } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React as a runtime value (classic transform compiles JSX to React.createElement)
import React, { useRef, useState } from "react";
import { type MultilineKey, lineAndColumn, processMultilineKey } from "./multiline-keys.js";
@@ -274,9 +274,24 @@ export function PromptInput({
{ isActive: !disabled },
);
// Narrow-terminal mode: drop the `you ›` (6 col) prefix to a `›` (2
// col) marker so the writable area on 80-col terminals reclaims 4
// cols. Threshold of 90 keeps the friendly `you ›` on anything that
// resembles a normal terminal; modern laptops tend to default to
// 100+ cols, narrow ones (split panes, embedded shells) tend to be
// <=90. Continuation indent shrinks proportionally so wrapped lines
// still align under the prompt arrow.
const { stdout } = useStdout();
const cols = stdout?.columns ?? 80;
const narrow = cols <= 90;
const promptPrefix = narrow ? "› " : "you › ";
const continuationIndent = narrow ? " " : " ";
const placeholderActive = narrow
? "type a message, or /command"
: "type a message, or /command · [Shift+Enter] / [Ctrl+J] newline";
const effectivePlaceholder = disabled
? (placeholder ?? "…waiting for response… · [Esc] to stop")
: (placeholder ?? "type a message, or /command · [Shift+Enter] / [Ctrl+J] newline");
? (placeholder ?? "…waiting for response…")
: (placeholder ?? placeholderActive);
const lines = value.length > 0 ? value.split("\n") : [""];
const borderColor = disabled ? "gray" : "cyan";
@@ -298,63 +313,70 @@ export function PromptInput({
const showHugeBufferHints = lines.length > 20;
return (
<Box borderStyle="round" borderColor={borderColor} paddingX={1} flexDirection="column">
{renderItems.map((item, renderIdx) => {
if (item.kind === "skip") {
<>
<Box borderStyle="round" borderColor={borderColor} paddingX={1} flexDirection="column">
{renderItems.map((item, renderIdx) => {
if (item.kind === "skip") {
return (
// biome-ignore lint/suspicious/noArrayIndexKey: stable — skip markers are derived from a fixed-size window over `lines`
<Box key={`skip-${renderIdx}`}>
<Text dimColor>{continuationIndent}</Text>
<Text
dimColor
>{`[… ${item.linesHidden} line${item.linesHidden === 1 ? "" : "s"} hidden — full content kept, submitted on Enter …]`}</Text>
</Box>
);
}
const line = item.line;
const i = item.originalIndex;
const isFirst = i === 0;
const showPlaceholder = isFirst && value.length === 0;
const isCursorLine = i === cursorLine;
return (
// biome-ignore lint/suspicious/noArrayIndexKey: stable — skip markers are derived from a fixed-size window over `lines`
<Box key={`skip-${renderIdx}`}>
<Text dimColor>{" "}</Text>
<Text
dimColor
>{`[… ${item.linesHidden} line${item.linesHidden === 1 ? "" : "s"} hidden — full content kept, submitted on Enter …]`}</Text>
<Box key={`ln-${i}`}>
{isFirst ? (
<Text bold color={borderColor}>
{promptPrefix}
</Text>
) : (
<Text dimColor>{continuationIndent}</Text>
)}
{showPlaceholder ? (
<>
{isCursorLine && !disabled ? (
<Text color={borderColor}>{showCursor ? "▌" : " "}</Text>
) : null}
<Text dimColor>{effectivePlaceholder}</Text>
</>
) : isCursorLine && !disabled ? (
<LineWithCursor
line={line}
col={cursorCol}
showCursor={showCursor}
borderColor={borderColor}
pastes={pastesRef.current}
/>
) : (
<RenderLine line={line} pastes={pastesRef.current} />
)}
</Box>
);
}
const line = item.line;
const i = item.originalIndex;
const isFirst = i === 0;
const showPlaceholder = isFirst && value.length === 0;
const isCursorLine = i === cursorLine;
return (
<Box key={`ln-${i}`}>
{isFirst ? (
<Text bold color={borderColor}>
you ›{" "}
</Text>
) : (
<Text dimColor>{" "}</Text>
)}
{showPlaceholder ? (
<>
{isCursorLine && !disabled ? (
<Text color={borderColor}>{showCursor ? "▌" : " "}</Text>
) : null}
<Text dimColor>{effectivePlaceholder}</Text>
</>
) : isCursorLine && !disabled ? (
<LineWithCursor
line={line}
col={cursorCol}
showCursor={showCursor}
borderColor={borderColor}
pastes={pastesRef.current}
/>
) : (
<RenderLine line={line} pastes={pastesRef.current} />
)}
})}
{showHugeBufferHints && !disabled ? (
<Box>
<Text dimColor>{continuationIndent}</Text>
<Text dimColor>
{`[${lines.length} lines · PageUp/PageDown jump to top/bottom · Ctrl+U clear · Ctrl+W del word]`}
</Text>
</Box>
);
})}
{showHugeBufferHints && !disabled ? (
<Box>
<Text dimColor>{" "}</Text>
<Text dimColor>
{`[${lines.length} lines · PageUp/PageDown jump to top/bottom · Ctrl+U clear · Ctrl+W del word]`}
</Text>
) : null}
</Box>
{disabled ? (
<Box paddingX={1}>
<Text dimColor>[Esc] to stop</Text>
</Box>
) : null}
</Box>
</>
);
}
+3 -3
View File
@@ -35,9 +35,9 @@ export interface ShellConfirmProps {
export function ShellConfirm({ command, allowPrefix, kind, onChoose }: ShellConfirmProps) {
const isBackground = kind === "run_background";
return (
<Box flexDirection="column" borderStyle="round" borderColor="yellow" paddingX={1} marginY={1}>
<Box flexDirection="column" borderStyle="round" borderColor="red" paddingX={1} marginY={1}>
<Box>
<Text bold color="yellow">
<Text bold color="red">
{isBackground
? "▸ model wants to start a BACKGROUND process"
: "▸ model wants to run a shell command"}
@@ -51,7 +51,7 @@ export function ShellConfirm({ command, allowPrefix, kind, onChoose }: ShellConf
</Box>
) : null}
<Box>
<Text color="yellow" dimColor>
<Text color="red" dimColor>
{"──────────────────────────────────────────"}
</Text>
</Box>
+3 -3
View File
@@ -39,7 +39,7 @@ export function SlashArgPicker({
// Hint mode: a single dim row explaining the argsHint + summary.
if (kind === "hint") {
return (
<Box paddingX={1}>
<Box paddingX={1} marginTop={1}>
<Text dimColor>
{" "}
<Text bold>/{spec.cmd}</Text>
@@ -52,7 +52,7 @@ export function SlashArgPicker({
if (matches === null) return null;
if (matches.length === 0) {
return (
<Box flexDirection="column" paddingX={1}>
<Box flexDirection="column" paddingX={1} marginTop={1}>
<Text dimColor>
{" "}
<Text bold>/{spec.cmd}</Text>
@@ -71,7 +71,7 @@ export function SlashArgPicker({
const hiddenAbove = windowStart;
const hiddenBelow = total - windowStart - shown.length;
return (
<Box flexDirection="column" paddingX={1}>
<Box flexDirection="column" paddingX={1} marginTop={1}>
<Text dimColor>
{" "}
<Text bold>/{spec.cmd}</Text>
+2 -2
View File
@@ -28,7 +28,7 @@ export function SlashSuggestions({
if (matches === null) return null;
if (matches.length === 0) {
return (
<Box paddingX={1}>
<Box paddingX={1} marginTop={1}>
<Text color="yellow">no slash command matches that prefix</Text>
<Text dimColor> — Backspace to edit, or /help for the full list</Text>
</Box>
@@ -45,7 +45,7 @@ export function SlashSuggestions({
const hiddenAbove = windowStart;
const hiddenBelow = total - windowStart - shown.length;
return (
<Box flexDirection="column" paddingX={1}>
<Box flexDirection="column" paddingX={1} marginTop={1}>
{hiddenAbove > 0 ? <Text dimColor> ↑ {hiddenAbove} more above</Text> : null}
{shown.map((spec, i) => (
<SuggestionRow key={spec.cmd} spec={spec} isSelected={windowStart + i === selectedIndex} />