@xynogen/pix-core 0.1.1 → 0.1.2
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/package.json +2 -1
- package/src/commands/agent-sop/agent-sop.ts +58 -0
- package/src/index.ts +3 -1
- package/src/nudge/capability.test.ts +14 -5
- package/src/nudge/capability.ts +14 -8
- package/src/tool/ask/ask.test.ts +2 -2
- package/src/tool/ask/components.ts +55 -0
- package/src/tool/ask/helpers.ts +77 -0
- package/src/tool/ask/index.ts +130 -0
- package/src/tool/ask/{ask.ts → questionnaire.ts} +60 -473
- package/src/tool/ask/rpc.ts +84 -0
- package/src/tool/ask/schema.ts +69 -0
- package/src/tool/ask/types.ts +17 -0
- package/src/tool/toolbox/toolbox.ts +9 -1
- package/src/ui/diagnostics.ts +3 -6
- package/src/ui/welcome.ts +5 -2
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Params } from "./schema.js";
|
|
2
|
+
import { SENTINEL_FREEFORM } from "./schema.js";
|
|
3
|
+
import type { QuestionAnswer, QuestionnaireResult } from "./types.js";
|
|
4
|
+
|
|
5
|
+
// ── RPC / non-TUI fallback ─────────────────────────────────────────────
|
|
6
|
+
// Used when ctx.hasUI is false (headless / JSON / print mode).
|
|
7
|
+
|
|
8
|
+
export async function rpcFallback(
|
|
9
|
+
ui: { select: Function; input: Function },
|
|
10
|
+
params: Params,
|
|
11
|
+
): Promise<QuestionnaireResult> {
|
|
12
|
+
const answers: QuestionAnswer[] = [];
|
|
13
|
+
let cancelled = false;
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < params.questions.length; i++) {
|
|
16
|
+
const q = params.questions[i]!;
|
|
17
|
+
const header = q.header;
|
|
18
|
+
|
|
19
|
+
if (q.multiSelect) {
|
|
20
|
+
const lines = q.options.map(
|
|
21
|
+
(o, idx) => `${idx + 1}. ${o.label} — ${o.description}`,
|
|
22
|
+
);
|
|
23
|
+
const raw = await ui.input(
|
|
24
|
+
`${header}: ${q.question}\n\n${lines.join("\n")}\n\nEnter numbers separated by commas:`,
|
|
25
|
+
"e.g. 1,3",
|
|
26
|
+
);
|
|
27
|
+
if (raw == null) {
|
|
28
|
+
cancelled = true;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
const indices = String(raw)
|
|
32
|
+
.split(",")
|
|
33
|
+
.map((s) => Number(s.trim()))
|
|
34
|
+
.filter((n) => n >= 1 && n <= q.options.length);
|
|
35
|
+
const selected = indices.map((n) => q.options[n - 1]?.label);
|
|
36
|
+
if (selected.length > 0) {
|
|
37
|
+
answers.push({
|
|
38
|
+
questionIndex: i,
|
|
39
|
+
question: q.question,
|
|
40
|
+
kind: "multi",
|
|
41
|
+
answer: null,
|
|
42
|
+
selected,
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
cancelled = true;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
const items = q.options.map((o) => `${o.label} — ${o.description}`);
|
|
50
|
+
items.push(SENTINEL_FREEFORM);
|
|
51
|
+
const chosen = await ui.select(`${header}: ${q.question}`, items);
|
|
52
|
+
if (chosen == null) {
|
|
53
|
+
cancelled = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (chosen === SENTINEL_FREEFORM) {
|
|
57
|
+
const text = await ui.input(q.question, "Type your answer...");
|
|
58
|
+
if (text == null) {
|
|
59
|
+
cancelled = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
answers.push({
|
|
63
|
+
questionIndex: i,
|
|
64
|
+
question: q.question,
|
|
65
|
+
kind: "custom",
|
|
66
|
+
answer: String(text),
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
const opt = q.options.find(
|
|
70
|
+
(o) =>
|
|
71
|
+
chosen === o.label || `${o.label} — ${o.description}` === chosen,
|
|
72
|
+
)!;
|
|
73
|
+
answers.push({
|
|
74
|
+
questionIndex: i,
|
|
75
|
+
question: q.question,
|
|
76
|
+
kind: "option",
|
|
77
|
+
answer: opt?.label ?? String(chosen),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { answers, cancelled };
|
|
84
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Static, Type } from "typebox";
|
|
2
|
+
|
|
3
|
+
// ── Constants ──────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export const MAX_QUESTIONS = 4;
|
|
6
|
+
export const MIN_OPTIONS = 2;
|
|
7
|
+
export const MAX_OPTIONS = 4;
|
|
8
|
+
export const MAX_HEADER_LENGTH = 16;
|
|
9
|
+
export const MAX_LABEL_LENGTH = 60;
|
|
10
|
+
|
|
11
|
+
export const SENTINEL_FREEFORM = "Type something.";
|
|
12
|
+
export const SENTINEL_CHAT = "Chat about this";
|
|
13
|
+
export const SENTINEL_NEXT = "Next";
|
|
14
|
+
|
|
15
|
+
export const SPLIT_PANE_MIN_WIDTH = 84;
|
|
16
|
+
export const SEPARATOR = " │ ";
|
|
17
|
+
|
|
18
|
+
// ── Schemas ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export const OptionSchema = Type.Object({
|
|
21
|
+
label: Type.String({
|
|
22
|
+
maxLength: MAX_LABEL_LENGTH,
|
|
23
|
+
description: `MAX ${MAX_LABEL_LENGTH} CHARACTERS. Display text for this option. Concise (1-5 words).`,
|
|
24
|
+
}),
|
|
25
|
+
description: Type.String({
|
|
26
|
+
description: "Explanation of what this option means or trade-offs.",
|
|
27
|
+
}),
|
|
28
|
+
preview: Type.Optional(
|
|
29
|
+
Type.String({
|
|
30
|
+
description:
|
|
31
|
+
"Optional markdown preview for side-by-side layout (single-select only).",
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const QuestionSchema = Type.Object({
|
|
37
|
+
question: Type.String({
|
|
38
|
+
description: "Clear, specific question ending with ?",
|
|
39
|
+
}),
|
|
40
|
+
header: Type.String({
|
|
41
|
+
maxLength: MAX_HEADER_LENGTH,
|
|
42
|
+
description: `MAX ${MAX_HEADER_LENGTH} CHARS — short chip/tag. E.g. "Auth method", "Approach".`,
|
|
43
|
+
}),
|
|
44
|
+
options: Type.Array(OptionSchema, {
|
|
45
|
+
minItems: MIN_OPTIONS,
|
|
46
|
+
maxItems: MAX_OPTIONS,
|
|
47
|
+
description:
|
|
48
|
+
"2-4 options. 'Type something.' and 'Chat about this' are auto-appended.",
|
|
49
|
+
}),
|
|
50
|
+
multiSelect: Type.Optional(
|
|
51
|
+
Type.Boolean({
|
|
52
|
+
default: false,
|
|
53
|
+
description:
|
|
54
|
+
"Allow multiple selections. Suppresses 'Type something.' row.",
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const QuestionsSchema = Type.Array(QuestionSchema, {
|
|
60
|
+
minItems: 1,
|
|
61
|
+
maxItems: MAX_QUESTIONS,
|
|
62
|
+
description: "1-4 questions",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const ParamsSchema = Type.Object({ questions: QuestionsSchema });
|
|
66
|
+
|
|
67
|
+
export type OptionData = Static<typeof OptionSchema>;
|
|
68
|
+
export type QuestionData = Static<typeof QuestionSchema>;
|
|
69
|
+
export type Params = Static<typeof ParamsSchema>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ── Answer & result types ──────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export type AnswerKind = "option" | "custom" | "chat" | "multi";
|
|
4
|
+
|
|
5
|
+
export interface QuestionAnswer {
|
|
6
|
+
questionIndex: number;
|
|
7
|
+
question: string;
|
|
8
|
+
kind: AnswerKind;
|
|
9
|
+
answer: string | null;
|
|
10
|
+
selected?: string[];
|
|
11
|
+
preview?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface QuestionnaireResult {
|
|
15
|
+
answers: QuestionAnswer[];
|
|
16
|
+
cancelled: boolean;
|
|
17
|
+
}
|
|
@@ -18,6 +18,7 @@ import { dirname, join } from "node:path";
|
|
|
18
18
|
import type {
|
|
19
19
|
ExtensionAPI,
|
|
20
20
|
ExtensionContext,
|
|
21
|
+
Theme,
|
|
21
22
|
ToolInfo,
|
|
22
23
|
} from "@earendil-works/pi-coding-agent";
|
|
23
24
|
import { DynamicBorder, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
@@ -27,10 +28,12 @@ import {
|
|
|
27
28
|
fuzzyFilter,
|
|
28
29
|
Input,
|
|
29
30
|
Key,
|
|
31
|
+
type KeybindingsManager,
|
|
30
32
|
matchesKey,
|
|
31
33
|
type SelectItem,
|
|
32
34
|
SelectList,
|
|
33
35
|
Text,
|
|
36
|
+
type TUI,
|
|
34
37
|
visibleWidth,
|
|
35
38
|
} from "@earendil-works/pi-tui";
|
|
36
39
|
|
|
@@ -339,7 +342,12 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
339
342
|
};
|
|
340
343
|
}): Promise<void> {
|
|
341
344
|
await ctx.ui.custom<null>(
|
|
342
|
-
(
|
|
345
|
+
(
|
|
346
|
+
tui: TUI,
|
|
347
|
+
theme: Theme,
|
|
348
|
+
_kb: KeybindingsManager,
|
|
349
|
+
done: (r: null) => void,
|
|
350
|
+
) => {
|
|
343
351
|
const accent = "accent";
|
|
344
352
|
const mute = (s: string) => theme.fg("muted", s);
|
|
345
353
|
const container = new Container();
|
package/src/ui/diagnostics.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Registers widget with id "pi-lens" to override external pi-lens widget.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
14
14
|
|
|
15
15
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
@@ -65,10 +65,7 @@ function recordFileTouched(filePath: string): void {
|
|
|
65
65
|
|
|
66
66
|
// ─── Render ───────────────────────────────────────────────────────────────────
|
|
67
67
|
|
|
68
|
-
function renderWidget(
|
|
69
|
-
width: number,
|
|
70
|
-
theme: { fg: (color: string, s: string) => string },
|
|
71
|
-
): string[] {
|
|
68
|
+
function renderWidget(width: number, theme: Theme): string[] {
|
|
72
69
|
const w = Math.max(1, width || 80);
|
|
73
70
|
|
|
74
71
|
const cyan = (s: string) => theme.fg("accent", s);
|
|
@@ -117,7 +114,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
117
114
|
if (!ctx.ui.setWidget) return;
|
|
118
115
|
ctx.ui.setWidget(
|
|
119
116
|
"pi-lens",
|
|
120
|
-
(tui
|
|
117
|
+
(tui, theme: Theme) => {
|
|
121
118
|
requestRenderFn = () => tui.requestRender();
|
|
122
119
|
return {
|
|
123
120
|
render: (width: number) => renderWidget(width, theme),
|
package/src/ui/welcome.ts
CHANGED
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
* ✓ Ignore up to date
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import type {
|
|
30
|
+
import type {
|
|
31
|
+
ExtensionAPI,
|
|
32
|
+
ExtensionContext,
|
|
33
|
+
} from "@earendil-works/pi-coding-agent";
|
|
31
34
|
|
|
32
35
|
// ─── Theme shim (same pattern as footer.ts) ───────────────────────────────────
|
|
33
36
|
|
|
@@ -283,7 +286,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
283
286
|
let dismissed = false;
|
|
284
287
|
let requestRender: (() => void) | null = null;
|
|
285
288
|
|
|
286
|
-
const dismiss = (ctx:
|
|
289
|
+
const dismiss = (ctx: ExtensionContext) => {
|
|
287
290
|
if (dismissed) return;
|
|
288
291
|
dismissed = true;
|
|
289
292
|
requestRender = null;
|