@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/LICENSE +22 -0
  3. package/README.md +282 -0
  4. package/docs/README.md +33 -0
  5. package/docs/configuration.md +406 -0
  6. package/docs/contract.md +309 -0
  7. package/docs/remote-events.md +187 -0
  8. package/package.json +130 -0
  9. package/skills/ask-user/SKILL.md +110 -0
  10. package/src/answer-commands.ts +361 -0
  11. package/src/answer-extraction.ts +354 -0
  12. package/src/ask-payload-store.ts +86 -0
  13. package/src/ask-settings-command.ts +14 -0
  14. package/src/ask-tool-helpers.ts +172 -0
  15. package/src/ask-tool.ts +84 -0
  16. package/src/config/defaults.ts +216 -0
  17. package/src/config/migrate.ts +70 -0
  18. package/src/config/migrations/index.ts +139 -0
  19. package/src/config/migrations/types.ts +10 -0
  20. package/src/config/schema.ts +287 -0
  21. package/src/config/store.ts +227 -0
  22. package/src/constants/keymaps.ts +721 -0
  23. package/src/constants/text.ts +12 -0
  24. package/src/constants/ui.ts +22 -0
  25. package/src/index.ts +30 -0
  26. package/src/math.ts +3 -0
  27. package/src/notifications.ts +119 -0
  28. package/src/remote-ask.ts +563 -0
  29. package/src/result-format.ts +157 -0
  30. package/src/result.ts +23 -0
  31. package/src/schema.ts +74 -0
  32. package/src/state/answers.ts +251 -0
  33. package/src/state/create.ts +18 -0
  34. package/src/state/editor.ts +70 -0
  35. package/src/state/navigation.ts +86 -0
  36. package/src/state/normalize.ts +326 -0
  37. package/src/state/question-type.ts +128 -0
  38. package/src/state/result.ts +263 -0
  39. package/src/state/selectors.ts +135 -0
  40. package/src/state/transitions.ts +330 -0
  41. package/src/state/view.ts +28 -0
  42. package/src/text.ts +98 -0
  43. package/src/types.ts +169 -0
  44. package/src/ui/auto-submit.ts +36 -0
  45. package/src/ui/autocomplete.ts +52 -0
  46. package/src/ui/controller.ts +645 -0
  47. package/src/ui/dismiss-guard.ts +26 -0
  48. package/src/ui/input.ts +160 -0
  49. package/src/ui/render-frame.ts +235 -0
  50. package/src/ui/render-helpers.ts +385 -0
  51. package/src/ui/render-question.ts +288 -0
  52. package/src/ui/render-submit.ts +168 -0
  53. package/src/ui/render-types.ts +33 -0
  54. package/src/ui/render.ts +53 -0
  55. package/src/ui/review-shortcuts.ts +43 -0
  56. package/src/ui/settings-list.ts +461 -0
  57. package/src/ui/show-settings.ts +37 -0
  58. package/src/ui/view-models/question.ts +203 -0
  59. 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
+ }
@@ -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
+ }