@mariozechner/pi-coding-agent 0.45.3 → 0.45.5
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/CHANGELOG.md +28 -0
- package/README.md +2 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +7 -9
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/model-registry.d.ts +4 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +6 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +1 -0
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +7 -5
- package/dist/core/sdk.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +3 -4
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/light.json +9 -9
- package/dist/utils/image-convert.d.ts.map +1 -1
- package/dist/utils/image-convert.js +11 -4
- package/dist/utils/image-convert.js.map +1 -1
- package/dist/utils/image-resize.d.ts +1 -1
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +47 -25
- package/dist/utils/image-resize.js.map +1 -1
- package/dist/utils/vips.d.ts +11 -0
- package/dist/utils/vips.d.ts.map +1 -0
- package/dist/utils/vips.js +35 -0
- package/dist/utils/vips.js.map +1 -0
- package/docs/extensions.md +18 -17
- package/docs/sdk.md +21 -48
- package/examples/README.md +5 -2
- package/examples/extensions/README.md +19 -2
- package/examples/extensions/plan-mode/README.md +65 -0
- package/examples/extensions/plan-mode/index.ts +340 -0
- package/examples/extensions/plan-mode/utils.ts +168 -0
- package/examples/extensions/question.ts +211 -13
- package/examples/extensions/questionnaire.ts +427 -0
- package/examples/extensions/summarize.ts +195 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/README.md +3 -4
- package/package.json +5 -5
- package/examples/extensions/plan-mode.ts +0 -548
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure utility functions for plan mode.
|
|
3
|
+
* Extracted for testability.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Destructive commands blocked in plan mode
|
|
7
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
8
|
+
/\brm\b/i,
|
|
9
|
+
/\brmdir\b/i,
|
|
10
|
+
/\bmv\b/i,
|
|
11
|
+
/\bcp\b/i,
|
|
12
|
+
/\bmkdir\b/i,
|
|
13
|
+
/\btouch\b/i,
|
|
14
|
+
/\bchmod\b/i,
|
|
15
|
+
/\bchown\b/i,
|
|
16
|
+
/\bchgrp\b/i,
|
|
17
|
+
/\bln\b/i,
|
|
18
|
+
/\btee\b/i,
|
|
19
|
+
/\btruncate\b/i,
|
|
20
|
+
/\bdd\b/i,
|
|
21
|
+
/\bshred\b/i,
|
|
22
|
+
/(^|[^<])>(?!>)/,
|
|
23
|
+
/>>/,
|
|
24
|
+
/\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
|
|
25
|
+
/\byarn\s+(add|remove|install|publish)/i,
|
|
26
|
+
/\bpnpm\s+(add|remove|install|publish)/i,
|
|
27
|
+
/\bpip\s+(install|uninstall)/i,
|
|
28
|
+
/\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
|
|
29
|
+
/\bbrew\s+(install|uninstall|upgrade)/i,
|
|
30
|
+
/\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
|
|
31
|
+
/\bsudo\b/i,
|
|
32
|
+
/\bsu\b/i,
|
|
33
|
+
/\bkill\b/i,
|
|
34
|
+
/\bpkill\b/i,
|
|
35
|
+
/\bkillall\b/i,
|
|
36
|
+
/\breboot\b/i,
|
|
37
|
+
/\bshutdown\b/i,
|
|
38
|
+
/\bsystemctl\s+(start|stop|restart|enable|disable)/i,
|
|
39
|
+
/\bservice\s+\S+\s+(start|stop|restart)/i,
|
|
40
|
+
/\b(vim?|nano|emacs|code|subl)\b/i,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Safe read-only commands allowed in plan mode
|
|
44
|
+
const SAFE_PATTERNS = [
|
|
45
|
+
/^\s*cat\b/,
|
|
46
|
+
/^\s*head\b/,
|
|
47
|
+
/^\s*tail\b/,
|
|
48
|
+
/^\s*less\b/,
|
|
49
|
+
/^\s*more\b/,
|
|
50
|
+
/^\s*grep\b/,
|
|
51
|
+
/^\s*find\b/,
|
|
52
|
+
/^\s*ls\b/,
|
|
53
|
+
/^\s*pwd\b/,
|
|
54
|
+
/^\s*echo\b/,
|
|
55
|
+
/^\s*printf\b/,
|
|
56
|
+
/^\s*wc\b/,
|
|
57
|
+
/^\s*sort\b/,
|
|
58
|
+
/^\s*uniq\b/,
|
|
59
|
+
/^\s*diff\b/,
|
|
60
|
+
/^\s*file\b/,
|
|
61
|
+
/^\s*stat\b/,
|
|
62
|
+
/^\s*du\b/,
|
|
63
|
+
/^\s*df\b/,
|
|
64
|
+
/^\s*tree\b/,
|
|
65
|
+
/^\s*which\b/,
|
|
66
|
+
/^\s*whereis\b/,
|
|
67
|
+
/^\s*type\b/,
|
|
68
|
+
/^\s*env\b/,
|
|
69
|
+
/^\s*printenv\b/,
|
|
70
|
+
/^\s*uname\b/,
|
|
71
|
+
/^\s*whoami\b/,
|
|
72
|
+
/^\s*id\b/,
|
|
73
|
+
/^\s*date\b/,
|
|
74
|
+
/^\s*cal\b/,
|
|
75
|
+
/^\s*uptime\b/,
|
|
76
|
+
/^\s*ps\b/,
|
|
77
|
+
/^\s*top\b/,
|
|
78
|
+
/^\s*htop\b/,
|
|
79
|
+
/^\s*free\b/,
|
|
80
|
+
/^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
|
|
81
|
+
/^\s*git\s+ls-/i,
|
|
82
|
+
/^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
|
|
83
|
+
/^\s*yarn\s+(list|info|why|audit)/i,
|
|
84
|
+
/^\s*node\s+--version/i,
|
|
85
|
+
/^\s*python\s+--version/i,
|
|
86
|
+
/^\s*curl\s/i,
|
|
87
|
+
/^\s*wget\s+-O\s*-/i,
|
|
88
|
+
/^\s*jq\b/,
|
|
89
|
+
/^\s*sed\s+-n/i,
|
|
90
|
+
/^\s*awk\b/,
|
|
91
|
+
/^\s*rg\b/,
|
|
92
|
+
/^\s*fd\b/,
|
|
93
|
+
/^\s*bat\b/,
|
|
94
|
+
/^\s*exa\b/,
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
export function isSafeCommand(command: string): boolean {
|
|
98
|
+
const isDestructive = DESTRUCTIVE_PATTERNS.some((p) => p.test(command));
|
|
99
|
+
const isSafe = SAFE_PATTERNS.some((p) => p.test(command));
|
|
100
|
+
return !isDestructive && isSafe;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface TodoItem {
|
|
104
|
+
step: number;
|
|
105
|
+
text: string;
|
|
106
|
+
completed: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function cleanStepText(text: string): string {
|
|
110
|
+
let cleaned = text
|
|
111
|
+
.replace(/\*{1,2}([^*]+)\*{1,2}/g, "$1") // Remove bold/italic
|
|
112
|
+
.replace(/`([^`]+)`/g, "$1") // Remove code
|
|
113
|
+
.replace(
|
|
114
|
+
/^(Use|Run|Execute|Create|Write|Read|Check|Verify|Update|Modify|Add|Remove|Delete|Install)\s+(the\s+)?/i,
|
|
115
|
+
"",
|
|
116
|
+
)
|
|
117
|
+
.replace(/\s+/g, " ")
|
|
118
|
+
.trim();
|
|
119
|
+
|
|
120
|
+
if (cleaned.length > 0) {
|
|
121
|
+
cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
122
|
+
}
|
|
123
|
+
if (cleaned.length > 50) {
|
|
124
|
+
cleaned = `${cleaned.slice(0, 47)}...`;
|
|
125
|
+
}
|
|
126
|
+
return cleaned;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function extractTodoItems(message: string): TodoItem[] {
|
|
130
|
+
const items: TodoItem[] = [];
|
|
131
|
+
const headerMatch = message.match(/\*{0,2}Plan:\*{0,2}\s*\n/i);
|
|
132
|
+
if (!headerMatch) return items;
|
|
133
|
+
|
|
134
|
+
const planSection = message.slice(message.indexOf(headerMatch[0]) + headerMatch[0].length);
|
|
135
|
+
const numberedPattern = /^\s*(\d+)[.)]\s+\*{0,2}([^*\n]+)/gm;
|
|
136
|
+
|
|
137
|
+
for (const match of planSection.matchAll(numberedPattern)) {
|
|
138
|
+
const text = match[2]
|
|
139
|
+
.trim()
|
|
140
|
+
.replace(/\*{1,2}$/, "")
|
|
141
|
+
.trim();
|
|
142
|
+
if (text.length > 5 && !text.startsWith("`") && !text.startsWith("/") && !text.startsWith("-")) {
|
|
143
|
+
const cleaned = cleanStepText(text);
|
|
144
|
+
if (cleaned.length > 3) {
|
|
145
|
+
items.push({ step: items.length + 1, text: cleaned, completed: false });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return items;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function extractDoneSteps(message: string): number[] {
|
|
153
|
+
const steps: number[] = [];
|
|
154
|
+
for (const match of message.matchAll(/\[DONE:(\d+)\]/gi)) {
|
|
155
|
+
const step = Number(match[1]);
|
|
156
|
+
if (Number.isFinite(step)) steps.push(step);
|
|
157
|
+
}
|
|
158
|
+
return steps;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function markCompletedSteps(text: string, items: TodoItem[]): number {
|
|
162
|
+
const doneSteps = extractDoneSteps(text);
|
|
163
|
+
for (const step of doneSteps) {
|
|
164
|
+
const item = items.find((t) => t.step === step);
|
|
165
|
+
if (item) item.completed = true;
|
|
166
|
+
}
|
|
167
|
+
return doneSteps.length;
|
|
168
|
+
}
|
|
@@ -1,23 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Question Tool -
|
|
2
|
+
* Question Tool - Single question with options
|
|
3
|
+
* Full custom UI: options list + inline editor for "Type something..."
|
|
4
|
+
* Escape in editor returns to options, Escape in options cancels
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
8
|
+
import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
|
7
9
|
import { Type } from "@sinclair/typebox";
|
|
8
10
|
|
|
11
|
+
interface OptionWithDesc {
|
|
12
|
+
label: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type DisplayOption = OptionWithDesc & { isOther?: boolean };
|
|
17
|
+
|
|
9
18
|
interface QuestionDetails {
|
|
10
19
|
question: string;
|
|
11
20
|
options: string[];
|
|
12
21
|
answer: string | null;
|
|
22
|
+
wasCustom?: boolean;
|
|
13
23
|
}
|
|
14
24
|
|
|
25
|
+
// Support both simple strings and objects with descriptions
|
|
26
|
+
const OptionSchema = Type.Union([
|
|
27
|
+
Type.String(),
|
|
28
|
+
Type.Object({
|
|
29
|
+
label: Type.String({ description: "Display label for the option" }),
|
|
30
|
+
description: Type.Optional(Type.String({ description: "Optional description shown below label" })),
|
|
31
|
+
}),
|
|
32
|
+
]);
|
|
33
|
+
|
|
15
34
|
const QuestionParams = Type.Object({
|
|
16
35
|
question: Type.String({ description: "The question to ask the user" }),
|
|
17
|
-
options: Type.Array(
|
|
36
|
+
options: Type.Array(OptionSchema, { description: "Options for the user to choose from" }),
|
|
18
37
|
});
|
|
19
38
|
|
|
20
|
-
|
|
39
|
+
// Normalize option to { label, description? }
|
|
40
|
+
function normalizeOption(opt: string | { label: string; description?: string }): OptionWithDesc {
|
|
41
|
+
if (typeof opt === "string") {
|
|
42
|
+
return { label: opt };
|
|
43
|
+
}
|
|
44
|
+
return opt;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function question(pi: ExtensionAPI) {
|
|
21
48
|
pi.registerTool({
|
|
22
49
|
name: "question",
|
|
23
50
|
label: "Question",
|
|
@@ -28,7 +55,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
28
55
|
if (!ctx.hasUI) {
|
|
29
56
|
return {
|
|
30
57
|
content: [{ type: "text", text: "Error: UI not available (running in non-interactive mode)" }],
|
|
31
|
-
details: {
|
|
58
|
+
details: {
|
|
59
|
+
question: params.question,
|
|
60
|
+
options: params.options.map((o) => (typeof o === "string" ? o : o.label)),
|
|
61
|
+
answer: null,
|
|
62
|
+
} as QuestionDetails,
|
|
32
63
|
};
|
|
33
64
|
}
|
|
34
65
|
|
|
@@ -39,25 +70,183 @@ export default function (pi: ExtensionAPI) {
|
|
|
39
70
|
};
|
|
40
71
|
}
|
|
41
72
|
|
|
42
|
-
|
|
73
|
+
// Normalize options
|
|
74
|
+
const normalizedOptions = params.options.map(normalizeOption);
|
|
75
|
+
const allOptions: DisplayOption[] = [...normalizedOptions, { label: "Type something.", isOther: true }];
|
|
76
|
+
|
|
77
|
+
const result = await ctx.ui.custom<{ answer: string; wasCustom: boolean; index?: number } | null>(
|
|
78
|
+
(tui, theme, _kb, done) => {
|
|
79
|
+
let optionIndex = 0;
|
|
80
|
+
let editMode = false;
|
|
81
|
+
let cachedLines: string[] | undefined;
|
|
82
|
+
|
|
83
|
+
const editorTheme: EditorTheme = {
|
|
84
|
+
borderColor: (s) => theme.fg("accent", s),
|
|
85
|
+
selectList: {
|
|
86
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
87
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
88
|
+
description: (t) => theme.fg("muted", t),
|
|
89
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
90
|
+
noMatch: (t) => theme.fg("warning", t),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const editor = new Editor(editorTheme);
|
|
94
|
+
|
|
95
|
+
editor.onSubmit = (value) => {
|
|
96
|
+
const trimmed = value.trim();
|
|
97
|
+
if (trimmed) {
|
|
98
|
+
done({ answer: trimmed, wasCustom: true });
|
|
99
|
+
} else {
|
|
100
|
+
editMode = false;
|
|
101
|
+
editor.setText("");
|
|
102
|
+
refresh();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function refresh() {
|
|
107
|
+
cachedLines = undefined;
|
|
108
|
+
tui.requestRender();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function handleInput(data: string) {
|
|
112
|
+
if (editMode) {
|
|
113
|
+
if (matchesKey(data, Key.escape)) {
|
|
114
|
+
editMode = false;
|
|
115
|
+
editor.setText("");
|
|
116
|
+
refresh();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
editor.handleInput(data);
|
|
120
|
+
refresh();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (matchesKey(data, Key.up)) {
|
|
125
|
+
optionIndex = Math.max(0, optionIndex - 1);
|
|
126
|
+
refresh();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (matchesKey(data, Key.down)) {
|
|
130
|
+
optionIndex = Math.min(allOptions.length - 1, optionIndex + 1);
|
|
131
|
+
refresh();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (matchesKey(data, Key.enter)) {
|
|
136
|
+
const selected = allOptions[optionIndex];
|
|
137
|
+
if (selected.isOther) {
|
|
138
|
+
editMode = true;
|
|
139
|
+
refresh();
|
|
140
|
+
} else {
|
|
141
|
+
done({ answer: selected.label, wasCustom: false, index: optionIndex + 1 });
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (matchesKey(data, Key.escape)) {
|
|
147
|
+
done(null);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function render(width: number): string[] {
|
|
152
|
+
if (cachedLines) return cachedLines;
|
|
43
153
|
|
|
44
|
-
|
|
154
|
+
const lines: string[] = [];
|
|
155
|
+
const add = (s: string) => lines.push(truncateToWidth(s, width));
|
|
156
|
+
|
|
157
|
+
add(theme.fg("accent", "─".repeat(width)));
|
|
158
|
+
add(theme.fg("text", ` ${params.question}`));
|
|
159
|
+
lines.push("");
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < allOptions.length; i++) {
|
|
162
|
+
const opt = allOptions[i];
|
|
163
|
+
const selected = i === optionIndex;
|
|
164
|
+
const isOther = opt.isOther === true;
|
|
165
|
+
const prefix = selected ? theme.fg("accent", "> ") : " ";
|
|
166
|
+
|
|
167
|
+
if (isOther && editMode) {
|
|
168
|
+
add(prefix + theme.fg("accent", `${i + 1}. ${opt.label} ✎`));
|
|
169
|
+
} else if (selected) {
|
|
170
|
+
add(prefix + theme.fg("accent", `${i + 1}. ${opt.label}`));
|
|
171
|
+
} else {
|
|
172
|
+
add(` ${theme.fg("text", `${i + 1}. ${opt.label}`)}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Show description if present
|
|
176
|
+
if (opt.description) {
|
|
177
|
+
add(` ${theme.fg("muted", opt.description)}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (editMode) {
|
|
182
|
+
lines.push("");
|
|
183
|
+
add(theme.fg("muted", " Your answer:"));
|
|
184
|
+
for (const line of editor.render(width - 2)) {
|
|
185
|
+
add(` ${line}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
lines.push("");
|
|
190
|
+
if (editMode) {
|
|
191
|
+
add(theme.fg("dim", " Enter to submit • Esc to go back"));
|
|
192
|
+
} else {
|
|
193
|
+
add(theme.fg("dim", " ↑↓ navigate • Enter to select • Esc to cancel"));
|
|
194
|
+
}
|
|
195
|
+
add(theme.fg("accent", "─".repeat(width)));
|
|
196
|
+
|
|
197
|
+
cachedLines = lines;
|
|
198
|
+
return lines;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
render,
|
|
203
|
+
invalidate: () => {
|
|
204
|
+
cachedLines = undefined;
|
|
205
|
+
},
|
|
206
|
+
handleInput,
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Build simple options list for details
|
|
212
|
+
const simpleOptions = normalizedOptions.map((o) => o.label);
|
|
213
|
+
|
|
214
|
+
if (!result) {
|
|
45
215
|
return {
|
|
46
216
|
content: [{ type: "text", text: "User cancelled the selection" }],
|
|
47
|
-
details: { question: params.question, options:
|
|
217
|
+
details: { question: params.question, options: simpleOptions, answer: null } as QuestionDetails,
|
|
48
218
|
};
|
|
49
219
|
}
|
|
50
220
|
|
|
221
|
+
if (result.wasCustom) {
|
|
222
|
+
return {
|
|
223
|
+
content: [{ type: "text", text: `User wrote: ${result.answer}` }],
|
|
224
|
+
details: {
|
|
225
|
+
question: params.question,
|
|
226
|
+
options: simpleOptions,
|
|
227
|
+
answer: result.answer,
|
|
228
|
+
wasCustom: true,
|
|
229
|
+
} as QuestionDetails,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
51
232
|
return {
|
|
52
|
-
content: [{ type: "text", text: `User selected: ${answer}` }],
|
|
53
|
-
details: {
|
|
233
|
+
content: [{ type: "text", text: `User selected: ${result.index}. ${result.answer}` }],
|
|
234
|
+
details: {
|
|
235
|
+
question: params.question,
|
|
236
|
+
options: simpleOptions,
|
|
237
|
+
answer: result.answer,
|
|
238
|
+
wasCustom: false,
|
|
239
|
+
} as QuestionDetails,
|
|
54
240
|
};
|
|
55
241
|
},
|
|
56
242
|
|
|
57
243
|
renderCall(args, theme) {
|
|
58
244
|
let text = theme.fg("toolTitle", theme.bold("question ")) + theme.fg("muted", args.question);
|
|
59
|
-
|
|
60
|
-
|
|
245
|
+
const opts = Array.isArray(args.options) ? args.options : [];
|
|
246
|
+
if (opts.length) {
|
|
247
|
+
const labels = opts.map((o: string | { label: string }) => (typeof o === "string" ? o : o.label));
|
|
248
|
+
const numbered = [...labels, "Type something."].map((o, i) => `${i + 1}. ${o}`);
|
|
249
|
+
text += `\n${theme.fg("dim", ` Options: ${numbered.join(", ")}`)}`;
|
|
61
250
|
}
|
|
62
251
|
return new Text(text, 0, 0);
|
|
63
252
|
},
|
|
@@ -73,7 +262,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
73
262
|
return new Text(theme.fg("warning", "Cancelled"), 0, 0);
|
|
74
263
|
}
|
|
75
264
|
|
|
76
|
-
|
|
265
|
+
if (details.wasCustom) {
|
|
266
|
+
return new Text(
|
|
267
|
+
theme.fg("success", "✓ ") + theme.fg("muted", "(wrote) ") + theme.fg("accent", details.answer),
|
|
268
|
+
0,
|
|
269
|
+
0,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const idx = details.options.indexOf(details.answer) + 1;
|
|
273
|
+
const display = idx > 0 ? `${idx}. ${details.answer}` : details.answer;
|
|
274
|
+
return new Text(theme.fg("success", "✓ ") + theme.fg("accent", display), 0, 0);
|
|
77
275
|
},
|
|
78
276
|
});
|
|
79
277
|
}
|