diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5597d222b..0da50c22f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/modes/components/ask-dialog.ts b/packages/coding-agent/src/modes/components/ask-dialog.ts index 80ed90e3c..ffb042a9b 100644 --- a/packages/coding-agent/src/modes/components/ask-dialog.ts +++ b/packages/coding-agent/src/modes/components/ask-dialog.ts @@ -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()) { diff --git a/packages/coding-agent/test/modes/components/ask-dialog.test.ts b/packages/coding-agent/test/modes/components/ask-dialog.test.ts index aa5251d59..b547470ec 100644 --- a/packages/coding-agent/test/modes/components/ask-dialog.test.ts +++ b/packages/coding-agent/test/modes/components/ask-dialog.test.ts @@ -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();