@mrclrchtr/supi-ask-user 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -67
- package/node_modules/@mrclrchtr/supi-core/README.md +52 -41
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +13 -13
- package/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts} +2 -2
- package/node_modules/@mrclrchtr/supi-core/src/{context-provider-registry.ts → context/context-provider-registry.ts} +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +13 -13
- package/node_modules/@mrclrchtr/supi-core/src/{settings-registry.ts → settings/settings-registry.ts} +1 -1
- package/package.json +2 -2
- package/src/api.ts +19 -0
- package/src/ask-user.ts +65 -131
- package/src/index.ts +23 -1
- package/src/normalize.ts +153 -142
- package/src/render/result.ts +98 -0
- package/src/render/transcript.ts +65 -0
- package/src/render/tree-summary.ts +10 -0
- package/src/schema.ts +41 -38
- package/src/session/controller.ts +163 -0
- package/src/session/lock.ts +19 -0
- package/src/tool/guidance.ts +15 -0
- package/src/types.ts +50 -56
- package/src/ui/choose-renderer.ts +11 -0
- package/src/ui/overlay-actions.ts +42 -0
- package/src/ui/overlay-render.ts +196 -0
- package/src/ui/overlay-view.ts +216 -0
- package/src/ui/overlay.ts +388 -0
- package/src/ui/types.ts +35 -0
- package/src/flow.ts +0 -224
- package/src/format.ts +0 -66
- package/src/render/ui-rich-render-editor.ts +0 -51
- package/src/render/ui-rich-render-env.ts +0 -15
- package/src/render/ui-rich-render-footer.ts +0 -55
- package/src/render/ui-rich-render-markdown.ts +0 -33
- package/src/render/ui-rich-render-notes.ts +0 -80
- package/src/render/ui-rich-render-types.ts +0 -17
- package/src/render/ui-rich-render.ts +0 -323
- package/src/render.ts +0 -95
- package/src/result.ts +0 -90
- package/src/ui/ui-rich-handlers.ts +0 -369
- package/src/ui/ui-rich-inline.ts +0 -77
- package/src/ui/ui-rich-state.ts +0 -179
- package/src/ui/ui-rich.ts +0 -144
- /package/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{settings-ui.ts → settings/settings-ui.ts} +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AskUserOutcome, NormalizedQuestionnaire } from "../types.ts";
|
|
2
|
+
import { runOverlayQuestionnaire } from "./overlay.ts";
|
|
3
|
+
import type { RunQuestionnaireOptions } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export async function runQuestionnaire(
|
|
6
|
+
questionnaire: NormalizedQuestionnaire,
|
|
7
|
+
opts: RunQuestionnaireOptions,
|
|
8
|
+
): Promise<AskUserOutcome | "unsupported"> {
|
|
9
|
+
if (typeof opts.ui.custom !== "function") return "unsupported";
|
|
10
|
+
return runOverlayQuestionnaire(questionnaire, opts);
|
|
11
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { SelectList } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { AskUserController } from "../session/controller.ts";
|
|
4
|
+
import { makeSelectListTheme } from "./overlay-render.ts";
|
|
5
|
+
import { buildTextActionItems, type OverlayAction } from "./overlay-view.ts";
|
|
6
|
+
|
|
7
|
+
export interface ActionListState {
|
|
8
|
+
entries: Array<{ action: OverlayAction; label: string }>;
|
|
9
|
+
list: SelectList | undefined;
|
|
10
|
+
index: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createActionList(args: {
|
|
14
|
+
controller: AskUserController;
|
|
15
|
+
theme: Theme;
|
|
16
|
+
actionIndex: number;
|
|
17
|
+
onIndexChange: (index: number) => void;
|
|
18
|
+
onAction: (action: OverlayAction) => void;
|
|
19
|
+
}): ActionListState {
|
|
20
|
+
const actions = buildTextActionItems(args.controller);
|
|
21
|
+
const entries = actions.map(({ action, item }) => ({ action, label: item.label }));
|
|
22
|
+
if (actions.length === 0) {
|
|
23
|
+
return { entries, list: undefined, index: 0 };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const index = Math.max(0, Math.min(args.actionIndex, actions.length - 1));
|
|
27
|
+
const list = new SelectList(
|
|
28
|
+
actions.map(({ item }) => item),
|
|
29
|
+
Math.min(actions.length, 6),
|
|
30
|
+
makeSelectListTheme(args.theme),
|
|
31
|
+
);
|
|
32
|
+
list.onSelectionChange = (item) => {
|
|
33
|
+
const nextIndex = actions.findIndex(({ item: candidate }) => candidate.value === item.value);
|
|
34
|
+
if (nextIndex >= 0) args.onIndexChange(nextIndex);
|
|
35
|
+
};
|
|
36
|
+
list.onSelect = (item) => {
|
|
37
|
+
const action = actions.find(({ item: candidate }) => candidate.value === item.value)?.action;
|
|
38
|
+
if (action) args.onAction(action);
|
|
39
|
+
};
|
|
40
|
+
list.setSelectedIndex(index);
|
|
41
|
+
return { entries, list, index };
|
|
42
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
type Editor,
|
|
4
|
+
type EditorTheme,
|
|
5
|
+
Markdown,
|
|
6
|
+
type SelectList,
|
|
7
|
+
type SelectListTheme,
|
|
8
|
+
} from "@earendil-works/pi-tui";
|
|
9
|
+
import type { AskUserController } from "../session/controller.ts";
|
|
10
|
+
import type { NormalizedQuestionnaire } from "../types.ts";
|
|
11
|
+
import { type FocusTarget, footerText, type OverlayMode, splitColumns } from "./overlay-view.ts";
|
|
12
|
+
|
|
13
|
+
export interface RenderOverlayFrameArgs {
|
|
14
|
+
width: number;
|
|
15
|
+
theme: Theme;
|
|
16
|
+
controller: AskUserController;
|
|
17
|
+
mode: OverlayMode;
|
|
18
|
+
focus: FocusTarget;
|
|
19
|
+
editor: Editor;
|
|
20
|
+
choiceList: SelectList | undefined;
|
|
21
|
+
actionList: SelectList | undefined;
|
|
22
|
+
textActionLabels: string[];
|
|
23
|
+
previewText?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderOverlayFrame(args: RenderOverlayFrameArgs): string[] {
|
|
27
|
+
const lines: string[] = [];
|
|
28
|
+
lines.push(args.theme.fg("accent", "─".repeat(args.width)));
|
|
29
|
+
lines.push(...renderHeader(args));
|
|
30
|
+
lines.push(...renderPrompt(args.controller.currentQuestion.prompt, args.width));
|
|
31
|
+
lines.push("");
|
|
32
|
+
lines.push(...renderBody(args));
|
|
33
|
+
lines.push("");
|
|
34
|
+
lines.push(
|
|
35
|
+
args.theme.fg(
|
|
36
|
+
"dim",
|
|
37
|
+
footerText({
|
|
38
|
+
controller: args.controller,
|
|
39
|
+
mode: args.mode,
|
|
40
|
+
focus: args.focus,
|
|
41
|
+
hasTextActions: args.textActionLabels.length > 0,
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
lines.push(args.theme.fg("accent", "─".repeat(args.width)));
|
|
46
|
+
return lines;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function makeEditorTheme(theme: Theme): EditorTheme {
|
|
50
|
+
return {
|
|
51
|
+
borderColor: (text) => theme.fg("accent", text),
|
|
52
|
+
selectList: makeSelectListTheme(theme),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function makeSelectListTheme(theme: Theme): SelectListTheme {
|
|
57
|
+
return {
|
|
58
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
59
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
60
|
+
description: (text) => theme.fg("muted", text),
|
|
61
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
62
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function currentPreviewText(
|
|
67
|
+
question: NormalizedQuestionnaire["questions"][number],
|
|
68
|
+
optionIndex: number,
|
|
69
|
+
): string | undefined {
|
|
70
|
+
return question.type === "choice" ? question.options[optionIndex]?.preview : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function currentTextValue(
|
|
74
|
+
controller: AskUserController,
|
|
75
|
+
initial: string | undefined,
|
|
76
|
+
): string {
|
|
77
|
+
const answer = controller.getAnswer(controller.currentQuestion.id);
|
|
78
|
+
return answer?.kind === "text" ? answer.value : (initial ?? "");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function currentCustomValue(controller: AskUserController): string {
|
|
82
|
+
const answer = controller.getAnswer(controller.currentQuestion.id);
|
|
83
|
+
return answer?.kind === "custom" ? answer.value : "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function clampIndex(index: number, length: number): number {
|
|
87
|
+
if (length === 0) return 0;
|
|
88
|
+
return Math.max(0, Math.min(index, length - 1));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderHeader(args: RenderOverlayFrameArgs): string[] {
|
|
92
|
+
const lines: string[] = [];
|
|
93
|
+
const { title, intro } = args.controller.questionnaire;
|
|
94
|
+
if (title) lines.push(args.theme.fg("accent", args.theme.bold(title)));
|
|
95
|
+
lines.push(
|
|
96
|
+
args.theme.fg(
|
|
97
|
+
"muted",
|
|
98
|
+
`${args.controller.currentIndex + 1}/${args.controller.questions.length} · ${args.controller.currentQuestion.header}`,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
if (intro) {
|
|
102
|
+
lines.push("");
|
|
103
|
+
lines.push(...renderPrompt(intro, args.width));
|
|
104
|
+
}
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function renderBody(args: RenderOverlayFrameArgs): string[] {
|
|
109
|
+
const question = args.controller.currentQuestion;
|
|
110
|
+
if (question.type === "text") {
|
|
111
|
+
return renderTextBody(args);
|
|
112
|
+
}
|
|
113
|
+
return renderChoiceBody(args);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderChoiceBody(args: RenderOverlayFrameArgs): string[] {
|
|
117
|
+
const leftLines = args.choiceList?.render(splitLeftWidth(args.width)) ?? [];
|
|
118
|
+
|
|
119
|
+
if (args.mode === "custom-input" || args.mode === "discuss-input") {
|
|
120
|
+
const rightLines = renderEditorLines(args, splitRightWidth(args.width));
|
|
121
|
+
if (args.width >= 100) {
|
|
122
|
+
return splitColumns({
|
|
123
|
+
width: args.width,
|
|
124
|
+
theme: args.theme,
|
|
125
|
+
leftLines,
|
|
126
|
+
rightLines,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return [...leftLines, "", ...rightLines];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (args.previewText && args.width >= 100) {
|
|
133
|
+
const rightLines = [
|
|
134
|
+
args.theme.fg("accent", "Preview"),
|
|
135
|
+
"",
|
|
136
|
+
...renderPrompt(args.previewText, splitRightWidth(args.width)),
|
|
137
|
+
];
|
|
138
|
+
return splitColumns({
|
|
139
|
+
width: args.width,
|
|
140
|
+
theme: args.theme,
|
|
141
|
+
leftLines,
|
|
142
|
+
rightLines,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!args.previewText) return leftLines;
|
|
147
|
+
return [
|
|
148
|
+
...leftLines,
|
|
149
|
+
"",
|
|
150
|
+
args.theme.fg("accent", "Preview"),
|
|
151
|
+
...renderPrompt(args.previewText, args.width),
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function renderTextBody(args: RenderOverlayFrameArgs): string[] {
|
|
156
|
+
const lines = renderEditorLines(args, args.width);
|
|
157
|
+
if (args.textActionLabels.length === 0) return lines;
|
|
158
|
+
|
|
159
|
+
if (args.focus === "actions") {
|
|
160
|
+
return [...lines, "", ...(args.actionList?.render(args.width) ?? [])];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return [...lines, "", ...args.textActionLabels.map((label) => ` ${label}`)];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function renderEditorLines(args: RenderOverlayFrameArgs, width: number): string[] {
|
|
167
|
+
const label =
|
|
168
|
+
args.mode === "discuss-input"
|
|
169
|
+
? "Discuss instead"
|
|
170
|
+
: args.mode === "custom-input"
|
|
171
|
+
? "Other answer"
|
|
172
|
+
: "Your answer";
|
|
173
|
+
|
|
174
|
+
const lines = [args.theme.fg("accent", label), ...args.editor.render(Math.max(20, width - 1))];
|
|
175
|
+
const question = args.controller.currentQuestion;
|
|
176
|
+
if (question.type !== "text" || args.editor.getText()) return lines;
|
|
177
|
+
|
|
178
|
+
if (question.initial) {
|
|
179
|
+
lines.push(args.theme.fg("dim", `Initial: ${question.initial}`));
|
|
180
|
+
} else if (question.placeholder) {
|
|
181
|
+
lines.push(args.theme.fg("dim", `Placeholder: ${question.placeholder}`));
|
|
182
|
+
}
|
|
183
|
+
return lines;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderPrompt(text: string, width: number): string[] {
|
|
187
|
+
return new Markdown(text, 0, 0, getMarkdownTheme()).render(Math.max(1, width));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function splitLeftWidth(width: number): number {
|
|
191
|
+
return Math.max(36, Math.floor(width * 0.55));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function splitRightWidth(width: number): number {
|
|
195
|
+
return Math.max(24, width - splitLeftWidth(width) - 3);
|
|
196
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { SelectItem } from "@earendil-works/pi-tui";
|
|
3
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import type { AskUserController } from "../session/controller.ts";
|
|
5
|
+
import type { NormalizedChoiceQuestion } from "../types.ts";
|
|
6
|
+
|
|
7
|
+
export type OverlayAction = "other" | "skip" | "discuss" | "partial";
|
|
8
|
+
export type FocusTarget = "choices" | "editor" | "actions";
|
|
9
|
+
export type OverlayMode = "choice" | "text" | "custom-input" | "discuss-input";
|
|
10
|
+
|
|
11
|
+
export type ChoiceRow =
|
|
12
|
+
| { kind: "option"; optionIndex: number }
|
|
13
|
+
| { kind: "action"; action: OverlayAction };
|
|
14
|
+
|
|
15
|
+
export function buildChoiceRows(
|
|
16
|
+
controller: AskUserController,
|
|
17
|
+
question: NormalizedChoiceQuestion,
|
|
18
|
+
): ChoiceRow[] {
|
|
19
|
+
const rows: ChoiceRow[] = question.options.map((_option, optionIndex) => ({
|
|
20
|
+
kind: "option",
|
|
21
|
+
optionIndex,
|
|
22
|
+
}));
|
|
23
|
+
if (question.allowOther) rows.push({ kind: "action", action: "other" });
|
|
24
|
+
rows.push(
|
|
25
|
+
...buildExceptionalActions(controller, question.required).map((action) => ({
|
|
26
|
+
kind: "action" as const,
|
|
27
|
+
action,
|
|
28
|
+
})),
|
|
29
|
+
);
|
|
30
|
+
return rows;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildChoiceItems(
|
|
34
|
+
controller: AskUserController,
|
|
35
|
+
question: NormalizedChoiceQuestion,
|
|
36
|
+
rows: ChoiceRow[],
|
|
37
|
+
): SelectItem[] {
|
|
38
|
+
const selectedIndexes = new Set(controller.getSelectedIndexes(question));
|
|
39
|
+
return rows.flatMap((row) => {
|
|
40
|
+
if (row.kind === "option") {
|
|
41
|
+
const option = question.options[row.optionIndex];
|
|
42
|
+
return option
|
|
43
|
+
? [
|
|
44
|
+
buildOptionItem({
|
|
45
|
+
question,
|
|
46
|
+
optionIndex: row.optionIndex,
|
|
47
|
+
label: option.label,
|
|
48
|
+
description: option.description,
|
|
49
|
+
selectedIndexes,
|
|
50
|
+
}),
|
|
51
|
+
]
|
|
52
|
+
: [];
|
|
53
|
+
}
|
|
54
|
+
return [buildActionItem(controller, question.id, row.action)];
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildTextActionItems(
|
|
59
|
+
controller: AskUserController,
|
|
60
|
+
): Array<{ action: OverlayAction; item: SelectItem }> {
|
|
61
|
+
return buildExceptionalActions(controller, controller.currentQuestion.required).map((action) => ({
|
|
62
|
+
action,
|
|
63
|
+
item: {
|
|
64
|
+
value: action,
|
|
65
|
+
label: actionLabel(action),
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function defaultChoiceRowIndex(
|
|
71
|
+
controller: AskUserController,
|
|
72
|
+
question: NormalizedChoiceQuestion,
|
|
73
|
+
rows: ChoiceRow[],
|
|
74
|
+
): number {
|
|
75
|
+
const current = controller.getAnswer(question.id);
|
|
76
|
+
if (current?.kind === "custom") {
|
|
77
|
+
return rows.findIndex((row) => row.kind === "action" && row.action === "other");
|
|
78
|
+
}
|
|
79
|
+
if (current?.kind === "choice") {
|
|
80
|
+
const first = current.selections[0];
|
|
81
|
+
if (!first) return 0;
|
|
82
|
+
const optionIndex = question.options.findIndex((option) => option.value === first.value);
|
|
83
|
+
return optionIndex >= 0 ? optionIndex : 0;
|
|
84
|
+
}
|
|
85
|
+
return question.initialIndexes[0] ?? question.recommendedIndexes[0] ?? 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function previewOptionIndexForRows(
|
|
89
|
+
rows: ChoiceRow[],
|
|
90
|
+
rowIndex: number,
|
|
91
|
+
fallbackOptionIndex: number,
|
|
92
|
+
): number | undefined {
|
|
93
|
+
const row = rows[rowIndex];
|
|
94
|
+
if (row?.kind === "option") return row.optionIndex;
|
|
95
|
+
return fallbackOptionIndex >= 0 ? fallbackOptionIndex : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function footerText(args: {
|
|
99
|
+
controller: AskUserController;
|
|
100
|
+
mode: OverlayMode;
|
|
101
|
+
focus: FocusTarget;
|
|
102
|
+
hasTextActions: boolean;
|
|
103
|
+
}): string {
|
|
104
|
+
const { controller, mode, focus, hasTextActions } = args;
|
|
105
|
+
const question = controller.currentQuestion;
|
|
106
|
+
|
|
107
|
+
if (question.type === "text") {
|
|
108
|
+
if (mode === "discuss-input") return "Enter submit • Esc cancel";
|
|
109
|
+
if (focus === "editor") {
|
|
110
|
+
return hasTextActions
|
|
111
|
+
? "Enter submit • ↓ actions • ← back • Esc cancel"
|
|
112
|
+
: "Enter submit • ← back • Esc cancel";
|
|
113
|
+
}
|
|
114
|
+
return "↑↓ move • Enter select • ↑ editor • ← back • Esc cancel";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (mode === "custom-input" || mode === "discuss-input") {
|
|
118
|
+
return "Enter submit • Esc cancel";
|
|
119
|
+
}
|
|
120
|
+
return question.multi
|
|
121
|
+
? "↑↓ move • Space toggle • Enter submit • ← back • Esc cancel"
|
|
122
|
+
: "↑↓ move • Space select • Enter submit • ← back • Esc cancel";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function splitColumns(args: {
|
|
126
|
+
width: number;
|
|
127
|
+
theme: Theme;
|
|
128
|
+
leftLines: string[];
|
|
129
|
+
rightLines: string[];
|
|
130
|
+
leftRatio?: number;
|
|
131
|
+
}): string[] {
|
|
132
|
+
const { width, theme, leftLines, rightLines, leftRatio = 0.55 } = args;
|
|
133
|
+
const leftWidth = Math.max(36, Math.floor(width * leftRatio));
|
|
134
|
+
const rightWidth = Math.max(24, width - leftWidth - 3);
|
|
135
|
+
const total = Math.max(leftLines.length, rightLines.length);
|
|
136
|
+
const lines: string[] = [];
|
|
137
|
+
|
|
138
|
+
for (let index = 0; index < total; index += 1) {
|
|
139
|
+
const left = padRight(leftLines[index] ?? "", leftWidth);
|
|
140
|
+
const right = padRight(rightLines[index] ?? "", rightWidth);
|
|
141
|
+
lines.push(`${left} ${theme.fg("accent", "│")} ${right}`);
|
|
142
|
+
}
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function choiceRowValue(row: ChoiceRow): string {
|
|
147
|
+
return row.kind === "option" ? `option:${row.optionIndex}` : `action:${row.action}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildOptionItem(args: {
|
|
151
|
+
question: NormalizedChoiceQuestion;
|
|
152
|
+
optionIndex: number;
|
|
153
|
+
label: string;
|
|
154
|
+
description: string | undefined;
|
|
155
|
+
selectedIndexes: Set<number>;
|
|
156
|
+
}): SelectItem {
|
|
157
|
+
const { question, optionIndex, label, description, selectedIndexes } = args;
|
|
158
|
+
const recommended = question.recommendedIndexes.includes(optionIndex) ? " (recommended)" : "";
|
|
159
|
+
const marker = question.multi
|
|
160
|
+
? selectedIndexes.has(optionIndex)
|
|
161
|
+
? "[x]"
|
|
162
|
+
: "[ ]"
|
|
163
|
+
: selectedIndexes.has(optionIndex)
|
|
164
|
+
? "(*)"
|
|
165
|
+
: "( )";
|
|
166
|
+
return {
|
|
167
|
+
value: choiceRowValue({ kind: "option", optionIndex }),
|
|
168
|
+
label: `${marker} ${label}${recommended}`,
|
|
169
|
+
description,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildActionItem(
|
|
174
|
+
controller: AskUserController,
|
|
175
|
+
questionId: string,
|
|
176
|
+
action: OverlayAction,
|
|
177
|
+
): SelectItem {
|
|
178
|
+
const answer = controller.getAnswer(questionId);
|
|
179
|
+
return {
|
|
180
|
+
value: choiceRowValue({ kind: "action", action }),
|
|
181
|
+
label:
|
|
182
|
+
action === "other" && answer?.kind === "custom"
|
|
183
|
+
? `Other — ${answer.value}`
|
|
184
|
+
: actionLabel(action),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function buildExceptionalActions(
|
|
189
|
+
controller: AskUserController,
|
|
190
|
+
required: boolean,
|
|
191
|
+
): OverlayAction[] {
|
|
192
|
+
const actions: OverlayAction[] = [];
|
|
193
|
+
if (!required) actions.push("skip");
|
|
194
|
+
if (controller.questionnaire.allowDiscuss) actions.push("discuss");
|
|
195
|
+
if (controller.canPartialSubmit()) actions.push("partial");
|
|
196
|
+
return actions;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function actionLabel(action: OverlayAction): string {
|
|
200
|
+
switch (action) {
|
|
201
|
+
case "other":
|
|
202
|
+
return "Other…";
|
|
203
|
+
case "skip":
|
|
204
|
+
return "Skip question";
|
|
205
|
+
case "discuss":
|
|
206
|
+
return "Discuss instead…";
|
|
207
|
+
case "partial":
|
|
208
|
+
return "Submit partial answers";
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function padRight(text: string, width: number): string {
|
|
213
|
+
const visible = visibleWidth(text);
|
|
214
|
+
if (visible >= width) return truncateToWidth(text, width);
|
|
215
|
+
return `${text}${" ".repeat(width - visible)}`;
|
|
216
|
+
}
|