@oh-my-pi/pi-coding-agent 4.5.0 → 4.7.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/CHANGELOG.md +30 -0
- package/package.json +5 -5
- package/src/cli/config-cli.ts +344 -0
- package/src/core/agent-session.ts +112 -1
- package/src/core/extensions/types.ts +2 -0
- package/src/core/hooks/loader.ts +6 -3
- package/src/core/hooks/tool-wrapper.ts +1 -0
- package/src/core/session-manager.ts +4 -1
- package/src/core/settings-manager.ts +43 -0
- package/src/core/tools/ask.ts +272 -99
- package/src/core/tools/index.ts +1 -1
- package/src/core/tools/schema-validation.test.ts +30 -0
- package/src/core/tools/task/model-resolver.ts +28 -2
- package/src/main.ts +12 -0
- package/src/modes/interactive/components/hook-editor.ts +1 -1
- package/src/modes/interactive/components/hook-message.ts +5 -0
- package/src/modes/interactive/components/hook-selector.ts +3 -1
- package/src/modes/interactive/components/index.ts +1 -0
- package/src/modes/interactive/components/read-tool-group.ts +12 -4
- package/src/modes/interactive/components/settings-defs.ts +9 -0
- package/src/modes/interactive/components/todo-display.ts +1 -1
- package/src/modes/interactive/components/todo-reminder.ts +42 -0
- package/src/modes/interactive/controllers/command-controller.ts +12 -0
- package/src/modes/interactive/controllers/event-controller.ts +18 -8
- package/src/modes/interactive/controllers/extension-ui-controller.ts +9 -2
- package/src/modes/interactive/controllers/input-controller.ts +48 -16
- package/src/modes/interactive/controllers/selector-controller.ts +38 -8
- package/src/modes/interactive/interactive-mode.ts +30 -4
- package/src/modes/interactive/types.ts +5 -1
- package/src/modes/rpc/rpc-mode.ts +11 -3
- package/src/prompts/tools/ask.md +14 -0
|
@@ -26,6 +26,7 @@ export interface RetrySettings {
|
|
|
26
26
|
|
|
27
27
|
export interface SkillsSettings {
|
|
28
28
|
enabled?: boolean; // default: true
|
|
29
|
+
enableSkillCommands?: boolean; // default: true - register skills as /skill:name commands
|
|
29
30
|
enableCodexUser?: boolean; // default: true
|
|
30
31
|
enableClaudeUser?: boolean; // default: true
|
|
31
32
|
enableClaudeProject?: boolean; // default: true
|
|
@@ -123,6 +124,11 @@ export interface TtsrSettings {
|
|
|
123
124
|
repeatGap?: number; // default: 10
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
export interface TodoCompletionSettings {
|
|
128
|
+
enabled?: boolean; // default: false - warn agent when it stops with incomplete todos
|
|
129
|
+
maxReminders?: number; // default: 3 - maximum reminders before giving up
|
|
130
|
+
}
|
|
131
|
+
|
|
126
132
|
export interface VoiceSettings {
|
|
127
133
|
enabled?: boolean; // default: false
|
|
128
134
|
transcriptionModel?: string; // default: "whisper-1"
|
|
@@ -206,6 +212,7 @@ export interface Settings {
|
|
|
206
212
|
lsp?: LspSettings;
|
|
207
213
|
edit?: EditSettings;
|
|
208
214
|
ttsr?: TtsrSettings;
|
|
215
|
+
todoCompletion?: TodoCompletionSettings;
|
|
209
216
|
voice?: VoiceSettings;
|
|
210
217
|
providers?: ProviderSettings;
|
|
211
218
|
disabledProviders?: string[]; // Discovery provider IDs that are disabled
|
|
@@ -766,6 +773,37 @@ export class SettingsManager {
|
|
|
766
773
|
};
|
|
767
774
|
}
|
|
768
775
|
|
|
776
|
+
getTodoCompletionSettings(): { enabled: boolean; maxReminders: number } {
|
|
777
|
+
return {
|
|
778
|
+
enabled: this.settings.todoCompletion?.enabled ?? false,
|
|
779
|
+
maxReminders: this.settings.todoCompletion?.maxReminders ?? 3,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
getTodoCompletionEnabled(): boolean {
|
|
784
|
+
return this.settings.todoCompletion?.enabled ?? false;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async setTodoCompletionEnabled(enabled: boolean): Promise<void> {
|
|
788
|
+
if (!this.globalSettings.todoCompletion) {
|
|
789
|
+
this.globalSettings.todoCompletion = {};
|
|
790
|
+
}
|
|
791
|
+
this.globalSettings.todoCompletion.enabled = enabled;
|
|
792
|
+
await this.save();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
getTodoCompletionMaxReminders(): number {
|
|
796
|
+
return this.settings.todoCompletion?.maxReminders ?? 3;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async setTodoCompletionMaxReminders(maxReminders: number): Promise<void> {
|
|
800
|
+
if (!this.globalSettings.todoCompletion) {
|
|
801
|
+
this.globalSettings.todoCompletion = {};
|
|
802
|
+
}
|
|
803
|
+
this.globalSettings.todoCompletion.maxReminders = maxReminders;
|
|
804
|
+
await this.save();
|
|
805
|
+
}
|
|
806
|
+
|
|
769
807
|
getThinkingBudgets(): ThinkingBudgetsSettings | undefined {
|
|
770
808
|
return this.settings.thinkingBudgets;
|
|
771
809
|
}
|
|
@@ -821,6 +859,7 @@ export class SettingsManager {
|
|
|
821
859
|
getSkillsSettings(): Required<SkillsSettings> {
|
|
822
860
|
return {
|
|
823
861
|
enabled: this.settings.skills?.enabled ?? true,
|
|
862
|
+
enableSkillCommands: this.settings.skills?.enableSkillCommands ?? true,
|
|
824
863
|
enableCodexUser: this.settings.skills?.enableCodexUser ?? true,
|
|
825
864
|
enableClaudeUser: this.settings.skills?.enableClaudeUser ?? true,
|
|
826
865
|
enableClaudeProject: this.settings.skills?.enableClaudeProject ?? true,
|
|
@@ -832,6 +871,10 @@ export class SettingsManager {
|
|
|
832
871
|
};
|
|
833
872
|
}
|
|
834
873
|
|
|
874
|
+
getEnableSkillCommands(): boolean {
|
|
875
|
+
return this.settings.skills?.enableSkillCommands ?? true;
|
|
876
|
+
}
|
|
877
|
+
|
|
835
878
|
getCommandsSettings(): Required<CommandsSettings> {
|
|
836
879
|
return {
|
|
837
880
|
enableClaudeUser: this.settings.commands?.enableClaudeUser ?? true,
|
package/src/core/tools/ask.ts
CHANGED
|
@@ -34,17 +34,31 @@ const OptionItem = Type.Object({
|
|
|
34
34
|
label: Type.String({ description: "Display label for this option" }),
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
const QuestionItem = Type.Object({
|
|
38
|
+
id: Type.String({ description: "Short identifier for this question (e.g., 'auth', 'cache')" }),
|
|
39
|
+
question: Type.String({ description: "The question text" }),
|
|
40
|
+
options: Type.Array(OptionItem, { description: "Options for this question" }),
|
|
41
|
+
multi: Type.Optional(Type.Boolean({ description: "Allow multiple selections for this question" })),
|
|
42
|
+
});
|
|
43
|
+
|
|
37
44
|
const askSchema = Type.Object({
|
|
38
|
-
question: Type.String({ description: "The question to ask the user" }),
|
|
39
|
-
options: Type.Array(OptionItem, { description: "Available options for the user to choose from." }),
|
|
45
|
+
question: Type.Optional(Type.String({ description: "The question to ask the user" })),
|
|
46
|
+
options: Type.Optional(Type.Array(OptionItem, { description: "Available options for the user to choose from." })),
|
|
40
47
|
multi: Type.Optional(
|
|
41
48
|
Type.Boolean({
|
|
42
49
|
description: "Allow multiple options to be selected (default: false)",
|
|
43
50
|
}),
|
|
44
51
|
),
|
|
52
|
+
questions: Type.Optional(
|
|
53
|
+
Type.Array(QuestionItem, {
|
|
54
|
+
description: "Multiple questions to ask in sequence, each with their own options",
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
45
57
|
});
|
|
46
58
|
|
|
47
|
-
|
|
59
|
+
/** Result for a single question */
|
|
60
|
+
export interface QuestionResult {
|
|
61
|
+
id: string;
|
|
48
62
|
question: string;
|
|
49
63
|
options: string[];
|
|
50
64
|
multi: boolean;
|
|
@@ -52,6 +66,17 @@ export interface AskToolDetails {
|
|
|
52
66
|
customInput?: string;
|
|
53
67
|
}
|
|
54
68
|
|
|
69
|
+
export interface AskToolDetails {
|
|
70
|
+
/** Single question mode (backwards compatible) */
|
|
71
|
+
question?: string;
|
|
72
|
+
options?: string[];
|
|
73
|
+
multi?: boolean;
|
|
74
|
+
selectedOptions?: string[];
|
|
75
|
+
customInput?: string;
|
|
76
|
+
/** Multi-part question mode */
|
|
77
|
+
results?: QuestionResult[];
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
// =============================================================================
|
|
56
81
|
// Constants
|
|
57
82
|
// =============================================================================
|
|
@@ -61,10 +86,123 @@ function getDoneOptionLabel(): string {
|
|
|
61
86
|
return `${theme.status.success} Done selecting`;
|
|
62
87
|
}
|
|
63
88
|
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// Question Selection Logic
|
|
91
|
+
// =============================================================================
|
|
92
|
+
|
|
93
|
+
interface SelectionResult {
|
|
94
|
+
selectedOptions: string[];
|
|
95
|
+
customInput?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface UIContext {
|
|
99
|
+
select(prompt: string, options: string[], options_?: { initialIndex?: number }): Promise<string | undefined>;
|
|
100
|
+
input(prompt: string): Promise<string | undefined>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function askSingleQuestion(
|
|
104
|
+
ui: UIContext,
|
|
105
|
+
question: string,
|
|
106
|
+
optionLabels: string[],
|
|
107
|
+
multi: boolean,
|
|
108
|
+
): Promise<SelectionResult> {
|
|
109
|
+
const doneLabel = getDoneOptionLabel();
|
|
110
|
+
let selectedOptions: string[] = [];
|
|
111
|
+
let customInput: string | undefined;
|
|
112
|
+
|
|
113
|
+
if (multi) {
|
|
114
|
+
const selected = new Set<string>();
|
|
115
|
+
let cursorIndex = 0;
|
|
116
|
+
|
|
117
|
+
while (true) {
|
|
118
|
+
const opts: string[] = [];
|
|
119
|
+
|
|
120
|
+
for (const opt of optionLabels) {
|
|
121
|
+
const checkbox = selected.has(opt) ? theme.checkbox.checked : theme.checkbox.unchecked;
|
|
122
|
+
opts.push(`${checkbox} ${opt}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Done after options, before Other - so cursor stays on options after toggle
|
|
126
|
+
if (selected.size > 0) {
|
|
127
|
+
opts.push(doneLabel);
|
|
128
|
+
}
|
|
129
|
+
opts.push(OTHER_OPTION);
|
|
130
|
+
|
|
131
|
+
const prefix = selected.size > 0 ? `(${selected.size} selected) ` : "";
|
|
132
|
+
const choice = await ui.select(`${prefix}${question}`, opts, { initialIndex: cursorIndex });
|
|
133
|
+
|
|
134
|
+
if (choice === undefined || choice === doneLabel) break;
|
|
135
|
+
|
|
136
|
+
if (choice === OTHER_OPTION) {
|
|
137
|
+
const input = await ui.input("Enter your response:");
|
|
138
|
+
if (input) customInput = input;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Find which index was selected and update cursor position
|
|
143
|
+
const selectedIdx = opts.indexOf(choice);
|
|
144
|
+
if (selectedIdx >= 0) {
|
|
145
|
+
cursorIndex = selectedIdx;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const checkedPrefix = `${theme.checkbox.checked} `;
|
|
149
|
+
const uncheckedPrefix = `${theme.checkbox.unchecked} `;
|
|
150
|
+
let opt: string | undefined;
|
|
151
|
+
if (choice.startsWith(checkedPrefix)) {
|
|
152
|
+
opt = choice.slice(checkedPrefix.length);
|
|
153
|
+
} else if (choice.startsWith(uncheckedPrefix)) {
|
|
154
|
+
opt = choice.slice(uncheckedPrefix.length);
|
|
155
|
+
}
|
|
156
|
+
if (opt) {
|
|
157
|
+
if (selected.has(opt)) {
|
|
158
|
+
selected.delete(opt);
|
|
159
|
+
} else {
|
|
160
|
+
selected.add(opt);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
selectedOptions = Array.from(selected);
|
|
165
|
+
} else {
|
|
166
|
+
const choice = await ui.select(question, [...optionLabels, OTHER_OPTION]);
|
|
167
|
+
if (choice === OTHER_OPTION) {
|
|
168
|
+
const input = await ui.input("Enter your response:");
|
|
169
|
+
if (input) customInput = input;
|
|
170
|
+
} else if (choice) {
|
|
171
|
+
selectedOptions = [choice];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { selectedOptions, customInput };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function formatQuestionResult(result: QuestionResult): string {
|
|
179
|
+
if (result.customInput) {
|
|
180
|
+
return `${result.id}: "${result.customInput}"`;
|
|
181
|
+
}
|
|
182
|
+
if (result.selectedOptions.length > 0) {
|
|
183
|
+
return result.multi
|
|
184
|
+
? `${result.id}: [${result.selectedOptions.join(", ")}]`
|
|
185
|
+
: `${result.id}: ${result.selectedOptions[0]}`;
|
|
186
|
+
}
|
|
187
|
+
return `${result.id}: (cancelled)`;
|
|
188
|
+
}
|
|
189
|
+
|
|
64
190
|
// =============================================================================
|
|
65
191
|
// Tool Implementation
|
|
66
192
|
// =============================================================================
|
|
67
193
|
|
|
194
|
+
interface AskParams {
|
|
195
|
+
question?: string;
|
|
196
|
+
options?: Array<{ label: string }>;
|
|
197
|
+
multi?: boolean;
|
|
198
|
+
questions?: Array<{
|
|
199
|
+
id: string;
|
|
200
|
+
question: string;
|
|
201
|
+
options: Array<{ label: string }>;
|
|
202
|
+
multi?: boolean;
|
|
203
|
+
}>;
|
|
204
|
+
}
|
|
205
|
+
|
|
68
206
|
export function createAskTool(session: ToolSession): null | AgentTool<typeof askSchema, AskToolDetails> {
|
|
69
207
|
if (!session.hasUI) {
|
|
70
208
|
return null;
|
|
@@ -77,99 +215,66 @@ export function createAskTool(session: ToolSession): null | AgentTool<typeof ask
|
|
|
77
215
|
|
|
78
216
|
async execute(
|
|
79
217
|
_toolCallId: string,
|
|
80
|
-
params:
|
|
218
|
+
params: AskParams,
|
|
81
219
|
_signal?: AbortSignal,
|
|
82
220
|
_onUpdate?: AgentToolUpdateCallback<AskToolDetails>,
|
|
83
221
|
context?: AgentToolContext,
|
|
84
222
|
) {
|
|
85
|
-
|
|
86
|
-
const optionLabels = options.map((o) => o.label);
|
|
87
|
-
const doneLabel = getDoneOptionLabel();
|
|
88
|
-
|
|
89
|
-
// Headless fallback - return error if no UI available
|
|
223
|
+
// Headless fallback
|
|
90
224
|
if (!context?.hasUI || !context.ui) {
|
|
91
225
|
return {
|
|
92
|
-
content: [
|
|
93
|
-
|
|
94
|
-
type: "text" as const,
|
|
95
|
-
text: "Error: User prompt requires interactive mode",
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
details: {
|
|
99
|
-
question,
|
|
100
|
-
options: optionLabels,
|
|
101
|
-
multi,
|
|
102
|
-
selectedOptions: [],
|
|
103
|
-
},
|
|
226
|
+
content: [{ type: "text" as const, text: "Error: User prompt requires interactive mode" }],
|
|
227
|
+
details: {},
|
|
104
228
|
};
|
|
105
229
|
}
|
|
106
230
|
|
|
107
231
|
const { ui } = context;
|
|
108
|
-
let selectedOptions: string[] = [];
|
|
109
|
-
let customInput: string | undefined;
|
|
110
|
-
|
|
111
|
-
if (multi) {
|
|
112
|
-
// Multi-select: show checkboxes in the label to indicate selection state
|
|
113
|
-
const selected = new Set<string>();
|
|
114
|
-
|
|
115
|
-
while (true) {
|
|
116
|
-
// Build options with checkbox indicators
|
|
117
|
-
const opts: string[] = [];
|
|
118
|
-
|
|
119
|
-
// Add "Done" option if any selected
|
|
120
|
-
if (selected.size > 0) {
|
|
121
|
-
opts.push(doneLabel);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Add all options with checkbox prefix
|
|
125
|
-
for (const opt of optionLabels) {
|
|
126
|
-
const checkbox = selected.has(opt) ? theme.checkbox.checked : theme.checkbox.unchecked;
|
|
127
|
-
opts.push(`${checkbox} ${opt}`);
|
|
128
|
-
}
|
|
129
232
|
|
|
130
|
-
|
|
131
|
-
|
|
233
|
+
// Multi-part questions mode
|
|
234
|
+
if (params.questions && params.questions.length > 0) {
|
|
235
|
+
const results: QuestionResult[] = [];
|
|
236
|
+
|
|
237
|
+
for (const q of params.questions) {
|
|
238
|
+
const optionLabels = q.options.map((o) => o.label);
|
|
239
|
+
const { selectedOptions, customInput } = await askSingleQuestion(
|
|
240
|
+
ui,
|
|
241
|
+
q.question,
|
|
242
|
+
optionLabels,
|
|
243
|
+
q.multi ?? false,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
results.push({
|
|
247
|
+
id: q.id,
|
|
248
|
+
question: q.question,
|
|
249
|
+
options: optionLabels,
|
|
250
|
+
multi: q.multi ?? false,
|
|
251
|
+
selectedOptions,
|
|
252
|
+
customInput,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
132
255
|
|
|
133
|
-
|
|
134
|
-
|
|
256
|
+
const details: AskToolDetails = { results };
|
|
257
|
+
const responseLines = results.map(formatQuestionResult);
|
|
258
|
+
const responseText = `User answers:\n${responseLines.join("\n")}`;
|
|
135
259
|
|
|
136
|
-
|
|
260
|
+
return { content: [{ type: "text" as const, text: responseText }], details };
|
|
261
|
+
}
|
|
137
262
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
263
|
+
// Single question mode (backwards compatible)
|
|
264
|
+
const question = params.question ?? "";
|
|
265
|
+
const options = params.options ?? [];
|
|
266
|
+
const multi = params.multi ?? false;
|
|
267
|
+
const optionLabels = options.map((o) => o.label);
|
|
143
268
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
opt = choice.slice(checkedPrefix.length);
|
|
150
|
-
} else if (choice.startsWith(uncheckedPrefix)) {
|
|
151
|
-
opt = choice.slice(uncheckedPrefix.length);
|
|
152
|
-
}
|
|
153
|
-
if (opt) {
|
|
154
|
-
if (selected.has(opt)) {
|
|
155
|
-
selected.delete(opt);
|
|
156
|
-
} else {
|
|
157
|
-
selected.add(opt);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
selectedOptions = Array.from(selected);
|
|
162
|
-
} else {
|
|
163
|
-
// Single select with "Other" option
|
|
164
|
-
const choice = await ui.select(question, [...optionLabels, OTHER_OPTION]);
|
|
165
|
-
if (choice === OTHER_OPTION) {
|
|
166
|
-
const input = await ui.input("Enter your response:");
|
|
167
|
-
if (input) customInput = input;
|
|
168
|
-
} else if (choice) {
|
|
169
|
-
selectedOptions = [choice];
|
|
170
|
-
}
|
|
269
|
+
if (!question || optionLabels.length === 0) {
|
|
270
|
+
return {
|
|
271
|
+
content: [{ type: "text" as const, text: "Error: question and options are required" }],
|
|
272
|
+
details: {},
|
|
273
|
+
};
|
|
171
274
|
}
|
|
172
275
|
|
|
276
|
+
const { selectedOptions, customInput } = await askSingleQuestion(ui, question, optionLabels, multi);
|
|
277
|
+
|
|
173
278
|
const details: AskToolDetails = {
|
|
174
279
|
question,
|
|
175
280
|
options: optionLabels,
|
|
@@ -207,21 +312,59 @@ export const askTool = createAskTool({
|
|
|
207
312
|
// =============================================================================
|
|
208
313
|
|
|
209
314
|
interface AskRenderArgs {
|
|
210
|
-
question
|
|
315
|
+
question?: string;
|
|
211
316
|
options?: Array<{ label: string }>;
|
|
212
317
|
multi?: boolean;
|
|
318
|
+
questions?: Array<{
|
|
319
|
+
id: string;
|
|
320
|
+
question: string;
|
|
321
|
+
options: Array<{ label: string }>;
|
|
322
|
+
multi?: boolean;
|
|
323
|
+
}>;
|
|
213
324
|
}
|
|
214
325
|
|
|
215
326
|
export const askToolRenderer = {
|
|
216
327
|
renderCall(args: AskRenderArgs, uiTheme: Theme): Component {
|
|
217
328
|
const ui = createToolUIKit(uiTheme);
|
|
329
|
+
const label = ui.title("Ask");
|
|
330
|
+
|
|
331
|
+
// Multi-part questions
|
|
332
|
+
if (args.questions && args.questions.length > 0) {
|
|
333
|
+
let text = `${label} ${uiTheme.fg("muted", `${args.questions.length} questions`)}`;
|
|
334
|
+
|
|
335
|
+
for (let i = 0; i < args.questions.length; i++) {
|
|
336
|
+
const q = args.questions[i];
|
|
337
|
+
const isLastQ = i === args.questions.length - 1;
|
|
338
|
+
const qBranch = isLastQ ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
339
|
+
const continuation = isLastQ ? " " : uiTheme.tree.vertical;
|
|
340
|
+
|
|
341
|
+
// Question line with metadata
|
|
342
|
+
const meta: string[] = [];
|
|
343
|
+
if (q.multi) meta.push("multi");
|
|
344
|
+
if (q.options?.length) meta.push(`options:${q.options.length}`);
|
|
345
|
+
const metaStr = meta.length > 0 ? uiTheme.fg("dim", ` · ${meta.join(" · ")}`) : "";
|
|
346
|
+
|
|
347
|
+
text += `\n ${uiTheme.fg("dim", qBranch)} ${uiTheme.fg("dim", `[${q.id}]`)} ${uiTheme.fg("accent", q.question)}${metaStr}`;
|
|
348
|
+
|
|
349
|
+
// Options under question
|
|
350
|
+
if (q.options?.length) {
|
|
351
|
+
for (let j = 0; j < q.options.length; j++) {
|
|
352
|
+
const opt = q.options[j];
|
|
353
|
+
const isLastOpt = j === q.options.length - 1;
|
|
354
|
+
const optBranch = isLastOpt ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
355
|
+
text += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("dim", uiTheme.checkbox.unchecked)} ${uiTheme.fg("muted", opt.label)}`;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return new Text(text, 0, 0);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Single question
|
|
218
363
|
if (!args.question) {
|
|
219
364
|
return new Text(ui.errorMessage("No question provided"), 0, 0);
|
|
220
365
|
}
|
|
221
366
|
|
|
222
|
-
const label = ui.title("Ask");
|
|
223
367
|
let text = `${label} ${uiTheme.fg("accent", args.question)}`;
|
|
224
|
-
|
|
225
368
|
const meta: string[] = [];
|
|
226
369
|
if (args.multi) meta.push("multi");
|
|
227
370
|
if (args.options?.length) meta.push(`options:${args.options.length}`);
|
|
@@ -250,7 +393,47 @@ export const askToolRenderer = {
|
|
|
250
393
|
return new Text(txt?.type === "text" && txt.text ? txt.text : "", 0, 0);
|
|
251
394
|
}
|
|
252
395
|
|
|
253
|
-
|
|
396
|
+
// Multi-part results
|
|
397
|
+
if (details.results && details.results.length > 0) {
|
|
398
|
+
const lines: string[] = [];
|
|
399
|
+
|
|
400
|
+
for (const r of details.results) {
|
|
401
|
+
const hasSelection = r.customInput || r.selectedOptions.length > 0;
|
|
402
|
+
const statusIcon = hasSelection
|
|
403
|
+
? uiTheme.styledSymbol("status.success", "success")
|
|
404
|
+
: uiTheme.styledSymbol("status.warning", "warning");
|
|
405
|
+
|
|
406
|
+
lines.push(`${statusIcon} ${uiTheme.fg("dim", `[${r.id}]`)} ${uiTheme.fg("accent", r.question)}`);
|
|
407
|
+
|
|
408
|
+
if (r.customInput) {
|
|
409
|
+
lines.push(
|
|
410
|
+
` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", r.customInput)}`,
|
|
411
|
+
);
|
|
412
|
+
} else if (r.selectedOptions.length > 0) {
|
|
413
|
+
for (let j = 0; j < r.selectedOptions.length; j++) {
|
|
414
|
+
const isLast = j === r.selectedOptions.length - 1;
|
|
415
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
416
|
+
lines.push(
|
|
417
|
+
` ${uiTheme.fg("dim", branch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${uiTheme.fg("toolOutput", r.selectedOptions[j])}`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
lines.push(
|
|
422
|
+
` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Single question result
|
|
431
|
+
if (!details.question) {
|
|
432
|
+
const txt = result.content[0];
|
|
433
|
+
return new Text(txt?.type === "text" && txt.text ? txt.text : "", 0, 0);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const hasSelection = details.customInput || (details.selectedOptions && details.selectedOptions.length > 0);
|
|
254
437
|
const statusIcon = hasSelection
|
|
255
438
|
? uiTheme.styledSymbol("status.success", "success")
|
|
256
439
|
: uiTheme.styledSymbol("status.warning", "warning");
|
|
@@ -258,25 +441,15 @@ export const askToolRenderer = {
|
|
|
258
441
|
let text = `${statusIcon} ${uiTheme.fg("accent", details.question)}`;
|
|
259
442
|
|
|
260
443
|
if (details.customInput) {
|
|
261
|
-
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
} else if (details.selectedOptions.length > 0) {
|
|
266
|
-
const selected = details.selectedOptions;
|
|
267
|
-
for (let i = 0; i < selected.length; i++) {
|
|
268
|
-
const isLast = i === selected.length - 1;
|
|
444
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", details.customInput)}`;
|
|
445
|
+
} else if (details.selectedOptions && details.selectedOptions.length > 0) {
|
|
446
|
+
for (let i = 0; i < details.selectedOptions.length; i++) {
|
|
447
|
+
const isLast = i === details.selectedOptions.length - 1;
|
|
269
448
|
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
270
|
-
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg(
|
|
271
|
-
"success",
|
|
272
|
-
uiTheme.checkbox.checked,
|
|
273
|
-
)} ${uiTheme.fg("toolOutput", selected[i])}`;
|
|
449
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${uiTheme.fg("toolOutput", details.selectedOptions[i])}`;
|
|
274
450
|
}
|
|
275
451
|
} else {
|
|
276
|
-
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol(
|
|
277
|
-
"status.warning",
|
|
278
|
-
"warning",
|
|
279
|
-
)} ${uiTheme.fg("warning", "Cancelled")}`;
|
|
452
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
|
|
280
453
|
}
|
|
281
454
|
|
|
282
455
|
return new Text(text, 0, 0);
|
package/src/core/tools/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { type AskToolDetails, askTool, createAskTool } from "./ask";
|
|
2
|
-
export { type BashOperations, type BashToolDetails, createBashTool } from "./bash";
|
|
2
|
+
export { type BashOperations, type BashToolDetails, type BashToolOptions, createBashTool } from "./bash";
|
|
3
3
|
export { type CalculatorToolDetails, createCalculatorTool } from "./calculator";
|
|
4
4
|
export { createCompleteTool } from "./complete";
|
|
5
5
|
export { createEditTool, type EditToolDetails } from "./edit";
|
|
@@ -198,6 +198,36 @@ describe("sanitizeSchemaForGoogle", () => {
|
|
|
198
198
|
expect(sanitizeSchemaForGoogle(true)).toBe(true);
|
|
199
199
|
expect(sanitizeSchemaForGoogle(null)).toBe(null);
|
|
200
200
|
});
|
|
201
|
+
|
|
202
|
+
it("preserves property names that match schema keywords (e.g., 'pattern')", () => {
|
|
203
|
+
const schema = {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
pattern: { type: "string", description: "The search pattern" },
|
|
207
|
+
format: { type: "string", description: "Output format" },
|
|
208
|
+
},
|
|
209
|
+
required: ["pattern"],
|
|
210
|
+
};
|
|
211
|
+
const sanitized = sanitizeSchemaForGoogle(schema) as Record<string, unknown>;
|
|
212
|
+
const props = sanitized.properties as Record<string, unknown>;
|
|
213
|
+
expect(props.pattern).toEqual({ type: "string", description: "The search pattern" });
|
|
214
|
+
expect(props.format).toEqual({ type: "string", description: "Output format" });
|
|
215
|
+
expect(sanitized.required).toEqual(["pattern"]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("still strips schema keywords from non-properties contexts", () => {
|
|
219
|
+
const schema = {
|
|
220
|
+
type: "string",
|
|
221
|
+
pattern: "^[a-z]+$",
|
|
222
|
+
format: "email",
|
|
223
|
+
minLength: 1,
|
|
224
|
+
};
|
|
225
|
+
const sanitized = sanitizeSchemaForGoogle(schema) as Record<string, unknown>;
|
|
226
|
+
expect(sanitized.pattern).toBeUndefined();
|
|
227
|
+
expect(sanitized.format).toBeUndefined();
|
|
228
|
+
expect(sanitized.minLength).toBeUndefined();
|
|
229
|
+
expect(sanitized.type).toBe("string");
|
|
230
|
+
});
|
|
201
231
|
});
|
|
202
232
|
|
|
203
233
|
describe("tool schema validation (post-sanitization)", () => {
|
|
@@ -118,6 +118,15 @@ function getModelId(fullModel: string): string {
|
|
|
118
118
|
return slashIdx > 0 ? fullModel.slice(slashIdx + 1) : fullModel;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Extract provider from "provider/modelId" format.
|
|
123
|
+
* Returns undefined if no provider prefix.
|
|
124
|
+
*/
|
|
125
|
+
function getProvider(fullModel: string): string | undefined {
|
|
126
|
+
const slashIdx = fullModel.indexOf("/");
|
|
127
|
+
return slashIdx > 0 ? fullModel.slice(0, slashIdx) : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
121
130
|
/**
|
|
122
131
|
* Resolve a fuzzy model pattern to "provider/modelId" format.
|
|
123
132
|
*
|
|
@@ -165,8 +174,25 @@ export async function resolveModelPattern(
|
|
|
165
174
|
const exactId = models.find((m) => getModelId(m).toLowerCase() === p.toLowerCase());
|
|
166
175
|
if (exactId) return exactId;
|
|
167
176
|
|
|
168
|
-
//
|
|
169
|
-
const
|
|
177
|
+
// Check if pattern has provider prefix (e.g., "zai/glm-4.7")
|
|
178
|
+
const patternProvider = getProvider(p);
|
|
179
|
+
const patternModelId = getModelId(p);
|
|
180
|
+
|
|
181
|
+
// If pattern has provider prefix, fuzzy match must stay within that provider
|
|
182
|
+
// (don't cross provider boundaries when user explicitly specifies provider)
|
|
183
|
+
if (patternProvider) {
|
|
184
|
+
const providerFuzzyMatch = models.find(
|
|
185
|
+
(m) =>
|
|
186
|
+
getProvider(m)?.toLowerCase() === patternProvider.toLowerCase() &&
|
|
187
|
+
getModelId(m).toLowerCase().includes(patternModelId.toLowerCase()),
|
|
188
|
+
);
|
|
189
|
+
if (providerFuzzyMatch) return providerFuzzyMatch;
|
|
190
|
+
// No match in specified provider - don't fall through to other providers
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// No provider prefix - fall back to general fuzzy match on model ID (substring)
|
|
195
|
+
const fuzzyMatch = models.find((m) => getModelId(m).toLowerCase().includes(patternModelId.toLowerCase()));
|
|
170
196
|
if (fuzzyMatch) return fuzzyMatch;
|
|
171
197
|
}
|
|
172
198
|
|
package/src/main.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { join, resolve } from "node:path";
|
|
|
10
10
|
import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
|
|
11
11
|
import chalk from "chalk";
|
|
12
12
|
import { type Args, parseArgs, printHelp } from "./cli/args";
|
|
13
|
+
import { parseConfigArgs, printConfigHelp, runConfigCommand } from "./cli/config-cli";
|
|
13
14
|
import { processFileArguments } from "./cli/file-processor";
|
|
14
15
|
import { listModels } from "./cli/list-models";
|
|
15
16
|
import { parsePluginArgs, printPluginHelp, runPluginCommand } from "./cli/plugin-cli";
|
|
@@ -418,6 +419,17 @@ export async function main(args: string[]) {
|
|
|
418
419
|
return;
|
|
419
420
|
}
|
|
420
421
|
|
|
422
|
+
// Handle config subcommand
|
|
423
|
+
const configCmd = parseConfigArgs(args);
|
|
424
|
+
if (configCmd) {
|
|
425
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
426
|
+
printConfigHelp();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
await runConfigCommand(configCmd);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
421
433
|
const parsed = parseArgs(args);
|
|
422
434
|
time("parseArgs");
|
|
423
435
|
await maybeAutoChdir(parsed);
|