@geoqiao/pi-ask 1.2.2 → 1.3.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/CHANGELOG.md +67 -85
- package/README.md +22 -15
- package/docs/architecture.md +6 -1
- package/docs/configuration.md +4 -3
- package/docs/contract.md +34 -14
- package/docs/remote-events.md +14 -14
- package/package.json +20 -77
- package/skills/ask-user/SKILL.md +2 -0
- package/src/answer-commands.ts +37 -10
- package/src/answer-extraction.ts +155 -115
- package/src/ask-payload-store.ts +23 -1
- package/src/ask-tool-helpers.ts +12 -11
- package/src/ask-tool.ts +2 -0
- package/src/constants/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/pending-ask.ts +128 -0
- package/src/remote-ask.ts +11 -6
- package/src/result-format.ts +13 -3
- package/src/result.ts +1 -1
- package/src/resume-pending-ask.ts +95 -0
- package/src/rpc/controller.ts +2 -1
- package/src/schema.ts +59 -24
- package/src/state/create.ts +1 -1
- package/src/state/normalize.ts +50 -31
- package/src/state/result.ts +3 -0
- package/src/types.ts +1 -0
- package/src/ui/render-helpers.ts +9 -2
- package/src/ui/render-question.ts +39 -7
- package/src/ui/view-models/question.ts +2 -0
package/src/result-format.ts
CHANGED
|
@@ -13,13 +13,14 @@ export function formatResultLines(
|
|
|
13
13
|
for (const question of result.questions) {
|
|
14
14
|
const answer = result.answers[question.id];
|
|
15
15
|
if (!answer) {
|
|
16
|
+
lines.push(formatUnansweredLine(question.label, options.mode));
|
|
16
17
|
continue;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const answerLine = formatAnswerLine(question.label, answer, options.mode);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
lines.push(
|
|
22
|
+
answerLine ?? formatUnansweredLine(question.label, options.mode)
|
|
23
|
+
);
|
|
23
24
|
|
|
24
25
|
if (hasPresentedTypeOverride(question.type, question.presentedType)) {
|
|
25
26
|
hasPresentationOverride = true;
|
|
@@ -44,6 +45,15 @@ export function formatResultLines(
|
|
|
44
45
|
return lines;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
function formatUnansweredLine(
|
|
49
|
+
questionLabel: string,
|
|
50
|
+
mode: "summary" | "render"
|
|
51
|
+
): string {
|
|
52
|
+
return mode === "summary"
|
|
53
|
+
? `${questionLabel}: (no answer)`
|
|
54
|
+
: `? ${questionLabel}: (no answer)`;
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
function formatAnswerLine(
|
|
48
58
|
questionLabel: string,
|
|
49
59
|
answer: AskResult["answers"][string],
|
package/src/result.ts
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
SessionStartEvent,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { successfulResponse } from "./ask-tool-helpers.ts";
|
|
7
|
+
import {
|
|
8
|
+
appendPendingAskDismissal,
|
|
9
|
+
findPendingAskToolCall,
|
|
10
|
+
type PendingAskToolCall,
|
|
11
|
+
} from "./pending-ask.ts";
|
|
12
|
+
import type { RemoteAskRuntime } from "./remote-ask.ts";
|
|
13
|
+
import { runAskFlow } from "./ui/controller.ts";
|
|
14
|
+
|
|
15
|
+
const REOPEN_REASONS: ReadonlySet<SessionStartEvent["reason"]> = new Set([
|
|
16
|
+
"startup",
|
|
17
|
+
"resume",
|
|
18
|
+
"fork",
|
|
19
|
+
]);
|
|
20
|
+
const DISMISS_NOTICE =
|
|
21
|
+
"Unanswered ask_user form dismissed; use /ask:replay to reopen it.";
|
|
22
|
+
|
|
23
|
+
export function registerPendingAskResume(
|
|
24
|
+
pi: ExtensionAPI,
|
|
25
|
+
remoteAsk: RemoteAskRuntime
|
|
26
|
+
): void {
|
|
27
|
+
let reopening = false;
|
|
28
|
+
|
|
29
|
+
pi.on("session_start", (event, ctx) => {
|
|
30
|
+
if (reopening || ctx.mode !== "tui" || !REOPEN_REASONS.has(event.reason)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const pendingAsk = findPendingAskToolCall(ctx);
|
|
35
|
+
if (!pendingAsk) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
reopening = true;
|
|
40
|
+
queueMicrotask(() => {
|
|
41
|
+
reopenPendingAsk(pi, ctx, pendingAsk, remoteAsk)
|
|
42
|
+
.catch((error) => {
|
|
43
|
+
ctx.ui.notify(
|
|
44
|
+
`Could not reopen unanswered ask_user form: ${formatError(error)}`,
|
|
45
|
+
"error"
|
|
46
|
+
);
|
|
47
|
+
})
|
|
48
|
+
.finally(() => {
|
|
49
|
+
reopening = false;
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function reopenPendingAsk(
|
|
56
|
+
pi: Pick<ExtensionAPI, "appendEntry" | "sendUserMessage">,
|
|
57
|
+
ctx: ExtensionContext,
|
|
58
|
+
pendingAsk: PendingAskToolCall,
|
|
59
|
+
remoteAsk: RemoteAskRuntime
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
ctx.ui.notify(
|
|
62
|
+
`Reopening unanswered ask_user form: ${pendingAsk.params.questions.length} question(s).`,
|
|
63
|
+
"info"
|
|
64
|
+
);
|
|
65
|
+
ctx.ui.setWorkingVisible(false);
|
|
66
|
+
|
|
67
|
+
let result: Awaited<ReturnType<typeof runAskFlow>>;
|
|
68
|
+
try {
|
|
69
|
+
result = await runAskFlow(ctx, pendingAsk.params, {
|
|
70
|
+
remote: {
|
|
71
|
+
runtime: remoteAsk,
|
|
72
|
+
source: "ask:resume",
|
|
73
|
+
toolCallId: pendingAsk.toolCallId,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
} finally {
|
|
77
|
+
ctx.ui.setWorkingVisible(true);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
appendPendingAskDismissal(pi, pendingAsk.toolCallId);
|
|
81
|
+
if (result.cancelled) {
|
|
82
|
+
ctx.ui.notify(DISMISS_NOTICE, "info");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const text = successfulResponse(result).content[0].text;
|
|
87
|
+
pi.sendUserMessage(
|
|
88
|
+
text,
|
|
89
|
+
ctx.isIdle() ? undefined : { deliverAs: "followUp" }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatError(error: unknown): string {
|
|
94
|
+
return error instanceof Error ? error.message : String(error);
|
|
95
|
+
}
|
package/src/rpc/controller.ts
CHANGED
|
@@ -247,13 +247,14 @@ function formatTitle(
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
function formatOption(option: AskDisplayOption, optionIndex: number): string {
|
|
250
|
+
const recommendation = option.recommended ? " (recommended)" : "";
|
|
250
251
|
const description = option.description
|
|
251
252
|
? ` — ${compactText(option.description)}`
|
|
252
253
|
: "";
|
|
253
254
|
const preview = option.preview
|
|
254
255
|
? ` — Preview: ${compactText(option.preview)}`
|
|
255
256
|
: "";
|
|
256
|
-
return `${optionIndex + 1}. ${compactText(option.label)}${description}${preview}`;
|
|
257
|
+
return `${optionIndex + 1}. ${compactText(option.label)}${recommendation}${description}${preview}`;
|
|
257
258
|
}
|
|
258
259
|
|
|
259
260
|
function compactText(value: string): string {
|
package/src/schema.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
1
2
|
import { Type } from "typebox";
|
|
2
3
|
|
|
3
4
|
export const AskOptionSchema = Type.Object({
|
|
4
|
-
value: Type.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Type.String({
|
|
12
|
-
description: "Required short visible option label shown in the list",
|
|
13
|
-
})
|
|
14
|
-
),
|
|
5
|
+
value: Type.String({
|
|
6
|
+
description:
|
|
7
|
+
"Required machine-readable value returned for this option in the result",
|
|
8
|
+
}),
|
|
9
|
+
label: Type.String({
|
|
10
|
+
description: "Required short visible option label shown in the list",
|
|
11
|
+
}),
|
|
15
12
|
description: Type.Optional(
|
|
16
13
|
Type.String({
|
|
17
14
|
description: "Optional one-line explanation to help the user choose",
|
|
@@ -23,28 +20,30 @@ export const AskOptionSchema = Type.Object({
|
|
|
23
20
|
"Optional preview content shown in the dedicated preview pane for preview questions",
|
|
24
21
|
})
|
|
25
22
|
),
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export const AskQuestionSchema = Type.Object({
|
|
29
|
-
id: Type.Optional(
|
|
30
|
-
Type.String({
|
|
23
|
+
recommended: Type.Optional(
|
|
24
|
+
Type.Boolean({
|
|
31
25
|
description:
|
|
32
|
-
"
|
|
26
|
+
"Optional presentation marker for a grounded preference; use the description to explain the reason",
|
|
33
27
|
})
|
|
34
28
|
),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const AskQuestionSchema = Type.Object({
|
|
32
|
+
id: Type.String({
|
|
33
|
+
description:
|
|
34
|
+
"Required stable question identifier used as the key in returned answers",
|
|
35
|
+
}),
|
|
35
36
|
label: Type.Optional(
|
|
36
37
|
Type.String({
|
|
37
38
|
description: "Short tab label, e.g. Goal, Audience, Tone, Scope",
|
|
38
39
|
})
|
|
39
40
|
),
|
|
40
|
-
prompt: Type.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
})
|
|
45
|
-
),
|
|
41
|
+
prompt: Type.String({
|
|
42
|
+
description:
|
|
43
|
+
"Required direct question shown to the user; ask about one decision at a time",
|
|
44
|
+
}),
|
|
46
45
|
type: Type.Optional(
|
|
47
|
-
|
|
46
|
+
StringEnum(["single", "multi", "preview"] as const, {
|
|
48
47
|
description:
|
|
49
48
|
"Question type: `single` means one answer is expected, `multi` means multiple answers could reasonably be selected, and `preview` means options need preview-pane detail. Use `preview` only when every option includes `preview` text; descriptions alone are not enough.",
|
|
50
49
|
})
|
|
@@ -72,3 +71,39 @@ export const AskParamsSchema = Type.Object({
|
|
|
72
71
|
description: "Questions to ask in the interactive clarification flow",
|
|
73
72
|
}),
|
|
74
73
|
});
|
|
74
|
+
|
|
75
|
+
const AnswerExtractionOptionSchema = Type.Object(
|
|
76
|
+
{
|
|
77
|
+
value: Type.String({ description: "Machine-readable option value" }),
|
|
78
|
+
label: Type.String({ description: "Short visible option label" }),
|
|
79
|
+
description: AskOptionSchema.properties.description,
|
|
80
|
+
preview: AskOptionSchema.properties.preview,
|
|
81
|
+
freeform: Type.Optional(
|
|
82
|
+
Type.Boolean({
|
|
83
|
+
description:
|
|
84
|
+
"Use only when the assistant offered no concrete choices and the user should type an answer",
|
|
85
|
+
})
|
|
86
|
+
),
|
|
87
|
+
},
|
|
88
|
+
{ additionalProperties: false }
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const AnswerExtractionQuestionSchema = Type.Object({
|
|
92
|
+
id: Type.String({ description: "Stable snake_case question identifier" }),
|
|
93
|
+
label: AskQuestionSchema.properties.label,
|
|
94
|
+
prompt: Type.String({ description: "Direct question shown to the user" }),
|
|
95
|
+
type: AskQuestionSchema.properties.type,
|
|
96
|
+
required: AskQuestionSchema.properties.required,
|
|
97
|
+
options: Type.Array(AnswerExtractionOptionSchema, {
|
|
98
|
+
description:
|
|
99
|
+
"Choices explicitly offered by the assistant, or one freeform option when the user should type an answer",
|
|
100
|
+
minItems: 1,
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export const AnswerExtractionParamsSchema = Type.Object({
|
|
105
|
+
...AskParamsSchema.properties,
|
|
106
|
+
questions: Type.Array(AnswerExtractionQuestionSchema, {
|
|
107
|
+
description: "Questions extracted from the assistant message",
|
|
108
|
+
}),
|
|
109
|
+
});
|
package/src/state/create.ts
CHANGED
package/src/state/normalize.ts
CHANGED
|
@@ -38,6 +38,51 @@ export function collectValidationIssues(
|
|
|
38
38
|
return collector.issues;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function prepareAskParams(input: unknown): unknown {
|
|
42
|
+
if (!(isRecord(input) && Array.isArray(input.questions))) {
|
|
43
|
+
return input;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
...input,
|
|
47
|
+
questions: input.questions.map((question) => {
|
|
48
|
+
if (!(isRecord(question) && Array.isArray(question.options))) {
|
|
49
|
+
return question;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
...question,
|
|
53
|
+
options: question.options.map(prepareOption),
|
|
54
|
+
};
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function prepareOption(input: unknown): unknown {
|
|
60
|
+
if (!isRecord(input)) {
|
|
61
|
+
return input;
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
input.label !== undefined &&
|
|
65
|
+
!(typeof input.label === "string" && !input.label.trim())
|
|
66
|
+
) {
|
|
67
|
+
return input;
|
|
68
|
+
}
|
|
69
|
+
if (typeof input.value !== "string") {
|
|
70
|
+
return input;
|
|
71
|
+
}
|
|
72
|
+
const words = input.value.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
73
|
+
if (!words) {
|
|
74
|
+
return input;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
...input,
|
|
78
|
+
label: words.charAt(0).toUpperCase() + words.slice(1),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
83
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
84
|
+
}
|
|
85
|
+
|
|
41
86
|
function normalizeQuestion(
|
|
42
87
|
question: AskQuestionInput,
|
|
43
88
|
index: number,
|
|
@@ -65,8 +110,11 @@ function normalizeOption(option: AskOption): AskOption {
|
|
|
65
110
|
return {
|
|
66
111
|
value: option.value.trim(),
|
|
67
112
|
label: option.label.trim(),
|
|
68
|
-
description: option.description?.trim(),
|
|
69
|
-
preview: option.preview?.trim(),
|
|
113
|
+
description: option.description?.trim() || undefined,
|
|
114
|
+
preview: option.preview?.trim() || undefined,
|
|
115
|
+
...(option.recommended === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: { recommended: option.recommended }),
|
|
70
118
|
...(option.freeform ? { freeform: true } : {}),
|
|
71
119
|
};
|
|
72
120
|
}
|
|
@@ -118,12 +166,6 @@ function validateQuestion(
|
|
|
118
166
|
`${questionPath}.id`,
|
|
119
167
|
`Question ${questionNumber}: duplicate question id "${questionId}"`
|
|
120
168
|
);
|
|
121
|
-
assertOptionalText(
|
|
122
|
-
question.label,
|
|
123
|
-
collector,
|
|
124
|
-
`${questionPath}.label`,
|
|
125
|
-
`Question ${questionNumber}: label must not be empty`
|
|
126
|
-
);
|
|
127
169
|
assertRequired(
|
|
128
170
|
question.prompt?.trim(),
|
|
129
171
|
collector,
|
|
@@ -218,18 +260,6 @@ function validateOption(
|
|
|
218
260
|
`${optionPath}.label`,
|
|
219
261
|
`${prefix}: label is required`
|
|
220
262
|
);
|
|
221
|
-
assertOptionalText(
|
|
222
|
-
option.description,
|
|
223
|
-
collector,
|
|
224
|
-
`${optionPath}.description`,
|
|
225
|
-
`${prefix}: description must not be empty`
|
|
226
|
-
);
|
|
227
|
-
assertOptionalText(
|
|
228
|
-
option.preview,
|
|
229
|
-
collector,
|
|
230
|
-
`${optionPath}.preview`,
|
|
231
|
-
`${prefix}: preview must not be empty`
|
|
232
|
-
);
|
|
233
263
|
if (questionType === "preview") {
|
|
234
264
|
assertRequired(
|
|
235
265
|
optionPreview,
|
|
@@ -287,17 +317,6 @@ function assertRequired(
|
|
|
287
317
|
}
|
|
288
318
|
}
|
|
289
319
|
|
|
290
|
-
function assertOptionalText(
|
|
291
|
-
value: string | undefined,
|
|
292
|
-
collector: IssueCollector,
|
|
293
|
-
path: string,
|
|
294
|
-
message: string
|
|
295
|
-
) {
|
|
296
|
-
if (value !== undefined && !value.trim()) {
|
|
297
|
-
collector.add(path, message);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
320
|
function assertUnique(
|
|
302
321
|
seen: Set<string>,
|
|
303
322
|
value: string | undefined,
|
package/src/state/result.ts
CHANGED
|
@@ -205,6 +205,9 @@ function cloneOption(option: AskState["questions"][number]["options"][number]) {
|
|
|
205
205
|
label: option.label,
|
|
206
206
|
...(option.description ? { description: option.description } : {}),
|
|
207
207
|
...(option.preview ? { preview: option.preview } : {}),
|
|
208
|
+
...(option.recommended === undefined
|
|
209
|
+
? {}
|
|
210
|
+
: { recommended: option.recommended }),
|
|
208
211
|
};
|
|
209
212
|
}
|
|
210
213
|
|
package/src/types.ts
CHANGED
package/src/ui/render-helpers.ts
CHANGED
|
@@ -311,16 +311,23 @@ export function mergeColumns(
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
export function measurePreviewLeftWidth(
|
|
314
|
-
options: Array<{
|
|
314
|
+
options: Array<{
|
|
315
|
+
description?: string;
|
|
316
|
+
label: string;
|
|
317
|
+
recommended?: boolean;
|
|
318
|
+
}>,
|
|
315
319
|
width: number
|
|
316
320
|
): number {
|
|
317
321
|
let widest = 0;
|
|
318
322
|
for (let index = 0; index < options.length; index++) {
|
|
319
323
|
const option = options[index];
|
|
324
|
+
const description = option.recommended
|
|
325
|
+
? `${UI_TEXT.recommendedMarker}${option.description ? ` | ${option.description}` : ""}`
|
|
326
|
+
: option.description;
|
|
320
327
|
widest = Math.max(
|
|
321
328
|
widest,
|
|
322
329
|
visibleWidth(`${index + 1}. ${option.label}`),
|
|
323
|
-
|
|
330
|
+
description ? visibleWidth(description) : 0
|
|
324
331
|
);
|
|
325
332
|
}
|
|
326
333
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
truncateToWidth,
|
|
3
|
+
visibleWidth,
|
|
4
|
+
wrapTextWithAnsi,
|
|
5
|
+
} from "@earendil-works/pi-tui";
|
|
6
|
+
import { UI_DIMENSIONS, UI_TEXT } from "../constants/ui.ts";
|
|
3
7
|
import {
|
|
4
8
|
measurePreviewLeftWidth,
|
|
5
9
|
mergeColumns,
|
|
@@ -83,7 +87,13 @@ function renderStandardOption(
|
|
|
83
87
|
row.pointer,
|
|
84
88
|
" ".repeat(visibleWidth(row.pointer))
|
|
85
89
|
);
|
|
86
|
-
|
|
90
|
+
renderOptionSubtitle(
|
|
91
|
+
lines,
|
|
92
|
+
row.description,
|
|
93
|
+
row.recommended,
|
|
94
|
+
context.width,
|
|
95
|
+
context.theme
|
|
96
|
+
);
|
|
87
97
|
renderOptionDetail(lines, row.detail, context, {
|
|
88
98
|
suppressLeadingGap: !!row.description,
|
|
89
99
|
});
|
|
@@ -170,7 +180,7 @@ function renderPreviewOptionList(
|
|
|
170
180
|
row.pointer,
|
|
171
181
|
" "
|
|
172
182
|
);
|
|
173
|
-
|
|
183
|
+
renderOptionSubtitle(lines, row.description, row.recommended, width, theme);
|
|
174
184
|
}
|
|
175
185
|
return lines;
|
|
176
186
|
}
|
|
@@ -275,14 +285,36 @@ function renderInteractiveCustomOption(
|
|
|
275
285
|
});
|
|
276
286
|
}
|
|
277
287
|
|
|
278
|
-
function
|
|
288
|
+
function renderOptionSubtitle(
|
|
279
289
|
lines: string[],
|
|
280
290
|
description: string | undefined,
|
|
291
|
+
recommended: boolean,
|
|
281
292
|
width: number,
|
|
282
293
|
theme: Theme
|
|
283
294
|
) {
|
|
284
|
-
if (!
|
|
295
|
+
if (!recommended) {
|
|
296
|
+
if (description) {
|
|
297
|
+
pushWrappedText(
|
|
298
|
+
lines,
|
|
299
|
+
description,
|
|
300
|
+
width,
|
|
301
|
+
theme,
|
|
302
|
+
"muted",
|
|
303
|
+
" ",
|
|
304
|
+
" "
|
|
305
|
+
);
|
|
306
|
+
}
|
|
285
307
|
return;
|
|
286
308
|
}
|
|
287
|
-
|
|
309
|
+
|
|
310
|
+
const indent = " ";
|
|
311
|
+
const text =
|
|
312
|
+
theme.fg("warning", UI_TEXT.recommendedMarker) +
|
|
313
|
+
(description ? theme.fg("muted", ` | ${description}`) : "");
|
|
314
|
+
for (const line of wrapTextWithAnsi(
|
|
315
|
+
text,
|
|
316
|
+
Math.max(1, width - visibleWidth(indent))
|
|
317
|
+
)) {
|
|
318
|
+
lines.push(truncateToWidth(`${indent}${line}`, width));
|
|
319
|
+
}
|
|
288
320
|
}
|
|
@@ -30,6 +30,7 @@ export interface OptionRowModel {
|
|
|
30
30
|
label: string;
|
|
31
31
|
pointer: string;
|
|
32
32
|
prefix: string;
|
|
33
|
+
recommended: boolean;
|
|
33
34
|
selected: boolean;
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -104,6 +105,7 @@ function buildOptionRowModel(
|
|
|
104
105
|
label: option.label,
|
|
105
106
|
pointer,
|
|
106
107
|
prefix: getOptionPrefix(question.type, option, answered),
|
|
108
|
+
recommended: option.recommended === true,
|
|
107
109
|
selected,
|
|
108
110
|
};
|
|
109
111
|
}
|