diff --git a/packages/coding-agent/examples/extensions/question.ts b/packages/coding-agent/examples/extensions/question.ts index eb1af51a4..1192b4904 100644 --- a/packages/coding-agent/examples/extensions/question.ts +++ b/packages/coding-agent/examples/extensions/question.ts @@ -5,7 +5,15 @@ */ 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"; interface OptionWithDesc { @@ -139,10 +147,27 @@ export default function question(pi: ExtensionAPI) { if (cachedLines) return cachedLines; const lines: string[] = []; - const add = (s: string) => lines.push(truncateToWidth(s, width)); + const renderWidth = Math.max(1, width); - add(theme.fg("accent", "─".repeat(width))); - add(theme.fg("text", ` ${params.question}`)); + function addWrapped(text: string) { + 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(""); for (let i = 0; i < allOptions.length; i++) { @@ -150,36 +175,32 @@ export default function question(pi: ExtensionAPI) { const selected = i === optionIndex; const isOther = opt.isOther === true; const prefix = selected ? theme.fg("accent", "> ") : " "; + const label = `${i + 1}. ${opt.label}${isOther && editMode ? " ✎" : ""}`; + const color = selected || (isOther && editMode) ? "accent" : "text"; - if (isOther && editMode) { - 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}`)}`); - } + addWrappedWithPrefix(prefix, theme.fg(color, label)); // Show description if present if (opt.description) { - add(` ${theme.fg("muted", opt.description)}`); + addWrappedWithPrefix(" ", theme.fg("muted", opt.description)); } } if (editMode) { lines.push(""); - add(theme.fg("muted", " Your answer:")); - for (const line of editor.render(width - 2)) { - add(` ${line}`); + addWrappedWithPrefix(" ", theme.fg("muted", "Your answer:")); + for (const line of editor.render(Math.max(1, renderWidth - 2))) { + lines.push(` ${line}`); } } lines.push(""); 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 { - 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; return lines; diff --git a/packages/coding-agent/examples/extensions/questionnaire.ts b/packages/coding-agent/examples/extensions/questionnaire.ts index 653bd8644..3a546ac31 100644 --- a/packages/coding-agent/examples/extensions/questionnaire.ts +++ b/packages/coding-agent/examples/extensions/questionnaire.ts @@ -6,7 +6,15 @@ */ 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"; // Types @@ -259,13 +267,28 @@ export default function questionnaire(pi: ExtensionAPI) { if (cachedLines) return cachedLines; const lines: string[] = []; + const renderWidth = Math.max(1, width); const q = currentQuestion(); const opts = currentOptions(); - // Helper to add truncated line - const add = (s: string) => lines.push(truncateToWidth(s, width)); + function addWrapped(text: string) { + 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) if (isMulti) { @@ -287,7 +310,7 @@ export default function questionnaire(pi: ExtensionAPI) { ? theme.bg("selectedBg", theme.fg("text", submitText)) : theme.fg(canSubmit ? "success" : "dim", submitText); tabs.push(`${submitStyled} →`); - add(` ${tabs.join("")}`); + addWrappedWithPrefix(" ", tabs.join("")); lines.push(""); } @@ -298,54 +321,52 @@ export default function questionnaire(pi: ExtensionAPI) { const selected = i === optionIndex; const isOther = opt.isOther === true; const prefix = selected ? theme.fg("accent", "> ") : " "; - const color = selected ? "accent" : "text"; - // Mark "Type something" differently when in input mode - if (isOther && inputMode) { - add(prefix + theme.fg("accent", `${i + 1}. ${opt.label} ✎`)); - } else { - add(prefix + theme.fg(color, `${i + 1}. ${opt.label}`)); - } + const label = `${i + 1}. ${opt.label}${isOther && inputMode ? " ✎" : ""}`; + const color = selected || (isOther && inputMode) ? "accent" : "text"; + + addWrappedWithPrefix(prefix, theme.fg(color, label)); if (opt.description) { - add(` ${theme.fg("muted", opt.description)}`); + addWrappedWithPrefix(" ", theme.fg("muted", opt.description)); } } } // Content if (inputMode && q) { - add(theme.fg("text", ` ${q.prompt}`)); + addWrappedWithPrefix(" ", theme.fg("text", q.prompt)); lines.push(""); // Show options for reference renderOptions(); lines.push(""); - add(theme.fg("muted", " Your answer:")); - for (const line of editor.render(width - 2)) { - add(` ${line}`); + addWrappedWithPrefix(" ", theme.fg("muted", "Your answer:")); + for (const line of editor.render(Math.max(1, renderWidth - 2))) { + lines.push(` ${line}`); } 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) { - add(theme.fg("accent", theme.bold(" Ready to submit"))); + addWrappedWithPrefix(" ", theme.fg("accent", theme.bold("Ready to submit"))); lines.push(""); for (const question of questions) { const answer = answers.get(question.id); if (answer) { 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(""); if (allAnswered()) { - add(theme.fg("success", " Press Enter to submit")); + addWrappedWithPrefix(" ", theme.fg("success", "Press Enter to submit")); } else { const missing = questions .filter((q) => !answers.has(q.id)) .map((q) => q.label) .join(", "); - add(theme.fg("warning", ` Unanswered: ${missing}`)); + addWrappedWithPrefix(" ", theme.fg("warning", `Unanswered: ${missing}`)); } } else if (q) { - add(theme.fg("text", ` ${q.prompt}`)); + addWrappedWithPrefix(" ", theme.fg("text", q.prompt)); lines.push(""); renderOptions(); } @@ -353,11 +374,11 @@ export default function questionnaire(pi: ExtensionAPI) { lines.push(""); if (!inputMode) { const help = isMulti - ? " Tab/←→ navigate • ↑↓ select • Enter confirm • Esc cancel" - : " ↑↓ navigate • Enter select • Esc cancel"; - add(theme.fg("dim", help)); + ? "Tab/←→ navigate • ↑↓ select • Enter confirm • Esc cancel" + : "↑↓ navigate • Enter select • Esc cancel"; + addWrappedWithPrefix(" ", theme.fg("dim", help)); } - add(theme.fg("accent", "─".repeat(width))); + lines.push(theme.fg("accent", "─".repeat(renderWidth))); cachedLines = lines; return lines; @@ -400,7 +421,7 @@ export default function questionnaire(pi: ExtensionAPI) { let text = theme.fg("toolTitle", theme.bold("questionnaire ")); text += theme.fg("muted", `${count} question${count !== 1 ? "s" : ""}`); if (labels) { - text += theme.fg("dim", ` (${truncateToWidth(labels, 40)})`); + text += theme.fg("dim", ` (${labels})`); } return new Text(text, 0, 0); },