fix(ask): wrap long option labels in the rich ask dialog instead of truncating
The rich AskDialogComponent (merged in PR #4375) hard-truncated every
option row via truncateToWidth(firstLine, width, Ellipsis.Unicode),
clipping the disambiguating tail of long labels with no way to read it.
The legacy HookSelector dialog has wrapped labels since June 1
(3c7d50d292), so the rich dialog was the remaining truncation path.
renderRowLabel now wraps the label with wrapTextWithAnsi, indenting
continuation lines under the radio/checkbox marker so the cursor stays
visually anchored; the body ScrollView carries vertical overflow. The
note marker is reserved on the first line. Fixes #1243, #8594.
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed long `ask` option labels being hard-truncated at the terminal width with no way to read the clipped tail; the rich ask dialog now wraps labels onto indented continuation lines, matching the legacy selector ([#1243](https://github.com/can1357/oh-my-pi/issues/1243), [#8594](https://github.com/can1357/oh-my-pi/issues/8594)).
|
||||
|
||||
## [17.3.4] - 2026-08-14
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Markdown,
|
||||
type MarkdownTheme,
|
||||
matchesKey,
|
||||
padding,
|
||||
renderInlineMarkdown,
|
||||
replaceTabs,
|
||||
ScrollView,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
Text,
|
||||
type TUI,
|
||||
truncateToWidth,
|
||||
visibleWidth,
|
||||
wrapTextWithAnsi,
|
||||
} from "@oh-my-pi/pi-tui";
|
||||
import type {
|
||||
@@ -315,8 +317,20 @@ function renderRowLabel(
|
||||
const cursor = selected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
||||
const label = renderInlineMarkdown(rowItem.label, mdTheme, t => theme.fg(color, t));
|
||||
const noteMarker = state.note && state.noteRowKey === rowItem.key ? theme.fg("success", " ✎ note") : "";
|
||||
const firstLine = `${cursor}${marker}${label}${noteMarker}`;
|
||||
const lines = [truncateToWidth(firstLine, width, Ellipsis.Unicode)];
|
||||
// Wrap the label onto continuation lines indented under the marker so the
|
||||
// cursor stays visually anchored and the full label stays readable (the
|
||||
// body ScrollView carries vertical overflow). The note marker is reserved
|
||||
// on the first line; the label wraps to the remaining width. The row()
|
||||
// chrome (borders + insets) consumes 4 columns, so the label must fit
|
||||
// `width - 4` or the outer fit() re-truncates it with an ellipsis.
|
||||
const noteWidth = noteMarker ? visibleWidth(noteMarker) : 0;
|
||||
const labelWidth = Math.max(1, width - 4 - visibleWidth(cursor) - visibleWidth(marker) - noteWidth);
|
||||
const wrappedLabel = wrapTextWithAnsi(label, labelWidth);
|
||||
const indent = padding(visibleWidth(cursor) + visibleWidth(marker));
|
||||
const lines = [`${cursor}${marker}${wrappedLabel[0] ?? ""}${noteMarker}`];
|
||||
for (let i = 1; i < wrappedLabel.length; i++) {
|
||||
lines.push(`${indent}${wrappedLabel[i] ?? ""}`);
|
||||
}
|
||||
if (rowItem.kind === "option") {
|
||||
const option = question.options[rowItem.optionIndex ?? -1];
|
||||
if (option?.description?.trim()) {
|
||||
|
||||
@@ -1250,6 +1250,36 @@ describe("AskDialogComponent", () => {
|
||||
expect(matches?.length ?? 0).toBeLessThan(10);
|
||||
});
|
||||
|
||||
it("wraps long option labels onto indented continuation lines instead of truncating", () => {
|
||||
const onSubmit = vi.fn();
|
||||
const longLabel = "This is a deliberately long option label ".repeat(4).trim();
|
||||
const questions: ExtensionAskDialogQuestion[] = [
|
||||
{
|
||||
id: "q1",
|
||||
question: "Pick one?",
|
||||
options: [{ label: longLabel }, { label: "Short" }],
|
||||
},
|
||||
];
|
||||
|
||||
const component = new AskDialogComponent(questions, {
|
||||
onSubmit,
|
||||
onCancel: vi.fn(),
|
||||
onPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const output = render(component);
|
||||
// The label tail must be present — no ellipsis truncation.
|
||||
expect(output).toContain("deliberately long option label");
|
||||
expect(output).not.toContain("…");
|
||||
// The first line carries the cursor glyph; continuation lines are
|
||||
// indented under the marker so the cursor stays visually anchored.
|
||||
const lines = output.split("\n");
|
||||
const first = lines.find(line => line.includes("This is a deliberately")) ?? "";
|
||||
const continuation = lines.find(line => line.includes("option label") && !line.includes("❯")) ?? "";
|
||||
expect(first).toMatch(/│ ❯/);
|
||||
expect(continuation).toMatch(/│ {3}/);
|
||||
});
|
||||
|
||||
it("Other editor cancel returns to the option list without submitting", async () => {
|
||||
const onPrompt = vi.fn().mockReturnValue(Promise.resolve(undefined));
|
||||
const onSubmit = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user