Skip to content

Commit 2b0b800

Browse files
roboompBrit
authored andcommitted
fix(tui): preserved prefixes in completed tool cards
- Wrapped answered Ask labels at the card content width for single and grouped questions. - Applied hover code-rail wrapping to collapsed as well as expanded LSP cards. Fixes #15677
1 parent fbcbf78 commit 2b0b800

4 files changed

Lines changed: 57 additions & 22 deletions

File tree

‎packages/tui/CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
### Fixed
1111

1212
- SVG fences in CRLF or CR-only assistant Markdown now render as figures instead of source code ([#15663](https://github.com/can1357/oh-my-pi/issues/15663)).
13-
- Tool-card continuations stay aligned beneath line numbers, tree connectors, and option or code rails in Write, Read, Edit, Todo, Ask, and LSP previews ([#15677](https://github.com/can1357/oh-my-pi/issues/15677)).
1413
- Fixed clipboard-image and image-path pastes being dropped while the composer draft is edited beneath an ask or approval dialog ([#15642](https://github.com/can1357/oh-my-pi/pull/15642) by [@H4vC](https://github.com/H4vC))
1514
- Fixed inline images inside tmux assuming Kitty graphics from the pane's `TERM`; the attached client is identified from tmux's terminal-type reply or its `TERM` (e.g. `xterm-kitty`), and unrecognized clients go to the SIXEL probe ([#15524](https://github.com/can1357/oh-my-pi/issues/15524)).
1615
- Fixed ```svg fences rendering as their source code instead of a drawing when no theme was installed yet, e.g. in an extension rendering a message that opens with the fence ([#15660](https://github.com/can1357/oh-my-pi/issues/15660))
1716
- Fixed pastes inside tmux 3.5a and older submitting line by line after another program turned bracketed paste off on the pane; omp now re-enables it on the next render or keypress ([#15596](https://github.com/can1357/oh-my-pi/issues/15596))
1817
- Fixed assistant replies containing a table (on image-capable terminals with table charts on) publishing still-streaming rows to native scrollback, which froze the block's append-only retirement and could leave overwritten reply rows in scrollback ([#15693](https://github.com/can1357/oh-my-pi/issues/15693))
1918
- Fixed streamed thinking publishing a line to native scrollback just before a code fence or `<!-- -->` sentinel split across deltas rewrote it with the prose-fold ellipsis ([#15693](https://github.com/can1357/oh-my-pi/issues/15693))
2019
- Fixed streamed assistant text or thinking containing a tab stopping all later rows of the reply from retiring into native scrollback until the turn ended ([#15693](https://github.com/can1357/oh-my-pi/issues/15693))
20+
- Fixed wrapped lines in Write, Read, Edit, Todo, Ask and LSP tool cards losing their alignment under line numbers, tree connectors, and option or code rails ([#15677](https://github.com/can1357/oh-my-pi/issues/15677))
2121

2222
## [18.10.4] - 2026-10-13
2323

‎packages/tui/src/tools/ask.ts‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,11 @@ export function optionMarker(uiTheme: Theme, multi: boolean | undefined, selecte
203203
return selected ? uiTheme.radio.selected : uiTheme.radio.unselected;
204204
}
205205

206+
function wrapOptionLabel(marker: string, label: string, width: number): string[] {
207+
const prefix = ` ${marker} `;
208+
return wrapPrefixedLine(prefix, " ".repeat(visibleWidth(prefix)), label, width);
209+
}
210+
206211
/** Render the offered options for a question form as flat marker bullets (no tree guides). */
207212
function renderQuestionOptionLines(
208213
uiTheme: Theme,
@@ -214,8 +219,7 @@ function renderQuestionOptionLines(
214219
const out: string[] = [];
215220
for (const opt of options) {
216221
const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
217-
const marker = ` ${uiTheme.fg("dim", optionMarker(uiTheme, multi, false))} `;
218-
out.push(...wrapPrefixedLine(marker, " ".repeat(visibleWidth(marker)), optLabel, width));
222+
out.push(...wrapOptionLabel(uiTheme.fg("dim", optionMarker(uiTheme, multi, false)), optLabel, width));
219223
if (opt.description?.trim()) {
220224
const description = renderInlineMarkdown(opt.description.trim(), mdTheme, t => uiTheme.fg("dim", t));
221225
const prefix = ` ${uiTheme.fg("dim", "↳")} `;
@@ -264,6 +268,7 @@ function renderAnswerOptionLines(
264268
customInput: string | undefined,
265269
note: string | undefined,
266270
width: number,
271+
contentWidth: number,
267272
selectedIndices?: ReadonlySet<number>,
268273
): string[] {
269274
const selected = new Set(selectedOptions ?? []);
@@ -284,7 +289,7 @@ function renderAnswerOptionLines(
284289
const labelStyled = renderInlineMarkdown(label, mdTheme, t =>
285290
isSelected ? uiTheme.fg("toolOutput", t) : uiTheme.fg("muted", t),
286291
);
287-
out.push(` ${markerStyled} ${labelStyled}`);
292+
out.push(...wrapOptionLabel(markerStyled, labelStyled, contentWidth));
288293
}
289294
if (customInput !== undefined) out.push(...renderCustomInputLines(uiTheme, customInput));
290295
if (note !== undefined) out.push(...renderNoteLines(uiTheme, note, width));
@@ -556,7 +561,7 @@ export const askToolRenderer = {
556561
},
557562
uiTheme,
558563
);
559-
return framedToolCard(uiTheme, ({ width }) => {
564+
return framedToolCard(uiTheme, ({ width, contentWidth }) => {
560565
const rawResults = rawDetails.results ?? [];
561566
const sections = results.map((r, index) => {
562567
// Sanitizing preserves order and length, so raw indices align with `r`.
@@ -573,6 +578,7 @@ export const askToolRenderer = {
573578
r.customInput,
574579
r.note,
575580
width,
581+
contentWidth,
576582
selectedIndicesFor(raw?.options, raw?.selectedOptions),
577583
),
578584
];
@@ -611,7 +617,7 @@ export const askToolRenderer = {
611617
const dCustom = details.customInput;
612618
const dNote = details.note;
613619
const dTimedOut = details.timedOut;
614-
return framedToolCard(uiTheme, ({ width }) => {
620+
return framedToolCard(uiTheme, ({ width, contentWidth }) => {
615621
// md() returns a shared cached array (module-level Markdown LRU) — copy before appending.
616622
const bodyLines = [
617623
...md(question, width),
@@ -624,6 +630,7 @@ export const askToolRenderer = {
624630
dCustom,
625631
dNote,
626632
width,
633+
contentWidth,
627634
selectedIndicesFor(rawDetails.options, rawDetails.selectedOptions),
628635
),
629636
];

‎packages/tui/src/tools/lsp.ts‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -265,12 +265,11 @@ export function renderResult(
265265
const header = `${icon} LSP ${actionLabel}`;
266266

267267
const rail = ` ${theme.fg("mdCodeBlockBorder", theme.boxRound.vertical)} `;
268-
const contentLines =
269-
codeBlockMatch && expanded
270-
? bodyLines.flatMap(line =>
271-
line.startsWith(rail) ? wrapPrefixedLine(rail, rail, line.slice(rail.length), contentWidth) : [line],
272-
)
273-
: bodyLines;
268+
const contentLines = codeBlockMatch
269+
? bodyLines.flatMap(line =>
270+
line.startsWith(rail) ? wrapPrefixedLine(rail, rail, line.slice(rail.length), contentWidth) : [line],
271+
)
272+
: bodyLines;
274273
return {
275274
header,
276275
phase: isPartial ? "partial" : state,

‎packages/tui/test/tool-hanging-wrap.test.ts‎

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,32 @@ describe("prefixed tool rows", () => {
116116
expect(rows[branch + 1]).toContain("remaining details");
117117
});
118118

119+
it("keeps long Ask answers beneath their markers after submission, including grouped questions", async () => {
120+
const theme = (await getThemeByName("dark"))!;
121+
const chosen = "Choose the longer alternative because it contains information important at the end of its label";
122+
const other = "Leave the other longer alternative visible so its ending still distinguishes the choices";
123+
const answers = { question: "Pick?", options: [chosen, other], selectedOptions: [chosen] };
124+
const cards = [
125+
askToolRenderer.renderResult({ content: [], details: answers }, { expanded: false, isPartial: false }, theme),
126+
askToolRenderer.renderResult(
127+
{ content: [], details: { results: [{ id: "choice", multi: false, ...answers }] } },
128+
{ expanded: false, isPartial: false },
129+
theme,
130+
),
131+
];
132+
for (const card of cards) {
133+
const rows = plain(card.render(width));
134+
const chosenRow = rows.findIndex(row => row.includes("◉ Choose"));
135+
const otherRow = rows.findIndex(row => row.includes("○ Leave"));
136+
expect(chosenRow).toBeGreaterThan(0);
137+
expect(rows[chosenRow + 1]).toStartWith("│ ");
138+
expect(rows[chosenRow + 1]).toContain("information important");
139+
expect(otherRow).toBeGreaterThan(0);
140+
expect(rows[otherRow + 1]).toStartWith("│ ");
141+
expect(rows[otherRow + 1]).toContain("ending still");
142+
}
143+
});
144+
119145
it("hangs Ask option descriptions and LSP hover code beneath their prefix", async () => {
120146
const theme = (await getThemeByName("dark"))!;
121147
const ask = plain(
@@ -141,15 +167,18 @@ describe("prefixed tool rows", () => {
141167
expect(ask[description + 1]).toStartWith("│ ");
142168
expect(ask[description + 1]).toContain("card.");
143169

144-
const hover = plain(
145-
renderLspResult(
146-
{ content: [{ type: "text", text: `\`\`\`ts\nconst note = "${"long text ".repeat(12)}";\n\`\`\`` }] },
147-
{ expanded: true, isPartial: false },
148-
theme,
149-
).render(width),
150-
);
151-
const codeRow = hover.findIndex(row => row.startsWith("│ │ "));
152-
expect(codeRow).toBeGreaterThan(0);
153-
expect(hover[codeRow + 1]).toStartWith("│ │ ");
170+
for (const expanded of [true, false]) {
171+
const hover = plain(
172+
renderLspResult(
173+
{ content: [{ type: "text", text: `\`\`\`ts\nconst note = "${"long text ".repeat(12)}";\n\`\`\`` }] },
174+
{ expanded, isPartial: false },
175+
theme,
176+
).render(width),
177+
);
178+
const codeRow = hover.findIndex(row => row.startsWith("│ │ "));
179+
expect(codeRow).toBeGreaterThan(0);
180+
expect(hover[codeRow + 1]).toStartWith("│ │ ");
181+
expect(hover[codeRow + 1]).toContain("long text");
182+
}
154183
});
155184
});

0 commit comments

Comments
 (0)