@oh-my-pi/pi-coding-agent 4.6.0 → 4.8.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 +23 -0
- package/package.json +8 -6
- 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/settings-manager.ts +37 -0
- package/src/core/tools/ask.ts +272 -99
- package/src/core/tools/task/model-resolver.ts +28 -2
- package/src/main.ts +12 -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 +1 -0
- package/src/modes/interactive/controllers/event-controller.ts +8 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +9 -2
- package/src/modes/interactive/controllers/input-controller.ts +1 -1
- package/src/modes/interactive/controllers/selector-controller.ts +2 -0
- package/src/modes/interactive/interactive-mode.ts +8 -3
- package/src/modes/interactive/types.ts +2 -1
- package/src/prompts/tools/ask.md +14 -0
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);
|
|
@@ -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);
|
|
@@ -21,11 +21,12 @@ import { DynamicBorder } from "./dynamic-border";
|
|
|
21
21
|
export interface HookSelectorOptions {
|
|
22
22
|
tui?: TUI;
|
|
23
23
|
timeout?: number;
|
|
24
|
+
initialIndex?: number;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export class HookSelectorComponent extends Container {
|
|
27
28
|
private options: string[];
|
|
28
|
-
private selectedIndex
|
|
29
|
+
private selectedIndex: number;
|
|
29
30
|
private listContainer: Container;
|
|
30
31
|
private onSelectCallback: (option: string) => void;
|
|
31
32
|
private onCancelCallback: () => void;
|
|
@@ -43,6 +44,7 @@ export class HookSelectorComponent extends Container {
|
|
|
43
44
|
super();
|
|
44
45
|
|
|
45
46
|
this.options = options;
|
|
47
|
+
this.selectedIndex = Math.min(opts?.initialIndex ?? 0, options.length - 1);
|
|
46
48
|
this.onSelectCallback = onSelect;
|
|
47
49
|
this.onCancelCallback = onCancel;
|
|
48
50
|
this.baseTitle = title;
|
|
@@ -31,6 +31,7 @@ export { ShowImagesSelectorComponent } from "./show-images-selector";
|
|
|
31
31
|
export { StatusLineComponent } from "./status-line";
|
|
32
32
|
export { ThemeSelectorComponent } from "./theme-selector";
|
|
33
33
|
export { ThinkingSelectorComponent } from "./thinking-selector";
|
|
34
|
+
export { TodoReminderComponent } from "./todo-reminder";
|
|
34
35
|
export { ToolExecutionComponent, type ToolExecutionHandle, type ToolExecutionOptions } from "./tool-execution";
|
|
35
36
|
export { TreeSelectorComponent } from "./tree-selector";
|
|
36
37
|
export { TtsrNotificationComponent } from "./ttsr-notification";
|
|
@@ -74,15 +74,23 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
74
74
|
|
|
75
75
|
private updateDisplay(): void {
|
|
76
76
|
const entries = [...this.entries.values()];
|
|
77
|
-
const header = `${theme.fg("toolTitle", theme.bold("Read"))}${
|
|
78
|
-
entries.length > 1 ? theme.fg("dim", ` (${entries.length})`) : ""
|
|
79
|
-
}`;
|
|
80
77
|
|
|
81
78
|
if (entries.length === 0) {
|
|
82
|
-
this.text.setText(` ${theme.format.bullet} ${
|
|
79
|
+
this.text.setText(` ${theme.format.bullet} ${theme.fg("toolTitle", theme.bold("Read"))}`);
|
|
83
80
|
return;
|
|
84
81
|
}
|
|
85
82
|
|
|
83
|
+
if (entries.length === 1) {
|
|
84
|
+
const entry = entries[0];
|
|
85
|
+
const statusSymbol = this.formatStatus(entry.status);
|
|
86
|
+
const pathDisplay = this.formatPath(entry);
|
|
87
|
+
this.text.setText(
|
|
88
|
+
` ${theme.format.bullet} ${theme.fg("toolTitle", theme.bold("Read"))} ${pathDisplay} ${statusSymbol}`.trimEnd(),
|
|
89
|
+
);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const header = `${theme.fg("toolTitle", theme.bold("Read"))}${theme.fg("dim", ` (${entries.length})`)}`;
|
|
86
94
|
const lines = [` ${theme.format.bullet} ${header}`];
|
|
87
95
|
const total = entries.length;
|
|
88
96
|
for (const [index, entry] of entries.entries()) {
|
|
@@ -97,6 +97,15 @@ export const SETTINGS_DEFS: SettingDef[] = [
|
|
|
97
97
|
get: (sm) => sm.getBranchSummaryEnabled(),
|
|
98
98
|
set: (sm, v) => sm.setBranchSummaryEnabled(v),
|
|
99
99
|
},
|
|
100
|
+
{
|
|
101
|
+
id: "todoCompletion",
|
|
102
|
+
tab: "config",
|
|
103
|
+
type: "boolean",
|
|
104
|
+
label: "Todo completion",
|
|
105
|
+
description: "Remind agent to complete todos before stopping (up to 3 reminders)",
|
|
106
|
+
get: (sm) => sm.getTodoCompletionEnabled(),
|
|
107
|
+
set: (sm, v) => sm.setTodoCompletionEnabled(v),
|
|
108
|
+
},
|
|
100
109
|
{
|
|
101
110
|
id: "showImages",
|
|
102
111
|
tab: "config",
|
|
@@ -98,7 +98,7 @@ export class TodoDisplayComponent {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
if (hasMore) {
|
|
101
|
-
lines.push(theme.fg("dim", `
|
|
101
|
+
lines.push(theme.fg("dim", ` ${theme.tree.hook} +${this.todos.length - 5} more (Ctrl+T to expand)`));
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
return lines;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Box, Container, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import type { TodoItem } from "../../../core/tools/todo-write";
|
|
3
|
+
import { theme } from "../theme/theme";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Component that renders a todo completion reminder notification.
|
|
7
|
+
* Shows when the agent stops with incomplete todos.
|
|
8
|
+
*/
|
|
9
|
+
export class TodoReminderComponent extends Container {
|
|
10
|
+
private todos: TodoItem[];
|
|
11
|
+
private attempt: number;
|
|
12
|
+
private maxAttempts: number;
|
|
13
|
+
private box: Box;
|
|
14
|
+
|
|
15
|
+
constructor(todos: TodoItem[], attempt: number, maxAttempts: number) {
|
|
16
|
+
super();
|
|
17
|
+
this.todos = todos;
|
|
18
|
+
this.attempt = attempt;
|
|
19
|
+
this.maxAttempts = maxAttempts;
|
|
20
|
+
|
|
21
|
+
this.addChild(new Spacer(1));
|
|
22
|
+
|
|
23
|
+
this.box = new Box(1, 1, (t) => theme.inverse(theme.fg("warning", t)));
|
|
24
|
+
this.addChild(this.box);
|
|
25
|
+
|
|
26
|
+
this.rebuild();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private rebuild(): void {
|
|
30
|
+
this.box.clear();
|
|
31
|
+
|
|
32
|
+
const count = this.todos.length;
|
|
33
|
+
const label = count === 1 ? "todo" : "todos";
|
|
34
|
+
const header = `${theme.icon.warning} ${count} incomplete ${label} - reminder ${this.attempt}/${this.maxAttempts}`;
|
|
35
|
+
|
|
36
|
+
this.box.addChild(new Text(header, 0, 0));
|
|
37
|
+
this.box.addChild(new Spacer(1));
|
|
38
|
+
|
|
39
|
+
const todoList = this.todos.map((t) => ` ${theme.checkbox.unchecked} ${t.content}`).join("\n");
|
|
40
|
+
this.box.addChild(new Text(theme.italic(todoList), 0, 0));
|
|
41
|
+
}
|
|
42
|
+
}
|