diff --git a/packages/coding-agent/src/modes/components/ask-dialog.ts b/packages/coding-agent/src/modes/components/ask-dialog.ts index ffb042a9b..bf4253b3b 100644 --- a/packages/coding-agent/src/modes/components/ask-dialog.ts +++ b/packages/coding-agent/src/modes/components/ask-dialog.ts @@ -317,14 +317,12 @@ 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") : ""; - // 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. + // `width` is already the inner content width consumed by row(); when a + // scrollbar is needed, renderRows() calls this again with one less column. + // Keep the cursor, option marker, first wrapped label line, and optional + // note marker within that budget so the outer fit() never truncates them. const noteWidth = noteMarker ? visibleWidth(noteMarker) : 0; - const labelWidth = Math.max(1, width - 4 - visibleWidth(cursor) - visibleWidth(marker) - noteWidth); + const labelWidth = Math.max(1, width - visibleWidth(cursor) - visibleWidth(marker) - noteWidth); const wrappedLabel = wrapTextWithAnsi(label, labelWidth); const indent = padding(visibleWidth(cursor) + visibleWidth(marker)); const lines = [`${cursor}${marker}${wrappedLabel[0] ?? ""}${noteMarker}`]; 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 b547470ec..8d5851ed5 100644 --- a/packages/coding-agent/test/modes/components/ask-dialog.test.ts +++ b/packages/coding-agent/test/modes/components/ask-dialog.test.ts @@ -1252,7 +1252,8 @@ describe("AskDialogComponent", () => { 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 tail = "UNIQUE_TAIL_MARKER_8654"; + const longLabel = `${"This is a deliberately long option label ".repeat(4)}${tail}`; const questions: ExtensionAskDialogQuestion[] = [ { id: "q1", @@ -1268,8 +1269,8 @@ describe("AskDialogComponent", () => { }); const output = render(component); - // The label tail must be present — no ellipsis truncation. - expect(output).toContain("deliberately long option label"); + // The unique label tail must be present — no ellipsis truncation. + expect(output).toContain(tail); expect(output).not.toContain("…"); // The first line carries the cursor glyph; continuation lines are // indented under the marker so the cursor stays visually anchored. @@ -1280,6 +1281,16 @@ describe("AskDialogComponent", () => { expect(continuation).toMatch(/│ {3}/); }); + it("does not wrap an option label that fits the dialog content width", () => { + const component = new AskDialogComponent( + [{ id: "q1", question: "Pick one?", options: [{ label: "x".repeat(70) }] }], + { onSubmit: vi.fn(), onCancel: vi.fn(), onPrompt: vi.fn() }, + ); + + const output = render(component); + expect(output.split("\n").filter(line => line.includes("x"))).toHaveLength(1); + }); + it("Other editor cancel returns to the option list without submitting", async () => { const onPrompt = vi.fn().mockReturnValue(Promise.resolve(undefined)); const onSubmit = vi.fn();