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 +135 -0
- package/ask/index.ts +218 -0
- package/ask/package.json +51 -0
- package/ask/picker.ts +686 -0
- package/ask/prompt.ts +37 -0
- package/ask/tests/picker.test.ts +588 -0
- package/ask/tests/prompt.test.ts +54 -0
- package/ask/tsconfig.json +28 -0
- package/index.ts +8 -0
- package/package.json +6 -2
- package/switch/README.md +107 -0
- package/switch/core.ts +202 -0
- package/switch/index.ts +300 -0
- package/switch/package.json +52 -0
- package/switch/prompt.ts +134 -0
- package/switch/tests/core.test.ts +188 -0
- package/switch/tests/index.test.ts +47 -0
- package/switch/tests/prompt.test.ts +106 -0
- package/switch/tsconfig.json +28 -0
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
|
+
}
|
package/ask/package.json
ADDED
|
@@ -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
|
+
}
|