pi-soly 0.3.0 → 0.5.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/ask/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # pi-ask — Claude Code-style multi-question picker for pi
2
+
3
+ A small pi-coding-agent extension that registers one tool (`ask_pro`) for
4
+ showing a **tabbed, multi-question picker** in pi's TUI. Inspired by Claude
5
+ Code's `AskUserQuestion`.
6
+
7
+ ## Features
8
+
9
+ - **Multi-question** — pass a list of questions; the user navigates between
10
+ them with `Tab` / `Shift+Tab` or arrow keys
11
+ - **Numbered options** — `1`–`4` instant-pick
12
+ - **Recommended answer** — first option (or the one with `recommended: true`)
13
+ is marked ⭐
14
+ - **Single-select** (default) — Enter on an option auto-advances to the next
15
+ question; on the last question, Enter submits
16
+ - **Multi-select** — Enter toggles checkboxes; the last question shows a
17
+ visible "Submit" row
18
+ - **Cancelled detection** — `Esc` resolves `{cancelled: true}`
19
+
20
+ ## Usage from an LLM
21
+
22
+ ```ts
23
+ ask_pro({
24
+ questions: [
25
+ {
26
+ header: "Auth", // 1-2 word tab label
27
+ question: "Which auth approach?",
28
+ options: [
29
+ { label: "JWT in httpOnly cookie", description: "Stateless, scales horizontally", recommended: true },
30
+ { label: "JWT in localStorage", description: "Simpler client, XSS risk" },
31
+ { label: "Server sessions + Redis", description: "Revocable, but extra dep" },
32
+ ],
33
+ multiSelect: false,
34
+ },
35
+ {
36
+ header: "Tokens",
37
+ question: "Token storage?",
38
+ options: [
39
+ { label: "httpOnly cookie" },
40
+ { label: "Bearer in Authorization" },
41
+ ],
42
+ },
43
+ ],
44
+ })
45
+ ```
46
+
47
+ Result:
48
+
49
+ ```ts
50
+ // user picked "JWT in httpOnly cookie" + "Bearer in Authorization":
51
+ { answers: { 0: 0, 1: 1 } }
52
+
53
+ // user pressed Esc:
54
+ { cancelled: true }
55
+ ```
56
+
57
+ ## UX
58
+
59
+ ```
60
+ ┌─ pi-ask — 2 questions ────────────────────────────────────────┐
61
+ │ ◉ Auth ○ Tokens │
62
+ │ │
63
+ │ Q1 of 2: Which auth approach? │
64
+ │ │
65
+ │ ❯ ⭐ JWT in httpOnly cookie │
66
+ │ Stateless, scales horizontally │
67
+ │ │
68
+ │ JWT in localStorage │
69
+ │ Simpler client, XSS risk │
70
+ │ │
71
+ │ Server sessions + Redis │
72
+ │ Revocable, but extra dependency │
73
+ │ │
74
+ │ ↑↓ navigate · 1-3 pick · tab/→ next · ⏎ next · esc cancel │
75
+ └─────────────────────────────────────────────────────────────────┘
76
+ ```
77
+
78
+ ## Keys
79
+
80
+ | Key | Action |
81
+ |---|---|
82
+ | `↑` / `k` | Move option up |
83
+ | `↓` / `j` | Move option down |
84
+ | `1` – `4` | Instant-pick that option |
85
+ | `Tab` / `→` | Next question |
86
+ | `Shift+Tab` / `←` / `Backspace` | Previous question |
87
+ | `Space` | Multi-select only: toggle the current option. On "Other…", opens the input dialog. |
88
+ | `Enter` | **Single-select:** confirm + advance (or submit on last). **Multi-select:** advance to next question (or submit on last + all answered). Does NOT toggle. |
89
+ | `Esc` | Cancel (returns `{cancelled: true}`) |
90
+
91
+ Multi-select follows the Claude Code convention: **Space toggles, Enter
92
+ advances/submits**. Single-select uses Enter as the universal action key
93
+ (toggle/pick + advance). When you're on the last question and all
94
+ questions are answered, the footer shows `⏎ submit` in accent color.
95
+
96
+ ## Limits
97
+
98
+ - 2–4 options per question (more is bad UX; the picker is meant for focused
99
+ choices, not long lists)
100
+ - 1–6 questions per call (more = tab-switching fatigue)
101
+ - At most 1 `recommended: true` per question
102
+ - TUI and RPC modes only (`hasUI: true`); print mode returns an error
103
+
104
+ ## Setup
105
+
106
+ Drop the directory in `~/.pi/agent/extensions/`:
107
+
108
+ ```bash
109
+ ls ~/.pi/agent/extensions/pi-ask/
110
+ # index.ts picker.ts tests/ package.json tsconfig.json README.md
111
+ ```
112
+
113
+ pi auto-discovers and loads it on next start. The `ask_pro` tool is then
114
+ available to the LLM. No config required.
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ cd ~/.pi/agent/extensions/pi-ask
120
+ bun test # runs tests/picker.test.ts
121
+ bun run typecheck # tsc --noEmit
122
+ ```
123
+
124
+ CI: not configured (this is a single-file TUI component, low risk).
125
+ Add `.github/workflows/ci.yml` if you want green-tick PRs.
126
+
127
+ ## Why a separate extension?
128
+
129
+ The picker is **generic** — any pi extension (soly, your own tool, etc.) can
130
+ use `ask_pro` to drive multi-question Q&A without re-implementing the TUI.
131
+ Keeping it separate from soly means:
132
+
133
+ - soly stays focused on the plan/execute/discuss workflow
134
+ - other extensions can adopt the same UX pattern
135
+ - the picker can evolve independently (new key bindings, themes, layouts)
package/ask/index.ts ADDED
@@ -0,0 +1,218 @@
1
+ // =============================================================================
2
+ // index.ts — pi-ask extension entry point
3
+ // =============================================================================
4
+ //
5
+ // Registers one LLM tool: `ask_pro`. Lets the LLM ask the user a list of
6
+ // questions at once via a Claude Code-style tabbed picker (tabs, numbered
7
+ // options, ⭐ recommended answer, optional multi-select). All answers
8
+ // returned in a single call.
9
+ //
10
+ // Usage from another extension / LLM:
11
+ // ask_pro({
12
+ // questions: [
13
+ // { header: "Auth", question: "Which auth?", options: [...], multiSelect: false },
14
+ // { header: "Tokens", question: "Token storage?", options: [...] },
15
+ // ]
16
+ // })
17
+ // → { answers: { 0: 1, 1: 0 } } or { cancelled: true }
18
+ //
19
+ // Generic — not soly-specific. Any pi extension that needs multi-question
20
+ // Q&A can use this.
21
+ // =============================================================================
22
+
23
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
24
+ import { Type } from "typebox";
25
+ import { AskProComponent, type AskProResult } from "./picker.ts";
26
+ import { buildAskProSection } from "./prompt.ts";
27
+
28
+ export default function piAskExtension(pi: ExtensionAPI) {
29
+ // Inject a "when to use ask_pro" section into the system prompt so the
30
+ // LLM reaches for the picker at the right times (and avoids overusing
31
+ // it for trivial yes/no or open-ended questions).
32
+ pi.on("before_agent_start", async (event) => {
33
+ return {
34
+ systemPrompt: event.systemPrompt + buildAskProSection(),
35
+ };
36
+ });
37
+
38
+ pi.registerTool({
39
+ name: "ask_pro",
40
+ label: "pi-ask ask_pro",
41
+ description:
42
+ "Ask the user multiple questions at once via a Claude Code-style tabbed picker. Each question is a tab at the top. Options are numbered (1-N instant-pick), the recommended answer is marked ⭐. Supports single-select (default, auto-advance on pick) and multi-select (Enter toggles, last question shows Submit). All answers returned in one call. Use for progressive Q&A flows like `soly discuss`.",
43
+ parameters: Type.Object({
44
+ questions: Type.Array(
45
+ Type.Object({
46
+ header: Type.String({
47
+ description: "Short label for the tab (1-2 words, max 12 chars).",
48
+ }),
49
+ question: Type.String({
50
+ description: "The full question to ask.",
51
+ }),
52
+ options: Type.Array(
53
+ Type.Object({
54
+ label: Type.String({
55
+ description: "Short label (1-5 words).",
56
+ }),
57
+ description: Type.Optional(
58
+ Type.String({
59
+ description: "1-2 sentence explanation. Shown below the label.",
60
+ }),
61
+ ),
62
+ recommended: Type.Optional(
63
+ Type.Boolean({
64
+ description: "Mark as ⭐ recommended answer.",
65
+ }),
66
+ ),
67
+ }),
68
+ { description: "2-4 concrete options." },
69
+ ),
70
+ multiSelect: Type.Optional(
71
+ Type.Boolean({
72
+ description:
73
+ "If true, user can pick multiple (checkboxes, Enter toggles). If false (default), single-select with auto-advance.",
74
+ }),
75
+ ),
76
+ }),
77
+ {
78
+ description:
79
+ "Questions to ask, in tab order. Max ~5 recommended (more hurts UX).",
80
+ },
81
+ ),
82
+ }),
83
+ async execute(_id, params, _signal, _onUpdate, ctx) {
84
+ // --- safety: UI required ---
85
+ if (!ctx.hasUI) {
86
+ return {
87
+ content: [
88
+ {
89
+ type: "text",
90
+ text: "ask_pro requires a UI-capable session (TUI or RPC mode). Run from the interactive pi TUI.",
91
+ },
92
+ ],
93
+ details: { error: "no_ui", mode: ctx.mode },
94
+ };
95
+ }
96
+
97
+ // --- validation ---
98
+ if (params.questions.length === 0) {
99
+ return {
100
+ content: [
101
+ { type: "text", text: "ask_pro: at least one question is required" },
102
+ ],
103
+ details: { error: "no_questions" },
104
+ };
105
+ }
106
+ if (params.questions.length > 6) {
107
+ return {
108
+ content: [
109
+ {
110
+ type: "text",
111
+ text: `ask_pro: ${params.questions.length} questions is a lot; max 6 recommended for UX (more = more tab-switching fatigue).`,
112
+ },
113
+ ],
114
+ details: { error: "too_many_questions", count: params.questions.length },
115
+ };
116
+ }
117
+ for (let i = 0; i < params.questions.length; i++) {
118
+ const q = params.questions[i];
119
+ if (!q) continue;
120
+ if (q.options.length < 2 || q.options.length > 4) {
121
+ return {
122
+ content: [
123
+ {
124
+ type: "text",
125
+ text: `ask_pro: Q${i + 1} ("${q.header}") has ${q.options.length} options, need 2-4.`,
126
+ },
127
+ ],
128
+ details: {
129
+ error: "bad_option_count",
130
+ questionIdx: i,
131
+ count: q.options.length,
132
+ },
133
+ };
134
+ }
135
+ // Recommend at most one ⭐ per question
136
+ const recommendedCount = q.options.filter((o) => o.recommended).length;
137
+ if (recommendedCount > 1) {
138
+ return {
139
+ content: [
140
+ {
141
+ type: "text",
142
+ text: `ask_pro: Q${i + 1} ("${q.header}") has ${recommendedCount} recommended options, at most 1 allowed.`,
143
+ },
144
+ ],
145
+ details: { error: "multiple_recommended", questionIdx: i },
146
+ };
147
+ }
148
+ }
149
+
150
+ // --- show the picker ---
151
+ const result = await ctx.ui.custom<AskProResult>(
152
+ (tui, theme, keybindings, done) => {
153
+ // pi-coding-agent's Theme is structurally compatible with our
154
+ // AskProTheme (same fg/bold signatures); the color type is
155
+ // just stricter on the agent's side. Cast to satisfy TS.
156
+ const askTheme = theme as unknown as ConstructorParameters<typeof AskProComponent>[0]["theme"];
157
+ return new AskProComponent({
158
+ questions: params.questions,
159
+ theme: askTheme,
160
+ keybindings,
161
+ done,
162
+ // Bridge to the parent's UI for the "Other…" text input.
163
+ // The picker stays decoupled from ExtensionContext.
164
+ onRequestInput: async (req) => {
165
+ if (!ctx.hasUI) return undefined;
166
+ return (await ctx.ui.input(req.title, req.placeholder)) ?? undefined;
167
+ },
168
+ title: `pi-ask — ${params.questions.length} question${params.questions.length > 1 ? "s" : ""}`,
169
+ });
170
+ },
171
+ );
172
+
173
+ // --- handle the result ---
174
+ if (result.cancelled) {
175
+ return {
176
+ content: [
177
+ {
178
+ type: "text",
179
+ text: "(user cancelled the picker — no answers captured)",
180
+ },
181
+ ],
182
+ details: { cancelled: true },
183
+ };
184
+ }
185
+
186
+ const answers = result.answers ?? {};
187
+ // Pretty-print for the LLM
188
+ const out: string[] = ["User answers:"];
189
+ for (let i = 0; i < params.questions.length; i++) {
190
+ const q = params.questions[i];
191
+ if (!q) continue;
192
+ const a = answers[i];
193
+ if (a === undefined) {
194
+ out.push(` Q${i + 1} (${q.header}): (no answer)`);
195
+ } else if (Array.isArray(a)) {
196
+ const parts: string[] = [];
197
+ for (const item of a) {
198
+ if (typeof item === "number") {
199
+ parts.push(q.options[item]?.label ?? `?${item}`);
200
+ } else {
201
+ parts.push(`"${item}"`);
202
+ }
203
+ }
204
+ out.push(` Q${i + 1} (${q.header}) [multi]: ${parts.join(", ")}`);
205
+ } else if (typeof a === "number") {
206
+ out.push(` Q${i + 1} (${q.header}): ${q.options[a]?.label ?? `?${a}`}`);
207
+ } else {
208
+ out.push(` Q${i + 1} (${q.header}) [Other]: "${a}"`);
209
+ }
210
+ }
211
+
212
+ return {
213
+ content: [{ type: "text", text: out.join("\n") }],
214
+ details: { answers },
215
+ };
216
+ },
217
+ });
218
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "pi-asked",
3
+ "version": "0.2.0",
4
+ "description": "Claude Code-style multi-question picker for pi. Tabbed picker with recommended answers, multi-select, and Other… text input.",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "scripts": {
8
+ "test": "bun test",
9
+ "typecheck": "bun x tsc --noEmit"
10
+ },
11
+ "dependencies": {},
12
+ "peerDependencies": {
13
+ "@earendil-works/pi-coding-agent": "*",
14
+ "@earendil-works/pi-tui": "*"
15
+ },
16
+ "devDependencies": {
17
+ "@earendil-works/pi-coding-agent": "0.78.1",
18
+ "@earendil-works/pi-tui": "0.78.1",
19
+ "@types/node": "^25.9.1",
20
+ "bun-types": "^1.3.14",
21
+ "typebox": "1.1.38",
22
+ "typescript": "^6.0.3"
23
+ },
24
+ "files": [
25
+ "index.ts",
26
+ "picker.ts",
27
+ "prompt.ts"
28
+ ],
29
+ "keywords": [
30
+ "pi",
31
+ "pi-extension",
32
+ "pi-package",
33
+ "ask-user",
34
+ "multi-question",
35
+ "picker"
36
+ ],
37
+ "license": "MIT",
38
+ "pi": {
39
+ "extensions": [
40
+ "./index.ts"
41
+ ]
42
+ },
43
+ "publishConfig": {
44
+ "registry": "https://registry.npmjs.org/"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "http://git.local.stbl/lowern1ght/pi-soly.framework.git",
49
+ "directory": "packages/pi-ask"
50
+ }
51
+ }