@oh-my-pi/pi-coding-agent 13.10.1 → 13.11.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 +43 -0
- package/package.json +7 -7
- package/src/commit/agentic/agent.ts +3 -1
- package/src/commit/agentic/index.ts +7 -1
- package/src/commit/analysis/conventional.ts +5 -1
- package/src/commit/analysis/summary.ts +5 -1
- package/src/commit/changelog/generate.ts +5 -1
- package/src/commit/changelog/index.ts +4 -0
- package/src/commit/map-reduce/index.ts +5 -0
- package/src/commit/map-reduce/map-phase.ts +17 -2
- package/src/commit/map-reduce/reduce-phase.ts +5 -1
- package/src/commit/model-selection.ts +38 -26
- package/src/commit/pipeline.ts +22 -11
- package/src/config/settings-schema.ts +20 -0
- package/src/config.ts +10 -3
- package/src/discovery/helpers.ts +7 -3
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/lsp/index.ts +4 -4
- package/src/lsp/utils.ts +81 -0
- package/src/modes/components/settings-defs.ts +1 -0
- package/src/modes/components/todo-reminder.ts +8 -1
- package/src/modes/controllers/command-controller.ts +75 -3
- package/src/modes/controllers/input-controller.ts +2 -3
- package/src/modes/interactive-mode.ts +11 -7
- package/src/modes/theme/theme.ts +30 -27
- package/src/modes/types.ts +2 -1
- package/src/patch/hashline.ts +3 -6
- package/src/prompts/system/eager-todo.md +13 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/find.md +1 -0
- package/src/prompts/tools/grep.md +1 -0
- package/src/prompts/tools/hashline.md +23 -111
- package/src/prompts/tools/todo-write.md +11 -1
- package/src/sdk.ts +1 -1
- package/src/session/agent-session.ts +85 -7
- package/src/slash-commands/builtin-registry.ts +10 -2
- package/src/task/executor.ts +9 -18
- package/src/task/index.ts +8 -4
- package/src/task/render.ts +5 -10
- package/src/task/template.ts +4 -1
- package/src/task/types.ts +2 -0
- package/src/tools/ast-edit.ts +26 -7
- package/src/tools/ast-grep.ts +26 -9
- package/src/tools/fetch.ts +36 -5
- package/src/tools/find.ts +13 -64
- package/src/tools/grep.ts +27 -10
- package/src/tools/output-meta.ts +2 -1
- package/src/tools/path-utils.ts +348 -0
- package/src/tools/todo-write.ts +27 -4
- package/src/utils/commit-message-generator.ts +27 -22
- package/src/utils/image-input.ts +1 -1
- package/src/utils/image-resize.ts +4 -4
- package/src/utils/title-generator.ts +36 -23
- package/src/utils/tool-choice.ts +28 -0
- package/src/web/parallel.ts +346 -0
- package/src/web/scrapers/youtube.ts +29 -0
- package/src/web/search/provider.ts +4 -1
- package/src/web/search/providers/parallel.ts +63 -0
- package/src/web/search/types.ts +1 -0
package/src/tools/path-utils.ts
CHANGED
|
@@ -4,6 +4,14 @@ import * as path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
6
6
|
const NARROW_NO_BREAK_SPACE = "\u202F";
|
|
7
|
+
const TOP_LEVEL_INTERNAL_URL_PREFIXES = [
|
|
8
|
+
"agent://",
|
|
9
|
+
"artifact://",
|
|
10
|
+
"skill://",
|
|
11
|
+
"rule://",
|
|
12
|
+
"local://",
|
|
13
|
+
"mcp://",
|
|
14
|
+
] as const;
|
|
7
15
|
|
|
8
16
|
function normalizeUnicodeSpaces(str: string): string {
|
|
9
17
|
return str.replace(UNICODE_SPACES, " ");
|
|
@@ -38,6 +46,15 @@ function fileExists(filePath: string): boolean {
|
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
|
|
49
|
+
async function pathExists(filePath: string): Promise<boolean> {
|
|
50
|
+
try {
|
|
51
|
+
await fs.promises.access(filePath, fs.constants.F_OK);
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
function normalizeAtPrefix(filePath: string): string {
|
|
42
59
|
if (!filePath.startsWith("@")) return filePath;
|
|
43
60
|
|
|
@@ -105,6 +122,24 @@ export interface ParsedSearchPath {
|
|
|
105
122
|
glob?: string;
|
|
106
123
|
}
|
|
107
124
|
|
|
125
|
+
export interface ParsedFindPattern {
|
|
126
|
+
basePath: string;
|
|
127
|
+
globPattern: string;
|
|
128
|
+
hasGlob: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ResolvedMultiSearchPath {
|
|
132
|
+
basePath: string;
|
|
133
|
+
glob?: string;
|
|
134
|
+
scopePath: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ResolvedMultiFindPattern {
|
|
138
|
+
basePath: string;
|
|
139
|
+
globPattern: string;
|
|
140
|
+
scopePath: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
108
143
|
/**
|
|
109
144
|
* Split a user path into a base path + glob pattern for tools that delegate to
|
|
110
145
|
* APIs accepting separate `path` and `glob` arguments.
|
|
@@ -128,6 +163,44 @@ export function parseSearchPath(filePath: string): ParsedSearchPath {
|
|
|
128
163
|
};
|
|
129
164
|
}
|
|
130
165
|
|
|
166
|
+
// Parse a find pattern into a base directory path and a glob pattern.
|
|
167
|
+
// Examples:
|
|
168
|
+
// src/app/**/\*.tsx -> { basePath: "src/app", globPattern: "**/*.tsx", hasGlob: true }
|
|
169
|
+
// src/app/\*.tsx -> { basePath: "src/app", globPattern: "*.tsx", hasGlob: true }
|
|
170
|
+
// \*.ts -> { basePath: ".", globPattern: "**/*.ts", hasGlob: true }
|
|
171
|
+
// **/\*.json -> { basePath: ".", globPattern: "**/*.json", hasGlob: true }
|
|
172
|
+
// /abs/path/**/\*.ts -> { basePath: "/abs/path", globPattern: "**/*.ts", hasGlob: true }
|
|
173
|
+
// src/app -> { basePath: "src/app", globPattern: "**/*", hasGlob: false }
|
|
174
|
+
export function parseFindPattern(pattern: string): ParsedFindPattern {
|
|
175
|
+
const segments = pattern.split("/");
|
|
176
|
+
let firstGlobIndex = -1;
|
|
177
|
+
for (let i = 0; i < segments.length; i++) {
|
|
178
|
+
if (hasGlobPathChars(segments[i])) {
|
|
179
|
+
firstGlobIndex = i;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (firstGlobIndex === -1) {
|
|
185
|
+
return { basePath: pattern, globPattern: "**/*", hasGlob: false };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (firstGlobIndex === 0) {
|
|
189
|
+
const needsRecursive = !pattern.startsWith("**/");
|
|
190
|
+
return {
|
|
191
|
+
basePath: ".",
|
|
192
|
+
globPattern: needsRecursive ? `**/${pattern}` : pattern,
|
|
193
|
+
hasGlob: true,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
basePath: segments.slice(0, firstGlobIndex).join("/"),
|
|
199
|
+
globPattern: segments.slice(firstGlobIndex).join("/"),
|
|
200
|
+
hasGlob: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
131
204
|
export function combineSearchGlobs(prefixGlob?: string, suffixGlob?: string): string | undefined {
|
|
132
205
|
if (!prefixGlob) return suffixGlob;
|
|
133
206
|
if (!suffixGlob) return prefixGlob;
|
|
@@ -138,6 +211,281 @@ export function combineSearchGlobs(prefixGlob?: string, suffixGlob?: string): st
|
|
|
138
211
|
return `${normalizedPrefix}/${normalizedSuffix}`;
|
|
139
212
|
}
|
|
140
213
|
|
|
214
|
+
type TopLevelSeparator = "comma" | "whitespace";
|
|
215
|
+
|
|
216
|
+
function splitTopLevel(value: string, separator: TopLevelSeparator): string[] {
|
|
217
|
+
const parts: string[] = [];
|
|
218
|
+
let current = "";
|
|
219
|
+
let braceDepth = 0;
|
|
220
|
+
let bracketDepth = 0;
|
|
221
|
+
let parenDepth = 0;
|
|
222
|
+
let quote: '"' | "'" | undefined;
|
|
223
|
+
let escaped = false;
|
|
224
|
+
|
|
225
|
+
const pushCurrent = () => {
|
|
226
|
+
const normalized = current.trim();
|
|
227
|
+
if (normalized.length > 0) {
|
|
228
|
+
parts.push(normalized);
|
|
229
|
+
}
|
|
230
|
+
current = "";
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
for (const char of value) {
|
|
234
|
+
if (escaped) {
|
|
235
|
+
current += char;
|
|
236
|
+
escaped = false;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (char === "\\") {
|
|
241
|
+
current += char;
|
|
242
|
+
escaped = true;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (quote) {
|
|
247
|
+
current += char;
|
|
248
|
+
if (char === quote) {
|
|
249
|
+
quote = undefined;
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (char === '"' || char === "'") {
|
|
255
|
+
quote = char;
|
|
256
|
+
current += char;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (char === "{") braceDepth += 1;
|
|
261
|
+
else if (char === "}" && braceDepth > 0) braceDepth -= 1;
|
|
262
|
+
else if (char === "[") bracketDepth += 1;
|
|
263
|
+
else if (char === "]" && bracketDepth > 0) bracketDepth -= 1;
|
|
264
|
+
else if (char === "(") parenDepth += 1;
|
|
265
|
+
else if (char === ")" && parenDepth > 0) parenDepth -= 1;
|
|
266
|
+
|
|
267
|
+
const topLevel = braceDepth === 0 && bracketDepth === 0 && parenDepth === 0;
|
|
268
|
+
const isWhitespace = /\s/.test(char);
|
|
269
|
+
if (topLevel && separator === "comma" && char === ",") {
|
|
270
|
+
pushCurrent();
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (topLevel && separator === "whitespace" && isWhitespace) {
|
|
274
|
+
pushCurrent();
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
current += char;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
pushCurrent();
|
|
282
|
+
return parts.length > 1 ? parts : [value.trim()];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function normalizePosixPath(filePath: string): string {
|
|
286
|
+
return filePath.replace(/\\/g, "/");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function joinRelativeGlob(basePath: string | undefined, globPattern: string): string {
|
|
290
|
+
if (!basePath || basePath === ".") return normalizePosixPath(globPattern).replace(/^\/+/, "");
|
|
291
|
+
const normalizedBase = normalizePosixPath(basePath).replace(/\/+$/, "");
|
|
292
|
+
const normalizedGlob = normalizePosixPath(globPattern).replace(/^\/+/, "");
|
|
293
|
+
return `${normalizedBase}/${normalizedGlob}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function buildBraceUnion(patterns: string[]): string | undefined {
|
|
297
|
+
const uniquePatterns = [...new Set(patterns.map(pattern => normalizePosixPath(pattern).trim()).filter(Boolean))];
|
|
298
|
+
if (uniquePatterns.length === 0) return undefined;
|
|
299
|
+
if (uniquePatterns.length === 1) return uniquePatterns[0];
|
|
300
|
+
return `{${uniquePatterns.join(",")}}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function findCommonBasePath(paths: string[]): string {
|
|
304
|
+
if (paths.length === 0) return ".";
|
|
305
|
+
let commonParts = path.resolve(paths[0]).split(path.sep);
|
|
306
|
+
for (const candidatePath of paths.slice(1)) {
|
|
307
|
+
const candidateParts = path.resolve(candidatePath).split(path.sep);
|
|
308
|
+
let sharedCount = 0;
|
|
309
|
+
const maxShared = Math.min(commonParts.length, candidateParts.length);
|
|
310
|
+
while (sharedCount < maxShared && commonParts[sharedCount] === candidateParts[sharedCount]) {
|
|
311
|
+
sharedCount += 1;
|
|
312
|
+
}
|
|
313
|
+
commonParts = commonParts.slice(0, sharedCount);
|
|
314
|
+
}
|
|
315
|
+
if (commonParts.length === 0) {
|
|
316
|
+
return path.parse(path.resolve(paths[0])).root;
|
|
317
|
+
}
|
|
318
|
+
const joined = commonParts.join(path.sep);
|
|
319
|
+
return joined || path.parse(path.resolve(paths[0])).root;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function toScopeDisplay(items: string[]): string {
|
|
323
|
+
return items.map(item => normalizePosixPath(item)).join(", ");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function looksLikeDelimitedPathToken(token: string): boolean {
|
|
327
|
+
return (
|
|
328
|
+
TOP_LEVEL_INTERNAL_URL_PREFIXES.some(prefix => token.startsWith(prefix)) ||
|
|
329
|
+
token.startsWith(".") ||
|
|
330
|
+
token.startsWith("/") ||
|
|
331
|
+
token.startsWith("~") ||
|
|
332
|
+
token.startsWith("@") ||
|
|
333
|
+
token.includes("/") ||
|
|
334
|
+
token.includes("\\") ||
|
|
335
|
+
hasGlobPathChars(token) ||
|
|
336
|
+
/\.[^./\\]+$/.test(token)
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function areDelimitedTokensResolvable(
|
|
341
|
+
tokens: string[],
|
|
342
|
+
cwd: string,
|
|
343
|
+
parseBasePath: (value: string) => string,
|
|
344
|
+
allowBareExistingTokens: boolean,
|
|
345
|
+
): Promise<boolean> {
|
|
346
|
+
for (const token of tokens) {
|
|
347
|
+
if (TOP_LEVEL_INTERNAL_URL_PREFIXES.some(prefix => token.startsWith(prefix))) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!allowBareExistingTokens && !looksLikeDelimitedPathToken(token)) {
|
|
352
|
+
// Bare names like "packages" don't look like path tokens syntactically,
|
|
353
|
+
// but may still be valid directory names. Check existence before rejecting.
|
|
354
|
+
const resolvedExactPath = resolveToCwd(token, cwd);
|
|
355
|
+
if (!(await pathExists(resolvedExactPath))) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const basePath = parseBasePath(token);
|
|
362
|
+
const resolvedBasePath = resolveToCwd(basePath, cwd);
|
|
363
|
+
if (await pathExists(resolvedBasePath)) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!allowBareExistingTokens) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const resolvedExactPath = resolveToCwd(token, cwd);
|
|
372
|
+
if (!(await pathExists(resolvedExactPath))) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function splitDelimitedSearchInput(
|
|
381
|
+
rawInput: string,
|
|
382
|
+
cwd: string,
|
|
383
|
+
parseBasePath: (value: string) => string,
|
|
384
|
+
): Promise<string[] | undefined> {
|
|
385
|
+
const trimmed = rawInput.trim();
|
|
386
|
+
if (!trimmed) return undefined;
|
|
387
|
+
|
|
388
|
+
const resolvedExactPath = resolveToCwd(trimmed, cwd);
|
|
389
|
+
if (await pathExists(resolvedExactPath)) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const commaSeparated = splitTopLevel(trimmed, "comma");
|
|
394
|
+
if (commaSeparated.length > 1 && (await areDelimitedTokensResolvable(commaSeparated, cwd, parseBasePath, true))) {
|
|
395
|
+
return [...new Set(commaSeparated)];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const whitespaceSeparated = splitTopLevel(trimmed, "whitespace");
|
|
399
|
+
if (
|
|
400
|
+
whitespaceSeparated.length > 1 &&
|
|
401
|
+
(await areDelimitedTokensResolvable(whitespaceSeparated, cwd, parseBasePath, false))
|
|
402
|
+
) {
|
|
403
|
+
return [...new Set(whitespaceSeparated)];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export async function resolveMultiSearchPath(
|
|
410
|
+
rawPath: string,
|
|
411
|
+
cwd: string,
|
|
412
|
+
suffixGlob?: string,
|
|
413
|
+
): Promise<ResolvedMultiSearchPath | undefined> {
|
|
414
|
+
const pathItems = await splitDelimitedSearchInput(rawPath, cwd, value => parseSearchPath(value).basePath);
|
|
415
|
+
if (!pathItems || pathItems.length <= 1) {
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const parsedItems = await Promise.all(
|
|
420
|
+
pathItems.map(async item => {
|
|
421
|
+
const parsedPath = parseSearchPath(item);
|
|
422
|
+
const absoluteBasePath = resolveToCwd(parsedPath.basePath, cwd);
|
|
423
|
+
const stat = await fs.promises.stat(absoluteBasePath);
|
|
424
|
+
return { raw: item, parsedPath, absoluteBasePath, stat };
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const commonBasePath = findCommonBasePath(parsedItems.map(item => item.absoluteBasePath));
|
|
429
|
+
const combinedPatterns = parsedItems.map(item => {
|
|
430
|
+
const relativeBasePath = normalizePosixPath(path.relative(commonBasePath, item.absoluteBasePath)) || ".";
|
|
431
|
+
if (item.parsedPath.glob) {
|
|
432
|
+
const pathGlob = joinRelativeGlob(relativeBasePath, item.parsedPath.glob);
|
|
433
|
+
return combineSearchGlobs(pathGlob, suffixGlob) ?? pathGlob;
|
|
434
|
+
}
|
|
435
|
+
if (suffixGlob) {
|
|
436
|
+
const pathPrefix = relativeBasePath === "." ? undefined : relativeBasePath;
|
|
437
|
+
return combineSearchGlobs(pathPrefix, suffixGlob) ?? suffixGlob;
|
|
438
|
+
}
|
|
439
|
+
if (item.stat.isDirectory()) {
|
|
440
|
+
return joinRelativeGlob(relativeBasePath, "**/*");
|
|
441
|
+
}
|
|
442
|
+
return relativeBasePath === "." ? path.basename(item.absoluteBasePath) : relativeBasePath;
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
basePath: commonBasePath,
|
|
447
|
+
glob: buildBraceUnion(combinedPatterns),
|
|
448
|
+
scopePath: toScopeDisplay(pathItems),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export async function resolveMultiFindPattern(
|
|
453
|
+
rawPattern: string,
|
|
454
|
+
cwd: string,
|
|
455
|
+
): Promise<ResolvedMultiFindPattern | undefined> {
|
|
456
|
+
const patternItems = await splitDelimitedSearchInput(rawPattern, cwd, value => parseFindPattern(value).basePath);
|
|
457
|
+
if (!patternItems || patternItems.length <= 1) {
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const parsedItems = await Promise.all(
|
|
462
|
+
patternItems.map(async item => {
|
|
463
|
+
const parsedPattern = parseFindPattern(item);
|
|
464
|
+
const absoluteBasePath = resolveToCwd(parsedPattern.basePath, cwd);
|
|
465
|
+
const stat = await fs.promises.stat(absoluteBasePath);
|
|
466
|
+
return { raw: item, parsedPattern, absoluteBasePath, stat };
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const commonBasePath = findCommonBasePath(parsedItems.map(item => item.absoluteBasePath));
|
|
471
|
+
const combinedPatterns = parsedItems.map(item => {
|
|
472
|
+
const relativeBasePath = normalizePosixPath(path.relative(commonBasePath, item.absoluteBasePath)) || ".";
|
|
473
|
+
if (item.parsedPattern.hasGlob) {
|
|
474
|
+
return joinRelativeGlob(relativeBasePath, item.parsedPattern.globPattern);
|
|
475
|
+
}
|
|
476
|
+
if (item.stat.isDirectory()) {
|
|
477
|
+
return joinRelativeGlob(relativeBasePath, "**/*");
|
|
478
|
+
}
|
|
479
|
+
return relativeBasePath === "." ? path.basename(item.absoluteBasePath) : relativeBasePath;
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
basePath: commonBasePath,
|
|
484
|
+
globPattern: buildBraceUnion(combinedPatterns) ?? "**/*",
|
|
485
|
+
scopePath: toScopeDisplay(patternItems),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
141
489
|
export function resolveReadPath(filePath: string, cwd: string): string {
|
|
142
490
|
const resolved = resolveToCwd(filePath, cwd);
|
|
143
491
|
const shellEscapedVariant = tryShellEscapedPath(resolved);
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface TodoItem {
|
|
|
24
24
|
content: string;
|
|
25
25
|
status: TodoStatus;
|
|
26
26
|
notes?: string;
|
|
27
|
+
details?: string;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export interface TodoPhase {
|
|
@@ -49,6 +50,9 @@ const InputTask = Type.Object({
|
|
|
49
50
|
content: Type.String({ description: "Task description" }),
|
|
50
51
|
status: Type.Optional(StatusEnum),
|
|
51
52
|
notes: Type.Optional(Type.String({ description: "Additional context or notes" })),
|
|
53
|
+
details: Type.Optional(
|
|
54
|
+
Type.String({ description: "Implementation details, file paths, and specifics (shown only when active)" }),
|
|
55
|
+
),
|
|
52
56
|
});
|
|
53
57
|
|
|
54
58
|
const InputPhase = Type.Object({
|
|
@@ -73,6 +77,7 @@ const todoWriteSchema = Type.Object({
|
|
|
73
77
|
phase: Type.String({ description: "Phase ID, e.g. phase-1" }),
|
|
74
78
|
content: Type.String({ description: "Task description" }),
|
|
75
79
|
notes: Type.Optional(Type.String({ description: "Additional context or notes" })),
|
|
80
|
+
details: Type.Optional(Type.String({ description: "Implementation details, file paths, and specifics" })),
|
|
76
81
|
}),
|
|
77
82
|
Type.Object({
|
|
78
83
|
op: Type.Literal("update"),
|
|
@@ -80,6 +85,7 @@ const todoWriteSchema = Type.Object({
|
|
|
80
85
|
status: Type.Optional(StatusEnum),
|
|
81
86
|
content: Type.Optional(Type.String({ description: "Updated task description" })),
|
|
82
87
|
notes: Type.Optional(Type.String({ description: "Additional context or notes" })),
|
|
88
|
+
details: Type.Optional(Type.String({ description: "Updated details" })),
|
|
83
89
|
}),
|
|
84
90
|
Type.Object({
|
|
85
91
|
op: Type.Literal("remove_task"),
|
|
@@ -118,14 +124,20 @@ function findTask(phases: TodoPhase[], id: string): TodoItem | undefined {
|
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
function buildPhaseFromInput(
|
|
121
|
-
input: { name: string; tasks?: Array<{ content: string; status?: TodoStatus; notes?: string }> },
|
|
127
|
+
input: { name: string; tasks?: Array<{ content: string; status?: TodoStatus; notes?: string; details?: string }> },
|
|
122
128
|
phaseId: string,
|
|
123
129
|
nextTaskId: number,
|
|
124
130
|
): { phase: TodoPhase; nextTaskId: number } {
|
|
125
131
|
const tasks: TodoItem[] = [];
|
|
126
132
|
let tid = nextTaskId;
|
|
127
133
|
for (const t of input.tasks ?? []) {
|
|
128
|
-
tasks.push({
|
|
134
|
+
tasks.push({
|
|
135
|
+
id: `task-${tid++}`,
|
|
136
|
+
content: t.content,
|
|
137
|
+
status: t.status ?? "pending",
|
|
138
|
+
notes: t.notes,
|
|
139
|
+
details: t.details,
|
|
140
|
+
});
|
|
129
141
|
}
|
|
130
142
|
return { phase: { id: phaseId, name: input.name, tasks }, nextTaskId: tid };
|
|
131
143
|
}
|
|
@@ -231,6 +243,7 @@ function applyOps(file: TodoFile, ops: TodoWriteParams["ops"]): { file: TodoFile
|
|
|
231
243
|
content: op.content,
|
|
232
244
|
status: "pending",
|
|
233
245
|
notes: op.notes,
|
|
246
|
+
details: op.details,
|
|
234
247
|
});
|
|
235
248
|
break;
|
|
236
249
|
}
|
|
@@ -244,6 +257,7 @@ function applyOps(file: TodoFile, ops: TodoWriteParams["ops"]): { file: TodoFile
|
|
|
244
257
|
if (op.status !== undefined) task.status = op.status;
|
|
245
258
|
if (op.content !== undefined) task.content = op.content;
|
|
246
259
|
if (op.notes !== undefined) task.notes = op.notes;
|
|
260
|
+
if (op.details !== undefined) task.details = op.details;
|
|
247
261
|
break;
|
|
248
262
|
}
|
|
249
263
|
|
|
@@ -293,6 +307,11 @@ function formatSummary(phases: TodoPhase[], errors: string[]): string {
|
|
|
293
307
|
lines.push(`Remaining items (${remainingTasks.length}):`);
|
|
294
308
|
for (const task of remainingTasks) {
|
|
295
309
|
lines.push(` - ${task.id} ${task.content} [${task.status}] (${task.phase})`);
|
|
310
|
+
if (task.status === "in_progress" && task.details) {
|
|
311
|
+
for (const line of task.details.split("\n")) {
|
|
312
|
+
lines.push(` ${line}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
296
315
|
}
|
|
297
316
|
}
|
|
298
317
|
lines.push(
|
|
@@ -364,8 +383,12 @@ function formatTodoLine(item: TodoItem, uiTheme: Theme, prefix: string): string
|
|
|
364
383
|
switch (item.status) {
|
|
365
384
|
case "completed":
|
|
366
385
|
return uiTheme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(item.content)}`);
|
|
367
|
-
case "in_progress":
|
|
368
|
-
|
|
386
|
+
case "in_progress": {
|
|
387
|
+
const main = uiTheme.fg("accent", `${prefix}${checkbox.unchecked} ${item.content}`);
|
|
388
|
+
if (!item.details) return main;
|
|
389
|
+
const detailLines = item.details.split("\n").map(l => uiTheme.fg("dim", `${prefix} ${l}`));
|
|
390
|
+
return [main, ...detailLines].join("\n");
|
|
391
|
+
}
|
|
369
392
|
case "abandoned":
|
|
370
393
|
return uiTheme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(item.content)}`);
|
|
371
394
|
default:
|
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
* Generate commit messages from diffs using a smol, fast model.
|
|
3
3
|
* Follows the same pattern as title-generator.ts.
|
|
4
4
|
*/
|
|
5
|
+
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
5
6
|
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
|
6
7
|
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
7
8
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
8
9
|
import type { ModelRegistry } from "../config/model-registry";
|
|
9
|
-
import {
|
|
10
|
+
import { resolveModelRoleValue } from "../config/model-resolver";
|
|
10
11
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
12
|
+
import type { Settings } from "../config/settings";
|
|
11
13
|
import MODEL_PRIO from "../priority.json" with { type: "json" };
|
|
12
14
|
import commitSystemPrompt from "../prompts/system/commit-message-system.md" with { type: "text" };
|
|
15
|
+
import { toReasoningEffort } from "../thinking";
|
|
13
16
|
|
|
14
17
|
const COMMIT_SYSTEM_PROMPT = renderPromptTemplate(commitSystemPrompt);
|
|
15
18
|
const MAX_DIFF_CHARS = 4000;
|
|
@@ -32,24 +35,26 @@ function filterDiffNoise(diff: string): string {
|
|
|
32
35
|
return filtered.join("\n");
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
function getSmolModelCandidates(
|
|
38
|
+
function getSmolModelCandidates(
|
|
39
|
+
registry: ModelRegistry,
|
|
40
|
+
settings: Settings,
|
|
41
|
+
): Array<{ model: Model<Api>; thinkingLevel?: ThinkingLevel }> {
|
|
36
42
|
const availableModels = registry.getAvailable();
|
|
37
43
|
if (availableModels.length === 0) return [];
|
|
38
44
|
|
|
39
|
-
const candidates: Model<Api>
|
|
40
|
-
const addCandidate = (model?: Model<Api
|
|
45
|
+
const candidates: Array<{ model: Model<Api>; thinkingLevel?: ThinkingLevel }> = [];
|
|
46
|
+
const addCandidate = (model?: Model<Api>, thinkingLevel?: ThinkingLevel): void => {
|
|
41
47
|
if (!model) return;
|
|
42
|
-
if (candidates.some(c => c.provider === model.provider && c.id === model.id)) return;
|
|
43
|
-
candidates.push(model);
|
|
48
|
+
if (candidates.some(c => c.model.provider === model.provider && c.model.id === model.id)) return;
|
|
49
|
+
candidates.push({ model, thinkingLevel });
|
|
44
50
|
};
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
52
|
+
const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
|
|
53
|
+
const configuredSmol = resolveModelRoleValue(settings.getModelRole("smol"), availableModels, {
|
|
54
|
+
settings,
|
|
55
|
+
matchPreferences,
|
|
56
|
+
});
|
|
57
|
+
addCandidate(configuredSmol.model, configuredSmol.thinkingLevel);
|
|
53
58
|
|
|
54
59
|
for (const pattern of MODEL_PRIO.smol) {
|
|
55
60
|
const needle = pattern.toLowerCase();
|
|
@@ -71,10 +76,10 @@ function getSmolModelCandidates(registry: ModelRegistry, savedSmolModel?: string
|
|
|
71
76
|
export async function generateCommitMessage(
|
|
72
77
|
diff: string,
|
|
73
78
|
registry: ModelRegistry,
|
|
74
|
-
|
|
79
|
+
settings: Settings,
|
|
75
80
|
sessionId?: string,
|
|
76
81
|
): Promise<string | null> {
|
|
77
|
-
const candidates = getSmolModelCandidates(registry,
|
|
82
|
+
const candidates = getSmolModelCandidates(registry, settings);
|
|
78
83
|
if (candidates.length === 0) {
|
|
79
84
|
logger.debug("commit-msg-generator: no smol model found");
|
|
80
85
|
return null;
|
|
@@ -89,22 +94,22 @@ export async function generateCommitMessage(
|
|
|
89
94
|
}
|
|
90
95
|
const userMessage = `<diff>\n${truncatedDiff}\n</diff>`;
|
|
91
96
|
|
|
92
|
-
for (const
|
|
93
|
-
const apiKey = await registry.getApiKey(model, sessionId);
|
|
97
|
+
for (const candidate of candidates) {
|
|
98
|
+
const apiKey = await registry.getApiKey(candidate.model, sessionId);
|
|
94
99
|
if (!apiKey) continue;
|
|
95
100
|
|
|
96
101
|
try {
|
|
97
102
|
const response = await completeSimple(
|
|
98
|
-
model,
|
|
103
|
+
candidate.model,
|
|
99
104
|
{
|
|
100
105
|
systemPrompt: COMMIT_SYSTEM_PROMPT,
|
|
101
106
|
messages: [{ role: "user", content: userMessage, timestamp: Date.now() }],
|
|
102
107
|
},
|
|
103
|
-
{ apiKey, maxTokens: 60 },
|
|
108
|
+
{ apiKey, maxTokens: 60, reasoning: toReasoningEffort(candidate.thinkingLevel) },
|
|
104
109
|
);
|
|
105
110
|
|
|
106
111
|
if (response.stopReason === "error") {
|
|
107
|
-
logger.debug("commit-msg-generator: error", { model: model.id, error: response.errorMessage });
|
|
112
|
+
logger.debug("commit-msg-generator: error", { model: candidate.model.id, error: response.errorMessage });
|
|
108
113
|
continue;
|
|
109
114
|
}
|
|
110
115
|
|
|
@@ -118,11 +123,11 @@ export async function generateCommitMessage(
|
|
|
118
123
|
// Clean up: remove wrapping quotes, backticks, trailing period
|
|
119
124
|
msg = msg.replace(/^[`"']|[`"']$/g, "").replace(/\.$/, "");
|
|
120
125
|
|
|
121
|
-
logger.debug("commit-msg-generator: generated", { model: model.id, msg });
|
|
126
|
+
logger.debug("commit-msg-generator: generated", { model: candidate.model.id, msg });
|
|
122
127
|
return msg;
|
|
123
128
|
} catch (err) {
|
|
124
129
|
logger.debug("commit-msg-generator: error", {
|
|
125
|
-
model: model.id,
|
|
130
|
+
model: candidate.model.id,
|
|
126
131
|
error: err instanceof Error ? err.message : String(err),
|
|
127
132
|
});
|
|
128
133
|
}
|
package/src/utils/image-input.ts
CHANGED
|
@@ -231,7 +231,7 @@ export async function loadImageInput(options: LoadImageInputOptions): Promise<Lo
|
|
|
231
231
|
throw new ImageInputTooLargeError(inputBuffer.byteLength, maxBytes);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
let outputData =
|
|
234
|
+
let outputData = Buffer.from(inputBuffer).toBase64();
|
|
235
235
|
let outputMimeType = mimeType;
|
|
236
236
|
let outputBytes = inputBuffer.byteLength;
|
|
237
237
|
let dimensionNote: string | undefined;
|
|
@@ -136,7 +136,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
136
136
|
height: finalHeight,
|
|
137
137
|
wasResized: true,
|
|
138
138
|
get data() {
|
|
139
|
-
return best.buffer.toBase64();
|
|
139
|
+
return Buffer.from(best.buffer).toBase64();
|
|
140
140
|
},
|
|
141
141
|
};
|
|
142
142
|
}
|
|
@@ -155,7 +155,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
155
155
|
height: finalHeight,
|
|
156
156
|
wasResized: true,
|
|
157
157
|
get data() {
|
|
158
|
-
return best.buffer.toBase64();
|
|
158
|
+
return Buffer.from(best.buffer).toBase64();
|
|
159
159
|
},
|
|
160
160
|
};
|
|
161
161
|
}
|
|
@@ -183,7 +183,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
183
183
|
height: finalHeight,
|
|
184
184
|
wasResized: true,
|
|
185
185
|
get data() {
|
|
186
|
-
return best.buffer.toBase64();
|
|
186
|
+
return Buffer.from(best.buffer).toBase64();
|
|
187
187
|
},
|
|
188
188
|
};
|
|
189
189
|
}
|
|
@@ -200,7 +200,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
200
200
|
height: finalHeight,
|
|
201
201
|
wasResized: true,
|
|
202
202
|
get data() {
|
|
203
|
-
return best.buffer.toBase64();
|
|
203
|
+
return Buffer.from(best.buffer).toBase64();
|
|
204
204
|
},
|
|
205
205
|
};
|
|
206
206
|
} catch {
|