@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
package/src/types.ts ADDED
@@ -0,0 +1,169 @@
1
+ export type AskQuestionType = "single" | "multi" | "preview";
2
+
3
+ export interface AskOption {
4
+ description?: string;
5
+ freeform?: boolean;
6
+ label: string;
7
+ preview?: string;
8
+ value: string;
9
+ }
10
+
11
+ export interface AskQuestionInput {
12
+ id: string;
13
+ label?: string;
14
+ options: AskOption[];
15
+ prompt: string;
16
+ required?: boolean;
17
+ type?: AskQuestionType;
18
+ }
19
+
20
+ export interface AskParams {
21
+ questions: AskQuestionInput[];
22
+ title?: string;
23
+ }
24
+
25
+ export interface AskValidationIssue {
26
+ message: string;
27
+ path: string;
28
+ }
29
+
30
+ export interface AskValidationError {
31
+ issues: AskValidationIssue[];
32
+ kind: "invalid_input";
33
+ }
34
+
35
+ export interface AskQuestion
36
+ extends Omit<AskQuestionInput, "type" | "required" | "label"> {
37
+ label: string;
38
+ presentedType?: AskQuestionType;
39
+ requestedType?: AskQuestionType;
40
+ required: boolean;
41
+ type: AskQuestionType;
42
+ }
43
+
44
+ export interface AskSelectedOption {
45
+ index: number;
46
+ label: string;
47
+ value: string;
48
+ }
49
+
50
+ export interface AskStateAnswer {
51
+ customSelected?: boolean;
52
+ customText?: string;
53
+ note?: string;
54
+ optionNotes?: Record<string, string>;
55
+ selected: AskSelectedOption[];
56
+ }
57
+
58
+ export interface AskResultAnswer {
59
+ customText?: string;
60
+ indices: number[];
61
+ labels: string[];
62
+ note?: string;
63
+ optionNotes?: Record<string, string>;
64
+ values: string[];
65
+ }
66
+
67
+ export interface AskQuestionSummary {
68
+ id: string;
69
+ label: string;
70
+ presentedType?: AskQuestionType;
71
+ prompt: string;
72
+ type: AskQuestionType;
73
+ }
74
+
75
+ export interface AskElaborationQuestionContext extends AskQuestionSummary {
76
+ options: AskOption[];
77
+ }
78
+
79
+ export interface AskElaborationQuestionItem {
80
+ answer?: AskResultAnswer;
81
+ answered: boolean;
82
+ note: string;
83
+ question: AskElaborationQuestionContext;
84
+ target: {
85
+ kind: "question";
86
+ };
87
+ }
88
+
89
+ export interface AskElaborationOptionItem {
90
+ answer?: AskResultAnswer;
91
+ answered: boolean;
92
+ note: string;
93
+ option: AskOption;
94
+ question: AskElaborationQuestionContext;
95
+ selected: boolean;
96
+ target: {
97
+ kind: "option";
98
+ optionValue: string;
99
+ };
100
+ }
101
+
102
+ export type AskElaborationItem =
103
+ | AskElaborationQuestionItem
104
+ | AskElaborationOptionItem;
105
+
106
+ export interface AskElaborationPayload {
107
+ instruction: string;
108
+ items: AskElaborationItem[];
109
+ nextAction: "clarify" | "clarify_then_reask";
110
+ }
111
+
112
+ export interface AskContinuationQuestionState {
113
+ status: "answered" | "needs_clarification" | "unanswered";
114
+ }
115
+
116
+ export interface AskContinuationPayload {
117
+ affectedQuestionIds: string[];
118
+ preservedAnswers: Record<string, AskResultAnswer>;
119
+ questionStates: Record<string, AskContinuationQuestionState>;
120
+ strategy: "refine_only" | "resume";
121
+ }
122
+
123
+ export interface AskResult {
124
+ answers: Record<string, AskResultAnswer>;
125
+ cancelled: boolean;
126
+ continuation?: AskContinuationPayload;
127
+ elaboration?: AskElaborationPayload;
128
+ error?: AskValidationError;
129
+ mode: "submit" | "elaborate";
130
+ questions: AskQuestionSummary[];
131
+ title?: string;
132
+ }
133
+
134
+ export type ViewState =
135
+ | { kind: "navigate" }
136
+ | { kind: "submit" }
137
+ | { kind: "input"; questionId: string }
138
+ | { kind: "note"; questionId: string; optionValue?: string };
139
+
140
+ export interface AskState {
141
+ activeOptionIndex: number;
142
+ activeSubmitActionIndex: number;
143
+ activeTabIndex: number;
144
+ answers: Record<string, AskStateAnswer>;
145
+ cancelled: boolean;
146
+ completed: boolean;
147
+ mode: "submit" | "elaborate";
148
+ questions: AskQuestion[];
149
+ title?: string;
150
+ view: ViewState;
151
+ }
152
+
153
+ export interface AskDisplayOption extends AskOption {
154
+ isCustomOption?: boolean;
155
+ isFreeformOnlyOption?: boolean;
156
+ }
157
+
158
+ export type AskAction =
159
+ | { type: "MOVE_TAB"; delta: 1 | -1 }
160
+ | { type: "MOVE_OPTION"; delta: 1 | -1 }
161
+ | { type: "OPEN_INPUT"; questionId: string }
162
+ | { type: "OPEN_QUESTION_NOTE"; questionId: string }
163
+ | { type: "OPEN_OPTION_NOTE"; questionId: string; optionValue: string }
164
+ | { type: "CONFIRM" }
165
+ | { type: "TOGGLE_MULTI" }
166
+ | { type: "NUMBER_SHORTCUT"; digit: number }
167
+ | { type: "SAVE_INPUT"; value: string; submit?: boolean }
168
+ | { type: "SAVE_NOTE"; value: string }
169
+ | { type: "CANCEL" };
@@ -0,0 +1,36 @@
1
+ import type { AskConfig } from "../config/schema.ts";
2
+ import { hasAnswerNotes } from "../state/answers.ts";
3
+ import { isQuestionAnswered, isSubmitTab } from "../state/selectors.ts";
4
+ import type { AskState } from "../types.ts";
5
+
6
+ export function maybeAutoSubmitState(
7
+ state: AskState,
8
+ config: AskConfig
9
+ ): AskState {
10
+ if (!shouldAutoSubmit(state, config)) {
11
+ return state;
12
+ }
13
+ return {
14
+ ...state,
15
+ completed: true,
16
+ mode: "submit",
17
+ };
18
+ }
19
+
20
+ export function shouldAutoSubmit(state: AskState, config: AskConfig): boolean {
21
+ if (!config.behaviour.autoSubmitWhenAnsweredWithoutNotes) {
22
+ return false;
23
+ }
24
+ if (state.completed || state.cancelled || !isSubmitTab(state)) {
25
+ return false;
26
+ }
27
+ for (const question of state.questions) {
28
+ if (!isQuestionAnswered(state, question.id)) {
29
+ return false;
30
+ }
31
+ if (hasAnswerNotes(state.answers[question.id])) {
32
+ return false;
33
+ }
34
+ }
35
+ return true;
36
+ }
@@ -0,0 +1,52 @@
1
+ import { accessSync, constants as fsConstants } from "node:fs";
2
+ import { delimiter, join } from "node:path";
3
+ import { CombinedAutocompleteProvider } from "@earendil-works/pi-tui";
4
+
5
+ const FD_BINARY_NAMES =
6
+ process.platform === "win32"
7
+ ? ["fd.exe", "fdfind.exe", "fd", "fdfind"]
8
+ : ["fd", "fdfind"];
9
+
10
+ /**
11
+ * pi resolves fd internally for its main editor, but that resolver is not part of
12
+ * the public extension API. Custom editors therefore need to supply the fd path
13
+ * themselves when reusing CombinedAutocompleteProvider for `@` file mentions.
14
+ */
15
+ export function createAskAutocompleteProvider(cwd: string) {
16
+ return Object.assign(
17
+ new CombinedAutocompleteProvider(
18
+ [],
19
+ cwd,
20
+ findAutocompleteBinary(FD_BINARY_NAMES)
21
+ ),
22
+ { triggerCharacters: ["@"] }
23
+ );
24
+ }
25
+
26
+ function findAutocompleteBinary(binaryNames: readonly string[]): string | null {
27
+ const pathValue = process.env.PATH;
28
+ if (!pathValue) {
29
+ return null;
30
+ }
31
+
32
+ const directories = pathValue.split(delimiter).filter(Boolean);
33
+ for (const binaryName of binaryNames) {
34
+ const executablePath = directories
35
+ .map((directory) => join(directory, binaryName))
36
+ .find(isExecutableFile);
37
+ if (executablePath) {
38
+ return executablePath;
39
+ }
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ function isExecutableFile(path: string): boolean {
46
+ try {
47
+ accessSync(path, fsConstants.X_OK);
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }