@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,12 @@
1
+ export const OTHER_OPTION_VALUE = "__other__";
2
+ export const OTHER_OPTION_LABEL = "Type your own";
3
+ export const SUBMIT_CHOICES = ["Submit", "Elaborate", "Cancel"] as const;
4
+ export const NO_PREVIEW_TEXT = "No preview available";
5
+ export const ELABORATION_INSTRUCTION =
6
+ "First answer the user's noted clarification directly and concisely using the provided question and option context. Do not treat these notes as final answers. Then re-ask only the affected questions if a choice is still needed afterward. Do not jump straight to a follow-up question unless the note is already resolved.";
7
+ export const CANCELLED_SUMMARY = "User cancelled the ask flow";
8
+ export const SUBMITTED_SUMMARY = "User submitted the ask flow";
9
+ export const ELABORATED_SUMMARY = "User asked for elaboration based on notes";
10
+ export const CANCELLED_RESULT_TEXT = "Cancelled";
11
+ export const SUBMITTED_RESULT_TEXT = "Submitted";
12
+ export const ELABORATED_RESULT_TEXT = "Elaboration requested";
@@ -0,0 +1,22 @@
1
+ export const UI_DIMENSIONS = {
2
+ boxMinWidth: 10,
3
+ callLabelTruncateWidth: 50,
4
+ editorContentPadding: 5,
5
+ editorIndentedPadding: 7,
6
+ editorMinWidth: 8,
7
+ previewWideMinWidth: 90,
8
+ previewMinRightWidth: 24,
9
+ previewLeftMinWidth: 22,
10
+ previewLeftMaxWidth: 34,
11
+ previewLeftRatio: 0.34,
12
+ submitWideMinWidth: 64,
13
+ submitMinReviewWidth: 24,
14
+ } as const;
15
+
16
+ export const UI_TEXT = {
17
+ questionNoteTitle: "Note:",
18
+ reviewTitle: "Review answers",
19
+ unanswered: "→ unanswered",
20
+ editorPlaceholderInput: "Type answer...",
21
+ editorPlaceholderNote: "Add a note...",
22
+ } as const;
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import { registerAnswerCommands } from "./answer-commands.ts";
5
+ import { registerAskSettingsCommand } from "./ask-settings-command.ts";
6
+ import { registerAskTool } from "./ask-tool.ts";
7
+ import { resetAskConfigStore } from "./config/store.ts";
8
+ import { createRemoteAskRuntime } from "./remote-ask.ts";
9
+
10
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
11
+ const CONFIGURATION_DOC_PATH = resolve(
12
+ PACKAGE_ROOT,
13
+ "docs",
14
+ "configuration.md"
15
+ );
16
+ const PI_ASK_CONFIG_PROMPT = `When the user asks to configure, customize, debug, or explain @eko24ive/pi-ask settings or keymaps, first read ${CONFIGURATION_DOC_PATH} and follow it as the source of truth before editing config files.`;
17
+
18
+ export default function askExtension(pi: ExtensionAPI) {
19
+ resetAskConfigStore();
20
+ pi.on("before_agent_start", async (event) => ({
21
+ systemPrompt: `${event.systemPrompt}\n\n${PI_ASK_CONFIG_PROMPT}`,
22
+ }));
23
+ const remoteAsk = createRemoteAskRuntime(pi.events);
24
+ pi.on("session_shutdown", () => {
25
+ remoteAsk.disposeAll();
26
+ });
27
+ registerAskTool(pi, remoteAsk);
28
+ registerAskSettingsCommand(pi);
29
+ registerAnswerCommands(pi, remoteAsk);
30
+ }
package/src/math.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function clamp(value: number, min: number, max: number): number {
2
+ return Math.min(max, Math.max(min, value));
3
+ }
@@ -0,0 +1,119 @@
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import type { AskConfig } from "./config/schema.ts";
4
+ import type { AskQuestion } from "./types.ts";
5
+
6
+ const execAsync = promisify(exec);
7
+ const DEFAULT_TITLE = "pi ask";
8
+ const EVENT = "question.waiting";
9
+ const MAX_MESSAGE_LENGTH = 120;
10
+ const COMMAND_TIMEOUT_MS = 5000;
11
+
12
+ export interface AskNotificationPayload {
13
+ event: typeof EVENT;
14
+ message: string;
15
+ title: string;
16
+ }
17
+
18
+ export type NotificationAttemptStatus = "attempted" | "failed" | "skipped";
19
+
20
+ export interface NotificationAttempt {
21
+ channel: string;
22
+ error?: string;
23
+ status: NotificationAttemptStatus;
24
+ }
25
+
26
+ export function createQuestionWaitingNotification(
27
+ question: Pick<AskQuestion, "label" | "prompt">
28
+ ): AskNotificationPayload {
29
+ const subject = question.label || question.prompt;
30
+ return {
31
+ event: EVENT,
32
+ title: DEFAULT_TITLE,
33
+ message: truncate(
34
+ `Question waiting: ${singleLine(subject)}`,
35
+ MAX_MESSAGE_LENGTH
36
+ ),
37
+ };
38
+ }
39
+
40
+ export async function notifyQuestionWaiting(
41
+ config: AskConfig,
42
+ payload: AskNotificationPayload
43
+ ): Promise<NotificationAttempt[]> {
44
+ if (!config.notifications.enabled) {
45
+ return [{ channel: "notifications", status: "skipped" }];
46
+ }
47
+ const attempts: NotificationAttempt[] = [];
48
+ for (const channel of config.notifications.channels) {
49
+ attempts.push(await notifyChannel(channel, payload));
50
+ }
51
+ return attempts;
52
+ }
53
+
54
+ async function notifyChannel(
55
+ channel: AskConfig["notifications"]["channels"][number],
56
+ payload: AskNotificationPayload
57
+ ): Promise<NotificationAttempt> {
58
+ const type = typeof channel === "string" ? channel : channel.type;
59
+ try {
60
+ switch (type) {
61
+ case "bell":
62
+ process.stdout.write("\x07");
63
+ return { channel: type, status: "attempted" };
64
+ case "osc9":
65
+ process.stdout.write(`\x1b]9;${sanitizeOsc(payload.message)}\x07`);
66
+ return { channel: type, status: "attempted" };
67
+ case "osc777":
68
+ process.stdout.write(
69
+ `\x1b]777;notify;${sanitizeOscField(payload.title)};${sanitizeOscField(payload.message)}\x07`
70
+ );
71
+ return { channel: type, status: "attempted" };
72
+ case "command": {
73
+ if (typeof channel === "string") {
74
+ return { channel: type, status: "failed", error: "Missing command" };
75
+ }
76
+ const env = { ...process.env };
77
+ env.ASK_NOTIFY_EVENT = payload.event;
78
+ env.ASK_NOTIFY_MESSAGE = payload.message;
79
+ env.ASK_NOTIFY_TITLE = payload.title;
80
+ await execAsync(channel.command, {
81
+ env,
82
+ timeout: COMMAND_TIMEOUT_MS,
83
+ });
84
+ return { channel: type, status: "attempted" };
85
+ }
86
+ default:
87
+ return { channel: String(type), status: "skipped" };
88
+ }
89
+ } catch (error) {
90
+ return {
91
+ channel: type,
92
+ error: error instanceof Error ? error.message : String(error),
93
+ status: "failed",
94
+ };
95
+ }
96
+ }
97
+
98
+ function singleLine(value: string): string {
99
+ return value.replace(/\s+/gu, " ").trim();
100
+ }
101
+
102
+ function truncate(value: string, maxLength: number): string {
103
+ return value.length <= maxLength
104
+ ? value
105
+ : `${value.slice(0, maxLength - 1)}…`;
106
+ }
107
+
108
+ function sanitizeOsc(value: string): string {
109
+ return [...singleLine(value)]
110
+ .filter((character) => {
111
+ const code = character.codePointAt(0) ?? 0;
112
+ return code > 31 && code !== 127;
113
+ })
114
+ .join("");
115
+ }
116
+
117
+ function sanitizeOscField(value: string): string {
118
+ return sanitizeOsc(value).replaceAll(";", ":");
119
+ }