@xynogen/pix-core 0.2.4 → 0.3.1
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/README.md +24 -21
- package/package.json +11 -17
- package/skills/ask-user/SKILL.md +0 -48
- package/src/commands/agent-sop/agent-sop.ts +0 -58
- package/src/commands/clear/clear.ts +0 -32
- package/src/commands/diff/diff.ts +0 -32
- package/src/commands/models/models.test.ts +0 -95
- package/src/commands/models/models.ts +0 -367
- package/src/commands/models/patch-builtin.test.ts +0 -66
- package/src/commands/models/patch-builtin.ts +0 -120
- package/src/commands/tools.test.ts +0 -15
- package/src/commands/update/update.test.ts +0 -112
- package/src/commands/update/update.ts +0 -271
- package/src/index.ts +0 -45
- package/src/lib/data.ts +0 -33
- package/src/nudge/capability.test.ts +0 -258
- package/src/nudge/capability.ts +0 -189
- package/src/nudge/index.ts +0 -17
- package/src/nudge/tools.test.ts +0 -157
- package/src/nudge/tools.ts +0 -212
- package/src/tool/ask/ask.test.ts +0 -243
- package/src/tool/ask/components.ts +0 -55
- package/src/tool/ask/helpers.ts +0 -77
- package/src/tool/ask/index.ts +0 -130
- package/src/tool/ask/questionnaire.ts +0 -693
- package/src/tool/ask/rpc.ts +0 -84
- package/src/tool/ask/schema.ts +0 -69
- package/src/tool/ask/single-select-layout.test.ts +0 -124
- package/src/tool/ask/single-select-layout.ts +0 -237
- package/src/tool/ask/types.ts +0 -17
- package/src/tool/todo/todo.test.ts +0 -646
- package/src/tool/todo/todo.ts +0 -218
- package/src/tool/toolbox/toolbox.test.ts +0 -314
- package/src/tool/toolbox/toolbox.ts +0 -570
- package/src/ui/diagnostics.ts +0 -145
- package/src/ui/footer.ts +0 -513
- package/src/ui/welcome.test.ts +0 -124
- package/src/ui/welcome.ts +0 -369
package/src/tool/ask/ask.test.ts
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ask.test.ts — tests for the ask questionnaire tool
|
|
3
|
-
*
|
|
4
|
-
* Tests cover pure functions (schema validation, sentinel logic, answer
|
|
5
|
-
* formatting). TUI components are not tested here.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, expect, test } from "bun:test";
|
|
9
|
-
import {
|
|
10
|
-
buildResponseText,
|
|
11
|
-
formatAnswerScalar,
|
|
12
|
-
hasAnyPreview,
|
|
13
|
-
type OptionData,
|
|
14
|
-
type QuestionData,
|
|
15
|
-
sentinelsFor,
|
|
16
|
-
} from "./index.ts";
|
|
17
|
-
|
|
18
|
-
// ── Fixtures ──────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
const opt = (
|
|
21
|
-
label: string,
|
|
22
|
-
description = "Test option",
|
|
23
|
-
preview?: string,
|
|
24
|
-
): OptionData => ({
|
|
25
|
-
label,
|
|
26
|
-
description,
|
|
27
|
-
...(preview ? { preview } : {}),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const qSingle: QuestionData = {
|
|
31
|
-
question: "Which approach?",
|
|
32
|
-
header: "Approach",
|
|
33
|
-
options: [
|
|
34
|
-
opt("REST", "Traditional REST API"),
|
|
35
|
-
opt("GraphQL", "Query language for APIs"),
|
|
36
|
-
],
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const qMulti: QuestionData = {
|
|
40
|
-
question: "Which features?",
|
|
41
|
-
header: "Features",
|
|
42
|
-
options: [
|
|
43
|
-
opt("Auth", "User authentication"),
|
|
44
|
-
opt("Search", "Full text search"),
|
|
45
|
-
opt("Export", "Data export"),
|
|
46
|
-
],
|
|
47
|
-
multiSelect: true,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const qWithPreview: QuestionData = {
|
|
51
|
-
question: "Pick a component?",
|
|
52
|
-
header: "Component",
|
|
53
|
-
options: [
|
|
54
|
-
opt("Button", "Clickable button", "<Button>Primary</Button>"),
|
|
55
|
-
opt("Card", "Container card", "<Card><Content/></Card>"),
|
|
56
|
-
],
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const qSingleNoPreview: QuestionData = {
|
|
60
|
-
question: "Color?",
|
|
61
|
-
header: "Color",
|
|
62
|
-
options: [opt("Red", "Ruby red"), opt("Blue", "Ocean blue")],
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// ── hasAnyPreview ─────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
describe("hasAnyPreview", () => {
|
|
68
|
-
test("returns false when no option has preview", () => {
|
|
69
|
-
expect(hasAnyPreview(qSingle)).toBe(false);
|
|
70
|
-
expect(hasAnyPreview(qMulti)).toBe(false);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("returns true when at least one option has preview", () => {
|
|
74
|
-
expect(hasAnyPreview(qWithPreview)).toBe(true);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("returns false for empty options", () => {
|
|
78
|
-
const q: QuestionData = { question: "?", header: "X", options: [] };
|
|
79
|
-
expect(hasAnyPreview(q)).toBe(false);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// ── sentinelsFor ──────────────────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
describe("sentinelsFor", () => {
|
|
86
|
-
test('single-select without preview appends "Type something."', () => {
|
|
87
|
-
const r = sentinelsFor(qSingleNoPreview);
|
|
88
|
-
expect(r).toHaveLength(1);
|
|
89
|
-
expect(r[0]?.kind).toBe("other");
|
|
90
|
-
expect(r[0]?.label).toBe("Type something.");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('single-select with preview appends nothing (only "Chat about this" is separate)', () => {
|
|
94
|
-
const r = sentinelsFor(qWithPreview);
|
|
95
|
-
expect(r).toHaveLength(0);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('multi-select appends "Next"', () => {
|
|
99
|
-
const r = sentinelsFor(qMulti);
|
|
100
|
-
expect(r).toHaveLength(1);
|
|
101
|
-
expect(r[0]?.kind).toBe("next");
|
|
102
|
-
expect(r[0]?.label).toBe("Next");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("multi-select never appends Type something.", () => {
|
|
106
|
-
const r = sentinelsFor({ ...qMulti, multiSelect: true });
|
|
107
|
-
expect(r.every((s) => s.kind !== "other")).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("empty options still gets freeform sentinel (no preview = single-select)", () => {
|
|
111
|
-
const r = sentinelsFor({ question: "?", header: "X", options: [] });
|
|
112
|
-
expect(r).toHaveLength(1);
|
|
113
|
-
expect(r[0]?.kind).toBe("other");
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// ── formatAnswerScalar ────────────────────────────────────────────────
|
|
118
|
-
|
|
119
|
-
describe("formatAnswerScalar", () => {
|
|
120
|
-
test("option kind returns the answer string", () => {
|
|
121
|
-
const a = {
|
|
122
|
-
questionIndex: 0,
|
|
123
|
-
question: "Q",
|
|
124
|
-
kind: "option" as const,
|
|
125
|
-
answer: "REST",
|
|
126
|
-
};
|
|
127
|
-
expect(formatAnswerScalar(a)).toBe("REST");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("multi kind joins selected with comma", () => {
|
|
131
|
-
const a = {
|
|
132
|
-
questionIndex: 0,
|
|
133
|
-
question: "Q",
|
|
134
|
-
kind: "multi" as const,
|
|
135
|
-
answer: null,
|
|
136
|
-
selected: ["Auth", "Search"],
|
|
137
|
-
};
|
|
138
|
-
expect(formatAnswerScalar(a)).toBe("Auth, Search");
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("custom kind returns the typed text", () => {
|
|
142
|
-
const a = {
|
|
143
|
-
questionIndex: 0,
|
|
144
|
-
question: "Q",
|
|
145
|
-
kind: "custom" as const,
|
|
146
|
-
answer: "my custom answer",
|
|
147
|
-
};
|
|
148
|
-
expect(formatAnswerScalar(a)).toBe("my custom answer");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("chat kind returns (chat)", () => {
|
|
152
|
-
const a = {
|
|
153
|
-
questionIndex: 0,
|
|
154
|
-
question: "Q",
|
|
155
|
-
kind: "chat" as const,
|
|
156
|
-
answer: null,
|
|
157
|
-
};
|
|
158
|
-
expect(formatAnswerScalar(a)).toBe("(chat)");
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// ── buildResponseText ─────────────────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
describe("buildResponseText", () => {
|
|
165
|
-
test("formats single answer", () => {
|
|
166
|
-
const answers = [
|
|
167
|
-
{
|
|
168
|
-
questionIndex: 0,
|
|
169
|
-
question: "Which approach?",
|
|
170
|
-
kind: "option" as const,
|
|
171
|
-
answer: "REST",
|
|
172
|
-
},
|
|
173
|
-
];
|
|
174
|
-
const text = buildResponseText(answers, [qSingle]);
|
|
175
|
-
expect(text).toContain("REST");
|
|
176
|
-
expect(text).toContain("Which approach?");
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test("formats multi-select answer", () => {
|
|
180
|
-
const answers = [
|
|
181
|
-
{
|
|
182
|
-
questionIndex: 0,
|
|
183
|
-
question: "Which features?",
|
|
184
|
-
kind: "multi" as const,
|
|
185
|
-
answer: null,
|
|
186
|
-
selected: ["Auth", "Search"],
|
|
187
|
-
},
|
|
188
|
-
];
|
|
189
|
-
const text = buildResponseText(answers, [qMulti]);
|
|
190
|
-
expect(text).toContain("Auth, Search");
|
|
191
|
-
expect(text).toContain("Which features?");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("includes preview in response when present", () => {
|
|
195
|
-
const answers = [
|
|
196
|
-
{
|
|
197
|
-
questionIndex: 0,
|
|
198
|
-
question: "Pick a component?",
|
|
199
|
-
kind: "option" as const,
|
|
200
|
-
answer: "Button",
|
|
201
|
-
preview: "<Button>Primary</Button>",
|
|
202
|
-
},
|
|
203
|
-
];
|
|
204
|
-
const text = buildResponseText(answers, [qWithPreview]);
|
|
205
|
-
expect(text).toContain("preview: <Button>Primary</Button>");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test("formats multiple answers", () => {
|
|
209
|
-
const qs = [qSingle, qMulti];
|
|
210
|
-
const answers = [
|
|
211
|
-
{
|
|
212
|
-
questionIndex: 0,
|
|
213
|
-
question: "Which approach?",
|
|
214
|
-
kind: "option" as const,
|
|
215
|
-
answer: "GraphQL",
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
questionIndex: 1,
|
|
219
|
-
question: "Which features?",
|
|
220
|
-
kind: "multi" as const,
|
|
221
|
-
answer: null,
|
|
222
|
-
selected: ["Export"],
|
|
223
|
-
},
|
|
224
|
-
];
|
|
225
|
-
const text = buildResponseText(answers, qs);
|
|
226
|
-
expect(text).toContain("GraphQL");
|
|
227
|
-
expect(text).toContain("Export");
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test("shows declined message when no answers", () => {
|
|
231
|
-
const text = buildResponseText([], [qSingle]);
|
|
232
|
-
expect(text).toContain("declined");
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// ── Tool registration shape ─────────────────────────────────────────
|
|
237
|
-
|
|
238
|
-
describe("registerAsk", () => {
|
|
239
|
-
test("exports a default function", async () => {
|
|
240
|
-
const mod = await import("./index.ts");
|
|
241
|
-
expect(typeof mod.default).toBe("function");
|
|
242
|
-
});
|
|
243
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { QuestionData } from "./schema.js";
|
|
4
|
-
|
|
5
|
-
// ── Color helpers ──────────────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
export function borderColor(theme: Theme): (s: string) => string {
|
|
8
|
-
return (s: string) => theme.fg("accent", s);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function dim(theme: Theme): (s: string) => string {
|
|
12
|
-
return (s: string) => theme.fg("dim", s);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// ── TabBar ─────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
export class TabBar implements Component {
|
|
18
|
-
private questions: QuestionData[];
|
|
19
|
-
private activeIndex: number;
|
|
20
|
-
private theme: Theme;
|
|
21
|
-
|
|
22
|
-
constructor(questions: QuestionData[], activeIndex: number, theme: Theme) {
|
|
23
|
-
this.questions = questions;
|
|
24
|
-
this.activeIndex = activeIndex;
|
|
25
|
-
this.theme = theme;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
invalidate(): void {}
|
|
29
|
-
|
|
30
|
-
render(width: number): string[] {
|
|
31
|
-
const t = this.theme;
|
|
32
|
-
const inner = Math.max(10, width - 2);
|
|
33
|
-
|
|
34
|
-
const parts: string[] = [];
|
|
35
|
-
for (let i = 0; i < this.questions.length; i++) {
|
|
36
|
-
const active = i === this.activeIndex;
|
|
37
|
-
const num = `${i + 1}`;
|
|
38
|
-
const tag = `${num}.${this.questions[i]?.header}`;
|
|
39
|
-
parts.push(active ? t.fg("accent", t.bold(tag)) : t.fg("dim", tag));
|
|
40
|
-
}
|
|
41
|
-
const line = parts.join(t.fg("dim", " "));
|
|
42
|
-
return [
|
|
43
|
-
truncateToWidth(
|
|
44
|
-
t.fg("accent", "╭─") +
|
|
45
|
-
line +
|
|
46
|
-
t.fg(
|
|
47
|
-
"accent",
|
|
48
|
-
`${"─".repeat(Math.max(0, inner - line.length - 1))}╮`,
|
|
49
|
-
),
|
|
50
|
-
width,
|
|
51
|
-
"",
|
|
52
|
-
),
|
|
53
|
-
].filter(Boolean);
|
|
54
|
-
}
|
|
55
|
-
}
|
package/src/tool/ask/helpers.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { MarkdownTheme } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { OptionData, QuestionData } from "./schema.js";
|
|
4
|
-
import { SENTINEL_CHAT, SENTINEL_FREEFORM, SENTINEL_NEXT } from "./schema.js";
|
|
5
|
-
import type { AnswerKind, QuestionAnswer } from "./types.js";
|
|
6
|
-
|
|
7
|
-
// ── Markdown theme ─────────────────────────────────────────────────────
|
|
8
|
-
|
|
9
|
-
export function safeMarkdownTheme(): MarkdownTheme | undefined {
|
|
10
|
-
try {
|
|
11
|
-
const md = getMarkdownTheme();
|
|
12
|
-
if (!md) return undefined;
|
|
13
|
-
md.bold("");
|
|
14
|
-
return md;
|
|
15
|
-
} catch {
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// ── Option / question helpers ──────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
export function hasAnyPreview(q: QuestionData): boolean {
|
|
23
|
-
return q.options.some(
|
|
24
|
-
(o) => typeof o.preview === "string" && o.preview.length > 0,
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Which sentinel rows are auto-appended for a question. */
|
|
29
|
-
export function sentinelsFor(
|
|
30
|
-
q: QuestionData,
|
|
31
|
-
): Array<{ kind: string; label: string }> {
|
|
32
|
-
const out: Array<{ kind: string; label: string }> = [];
|
|
33
|
-
if (q.multiSelect) {
|
|
34
|
-
out.push({ kind: "next", label: SENTINEL_NEXT });
|
|
35
|
-
} else if (!hasAnyPreview(q)) {
|
|
36
|
-
out.push({ kind: "other", label: SENTINEL_FREEFORM });
|
|
37
|
-
}
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ── Answer formatting ──────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
export function formatAnswerScalar(a: QuestionAnswer): string {
|
|
44
|
-
if (a.kind === "multi") return (a.selected ?? []).join(", ");
|
|
45
|
-
if (a.kind === "custom") return a.answer ?? "(custom)";
|
|
46
|
-
if (a.kind === "chat") return "(chat)";
|
|
47
|
-
return a.answer ?? "(selected)";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function buildResponseText(
|
|
51
|
-
answers: QuestionAnswer[],
|
|
52
|
-
questions: QuestionData[],
|
|
53
|
-
): string {
|
|
54
|
-
const segs: string[] = [];
|
|
55
|
-
for (const a of answers) {
|
|
56
|
-
const q = questions[a.questionIndex]?.question ?? `Q${a.questionIndex + 1}`;
|
|
57
|
-
let s = `"${q}"="${formatAnswerScalar(a)}"`;
|
|
58
|
-
if (a.preview) s += `. selected preview: ${a.preview}`;
|
|
59
|
-
segs.push(s);
|
|
60
|
-
}
|
|
61
|
-
return segs.length
|
|
62
|
-
? `User answered: ${segs.join(". ")}.`
|
|
63
|
-
: "User declined to answer questions.";
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ── Scroll indicator ───────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
export function scrollIndicator(index: number, total: number): string {
|
|
69
|
-
if (total <= 1) return "";
|
|
70
|
-
const pos = Math.round((index / (total - 1)) * 6);
|
|
71
|
-
const bar = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦"][pos] ?? "·";
|
|
72
|
-
return ` ${bar} ${index + 1}/${total}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export type { AnswerKind, OptionData, QuestionData };
|
|
76
|
-
// Re-export sentinel constants so callers don't need to import schema directly
|
|
77
|
-
export { SENTINEL_CHAT, SENTINEL_FREEFORM, SENTINEL_NEXT };
|
package/src/tool/ask/index.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
-
|
|
4
|
-
import { buildResponseText } from "./helpers.js";
|
|
5
|
-
import { AskQuestionnaire } from "./questionnaire.js";
|
|
6
|
-
import { rpcFallback } from "./rpc.js";
|
|
7
|
-
import type { Params } from "./schema.js";
|
|
8
|
-
import {
|
|
9
|
-
MAX_OPTIONS,
|
|
10
|
-
MAX_QUESTIONS,
|
|
11
|
-
MIN_OPTIONS,
|
|
12
|
-
ParamsSchema,
|
|
13
|
-
SENTINEL_CHAT,
|
|
14
|
-
SENTINEL_FREEFORM,
|
|
15
|
-
} from "./schema.js";
|
|
16
|
-
import type { QuestionAnswer, QuestionnaireResult } from "./types.js";
|
|
17
|
-
|
|
18
|
-
// ── Re-exports (consumed by tests and single-select-layout) ───────────
|
|
19
|
-
|
|
20
|
-
export {
|
|
21
|
-
buildResponseText,
|
|
22
|
-
formatAnswerScalar,
|
|
23
|
-
hasAnyPreview,
|
|
24
|
-
sentinelsFor,
|
|
25
|
-
} from "./helpers.js";
|
|
26
|
-
export type { OptionData, QuestionData } from "./schema.js";
|
|
27
|
-
export type {
|
|
28
|
-
AnswerKind,
|
|
29
|
-
QuestionAnswer,
|
|
30
|
-
QuestionnaireResult,
|
|
31
|
-
} from "./types.js";
|
|
32
|
-
|
|
33
|
-
// ── Tool registration ──────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
export default function registerAsk(pi: ExtensionAPI): void {
|
|
36
|
-
pi.registerTool({
|
|
37
|
-
name: "ask_user",
|
|
38
|
-
label: "Ask",
|
|
39
|
-
description: `Ask the user up to ${MAX_QUESTIONS} structured questions (${MIN_OPTIONS}-${MAX_OPTIONS} options each) when requirements are ambiguous.`,
|
|
40
|
-
promptSnippet: `Ask the user up to ${MAX_QUESTIONS} structured questions (${MIN_OPTIONS}-${MAX_OPTIONS} options each) when requirements are ambiguous`,
|
|
41
|
-
promptGuidelines: [
|
|
42
|
-
`Use ask whenever the user's request is underspecified and you cannot proceed without concrete decisions — you can ask up to ${MAX_QUESTIONS} questions per invocation.`,
|
|
43
|
-
`Each question MUST have ${MIN_OPTIONS}-${MAX_OPTIONS} options. Every option requires a concise label (1-5 words) and a description explaining what the choice means or its trade-offs. The user can additionally type a custom answer ("${SENTINEL_FREEFORM}" row is appended automatically to single-select questions) or pick "${SENTINEL_CHAT}" to abandon the questionnaire.`,
|
|
44
|
-
`Set multiSelect: true when multiple answers are valid; this suppresses the "${SENTINEL_FREEFORM}" row. Provide an options[].preview markdown string when an option benefits from richer side-by-side context (mockups, code snippets, diagrams, configs) — single-select only. NOTE: any non-empty preview on a single-select question ALSO suppresses the "${SENTINEL_FREEFORM}" row (no room in the side-by-side layout); "${SENTINEL_CHAT}" remains the escape hatch. If you recommend a specific option, make it the first option and append "(Recommended)" to its label.`,
|
|
45
|
-
"Do not stack multiple ask calls back-to-back — group all clarifying questions into one invocation.",
|
|
46
|
-
],
|
|
47
|
-
executionMode: "sequential",
|
|
48
|
-
parameters: ParamsSchema,
|
|
49
|
-
|
|
50
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
51
|
-
if (signal?.aborted) {
|
|
52
|
-
return {
|
|
53
|
-
content: [{ type: "text", text: "Cancelled" }],
|
|
54
|
-
details: { answers: [], cancelled: true },
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const typed = params as unknown as Params;
|
|
59
|
-
|
|
60
|
-
if (!Array.isArray(typed.questions) || typed.questions.length === 0) {
|
|
61
|
-
return {
|
|
62
|
-
content: [
|
|
63
|
-
{ type: "text", text: "At least one question is required." },
|
|
64
|
-
],
|
|
65
|
-
isError: true,
|
|
66
|
-
details: { answers: [], cancelled: true },
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (!ctx.hasUI) {
|
|
71
|
-
const result = await rpcFallback(ctx.ui, typed);
|
|
72
|
-
const text = result.cancelled
|
|
73
|
-
? "User cancelled the questionnaire"
|
|
74
|
-
: buildResponseText(result.answers, typed.questions);
|
|
75
|
-
return { content: [{ type: "text", text }], details: result };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const result = await ctx.ui.custom<QuestionnaireResult | null>(
|
|
79
|
-
(tui, theme, keybindings, done) => {
|
|
80
|
-
if (signal) {
|
|
81
|
-
signal.addEventListener(
|
|
82
|
-
"abort",
|
|
83
|
-
() => done({ answers: [], cancelled: true }),
|
|
84
|
-
{ once: true },
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
return new AskQuestionnaire(typed, tui, theme, keybindings, done);
|
|
88
|
-
},
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
if (!result || result.cancelled) {
|
|
92
|
-
return {
|
|
93
|
-
content: [{ type: "text", text: "User cancelled the questionnaire" }],
|
|
94
|
-
details: result ?? { answers: [], cancelled: true },
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const text = buildResponseText(result.answers, typed.questions);
|
|
99
|
-
return { content: [{ type: "text", text }], details: result };
|
|
100
|
-
},
|
|
101
|
-
|
|
102
|
-
renderCall(args, theme) {
|
|
103
|
-
const questions = Array.isArray(args.questions) ? args.questions : [];
|
|
104
|
-
const count = questions.length;
|
|
105
|
-
const firstQ = (questions[0]?.question ?? "") as string;
|
|
106
|
-
let text = theme.fg("toolTitle", theme.bold(`ask (${count}) `));
|
|
107
|
-
text += theme.fg("muted", firstQ);
|
|
108
|
-
if (count > 1) text += theme.fg("dim", ` +${count - 1} more`);
|
|
109
|
-
return new Text(text, 0, 0);
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
renderResult(result, options, theme) {
|
|
113
|
-
const details = result.details as
|
|
114
|
-
| { answers?: QuestionAnswer[]; cancelled?: boolean }
|
|
115
|
-
| undefined;
|
|
116
|
-
if (options.isPartial) {
|
|
117
|
-
return new Text(theme.fg("muted", "Waiting for user input..."), 0, 0);
|
|
118
|
-
}
|
|
119
|
-
if (!details || details.cancelled || !details.answers?.length) {
|
|
120
|
-
return new Text(theme.fg("warning", "Cancelled"), 0, 0);
|
|
121
|
-
}
|
|
122
|
-
const texts = details.answers.map((a) => {
|
|
123
|
-
const v =
|
|
124
|
-
a.kind === "multi" ? (a.selected ?? []).join(", ") : (a.answer ?? "");
|
|
125
|
-
return `${a.questionIndex + 1}: ${v}`;
|
|
126
|
-
});
|
|
127
|
-
return new Text(theme.fg("success", `✓ ${texts.join(" • ")}`), 0, 0);
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
}
|