diff --git a/src/constants.ts b/src/constants.ts
index cb69397..590a12e 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,6 +1,6 @@
export const OTHER_OPTION_VALUE = "__other__";
export const OTHER_OPTION_LABEL = "Type your own";
-export const SUBMIT_CHOICES = ["Submit answers", "Cancel"] as const;
+export const SUBMIT_CHOICES = ["Submit", "Cancel"] as const;
export const NO_PREVIEW_TEXT = "No preview available";
export const CANCELLED_SUMMARY = "User cancelled the ask flow";
export const SUBMITTED_SUMMARY = "User submitted the ask flow";
diff --git a/src/ui/constants.ts b/src/ui/constants.ts
index e567c63..ebc6bf9 100644
--- a/src/ui/constants.ts
+++ b/src/ui/constants.ts
@@ -14,7 +14,6 @@ export const UI_DIMENSIONS = {
export const UI_TEXT = {
questionNoteTitle: "Note:",
reviewTitle: "Review answers",
- readyToSubmit: "Submit answers?",
unanswered: "→ unanswered",
footerInput: " Enter submit · Esc close",
footerNote: " Enter save · Esc close",
diff --git a/src/ui/render-submit.ts b/src/ui/render-submit.ts
index 4d605ca..f3f6a9e 100644
--- a/src/ui/render-submit.ts
+++ b/src/ui/render-submit.ts
@@ -1,4 +1,4 @@
-import { truncateToWidth } from "@mariozechner/pi-tui";
+import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
import { SUBMIT_CHOICES } from "../constants.ts";
import { toAskResult } from "../state/result.ts";
import type { AskResult, AskState } from "../types.ts";
@@ -16,7 +16,8 @@ export function renderSubmitScreen(
lines.push("");
const submittedAnswers = toAskResult(state).answers;
- for (const question of state.questions) {
+ for (let index = 0; index < state.questions.length; index++) {
+ const question = state.questions[index];
renderSubmittedQuestion(
lines,
question,
@@ -24,18 +25,11 @@ export function renderSubmitScreen(
theme,
width
);
+ if (index < state.questions.length - 1) {
+ lines.push("");
+ }
}
- lines.push("");
- pushWrappedText(
- lines,
- UI_TEXT.readyToSubmit,
- width,
- theme,
- "success",
- " ",
- " "
- );
lines.push("");
renderSubmitActions(lines, state, theme, width);
}
@@ -47,10 +41,10 @@ function renderSubmittedQuestion(
theme: Theme,
width: number
) {
- pushWrappedText(lines, question.prompt, width, theme, "muted", " ● ", " ");
+ pushWrappedText(lines, question.label, width, theme, "text", " ", " ");
if (!answer) {
lines.push(
- truncateToWidth(` ${theme.fg("warning", UI_TEXT.unanswered)}`, width)
+ truncateToWidth(` ${theme.fg("dim", UI_TEXT.unanswered)}`, width)
);
return;
}
@@ -64,48 +58,90 @@ function renderSubmittedAnswer(
theme: Theme,
width: number
) {
- const answerText = answer.labels.join(", ");
- if (answerText) {
- pushWrappedText(
- lines,
- `→ ${answerText}`,
- width,
- theme,
- isCustomOnlyAnswer(answer) ? "text" : "success",
- " ",
- " "
- );
- }
if (answer.note) {
+ renderSubmittedNote(lines, answer.note, theme, width);
+ }
+
+ if (shouldRenderAnswersIndividually(answer)) {
+ renderSubmittedSelections(lines, answer, theme, width);
+ return;
+ }
+
+ const answerText = answer.labels.join(", ");
+ if (!answerText) {
+ return;
+ }
+
+ pushWrappedText(
+ lines,
+ `→ ${answerText}`,
+ width,
+ theme,
+ isCustomOnlyAnswer(answer) ? "text" : "success",
+ " ",
+ " "
+ );
+}
+
+function renderSubmittedSelections(
+ lines: string[],
+ answer: AskResult["answers"][string],
+ theme: Theme,
+ width: number
+) {
+ for (let index = 0; index < answer.labels.length; index++) {
+ const label = answer.labels[index];
+ const value = answer.values[index] ?? label;
pushWrappedText(
lines,
- `Question note: ${answer.note}`,
+ `→ ${label}`,
width,
theme,
- "muted",
+ "success",
" ",
" "
);
- }
- for (let index = 0; index < answer.values.length; index++) {
- const value = answer.values[index];
- const label = answer.labels[index] ?? value;
+
const note = answer.optionNotes?.[value];
- if (!note) {
- continue;
+ if (note) {
+ renderSubmittedNote(lines, note, theme, width);
}
- pushWrappedText(
- lines,
- `${label} note: ${note}`,
- width,
- theme,
- "muted",
- " ",
- " "
- );
}
}
+function shouldRenderAnswersIndividually(
+ answer: AskResult["answers"][string]
+): boolean {
+ if (!answer.labels.length) {
+ return false;
+ }
+
+ return (
+ answer.labels.length > 1 ||
+ Boolean(answer.optionNotes && Object.keys(answer.optionNotes).length > 0)
+ );
+}
+
+function renderSubmittedNote(
+ lines: string[],
+ note: string,
+ theme: Theme,
+ width: number
+) {
+ const indent = " ";
+ const notePrefix = `${indent}${theme.fg("syntaxString", UI_TEXT.questionNoteTitle)} `;
+ const continuationPrefix = `${indent}${" ".repeat(visibleWidth(UI_TEXT.questionNoteTitle) + 1)}`;
+ pushWrappedText(
+ lines,
+ note,
+ width,
+ theme,
+ "muted",
+ notePrefix,
+ continuationPrefix
+ );
+}
+
function isCustomOnlyAnswer(answer: AskResult["answers"][string]): boolean {
return answer.indices.length === 0 && !!answer.customText;
}
diff --git a/tests/render-submit.test.ts b/tests/render-submit.test.ts
new file mode 100644
index 0000000..720852f
--- /dev/null
+++ b/tests/render-submit.test.ts
@@ -0,0 +1,161 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { createInitialState } from "../src/state/create.ts";
+import {
+ applyNumberShortcut,
+ enterOptionNoteMode,
+ enterQuestionNoteMode,
+ saveNote,
+} from "../src/state/transitions.ts";
+import { renderSubmitScreen } from "../src/ui/render-submit.ts";
+
+function mockTheme() {
+ return {
+ fg(color: string, text: string) {
+ return `<${color}>${text}${color}>`;
+ },
+ bg(color: string, text: string) {
+ return `{${color}}${text}{/${color}}`;
+ },
+ bold(text: string) {
+ return text;
+ },
+ } as never;
+}
+
+test("submit screen uses shorter action labels without extra prompt text", () => {
+ const state = createInitialState({
+ questions: [
+ {
+ id: "q1",
+ label: "Single",
+ prompt: "Pick one primary demo style.",
+ options: [{ value: "code", label: "Code demo" }],
+ },
+ ],
+ });
+
+ const lines: string[] = [];
+ renderSubmitScreen(lines, state, mockTheme(), 80);
+
+ assert(!lines.some((line) => line.includes("Submit answers?")));
+ assert(lines.some((line) => line.includes("1. Submit")));
+ assert(lines.some((line) => line.includes("2. Cancel")));
+ assert(!lines.some((line) => line.includes("1. Submit answers")));
+});
+
+test("submit screen renders indented notes before answer and separates questions", () => {
+ let state = createInitialState({
+ questions: [
+ {
+ id: "q1",
+ label: "Single",
+ prompt: "Pick one primary demo style.",
+ options: [{ value: "code", label: "Code demo" }],
+ },
+ {
+ id: "q2",
+ label: "Multi",
+ prompt: "Pick any extra things to include.",
+ options: [{ value: "extra", label: "Extra" }],
+ },
+ ],
+ });
+
+ state = enterQuestionNoteMode(state, "q1");
+ state = saveNote(state, "Question note");
+ state = applyNumberShortcut(state, 1);
+ state = enterOptionNoteMode(state, "q1", "code");
+ state = saveNote(state, "Option note");
+ state = enterQuestionNoteMode(state, "q2");
+ state = saveNote(state, "Second note");
+
+ const lines: string[] = [];
+ renderSubmitScreen(lines, state, mockTheme(), 80);
+
+ const firstQuestionIndex = lines.findIndex((line) =>
+ line.includes("Single")
+ );
+ const firstQuestionNoteIndex = lines.findIndex((line) =>
+ line.includes("Question note")
+ );
+ const firstAnswerIndex = lines.findIndex((line) =>
+ line.includes("→ Code demo")
+ );
+ const optionNoteIndex = lines.findIndex((line) =>
+ line.includes("Option note")
+ );
+ const secondQuestionIndex = lines.findIndex((line) =>
+ line.includes("Multi")
+ );
+ const secondQuestionNoteIndex = lines.findIndex((line) =>
+ line.includes("Second note")
+ );
+
+ assert.notEqual(firstQuestionIndex, -1);
+ assert.notEqual(firstQuestionNoteIndex, -1);
+ assert.notEqual(firstAnswerIndex, -1);
+ assert.notEqual(optionNoteIndex, -1);
+ assert.notEqual(secondQuestionIndex, -1);
+ assert.notEqual(secondQuestionNoteIndex, -1);
+ assert(firstQuestionNoteIndex < firstAnswerIndex);
+ assert(optionNoteIndex > firstAnswerIndex);
+ assert(lines[firstQuestionNoteIndex]?.startsWith(" "));
+ assert(lines[optionNoteIndex]?.startsWith(" "));
+ assert.equal(lines[secondQuestionIndex - 1], "");
+ assert(
+ lines.some((line) => line.includes("Note:"))
+ );
+ assert(!lines.some((line) => line.includes("Question note:")));
+ assert(!lines.some((line) => line.includes("Code demo note:")));
+ assert(!lines.some((line) => line.includes("Pick one primary demo style.")));
+ assert(
+ !lines.some((line) => line.includes("Pick any extra things to include."))
+ );
+});
+
+test("submit screen renders multi-select option notes under their related answers", () => {
+ let state = createInitialState({
+ questions: [
+ {
+ id: "q1",
+ label: "Multi",
+ prompt: "Pick extras",
+ type: "multi",
+ options: [
+ { value: "follow-up", label: "Follow-up action" },
+ { value: "docs", label: "Docs demo" },
+ ],
+ },
+ ],
+ });
+
+ state = applyNumberShortcut(state, 1);
+ state = applyNumberShortcut(state, 2);
+ state = enterOptionNoteMode(state, "q1", "follow-up");
+ state = saveNote(state, "First note");
+ state = enterOptionNoteMode(state, "q1", "docs");
+ state = saveNote(state, "Second note");
+
+ const lines: string[] = [];
+ renderSubmitScreen(lines, state, mockTheme(), 80);
+
+ const firstAnswerIndex = lines.findIndex((line) =>
+ line.includes("→ Follow-up action")
+ );
+ const firstNoteIndex = lines.findIndex((line) => line.includes("First note"));
+ const secondAnswerIndex = lines.findIndex((line) =>
+ line.includes("→ Docs demo")
+ );
+ const secondNoteIndex = lines.findIndex((line) =>
+ line.includes("Second note")
+ );
+
+ assert.notEqual(firstAnswerIndex, -1);
+ assert.notEqual(firstNoteIndex, -1);
+ assert.notEqual(secondAnswerIndex, -1);
+ assert.notEqual(secondNoteIndex, -1);
+ assert(firstAnswerIndex < firstNoteIndex);
+ assert(firstNoteIndex < secondAnswerIndex);
+ assert(secondAnswerIndex < secondNoteIndex);
+});