@howaboua/pi-ask 0.0.5 → 0.0.6

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/ask/tui.ts CHANGED
@@ -1,430 +1,28 @@
1
- import {
2
- type ExtensionContext,
3
- type KeybindingsManager,
4
- } from "@earendil-works/pi-coding-agent";
5
- import {
6
- Editor,
7
- truncateToWidth,
8
- wrapTextWithAnsi,
9
- } from "@earendil-works/pi-tui";
10
- import { OTHER_OPTION_LABEL } from "./constants.js";
11
- import type {
12
- AskPrompt,
13
- AskResponse,
14
- PromptChoice,
15
- PromptState,
16
- } from "./contracts.js";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Editor, truncateToWidth } from "@earendil-works/pi-tui";
3
+ import type { AskPrompt, AskResponse } from "./contracts.js";
17
4
  import {
18
5
  clearCustomSelection,
19
6
  createPromptState,
20
7
  pickChoiceSelection,
21
8
  promptStateResponded,
22
- promptStatesToResponses,
23
9
  saveComment,
24
10
  saveCustomSelection,
25
11
  } from "./state.js";
12
+ import {
13
+ type AskUiState,
14
+ type EditingKind,
15
+ handleAskEditingInput,
16
+ handleAskNavigationInput,
17
+ handleAskPromptInput,
18
+ } from "./tui-input.js";
19
+ import {
20
+ renderAskPrompt,
21
+ renderAskReview,
22
+ renderAskTabs,
23
+ } from "./tui-render.js";
26
24
 
27
- type AskTheme = ExtensionContext["ui"]["theme"];
28
- type AskKeybinding = Parameters<KeybindingsManager["getKeys"]>[0];
29
25
  type AddLine = (line?: string) => void;
