@oh-my-pi/pi-coding-agent 14.5.13 → 14.6.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 +52 -0
- package/package.json +7 -7
- package/src/autoresearch/command-resume.md +5 -8
- package/src/autoresearch/git.ts +41 -51
- package/src/autoresearch/helpers.ts +43 -359
- package/src/autoresearch/index.ts +281 -273
- package/src/autoresearch/prompt-setup.md +43 -0
- package/src/autoresearch/prompt.md +52 -193
- package/src/autoresearch/resume-message.md +2 -8
- package/src/autoresearch/state.ts +59 -166
- package/src/autoresearch/storage.ts +687 -0
- package/src/autoresearch/tools/init-experiment.ts +201 -290
- package/src/autoresearch/tools/log-experiment.ts +304 -517
- package/src/autoresearch/tools/run-experiment.ts +117 -296
- package/src/autoresearch/tools/update-notes.ts +116 -0
- package/src/autoresearch/types.ts +16 -66
- package/src/commit/pipeline.ts +4 -3
- package/src/config/settings-schema.ts +1 -1
- package/src/config/settings.ts +20 -1
- package/src/config.ts +9 -6
- package/src/cursor.ts +1 -1
- package/src/edit/index.ts +9 -31
- package/src/edit/line-hash.ts +70 -43
- package/src/edit/modes/hashline.lark +26 -0
- package/src/edit/modes/hashline.ts +898 -1099
- package/src/edit/modes/patch.ts +0 -7
- package/src/edit/modes/replace.ts +0 -4
- package/src/edit/renderer.ts +22 -20
- package/src/edit/streaming.ts +8 -28
- package/src/eval/eval.lark +24 -30
- package/src/eval/js/context-manager.ts +5 -162
- package/src/eval/js/prelude.txt +0 -12
- package/src/eval/parse.ts +129 -129
- package/src/eval/py/kernel.ts +4 -4
- package/src/eval/py/prelude.py +1 -219
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +2 -2
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +10 -0
- package/src/mcp/manager.ts +22 -0
- package/src/modes/components/session-observer-overlay.ts +5 -2
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tree-selector.ts +4 -5
- package/src/modes/components/welcome.ts +11 -1
- package/src/modes/controllers/command-controller.ts +2 -6
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/extension-ui-controller.ts +3 -15
- package/src/modes/controllers/input-controller.ts +0 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -7
- package/src/modes/rpc/rpc-client.ts +9 -0
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/prompts/system/system-prompt.md +14 -38
- package/src/prompts/tools/ast-edit.md +8 -8
- package/src/prompts/tools/ast-grep.md +10 -10
- package/src/prompts/tools/eval.md +13 -31
- package/src/prompts/tools/find.md +2 -1
- package/src/prompts/tools/hashline.md +66 -57
- package/src/prompts/tools/search.md +2 -2
- package/src/sdk.ts +19 -4
- package/src/session/agent-session.ts +110 -4
- package/src/session/session-manager.ts +17 -13
- package/src/task/agents.ts +4 -5
- package/src/tools/archive-reader.ts +9 -3
- package/src/tools/ast-edit.ts +141 -44
- package/src/tools/ast-grep.ts +112 -36
- package/src/tools/browser/readable.ts +11 -6
- package/src/tools/browser/tab-supervisor.ts +2 -2
- package/src/tools/browser.ts +5 -3
- package/src/tools/eval.ts +2 -53
- package/src/tools/find.ts +16 -15
- package/src/tools/image-gen.ts +2 -2
- package/src/tools/path-utils.ts +36 -196
- package/src/tools/search.ts +56 -35
- package/src/tools/write.ts +8 -1
- package/src/utils/edit-mode.ts +2 -11
- package/src/utils/file-display-mode.ts +1 -1
- package/src/utils/git.ts +17 -0
- package/src/utils/session-color.ts +0 -12
- package/src/utils/title-generator.ts +22 -38
- package/src/web/scrapers/crossref.ts +3 -3
- package/src/web/scrapers/devto.ts +1 -1
- package/src/web/scrapers/discourse.ts +5 -5
- package/src/web/scrapers/firefox-addons.ts +1 -1
- package/src/web/scrapers/flathub.ts +2 -2
- package/src/web/scrapers/gitlab.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/jetbrains-marketplace.ts +1 -1
- package/src/web/scrapers/mastodon.ts +9 -9
- package/src/web/scrapers/mdn.ts +11 -7
- package/src/web/scrapers/pub-dev.ts +1 -1
- package/src/web/scrapers/rawg.ts +3 -3
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/spdx.ts +1 -1
- package/src/web/scrapers/stackoverflow.ts +2 -2
- package/src/web/scrapers/types.ts +53 -39
- package/src/web/scrapers/w3c.ts +1 -1
- package/src/web/search/providers/gemini.ts +2 -2
- package/src/autoresearch/apply-contract-to-state.ts +0 -24
- package/src/autoresearch/contract.ts +0 -288
- package/src/edit/modes/atom.lark +0 -29
- package/src/edit/modes/atom.ts +0 -1773
- package/src/prompts/tools/atom.md +0 -150
package/src/tools/path-utils.ts
CHANGED
|
@@ -47,15 +47,6 @@ function fileExists(filePath: string): boolean {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
async function pathExists(filePath: string): Promise<boolean> {
|
|
51
|
-
try {
|
|
52
|
-
await fs.promises.access(filePath, fs.constants.F_OK);
|
|
53
|
-
return true;
|
|
54
|
-
} catch {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
50
|
function normalizeAtPrefix(filePath: string): string {
|
|
60
51
|
if (!filePath.startsWith("@")) return filePath;
|
|
61
52
|
|
|
@@ -210,11 +201,17 @@ export interface ParsedFindPattern {
|
|
|
210
201
|
hasGlob: boolean;
|
|
211
202
|
}
|
|
212
203
|
|
|
204
|
+
export interface ResolvedSearchTarget {
|
|
205
|
+
basePath: string;
|
|
206
|
+
glob?: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
213
209
|
export interface ResolvedMultiSearchPath {
|
|
214
210
|
basePath: string;
|
|
215
211
|
glob?: string;
|
|
216
212
|
scopePath: string;
|
|
217
213
|
exactFilePaths?: string[];
|
|
214
|
+
targets?: ResolvedSearchTarget[];
|
|
218
215
|
}
|
|
219
216
|
|
|
220
217
|
export interface ResolvedMultiFindPattern {
|
|
@@ -294,77 +291,6 @@ export function combineSearchGlobs(prefixGlob?: string, suffixGlob?: string): st
|
|
|
294
291
|
return `${normalizedPrefix}/${normalizedSuffix}`;
|
|
295
292
|
}
|
|
296
293
|
|
|
297
|
-
type TopLevelSeparator = "comma" | "whitespace";
|
|
298
|
-
|
|
299
|
-
function splitTopLevel(value: string, separator: TopLevelSeparator): string[] {
|
|
300
|
-
const parts: string[] = [];
|
|
301
|
-
let current = "";
|
|
302
|
-
let braceDepth = 0;
|
|
303
|
-
let bracketDepth = 0;
|
|
304
|
-
let parenDepth = 0;
|
|
305
|
-
let quote: '"' | "'" | undefined;
|
|
306
|
-
let escaped = false;
|
|
307
|
-
|
|
308
|
-
const pushCurrent = () => {
|
|
309
|
-
const normalized = current.trim();
|
|
310
|
-
if (normalized.length > 0) {
|
|
311
|
-
parts.push(normalized);
|
|
312
|
-
}
|
|
313
|
-
current = "";
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
for (const char of value) {
|
|
317
|
-
if (escaped) {
|
|
318
|
-
current += char;
|
|
319
|
-
escaped = false;
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (char === "\\") {
|
|
324
|
-
current += char;
|
|
325
|
-
escaped = true;
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (quote) {
|
|
330
|
-
current += char;
|
|
331
|
-
if (char === quote) {
|
|
332
|
-
quote = undefined;
|
|
333
|
-
}
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (char === '"' || char === "'") {
|
|
338
|
-
quote = char;
|
|
339
|
-
current += char;
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (char === "{") braceDepth += 1;
|
|
344
|
-
else if (char === "}" && braceDepth > 0) braceDepth -= 1;
|
|
345
|
-
else if (char === "[") bracketDepth += 1;
|
|
346
|
-
else if (char === "]" && bracketDepth > 0) bracketDepth -= 1;
|
|
347
|
-
else if (char === "(") parenDepth += 1;
|
|
348
|
-
else if (char === ")" && parenDepth > 0) parenDepth -= 1;
|
|
349
|
-
|
|
350
|
-
const topLevel = braceDepth === 0 && bracketDepth === 0 && parenDepth === 0;
|
|
351
|
-
const isWhitespace = /\s/.test(char);
|
|
352
|
-
if (topLevel && separator === "comma" && char === ",") {
|
|
353
|
-
pushCurrent();
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
if (topLevel && separator === "whitespace" && isWhitespace) {
|
|
357
|
-
pushCurrent();
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
current += char;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
pushCurrent();
|
|
365
|
-
return parts.length > 1 ? parts : [value.trim()];
|
|
366
|
-
}
|
|
367
|
-
|
|
368
294
|
function normalizePosixPath(filePath: string): string {
|
|
369
295
|
return filePath.replace(/\\/g, "/");
|
|
370
296
|
}
|
|
@@ -412,121 +338,12 @@ function toScopeDisplay(items: string[], cwd: string): string {
|
|
|
412
338
|
.join(", ");
|
|
413
339
|
}
|
|
414
340
|
|
|
415
|
-
function
|
|
416
|
-
|
|
417
|
-
TOP_LEVEL_INTERNAL_URL_PREFIXES.some(prefix => token.startsWith(prefix)) ||
|
|
418
|
-
token.startsWith(".") ||
|
|
419
|
-
token.startsWith("/") ||
|
|
420
|
-
token.startsWith("~") ||
|
|
421
|
-
token.startsWith("@") ||
|
|
422
|
-
token.includes("/") ||
|
|
423
|
-
token.includes("\\") ||
|
|
424
|
-
hasGlobPathChars(token) ||
|
|
425
|
-
/\.[^./\\]+$/.test(token)
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
async function areDelimitedTokensResolvable(
|
|
430
|
-
tokens: string[],
|
|
431
|
-
cwd: string,
|
|
432
|
-
parseBasePath: (value: string) => string,
|
|
433
|
-
allowBareExistingTokens: boolean,
|
|
434
|
-
): Promise<boolean> {
|
|
435
|
-
for (const token of tokens) {
|
|
436
|
-
if (TOP_LEVEL_INTERNAL_URL_PREFIXES.some(prefix => token.startsWith(prefix))) {
|
|
437
|
-
return false;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (!allowBareExistingTokens && !looksLikeDelimitedPathToken(token)) {
|
|
441
|
-
// Bare names like "packages" don't look like path tokens syntactically,
|
|
442
|
-
// but may still be valid directory names. Check existence before rejecting.
|
|
443
|
-
const resolvedExactPath = resolveToCwd(token, cwd);
|
|
444
|
-
if (!(await pathExists(resolvedExactPath))) {
|
|
445
|
-
return false;
|
|
446
|
-
}
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const basePath = parseBasePath(token);
|
|
451
|
-
const resolvedBasePath = resolveToCwd(basePath, cwd);
|
|
452
|
-
if (await pathExists(resolvedBasePath)) {
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (!allowBareExistingTokens) {
|
|
457
|
-
return false;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const resolvedExactPath = resolveToCwd(token, cwd);
|
|
461
|
-
if (!(await pathExists(resolvedExactPath))) {
|
|
462
|
-
return false;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
return true;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
async function filterResolvableTokens(
|
|
470
|
-
tokens: string[],
|
|
471
|
-
cwd: string,
|
|
472
|
-
parseBasePath: (value: string) => string,
|
|
473
|
-
): Promise<string[]> {
|
|
474
|
-
const out: string[] = [];
|
|
475
|
-
for (const token of tokens) {
|
|
476
|
-
if (TOP_LEVEL_INTERNAL_URL_PREFIXES.some(prefix => token.startsWith(prefix))) continue;
|
|
477
|
-
const basePath = parseBasePath(token);
|
|
478
|
-
const resolvedBasePath = resolveToCwd(basePath, cwd);
|
|
479
|
-
if (await pathExists(resolvedBasePath)) {
|
|
480
|
-
out.push(token);
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
const resolvedExactPath = resolveToCwd(token, cwd);
|
|
484
|
-
if (await pathExists(resolvedExactPath)) {
|
|
485
|
-
out.push(token);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return out;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
async function splitDelimitedSearchInput(
|
|
492
|
-
rawInput: string,
|
|
493
|
-
cwd: string,
|
|
494
|
-
parseBasePath: (value: string) => string,
|
|
495
|
-
): Promise<string[] | undefined> {
|
|
496
|
-
const trimmed = rawInput.trim();
|
|
497
|
-
if (!trimmed) return undefined;
|
|
498
|
-
|
|
499
|
-
const resolvedExactPath = resolveToCwd(trimmed, cwd);
|
|
500
|
-
if (await pathExists(resolvedExactPath)) {
|
|
501
|
-
return undefined;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const commaSeparated = splitTopLevel(trimmed, "comma");
|
|
505
|
-
if (commaSeparated.length > 1) {
|
|
506
|
-
const resolvable = await filterResolvableTokens(commaSeparated, cwd, parseBasePath);
|
|
507
|
-
if (resolvable.length >= 1) {
|
|
508
|
-
return [...new Set(resolvable)];
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const whitespaceSeparated = splitTopLevel(trimmed, "whitespace");
|
|
513
|
-
if (
|
|
514
|
-
whitespaceSeparated.length > 1 &&
|
|
515
|
-
(await areDelimitedTokensResolvable(whitespaceSeparated, cwd, parseBasePath, false))
|
|
516
|
-
) {
|
|
517
|
-
return [...new Set(whitespaceSeparated)];
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
return undefined;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
export async function resolveMultiSearchPath(
|
|
524
|
-
rawPath: string,
|
|
341
|
+
async function resolveSearchPathItems(
|
|
342
|
+
pathItems: string[],
|
|
525
343
|
cwd: string,
|
|
526
344
|
suffixGlob?: string,
|
|
527
345
|
): Promise<ResolvedMultiSearchPath | undefined> {
|
|
528
|
-
|
|
529
|
-
if (!pathItems || pathItems.length < 1) {
|
|
346
|
+
if (pathItems.length < 1) {
|
|
530
347
|
return undefined;
|
|
531
348
|
}
|
|
532
349
|
|
|
@@ -556,21 +373,37 @@ export async function resolveMultiSearchPath(
|
|
|
556
373
|
}
|
|
557
374
|
return relativeBasePath === "." ? path.basename(item.absoluteBasePath) : relativeBasePath;
|
|
558
375
|
});
|
|
376
|
+
const rootPath = path.parse(commonBasePath).root;
|
|
377
|
+
const isDegenerateRoot = commonBasePath === rootPath && parsedItems.length > 1;
|
|
378
|
+
const targets = isDegenerateRoot
|
|
379
|
+
? parsedItems.map(item => ({
|
|
380
|
+
basePath: item.absoluteBasePath,
|
|
381
|
+
glob: item.parsedPath.glob ? combineSearchGlobs(item.parsedPath.glob, suffixGlob) : suffixGlob,
|
|
382
|
+
}))
|
|
383
|
+
: undefined;
|
|
559
384
|
|
|
560
385
|
return {
|
|
561
386
|
basePath: commonBasePath,
|
|
562
387
|
glob: buildBraceUnion(combinedPatterns),
|
|
563
388
|
scopePath: toScopeDisplay(pathItems, cwd),
|
|
564
389
|
exactFilePaths: allExactFiles ? parsedItems.map(item => item.absoluteBasePath) : undefined,
|
|
390
|
+
targets,
|
|
565
391
|
};
|
|
566
392
|
}
|
|
567
393
|
|
|
568
|
-
export async function
|
|
569
|
-
|
|
394
|
+
export async function resolveExplicitSearchPaths(
|
|
395
|
+
pathItems: string[],
|
|
396
|
+
cwd: string,
|
|
397
|
+
suffixGlob?: string,
|
|
398
|
+
): Promise<ResolvedMultiSearchPath | undefined> {
|
|
399
|
+
return resolveSearchPathItems([...new Set(pathItems)], cwd, suffixGlob);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function resolveFindPatternItems(
|
|
403
|
+
patternItems: string[],
|
|
570
404
|
cwd: string,
|
|
571
405
|
): Promise<ResolvedMultiFindPattern | undefined> {
|
|
572
|
-
|
|
573
|
-
if (!patternItems || patternItems.length <= 1) {
|
|
406
|
+
if (patternItems.length <= 1) {
|
|
574
407
|
return undefined;
|
|
575
408
|
}
|
|
576
409
|
|
|
@@ -602,6 +435,13 @@ export async function resolveMultiFindPattern(
|
|
|
602
435
|
};
|
|
603
436
|
}
|
|
604
437
|
|
|
438
|
+
export async function resolveExplicitFindPatterns(
|
|
439
|
+
patternItems: string[],
|
|
440
|
+
cwd: string,
|
|
441
|
+
): Promise<ResolvedMultiFindPattern | undefined> {
|
|
442
|
+
return resolveFindPatternItems([...new Set(patternItems)], cwd);
|
|
443
|
+
}
|
|
444
|
+
|
|
605
445
|
export function resolveReadPath(filePath: string, cwd: string): string {
|
|
606
446
|
const resolved = resolveToCwd(filePath, cwd);
|
|
607
447
|
const shellEscapedVariant = tryShellEscapedPath(resolved);
|
package/src/tools/search.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
hasGlobPathChars,
|
|
23
23
|
normalizePathLikeInput,
|
|
24
24
|
parseSearchPath,
|
|
25
|
-
|
|
25
|
+
resolveExplicitSearchPaths,
|
|
26
26
|
resolveToCwd,
|
|
27
27
|
} from "./path-utils";
|
|
28
28
|
import {
|
|
@@ -37,9 +37,10 @@ import { toolResult } from "./tool-result";
|
|
|
37
37
|
|
|
38
38
|
const searchSchema = Type.Object({
|
|
39
39
|
pattern: Type.String({ description: "regex pattern", examples: ["function\\s+\\w+", "TODO"] }),
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
paths: Type.Array(Type.String({ description: "file, directory, glob, or internal URL to search" }), {
|
|
41
|
+
minItems: 1,
|
|
42
|
+
description: "files, directories, globs, or internal URLs to search",
|
|
43
|
+
examples: [["src/"], ["src/foo.ts"], ["src/**/*.ts"], ["src/", "packages/"]],
|
|
43
44
|
}),
|
|
44
45
|
i: Type.Optional(Type.Boolean({ description: "case-insensitive search", default: false })),
|
|
45
46
|
gitignore: Type.Optional(Type.Boolean({ description: "respect gitignore", default: true })),
|
|
@@ -48,7 +49,7 @@ const searchSchema = Type.Object({
|
|
|
48
49
|
|
|
49
50
|
export type SearchToolInput = Static<typeof searchSchema>;
|
|
50
51
|
|
|
51
|
-
const DEFAULT_MATCH_LIMIT =
|
|
52
|
+
const DEFAULT_MATCH_LIMIT = 500;
|
|
52
53
|
|
|
53
54
|
export interface SearchToolDetails {
|
|
54
55
|
truncation?: TruncationResult;
|
|
@@ -93,7 +94,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
93
94
|
_onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
|
|
94
95
|
_toolContext?: AgentToolContext,
|
|
95
96
|
): Promise<AgentToolResult<SearchToolDetails>> {
|
|
96
|
-
const { pattern,
|
|
97
|
+
const { pattern, paths, i, gitignore, skip } = params;
|
|
97
98
|
|
|
98
99
|
return untilAborted(signal, async () => {
|
|
99
100
|
const normalizedPattern = pattern.trim();
|
|
@@ -117,13 +118,19 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
117
118
|
let searchPath: string;
|
|
118
119
|
let scopePath: string;
|
|
119
120
|
let exactFilePaths: string[] | undefined;
|
|
121
|
+
let multiTargets: Array<{ basePath: string; glob?: string }> | undefined;
|
|
120
122
|
let globFilter: string | undefined;
|
|
121
|
-
const
|
|
122
|
-
if (rawPath.length === 0) {
|
|
123
|
-
throw new ToolError("`
|
|
123
|
+
const rawPaths = paths.map(normalizePathLikeInput);
|
|
124
|
+
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
125
|
+
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
124
126
|
}
|
|
125
127
|
const internalRouter = this.session.internalRouter;
|
|
126
|
-
|
|
128
|
+
const resolvedPathInputs: string[] = [];
|
|
129
|
+
for (const rawPath of rawPaths) {
|
|
130
|
+
if (!internalRouter?.canHandle(rawPath)) {
|
|
131
|
+
resolvedPathInputs.push(rawPath);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
127
134
|
if (hasGlobPathChars(rawPath)) {
|
|
128
135
|
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
129
136
|
}
|
|
@@ -131,28 +138,30 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
131
138
|
if (!resource.sourcePath) {
|
|
132
139
|
throw new ToolError(`Cannot search internal URL without a backing file: ${rawPath}`);
|
|
133
140
|
}
|
|
134
|
-
|
|
141
|
+
resolvedPathInputs.push(resource.sourcePath);
|
|
142
|
+
}
|
|
143
|
+
if (resolvedPathInputs.length === 1) {
|
|
144
|
+
const parsedPath = parseSearchPath(resolvedPathInputs[0] ?? ".");
|
|
145
|
+
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
146
|
+
globFilter = parsedPath.glob;
|
|
135
147
|
scopePath = formatScopePath(searchPath);
|
|
136
148
|
} else {
|
|
137
|
-
const multiSearchPath = await
|
|
138
|
-
if (multiSearchPath) {
|
|
139
|
-
|
|
140
|
-
globFilter = multiSearchPath.exactFilePaths ? undefined : multiSearchPath.glob;
|
|
141
|
-
exactFilePaths = multiSearchPath.exactFilePaths;
|
|
142
|
-
scopePath = multiSearchPath.scopePath;
|
|
143
|
-
} else {
|
|
144
|
-
const parsedPath = parseSearchPath(rawPath);
|
|
145
|
-
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
146
|
-
globFilter = parsedPath.glob;
|
|
147
|
-
scopePath = formatScopePath(searchPath);
|
|
149
|
+
const multiSearchPath = await resolveExplicitSearchPaths(resolvedPathInputs, this.session.cwd, globFilter);
|
|
150
|
+
if (!multiSearchPath) {
|
|
151
|
+
throw new ToolError("`paths` must contain at least one path or glob");
|
|
148
152
|
}
|
|
153
|
+
searchPath = multiSearchPath.basePath;
|
|
154
|
+
exactFilePaths = multiSearchPath.exactFilePaths;
|
|
155
|
+
multiTargets = multiSearchPath.targets;
|
|
156
|
+
globFilter = exactFilePaths || multiTargets ? undefined : multiSearchPath.glob;
|
|
157
|
+
scopePath = multiSearchPath.scopePath;
|
|
149
158
|
}
|
|
150
159
|
let isDirectory: boolean;
|
|
151
160
|
try {
|
|
152
161
|
const stat = await Bun.file(searchPath).stat();
|
|
153
162
|
isDirectory = stat.isDirectory();
|
|
154
163
|
} catch {
|
|
155
|
-
const hint =
|
|
164
|
+
const hint = rawPaths.length > 1 ? " (`paths` entries must each exist relative to cwd)" : "";
|
|
156
165
|
throw new ToolError(`Path not found: ${scopePath}${hint}`);
|
|
157
166
|
}
|
|
158
167
|
|
|
@@ -163,19 +172,26 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
163
172
|
// Run grep
|
|
164
173
|
let result: GrepResult;
|
|
165
174
|
try {
|
|
166
|
-
if (exactFilePaths) {
|
|
175
|
+
if (exactFilePaths || multiTargets) {
|
|
167
176
|
const matches: GrepMatch[] = [];
|
|
168
177
|
let limitReached = false;
|
|
169
|
-
|
|
170
|
-
|
|
178
|
+
let totalMatches = 0;
|
|
179
|
+
let filesSearched = 0;
|
|
180
|
+
const targets = exactFilePaths
|
|
181
|
+
? exactFilePaths.map(filePath => ({ basePath: filePath, glob: undefined as string | undefined }))
|
|
182
|
+
: (multiTargets ?? []);
|
|
183
|
+
for (const target of targets) {
|
|
184
|
+
const targetResult = await grep(
|
|
171
185
|
{
|
|
172
186
|
pattern: normalizedPattern,
|
|
173
|
-
path:
|
|
187
|
+
path: target.basePath,
|
|
188
|
+
glob: target.glob,
|
|
174
189
|
ignoreCase,
|
|
175
190
|
multiline: effectiveMultiline,
|
|
176
191
|
hidden: true,
|
|
177
192
|
gitignore: useGitignore,
|
|
178
193
|
cache: false,
|
|
194
|
+
maxCount: exactFilePaths ? undefined : internalLimit,
|
|
179
195
|
contextBefore: normalizedContextBefore,
|
|
180
196
|
contextAfter: normalizedContextAfter,
|
|
181
197
|
maxColumns: DEFAULT_MAX_COLUMN,
|
|
@@ -183,16 +199,21 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
183
199
|
},
|
|
184
200
|
undefined,
|
|
185
201
|
);
|
|
186
|
-
limitReached = limitReached || Boolean(
|
|
187
|
-
|
|
188
|
-
|
|
202
|
+
limitReached = limitReached || Boolean(targetResult.limitReached);
|
|
203
|
+
totalMatches += targetResult.totalMatches;
|
|
204
|
+
filesSearched += targetResult.filesSearched;
|
|
205
|
+
for (const match of targetResult.matches) {
|
|
206
|
+
const absolute = path.resolve(target.basePath, match.path);
|
|
207
|
+
const rebased = path.relative(searchPath, absolute).replace(/\\/g, "/");
|
|
208
|
+
matches.push({ ...match, path: rebased });
|
|
209
|
+
}
|
|
189
210
|
}
|
|
190
211
|
const offsetMatches = matches.slice(normalizedSkip);
|
|
191
212
|
result = {
|
|
192
213
|
matches: offsetMatches,
|
|
193
|
-
totalMatches: offsetMatches.length,
|
|
214
|
+
totalMatches: exactFilePaths ? offsetMatches.length : totalMatches,
|
|
194
215
|
filesWithMatches: new Set(offsetMatches.map(match => match.path)).size,
|
|
195
|
-
filesSearched: exactFilePaths.length,
|
|
216
|
+
filesSearched: exactFilePaths ? exactFilePaths.length : filesSearched,
|
|
196
217
|
limitReached,
|
|
197
218
|
};
|
|
198
219
|
} else {
|
|
@@ -261,7 +282,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
261
282
|
: result.matches.slice(0, effectiveLimit);
|
|
262
283
|
const matchLimitReached = result.matches.length > effectiveLimit;
|
|
263
284
|
const nextSkip = normalizedSkip + selectedMatches.length;
|
|
264
|
-
const limitMessage = `Result limit reached; narrow
|
|
285
|
+
const limitMessage = `Result limit reached; narrow paths or use skip=${nextSkip}.`;
|
|
265
286
|
const { record: recordFile, list: fileList } = createFileRecorder();
|
|
266
287
|
const fileMatchCounts = new Map<string, number>();
|
|
267
288
|
if (selectedMatches.length === 0) {
|
|
@@ -381,7 +402,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
381
402
|
|
|
382
403
|
interface SearchRenderArgs {
|
|
383
404
|
pattern: string;
|
|
384
|
-
|
|
405
|
+
paths?: string[];
|
|
385
406
|
i?: boolean;
|
|
386
407
|
gitignore?: boolean;
|
|
387
408
|
skip?: number;
|
|
@@ -393,7 +414,7 @@ export const searchToolRenderer = {
|
|
|
393
414
|
inline: true,
|
|
394
415
|
renderCall(args: SearchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
395
416
|
const meta: string[] = [];
|
|
396
|
-
if (args.
|
|
417
|
+
if (args.paths?.length) meta.push(`in ${args.paths.join(", ")}`);
|
|
397
418
|
if (args.i) meta.push("case:insensitive");
|
|
398
419
|
if (args.gitignore === false) meta.push("gitignore:false");
|
|
399
420
|
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
package/src/tools/write.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
9
|
-
import { unzipSync, zipSync } from "fflate";
|
|
10
9
|
import { stripHashlinePrefixes } from "../edit";
|
|
11
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
11
|
import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
|
|
@@ -44,6 +43,12 @@ import {
|
|
|
44
43
|
import { ToolError } from "./tool-errors";
|
|
45
44
|
import { toolResult } from "./tool-result";
|
|
46
45
|
|
|
46
|
+
let fflateModulePromise: Promise<typeof import("fflate")> | undefined;
|
|
47
|
+
async function loadFflate(): Promise<typeof import("fflate")> {
|
|
48
|
+
if (!fflateModulePromise) fflateModulePromise = import("fflate");
|
|
49
|
+
return fflateModulePromise;
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
const writeSchema = Type.Object({
|
|
48
53
|
path: Type.String({ description: "file path", examples: ["src/new.ts"] }),
|
|
49
54
|
content: Type.String({ description: "file content" }),
|
|
@@ -229,6 +234,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
229
234
|
if (resolvedArchivePath.exists) {
|
|
230
235
|
try {
|
|
231
236
|
const bytes = await Bun.file(resolvedArchivePath.absolutePath).bytes();
|
|
237
|
+
const { unzipSync } = await loadFflate();
|
|
232
238
|
const existing = unzipSync(new Uint8Array(bytes));
|
|
233
239
|
for (const [entryPath, data] of Object.entries(existing)) {
|
|
234
240
|
zipEntries[entryPath.replace(/\\/g, "/")] = data;
|
|
@@ -241,6 +247,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
241
247
|
zipEntries[resolvedArchivePath.archiveSubPath] = new TextEncoder().encode(content);
|
|
242
248
|
|
|
243
249
|
try {
|
|
250
|
+
const { zipSync } = await loadFflate();
|
|
244
251
|
const zipBuffer = zipSync(zipEntries);
|
|
245
252
|
await Bun.write(resolvedArchivePath.absolutePath, zipBuffer);
|
|
246
253
|
} catch (error) {
|
package/src/utils/edit-mode.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { $env
|
|
1
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
|
-
export type EditMode = "replace" | "patch" | "hashline" | "vim" | "apply_patch"
|
|
3
|
+
export type EditMode = "replace" | "patch" | "hashline" | "vim" | "apply_patch";
|
|
4
4
|
|
|
5
5
|
export const DEFAULT_EDIT_MODE: EditMode = "hashline";
|
|
6
6
|
|
|
7
7
|
const EDIT_MODE_IDS = {
|
|
8
8
|
apply_patch: "apply_patch",
|
|
9
|
-
atom: "atom",
|
|
10
9
|
hashline: "hashline",
|
|
11
10
|
patch: "patch",
|
|
12
11
|
replace: "replace",
|
|
@@ -38,14 +37,6 @@ export function resolveEditMode(session: EditModeSessionLike): EditMode {
|
|
|
38
37
|
const envMode = normalizeEditMode($env.PI_EDIT_VARIANT);
|
|
39
38
|
if (envMode) return envMode;
|
|
40
39
|
|
|
41
|
-
if (!$flag("PI_STRICT_EDIT_MODE")) {
|
|
42
|
-
if (activeModel?.includes("spark")) return "apply_patch";
|
|
43
|
-
if (activeModel?.includes("nano")) return "replace";
|
|
44
|
-
if (activeModel?.includes("mini")) return "replace";
|
|
45
|
-
if (activeModel?.includes("haiku")) return "replace";
|
|
46
|
-
if (activeModel?.includes("flash")) return "replace";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
40
|
const settingsMode = normalizeEditMode(String(session.settings.get("edit.mode") ?? ""));
|
|
50
41
|
return settingsMode ?? DEFAULT_EDIT_MODE;
|
|
51
42
|
}
|
|
@@ -29,7 +29,7 @@ export function resolveFileDisplayMode(session: FileDisplayModeSession, options?
|
|
|
29
29
|
const { settings } = session;
|
|
30
30
|
const hasEditTool = session.hasEditTool ?? true;
|
|
31
31
|
const editMode = resolveEditMode(session);
|
|
32
|
-
const usesHashLineAnchors = editMode === "hashline"
|
|
32
|
+
const usesHashLineAnchors = editMode === "hashline";
|
|
33
33
|
const raw = options?.raw === true;
|
|
34
34
|
const hashLines = !raw && hasEditTool && usesHashLineAnchors && settings.get("readHashLines") !== false;
|
|
35
35
|
return {
|
package/src/utils/git.ts
CHANGED
|
@@ -1258,6 +1258,23 @@ export async function restore(cwd: string, options: RestoreOptions = {}): Promis
|
|
|
1258
1258
|
await runEffect(cwd, args, { signal: options.signal });
|
|
1259
1259
|
}
|
|
1260
1260
|
|
|
1261
|
+
/**
|
|
1262
|
+
* Run `git reset` with options. Default is a soft reset (no flag); pass `hard: true` for a destructive reset.
|
|
1263
|
+
*
|
|
1264
|
+
* NOTE: stage.reset() handles the per-file unstaging case. This helper exists for tree-wide resets.
|
|
1265
|
+
*/
|
|
1266
|
+
export async function reset(
|
|
1267
|
+
cwd: string,
|
|
1268
|
+
options: { hard?: boolean; mixed?: boolean; soft?: boolean; target?: string; signal?: AbortSignal } = {},
|
|
1269
|
+
): Promise<void> {
|
|
1270
|
+
const args = ["reset"];
|
|
1271
|
+
if (options.hard) args.push("--hard");
|
|
1272
|
+
else if (options.mixed) args.push("--mixed");
|
|
1273
|
+
else if (options.soft) args.push("--soft");
|
|
1274
|
+
if (options.target) args.push(options.target);
|
|
1275
|
+
await runEffect(cwd, args, { signal: options.signal });
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1261
1278
|
export async function clean(
|
|
1262
1279
|
cwd: string,
|
|
1263
1280
|
options: { ignoredOnly?: boolean; paths?: readonly string[]; signal?: AbortSignal } = {},
|
|
@@ -33,18 +33,6 @@ export function getSessionAccentHex(name: string): string {
|
|
|
33
33
|
return hslToHex(nameToHue(name), 0.9, 0.72);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
* Auto-generated titles should not drive the session accent.
|
|
38
|
-
* Legacy sessions with unknown title source keep the old behavior.
|
|
39
|
-
*/
|
|
40
|
-
export function getSessionAccentHexForTitle(
|
|
41
|
-
name: string | undefined,
|
|
42
|
-
titleSource: "auto" | "user" | undefined,
|
|
43
|
-
): string | undefined {
|
|
44
|
-
if (!name || titleSource === "auto") return undefined;
|
|
45
|
-
return getSessionAccentHex(name);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
36
|
/**
|
|
49
37
|
* Convert a hex accent color to an ANSI-16m foreground escape sequence.
|
|
50
38
|
* Returns `undefined` if `hex` is nullish or Bun.color conversion fails.
|