feat(examples): Wrap question extension text instead of truncating (#5708)

This commit is contained in:
Alexey Zaytsev
2026-06-14 08:02:58 -05:00
committed by GitHub
parent 11b5403fad
commit 6b40c99a59
2 changed files with 88 additions and 46 deletions

View File

@@ -5,7 +5,15 @@
*/ */
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui"; import {
Editor,
type EditorTheme,
Key,
matchesKey,
Text,
visibleWidth,
wrapTextWithAnsi,
} from "@earendil-works/pi-tui";
import { Type } from "typebox"; import { Type } from "typebox";
interface OptionWithDesc { interface OptionWithDesc {
@@ -139,10 +147,27 @@ export default function question(pi: ExtensionAPI) {
if (cachedLines) return cachedLines; if (cachedLines) return cachedLines;
const lines: string[] = []; const lines: string[] = [];
const add = (s: string) => lines.push(truncateToWidth(s, width)); const renderWidth = Math.max(1, width);
add(theme.fg("accent", "─".repeat(width))); function addWrapped(text: string) {
add(theme.fg("text", ` ${params.question}`)); lines.push(...wrapTextWithAnsi(text, renderWidth));
}
function addWrappedWithPrefix(prefix: string, text: string) {
const prefixWidth = visibleWidth(prefix);
if (prefixWidth >= renderWidth) {
addWrapped(prefix + text);
return;
}
const wrapped = wrapTextWithAnsi(text, renderWidth - prefixWidth);
const continuationPrefix = " ".repeat(prefixWidth);
for (let i = 0; i < wrapped.length; i++) {
lines.push(`${i === 0 ? prefix : continuationPrefix}${wrapped[i]}`);
}
}
lines.push(theme.fg("accent", "─".repeat(renderWidth)));
addWrappedWithPrefix(" ", theme.fg("text", params.question));
lines.push(""); lines.push("");
for (let i = 0; i < allOptions.length; i++) { for (let i = 0; i < allOptions.length; i++) {
@@ -150,36 +175,32 @@ export default function question(pi: ExtensionAPI) {
const selected = i === optionIndex; const selected = i === optionIndex;
const isOther = opt.isOther === true; const isOther = opt.isOther === true;
const prefix = selected ? theme.fg("accent", "> ") : " "; const prefix = selected ? theme.fg("accent", "> ") : " ";
const label = `${i + 1}. ${opt.label}${isOther && editMode ? " ✎" : ""}`;
const color = selected || (isOther && editMode) ? "accent" : "text";
if (isOther && editMode) { addWrappedWithPrefix(prefix, theme.fg(color, label));
add(prefix + theme.fg("accent", `${i + 1}. ${opt.label}`));
} else if (selected) {
add(prefix + theme.fg("accent", `${i + 1}. ${opt.label}`));
} else {
add(` ${theme.fg("text", `${i + 1}. ${opt.label}`)}`);
}
// Show description if present // Show description if present
if (opt.description) { if (opt.description) {
add(` ${theme.fg("muted", opt.description)}`); addWrappedWithPrefix(" ", theme.fg("muted", opt.description));
} }
} }
if (editMode) { if (editMode) {
lines.push(""); lines.push("");
add(theme.fg("muted", " Your answer:")); addWrappedWithPrefix(" ", theme.fg("muted", "Your answer:"));
for (const line of editor.render(width - 2)) { for (const line of editor.render(Math.max(1, renderWidth - 2))) {
add(` ${line}`); lines.push(` ${line}`);
} }
} }
lines.push(""); lines.push("");
if (editMode) { if (editMode) {
add(theme.fg("dim", " Enter to submit • Esc to go back")); addWrappedWithPrefix(" ", theme.fg("dim", "Enter to submit • Esc to go back"));
} else { } else {
add(theme.fg("dim", " ↑↓ navigate • Enter to select • Esc to cancel")); addWrappedWithPrefix(" ", theme.fg("dim", "↑↓ navigate • Enter to select • Esc to cancel"));
} }
add(theme.fg("accent", "─".repeat(width))); lines.push(theme.fg("accent", "─".repeat(renderWidth)));
cachedLines = lines; cachedLines = lines;
return lines; return lines;

View File

@@ -6,7 +6,15 @@
*/ */
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui"; import {
Editor,
type EditorTheme,
Key,
matchesKey,
Text,
visibleWidth,
wrapTextWithAnsi,
} from "@earendil-works/pi-tui";
import { Type } from "typebox"; import { Type } from "typebox";
// Types // Types
@@ -259,13 +267,28 @@ export default function questionnaire(pi: ExtensionAPI) {
if (cachedLines) return cachedLines; if (cachedLines) return cachedLines;
const lines: string[] = []; const lines: string[] = [];
const renderWidth = Math.max(1, width);
const q = currentQuestion(); const q = currentQuestion();
const opts = currentOptions(); const opts = currentOptions();
// Helper to add truncated line function addWrapped(text: string) {
const add = (s: string) => lines.push(truncateToWidth(s, width)); lines.push(...wrapTextWithAnsi(text, renderWidth));
}
add(theme.fg("accent", "─".repeat(width))); function addWrappedWithPrefix(prefix: string, text: string) {
const prefixWidth = visibleWidth(prefix);
if (prefixWidth >= renderWidth) {
addWrapped(prefix + text);
return;
}
const wrapped = wrapTextWithAnsi(text, renderWidth - prefixWidth);
const continuationPrefix = " ".repeat(prefixWidth);
for (let i = 0; i < wrapped.length; i++) {
lines.push(`${i === 0 ? prefix : continuationPrefix}${wrapped[i]}`);
}
}
lines.push(theme.fg("accent", "─".repeat(renderWidth)));
// Tab bar (multi-question only) // Tab bar (multi-question only)
if (isMulti) { if (isMulti) {
@@ -287,7 +310,7 @@ export default function questionnaire(pi: ExtensionAPI) {
? theme.bg("selectedBg", theme.fg("text", submitText)) ? theme.bg("selectedBg", theme.fg("text", submitText))
: theme.fg(canSubmit ? "success" : "dim", submitText); : theme.fg(canSubmit ? "success" : "dim", submitText);
tabs.push(`${submitStyled}`); tabs.push(`${submitStyled}`);
add(` ${tabs.join("")}`); addWrappedWithPrefix(" ", tabs.join(""));
lines.push(""); lines.push("");
} }
@@ -298,54 +321,52 @@ export default function questionnaire(pi: ExtensionAPI) {
const selected = i === optionIndex; const selected = i === optionIndex;
const isOther = opt.isOther === true; const isOther = opt.isOther === true;
const prefix = selected ? theme.fg("accent", "> ") : " "; const prefix = selected ? theme.fg("accent", "> ") : " ";
const color = selected ? "accent" : "text"; const label = `${i + 1}. ${opt.label}${isOther && inputMode ? "" : ""}`;
// Mark "Type something" differently when in input mode const color = selected || (isOther && inputMode) ? "accent" : "text";
if (isOther && inputMode) {
add(prefix + theme.fg("accent", `${i + 1}. ${opt.label}`)); addWrappedWithPrefix(prefix, theme.fg(color, label));
} else {
add(prefix + theme.fg(color, `${i + 1}. ${opt.label}`));
}
if (opt.description) { if (opt.description) {
add(` ${theme.fg("muted", opt.description)}`); addWrappedWithPrefix(" ", theme.fg("muted", opt.description));
} }
} }
} }
// Content // Content
if (inputMode && q) { if (inputMode && q) {
add(theme.fg("text", ` ${q.prompt}`)); addWrappedWithPrefix(" ", theme.fg("text", q.prompt));
lines.push(""); lines.push("");
// Show options for reference // Show options for reference
renderOptions(); renderOptions();
lines.push(""); lines.push("");
add(theme.fg("muted", " Your answer:")); addWrappedWithPrefix(" ", theme.fg("muted", "Your answer:"));
for (const line of editor.render(width - 2)) { for (const line of editor.render(Math.max(1, renderWidth - 2))) {
add(` ${line}`); lines.push(` ${line}`);
} }
lines.push(""); lines.push("");
add(theme.fg("dim", " Enter to submit • Esc to cancel")); addWrappedWithPrefix(" ", theme.fg("dim", "Enter to submit • Esc to cancel"));
} else if (currentTab === questions.length) { } else if (currentTab === questions.length) {
add(theme.fg("accent", theme.bold(" Ready to submit"))); addWrappedWithPrefix(" ", theme.fg("accent", theme.bold("Ready to submit")));
lines.push(""); lines.push("");
for (const question of questions) { for (const question of questions) {
const answer = answers.get(question.id); const answer = answers.get(question.id);
if (answer) { if (answer) {
const prefix = answer.wasCustom ? "(wrote) " : ""; const prefix = answer.wasCustom ? "(wrote) " : "";
add(`${theme.fg("muted", ` ${question.label}: `)}${theme.fg("text", prefix + answer.label)}`); const summary = `${theme.fg("muted", `${question.label}: `)}${theme.fg("text", prefix + answer.label)}`;
addWrappedWithPrefix(" ", summary);
} }
} }
lines.push(""); lines.push("");
if (allAnswered()) { if (allAnswered()) {
add(theme.fg("success", " Press Enter to submit")); addWrappedWithPrefix(" ", theme.fg("success", "Press Enter to submit"));
} else { } else {
const missing = questions const missing = questions
.filter((q) => !answers.has(q.id)) .filter((q) => !answers.has(q.id))
.map((q) => q.label) .map((q) => q.label)
.join(", "); .join(", ");
add(theme.fg("warning", ` Unanswered: ${missing}`)); addWrappedWithPrefix(" ", theme.fg("warning", `Unanswered: ${missing}`));
} }
} else if (q) { } else if (q) {
add(theme.fg("text", ` ${q.prompt}`)); addWrappedWithPrefix(" ", theme.fg("text", q.prompt));
lines.push(""); lines.push("");
renderOptions(); renderOptions();
} }
@@ -353,11 +374,11 @@ export default function questionnaire(pi: ExtensionAPI) {
lines.push(""); lines.push("");
if (!inputMode) { if (!inputMode) {
const help = isMulti const help = isMulti
? " Tab/←→ navigate • ↑↓ select • Enter confirm • Esc cancel" ? "Tab/←→ navigate • ↑↓ select • Enter confirm • Esc cancel"
: " ↑↓ navigate • Enter select • Esc cancel"; : "↑↓ navigate • Enter select • Esc cancel";
add(theme.fg("dim", help)); addWrappedWithPrefix(" ", theme.fg("dim", help));
} }
add(theme.fg("accent", "─".repeat(width))); lines.push(theme.fg("accent", "─".repeat(renderWidth)));
cachedLines = lines; cachedLines = lines;
return lines; return lines;
@@ -400,7 +421,7 @@ export default function questionnaire(pi: ExtensionAPI) {
let text = theme.fg("toolTitle", theme.bold("questionnaire ")); let text = theme.fg("toolTitle", theme.bold("questionnaire "));
text += theme.fg("muted", `${count} question${count !== 1 ? "s" : ""}`); text += theme.fg("muted", `${count} question${count !== 1 ? "s" : ""}`);
if (labels) { if (labels) {
text += theme.fg("dim", ` (${truncateToWidth(labels, 40)})`); text += theme.fg("dim", ` (${labels})`);
} }
return new Text(text, 0, 0); return new Text(text, 0, 0);
}, },