30
- type EditingKind = "other" | "comment";
31
-
32
- interface AskUiState {
33
- tab: number;
34
- focus: number;
35
- editing: EditingKind | null;
36
- }
37
-
38
- interface RenderTabsOptions {
39
- handoff: boolean;
40
- prompts: AskPrompt[];
41
- responded: (index: number) => boolean;
42
- tab: number;
43
- theme: AskTheme;
44
- }
45
-
46
- interface RenderReviewOptions {
47
- add: AddLine;
48
- handoff: boolean;
49
- keybindings: KeybindingsManager;
50
- promptStates: PromptState[];
51
- prompts: AskPrompt[];
52
- theme: AskTheme;
53
- width: number;
54
- }
55
-
56
- interface RenderChoiceOptions {
57
- add: AddLine;
58
- checked: boolean | undefined;
59
- choice: PromptChoice;
60
- selected: boolean;
61
- theme: AskTheme;
62
- width: number;
63
- }
64
-
65
- interface RenderPromptOptions {
66
- add: AddLine;
67
- editor: Editor;
68
- handoff: boolean;
69
- isEditing: EditingKind | null;
70
- keybindings: KeybindingsManager;
71
- choices: PromptChoice[];
72
- prompt: AskPrompt;
73
- promptState: PromptState | undefined;
74
- state: AskUiState;
75
- theme: AskTheme;
76
- width: number;
77
- }
78
-
79
- function addWrapped(
80
- add: AddLine,
81
- text: string,
82
- width: number,
83
- indent = "",
84
- ): void {
85
- for (const line of wrapTextWithAnsi(
86
- text,
87
- Math.max(1, width - indent.length),
88
- )) {
89
- add(`${indent}${line}`);
90
- }
91
- }
92
-
93
- function renderKeyHint(
94
- theme: AskTheme,
95
- keybindings: KeybindingsManager,
96
- action: AskKeybinding,
97
- description: string,
98
- ): string {
99
- return (
100
- theme.fg("dim", keybindings.getKeys(action).join("/")) +
101
- theme.fg("muted", ` ${description}`)
102
- );
103
- }
104
-
105
- function renderAskTabs({
106
- handoff,
107
- prompts,
108
- responded,
109
- tab,
110
- theme,
111
- }: RenderTabsOptions): string {
112
- return ` ${Array.from({ length: prompts.length + 1 }, (_, index) => {
113
- const active = index === tab;
114
- const label =
115
- index === prompts.length
116
- ? handoff
117
- ? "Resume"
118
- : "Review"
119
- : `${responded(index) ? "■" : "□"} ${index + 1}`;
120
- return active
121
- ? theme.bg("selectedBg", theme.fg("text", ` ${label} `))
122
- : theme.fg("muted", ` ${label} `);
123
- }).join(" ")}`;
124
- }
125
-
126
- function renderAskReview({
127
- add,
128
- handoff,
129
- keybindings,
130
- promptStates,
131
- prompts,
132
- theme,
133
- width,
134
- }: RenderReviewOptions): void {
135
- add(theme.fg("accent", handoff ? " Resume agent" : " Review"));
136
- add();
137
- prompts.forEach((prompt, index) => {
138
- const promptState = promptStates[index];
139
- const selections = promptState?.selections ?? [];
140
- addWrapped(
141
- add,
142
- theme.fg("muted", `${index + 1}. ${prompt.title}`),
143
- width,
144
- " ",
145
- );
146
- addWrapped(
147
- add,
148
- theme.fg("text", selections.join(", ") || "No selection"),
149
- width,
150
- " ",
151
- );
152
- if (promptState?.comment.trim()) {
153
- addWrapped(
154
- add,
155
- theme.fg("muted", `Comment: ${promptState.comment.trim()}`),
156
- width,
157
- " ",
158
- );
159
- }
160
- });
161
- add();
162
- addWrapped(
163
- add,
164
- " " +
165
- renderKeyHint(
166
- theme,
167
- keybindings,
168
- "tui.select.confirm",
169
- handoff ? "return control" : "submit",
170
- ) +
171
- " • " +
172
- renderKeyHint(
173
- theme,
174
- keybindings,
175
- "tui.editor.cursorLeft",
176
- "previous prompt",
177
- ) +
178
- " • " +
179
- renderKeyHint(
180
- theme,
181
- keybindings,
182
- "tui.editor.cursorRight",
183
- "next prompt",
184
- ) +
185
- " • " +
186
- renderKeyHint(theme, keybindings, "tui.select.cancel", "dismiss"),
187
- width,
188
- );
189
- }
190
-
191
- function renderAskChoice({
192
- add,
193
- checked,
194
- choice,
195
- selected,
196
- theme,
197
- width,
198
- }: RenderChoiceOptions): void {
199
- add(
200
- `${selected ? theme.fg("accent", "> ") : " "}${checked ? theme.fg("success", "✓ ") : " "}${theme.fg(selected ? "accent" : "text", choice.label)}`,
201
- );
202
- if (choice.description) {
203
- addWrapped(add, theme.fg("muted", choice.description), width, " ");
204
- }
205
- }
206
-
207
- function renderAskPrompt({
208
- add,
209
- editor,
210
- handoff,
211
- isEditing,
212
- keybindings,
213
- choices,
214
- prompt,
215
- promptState,
216
- state,
217
- theme,
218
- width,
219
- }: RenderPromptOptions): void {
220
- if (handoff) {
221
- add(theme.fg("accent", " Human action needed"));
222
- add();
223
- }
224
- addWrapped(add, theme.fg("text", prompt.title), width, " ");
225
- add(
226
- theme.fg(
227
- "muted",
228
- ` ${prompt.multiple ? "Choose any that apply" : "Choose one"}`,
229
- ),
230
- );
231
- if (prompt.body) {
232
- add();
233
- addWrapped(add, theme.fg("muted", prompt.body), width, " ");
234
- }
235
- add();
236
- choices.forEach((choice, index) => {
237
- renderAskChoice({
238
- add,
239
- checked: promptState?.selections.includes(choice.label),
240
- choice,
241
- selected: state.focus === index,
242
- theme,
243
- width,
244
- });
245
- });
246
- const selectedOther = state.focus === choices.length;
247
- add(
248
- `${selectedOther ? theme.fg("accent", "> ") : " "}${promptState?.customEnabled ? theme.fg("success", "✓ ") : " "}${theme.fg(selectedOther ? "accent" : "muted", OTHER_OPTION_LABEL)}${promptState?.customText.trim() ? theme.fg("text", `: ${promptState.customText.trim()}`) : ""}`,
249
- );
250
- if (isEditing === "other") {
251
- add();
252
- for (const line of editor.render(width - 2)) add(` ${line}`);
253
- }
254
- const selectedComment = state.focus === choices.length + 1;
255
- add();
256
- add(
257
- `${selectedComment ? theme.fg("accent", "> ") : " "}${theme.fg(selectedComment ? "accent" : "text", "Comment (optional)")}`,
258
- );
259
- if (promptState?.comment.trim()) {
260
- addWrapped(
261
- add,
262
- theme.fg("muted", promptState.comment.trim()),
263
- width,
264
- " ",
265
- );
266
- }
267
- if (isEditing === "comment") {
268
- add();
269
- for (const line of editor.render(width - 2)) add(` ${line}`);
270
- }
271
- add();
272
- if (isEditing) {
273
- addWrapped(
274
- add,
275
- " " +
276
- renderKeyHint(theme, keybindings, "tui.input.submit", "save") +
277
- " • " +
278
- renderKeyHint(theme, keybindings, "tui.input.tab", "next/default") +
279
- " • " +
280
- renderKeyHint(theme, keybindings, "tui.select.cancel", "cancel edit"),
281
- width,
282
- );
283
- return;
284
- }
285
- addWrapped(
286
- add,
287
- " " +
288
- renderKeyHint(theme, keybindings, "tui.select.up", "up") +
289
- " • " +
290
- renderKeyHint(theme, keybindings, "tui.select.down", "down") +
291
- " • " +
292
- renderKeyHint(theme, keybindings, "tui.select.confirm", "choose/type") +
293
- " • " +
294
- renderKeyHint(theme, keybindings, "tui.input.tab", "next/default") +
295
- " • blank Other/rephrase = follow-up • " +
296
- renderKeyHint(theme, keybindings, "tui.select.cancel", "close"),
297
- width,
298
- );
299
- }
300
-
301
- interface EditingInputOptions {
302
- advance: () => void;
303
- editor: Editor;
304
- keybindings: KeybindingsManager;
305
- refresh: () => void;
306
- saveEditing: (submittedText?: string) => void;
307
- state: AskUiState;
308
- }
309
-
310
- function handleAskEditingInput(
311
- data: string,
312
- {
313
- advance,
314
- editor,
315
- keybindings,
316
- refresh,
317
- saveEditing,
318
- state,
319
- }: EditingInputOptions,
320
- ): void {
321
- if (keybindings.matches(data, "tui.input.tab")) {
322
- saveEditing(editor.getText());
323
- state.editing = null;
324
- advance();
325
- return;
326
- }
327
- if (keybindings.matches(data, "tui.select.cancel")) {
328
- state.editing = null;
329
- refresh();
330
- return;
331
- }
332
- editor.handleInput(data);
333
- refresh();
334
- }
335
-
336
- interface PromptInputOptions {
337
- advanceWithDefault: () => void;
338
- choices: () => PromptChoice[];
339
- count: () => number;
340
- keybindings: KeybindingsManager;
341
- pick: (index: number) => void;
342
- refresh: () => void;
343
- selectOther: () => void;
344
- startEditingComment: () => void;
345
- state: AskUiState;
346
- }
347
-
348
- function handleAskPromptInput(
349
- data: string,
350
- {
351
- advanceWithDefault,
352
- choices,
353
- count,
354
- keybindings,
355
- pick,
356
- refresh,
357
- selectOther,
358
- startEditingComment,
359
- state,
360
- }: PromptInputOptions,
361
- ): void {
362
- if (keybindings.matches(data, "tui.select.up")) {
363
- state.focus = Math.max(0, state.focus - 1);
364
- refresh();
365
- return;
366
- }
367
- if (keybindings.matches(data, "tui.select.down")) {
368
- state.focus = Math.min(count() - 1, state.focus + 1);
369
- refresh();
370
- return;
371
- }
372
- if (keybindings.matches(data, "tui.input.tab")) {
373
- advanceWithDefault();
374
- return;
375
- }
376
- if (!keybindings.matches(data, "tui.select.confirm")) return;
377
- if (state.focus === choices().length) {
378
- selectOther();
379
- return;
380
- }
381
- if (state.focus === choices().length + 1) {
382
- startEditingComment();
383
- return;
384
- }
385
- pick(state.focus);
386
- }
387
-
388
- interface NavigationInputOptions {
389
- done: (responses: AskResponse[] | null) => void;
390
- isReview: () => boolean;
391
- keybindings: KeybindingsManager;
392
- promptStates: PromptState[];
393
- prompts: AskPrompt[];
394
- setTab: (next: number) => void;
395
- state: AskUiState;
396
- }
397
-
398
- function handleAskNavigationInput(
399
- data: string,
400
- {
401
- done,
402
- isReview,
403
- keybindings,
404
- promptStates,
405
- prompts,
406
- setTab,
407
- state,
408
- }: NavigationInputOptions,
409
- ): boolean {
410
- if (keybindings.matches(data, "tui.select.cancel")) {
411
- done(null);
412
- return true;
413
- }
414
- if (keybindings.matches(data, "tui.editor.cursorLeft")) {
415
- setTab(state.tab - 1);
416
- return true;
417
- }
418
- if (keybindings.matches(data, "tui.editor.cursorRight")) {
419
- setTab(state.tab + 1);
420
- return true;
421
- }
422
- if (!isReview()) return false;
423
- if (keybindings.matches(data, "tui.select.confirm")) {
424
- done(promptStatesToResponses(prompts, promptStates));
425
- }
426
- return true;
427
- }
428
26
 
429
27
  export async function askInTui(
430
28
  ctx: ExtensionContext,
package/biome.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
2
+ "$schema": "https://biomejs.dev/schemas/2.5.10/schema.json",
3
3
  "vcs": {
4
4
  "enabled": true,
5
5
  "clientKind": "git",