@geoqiao/pi-ask 1.1.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 +194 -0
- package/LICENSE +22 -0
- package/README.md +282 -0
- package/docs/README.md +33 -0
- package/docs/configuration.md +406 -0
- package/docs/contract.md +309 -0
- package/docs/remote-events.md +187 -0
- package/package.json +130 -0
- package/skills/ask-user/SKILL.md +110 -0
- package/src/answer-commands.ts +361 -0
- package/src/answer-extraction.ts +354 -0
- package/src/ask-payload-store.ts +86 -0
- package/src/ask-settings-command.ts +14 -0
- package/src/ask-tool-helpers.ts +172 -0
- package/src/ask-tool.ts +84 -0
- package/src/config/defaults.ts +216 -0
- package/src/config/migrate.ts +70 -0
- package/src/config/migrations/index.ts +139 -0
- package/src/config/migrations/types.ts +10 -0
- package/src/config/schema.ts +287 -0
- package/src/config/store.ts +227 -0
- package/src/constants/keymaps.ts +721 -0
- package/src/constants/text.ts +12 -0
- package/src/constants/ui.ts +22 -0
- package/src/index.ts +30 -0
- package/src/math.ts +3 -0
- package/src/notifications.ts +119 -0
- package/src/remote-ask.ts +563 -0
- package/src/result-format.ts +157 -0
- package/src/result.ts +23 -0
- package/src/schema.ts +74 -0
- package/src/state/answers.ts +251 -0
- package/src/state/create.ts +18 -0
- package/src/state/editor.ts +70 -0
- package/src/state/navigation.ts +86 -0
- package/src/state/normalize.ts +326 -0
- package/src/state/question-type.ts +128 -0
- package/src/state/result.ts +263 -0
- package/src/state/selectors.ts +135 -0
- package/src/state/transitions.ts +330 -0
- package/src/state/view.ts +28 -0
- package/src/text.ts +98 -0
- package/src/types.ts +169 -0
- package/src/ui/auto-submit.ts +36 -0
- package/src/ui/autocomplete.ts +52 -0
- package/src/ui/controller.ts +645 -0
- package/src/ui/dismiss-guard.ts +26 -0
- package/src/ui/input.ts +160 -0
- package/src/ui/render-frame.ts +235 -0
- package/src/ui/render-helpers.ts +385 -0
- package/src/ui/render-question.ts +288 -0
- package/src/ui/render-submit.ts +168 -0
- package/src/ui/render-types.ts +33 -0
- package/src/ui/render.ts +53 -0
- package/src/ui/review-shortcuts.ts +43 -0
- package/src/ui/settings-list.ts +461 -0
- package/src/ui/show-settings.ts +37 -0
- package/src/ui/view-models/question.ts +203 -0
- package/src/ui/view-models/review.ts +100 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { UI_TEXT } from "../constants/ui.ts";
|
|
3
|
+
import type { AskState } from "../types.ts";
|
|
4
|
+
import {
|
|
5
|
+
mergeColumns,
|
|
6
|
+
pushSavedNote,
|
|
7
|
+
pushWrappedText,
|
|
8
|
+
} from "./render-helpers.ts";
|
|
9
|
+
import type { Theme } from "./render-types.ts";
|
|
10
|
+
import { buildReviewScreenModel } from "./view-models/review.ts";
|
|
11
|
+
|
|
12
|
+
export function renderSubmitScreen(
|
|
13
|
+
lines: string[],
|
|
14
|
+
state: AskState,
|
|
15
|
+
theme: Theme,
|
|
16
|
+
width: number,
|
|
17
|
+
reviewShortcutHint?: string
|
|
18
|
+
) {
|
|
19
|
+
const model = buildReviewScreenModel(state, width);
|
|
20
|
+
if (model.layout === "wide") {
|
|
21
|
+
const rightWidth = Math.max(1, width - model.actionColumnWidth - 2);
|
|
22
|
+
const reviewLines = renderSubmitReviewLines(model, theme, rightWidth);
|
|
23
|
+
const actionLines = renderSubmitActions(
|
|
24
|
+
model,
|
|
25
|
+
theme,
|
|
26
|
+
model.actionColumnWidth
|
|
27
|
+
);
|
|
28
|
+
for (const line of mergeColumns(
|
|
29
|
+
actionLines,
|
|
30
|
+
reviewLines,
|
|
31
|
+
model.actionColumnWidth,
|
|
32
|
+
width
|
|
33
|
+
)) {
|
|
34
|
+
lines.push(line);
|
|
35
|
+
}
|
|
36
|
+
appendReviewShortcutHint(lines, reviewShortcutHint, theme, width);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const reviewLines = renderSubmitReviewLines(model, theme, width);
|
|
41
|
+
const actionLines = renderSubmitActions(model, theme, width);
|
|
42
|
+
lines.push(...reviewLines);
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push(...actionLines);
|
|
45
|
+
appendReviewShortcutHint(lines, reviewShortcutHint, theme, width);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderSubmitReviewLines(
|
|
49
|
+
model: ReturnType<typeof buildReviewScreenModel>,
|
|
50
|
+
theme: Theme,
|
|
51
|
+
width: number
|
|
52
|
+
): string[] {
|
|
53
|
+
const lines: string[] = [];
|
|
54
|
+
pushWrappedText(lines, UI_TEXT.reviewTitle, width, theme, "accent", " ", " ");
|
|
55
|
+
lines.push("");
|
|
56
|
+
|
|
57
|
+
for (const [index, question] of model.questions.entries()) {
|
|
58
|
+
renderReviewQuestion(lines, question, theme, width);
|
|
59
|
+
if (index < model.questions.length - 1) {
|
|
60
|
+
lines.push("");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return lines;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderReviewQuestion(
|
|
68
|
+
lines: string[],
|
|
69
|
+
question: ReturnType<typeof buildReviewScreenModel>["questions"][number],
|
|
70
|
+
theme: Theme,
|
|
71
|
+
width: number
|
|
72
|
+
) {
|
|
73
|
+
pushWrappedText(lines, question.label, width, theme, "text", " ", " ");
|
|
74
|
+
if (question.unanswered) {
|
|
75
|
+
lines.push(
|
|
76
|
+
truncateToWidth(` ${theme.fg("dim", UI_TEXT.unanswered)}`, width)
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (question.note) {
|
|
82
|
+
pushSavedNote({
|
|
83
|
+
lines,
|
|
84
|
+
note: question.note,
|
|
85
|
+
width,
|
|
86
|
+
theme,
|
|
87
|
+
indent: " ",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const selection of question.selections ?? []) {
|
|
92
|
+
pushWrappedText(
|
|
93
|
+
lines,
|
|
94
|
+
`→ ${selection.label}`,
|
|
95
|
+
width,
|
|
96
|
+
theme,
|
|
97
|
+
"success",
|
|
98
|
+
" ",
|
|
99
|
+
" "
|
|
100
|
+
);
|
|
101
|
+
if (selection.note) {
|
|
102
|
+
pushSavedNote({
|
|
103
|
+
lines,
|
|
104
|
+
note: selection.note,
|
|
105
|
+
width,
|
|
106
|
+
theme,
|
|
107
|
+
indent: " ",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (question.answerText) {
|
|
113
|
+
pushWrappedText(
|
|
114
|
+
lines,
|
|
115
|
+
`→ ${question.answerText}`,
|
|
116
|
+
width,
|
|
117
|
+
theme,
|
|
118
|
+
question.isCustomOnly ? "text" : "success",
|
|
119
|
+
" ",
|
|
120
|
+
" "
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const optionNote of question.extraOptionNotes ?? []) {
|
|
125
|
+
pushSavedNote({
|
|
126
|
+
lines,
|
|
127
|
+
note: optionNote.note,
|
|
128
|
+
width,
|
|
129
|
+
theme,
|
|
130
|
+
indent: " ",
|
|
131
|
+
label: optionNote.label,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderSubmitActions(
|
|
137
|
+
model: ReturnType<typeof buildReviewScreenModel>,
|
|
138
|
+
theme: Theme,
|
|
139
|
+
width: number
|
|
140
|
+
): string[] {
|
|
141
|
+
const lines: string[] = [];
|
|
142
|
+
for (const [index, action] of model.actions.entries()) {
|
|
143
|
+
const prefix = action.selected ? "❯ " : " ";
|
|
144
|
+
pushWrappedText(
|
|
145
|
+
lines,
|
|
146
|
+
`${index + 1}. ${action.label}`,
|
|
147
|
+
width,
|
|
148
|
+
theme,
|
|
149
|
+
action.selected ? "accent" : "text",
|
|
150
|
+
prefix,
|
|
151
|
+
prefix
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return lines;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function appendReviewShortcutHint(
|
|
158
|
+
lines: string[],
|
|
159
|
+
hint: string | undefined,
|
|
160
|
+
theme: Theme,
|
|
161
|
+
width: number
|
|
162
|
+
) {
|
|
163
|
+
if (!hint) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
lines.push("");
|
|
167
|
+
pushWrappedText(lines, hint, width, theme, "dim", " ", " ");
|
|
168
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Editor } from "@earendil-works/pi-tui";
|
|
3
|
+
import type {
|
|
4
|
+
getAnswer,
|
|
5
|
+
getCurrentQuestion,
|
|
6
|
+
getRenderableOptions,
|
|
7
|
+
} from "../state/selectors.ts";
|
|
8
|
+
import type { AskDisplayOption, AskState } from "../types.ts";
|
|
9
|
+
|
|
10
|
+
export type Theme = ExtensionContext["ui"]["theme"];
|
|
11
|
+
|
|
12
|
+
export interface QuestionRenderContext {
|
|
13
|
+
editor: Editor;
|
|
14
|
+
lines: string[];
|
|
15
|
+
options: ReturnType<typeof getRenderableOptions>;
|
|
16
|
+
question: NonNullable<ReturnType<typeof getCurrentQuestion>>;
|
|
17
|
+
state: AskState;
|
|
18
|
+
theme: Theme;
|
|
19
|
+
width: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface OptionDetailRenderContext {
|
|
23
|
+
answer: ReturnType<typeof getAnswer>;
|
|
24
|
+
editor: Editor;
|
|
25
|
+
lines: string[];
|
|
26
|
+
option: AskDisplayOption | undefined;
|
|
27
|
+
questionId: string;
|
|
28
|
+
selected?: boolean;
|
|
29
|
+
state: AskState;
|
|
30
|
+
theme: Theme;
|
|
31
|
+
width: number;
|
|
32
|
+
withGap?: boolean;
|
|
33
|
+
}
|
package/src/ui/render.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AskConfig } from "../config/schema.ts";
|
|
2
|
+
import {
|
|
3
|
+
getCurrentQuestion,
|
|
4
|
+
getRenderableOptions,
|
|
5
|
+
isSubmitTab,
|
|
6
|
+
} from "../state/selectors.ts";
|
|
7
|
+
import type { AskState } from "../types.ts";
|
|
8
|
+
import { renderFrameFooter, renderFrameHeader } from "./render-frame.ts";
|
|
9
|
+
import { renderQuestionScreen } from "./render-question.ts";
|
|
10
|
+
import { renderSubmitScreen } from "./render-submit.ts";
|
|
11
|
+
import type { QuestionRenderContext, Theme } from "./render-types.ts";
|
|
12
|
+
|
|
13
|
+
export function renderAskScreen(args: {
|
|
14
|
+
config: AskConfig;
|
|
15
|
+
footerNotice?: string;
|
|
16
|
+
reviewShortcutHint?: string;
|
|
17
|
+
state: AskState;
|
|
18
|
+
theme: Theme;
|
|
19
|
+
width: number;
|
|
20
|
+
editor: QuestionRenderContext["editor"];
|
|
21
|
+
}): string[] {
|
|
22
|
+
const {
|
|
23
|
+
config,
|
|
24
|
+
footerNotice,
|
|
25
|
+
reviewShortcutHint,
|
|
26
|
+
state,
|
|
27
|
+
theme,
|
|
28
|
+
width,
|
|
29
|
+
editor,
|
|
30
|
+
} = args;
|
|
31
|
+
const lines: string[] = [];
|
|
32
|
+
const question = getCurrentQuestion(state);
|
|
33
|
+
const options = getRenderableOptions(question);
|
|
34
|
+
|
|
35
|
+
renderFrameHeader({ lines, state, theme, width });
|
|
36
|
+
|
|
37
|
+
if (isSubmitTab(state)) {
|
|
38
|
+
renderSubmitScreen(lines, state, theme, width, reviewShortcutHint);
|
|
39
|
+
} else if (question) {
|
|
40
|
+
renderQuestionScreen({
|
|
41
|
+
lines,
|
|
42
|
+
state,
|
|
43
|
+
question,
|
|
44
|
+
options,
|
|
45
|
+
theme,
|
|
46
|
+
width,
|
|
47
|
+
editor,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
renderFrameFooter({ config, footerNotice, lines, state, theme, width });
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { SUBMIT_CHOICES } from "../constants/text.ts";
|
|
2
|
+
|
|
3
|
+
const REVIEW_SHORTCUT_DIGIT_MIN = 1;
|
|
4
|
+
const REVIEW_SHORTCUT_DIGIT_MAX = 3;
|
|
5
|
+
|
|
6
|
+
export interface ReviewShortcutResolution {
|
|
7
|
+
actionIndex?: number;
|
|
8
|
+
confirmed: boolean;
|
|
9
|
+
pendingActionIndex?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveReviewShortcutDoublePress(
|
|
13
|
+
digit: number,
|
|
14
|
+
pendingActionIndex?: number
|
|
15
|
+
): ReviewShortcutResolution {
|
|
16
|
+
if (digit < REVIEW_SHORTCUT_DIGIT_MIN || digit > REVIEW_SHORTCUT_DIGIT_MAX) {
|
|
17
|
+
return { confirmed: false };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const actionIndex = digit - 1;
|
|
21
|
+
if (pendingActionIndex === actionIndex) {
|
|
22
|
+
return { actionIndex, confirmed: true };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
actionIndex,
|
|
27
|
+
confirmed: false,
|
|
28
|
+
pendingActionIndex: actionIndex,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getReviewShortcutHint(pendingActionIndex?: number): string {
|
|
33
|
+
if (pendingActionIndex === undefined) {
|
|
34
|
+
return "Press 1, 2, or 3 twice to confirm a review action.";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const actionLabel = SUBMIT_CHOICES[pendingActionIndex];
|
|
38
|
+
if (!actionLabel) {
|
|
39
|
+
return "Press 1, 2, or 3 twice to confirm a review action.";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return `Press ${pendingActionIndex + 1} again to ${actionLabel}.`;
|
|
43
|
+
}
|