@oh-my-pi/pi-coding-agent 14.5.14 → 14.6.1

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/package.json +7 -7
  3. package/src/autoresearch/command-resume.md +5 -8
  4. package/src/autoresearch/git.ts +41 -51
  5. package/src/autoresearch/helpers.ts +43 -359
  6. package/src/autoresearch/index.ts +281 -273
  7. package/src/autoresearch/prompt-setup.md +43 -0
  8. package/src/autoresearch/prompt.md +52 -193
  9. package/src/autoresearch/resume-message.md +2 -8
  10. package/src/autoresearch/state.ts +59 -166
  11. package/src/autoresearch/storage.ts +687 -0
  12. package/src/autoresearch/tools/init-experiment.ts +201 -290
  13. package/src/autoresearch/tools/log-experiment.ts +304 -517
  14. package/src/autoresearch/tools/run-experiment.ts +117 -296
  15. package/src/autoresearch/tools/update-notes.ts +116 -0
  16. package/src/autoresearch/types.ts +16 -66
  17. package/src/cli/list-models.ts +66 -0
  18. package/src/config/settings-schema.ts +1 -1
  19. package/src/config/settings.ts +20 -1
  20. package/src/cursor.ts +1 -1
  21. package/src/edit/index.ts +9 -31
  22. package/src/edit/line-hash.ts +70 -43
  23. package/src/edit/modes/hashline.lark +26 -0
  24. package/src/edit/modes/hashline.ts +898 -1099
  25. package/src/edit/modes/patch.ts +0 -7
  26. package/src/edit/modes/replace.ts +0 -4
  27. package/src/edit/renderer.ts +22 -20
  28. package/src/edit/streaming.ts +8 -28
  29. package/src/eval/eval.lark +24 -30
  30. package/src/eval/js/context-manager.ts +5 -162
  31. package/src/eval/js/prelude.txt +0 -12
  32. package/src/eval/parse.ts +129 -129
  33. package/src/eval/py/prelude.py +1 -219
  34. package/src/export/html/template.generated.ts +1 -1
  35. package/src/export/html/template.js +2 -2
  36. package/src/internal-urls/docs-index.generated.ts +2 -2
  37. package/src/main.ts +18 -3
  38. package/src/modes/components/session-observer-overlay.ts +5 -2
  39. package/src/modes/components/status-line/segments.ts +1 -1
  40. package/src/modes/components/status-line.ts +3 -5
  41. package/src/modes/components/tree-selector.ts +4 -5
  42. package/src/modes/components/welcome.ts +11 -1
  43. package/src/modes/controllers/command-controller.ts +2 -6
  44. package/src/modes/controllers/event-controller.ts +7 -5
  45. package/src/modes/controllers/extension-ui-controller.ts +3 -15
  46. package/src/modes/controllers/input-controller.ts +0 -1
  47. package/src/modes/controllers/selector-controller.ts +1 -1
  48. package/src/modes/interactive-mode.ts +5 -7
  49. package/src/prompts/system/system-prompt.md +14 -38
  50. package/src/prompts/tools/ast-edit.md +8 -8
  51. package/src/prompts/tools/ast-grep.md +10 -10
  52. package/src/prompts/tools/eval.md +13 -31
  53. package/src/prompts/tools/find.md +2 -1
  54. package/src/prompts/tools/hashline.md +66 -57
  55. package/src/prompts/tools/search.md +2 -2
  56. package/src/session/agent-session.ts +1 -1
  57. package/src/session/session-manager.ts +17 -13
  58. package/src/tools/ast-edit.ts +141 -44
  59. package/src/tools/ast-grep.ts +112 -36
  60. package/src/tools/eval.ts +2 -53
  61. package/src/tools/find.ts +16 -15
  62. package/src/tools/gh-renderer.ts +184 -59
  63. package/src/tools/path-utils.ts +36 -196
  64. package/src/tools/search.ts +56 -35
  65. package/src/utils/edit-mode.ts +2 -11
  66. package/src/utils/file-display-mode.ts +1 -1
  67. package/src/utils/git.ts +59 -24
  68. package/src/utils/session-color.ts +0 -12
  69. package/src/utils/title-generator.ts +22 -38
  70. package/src/autoresearch/apply-contract-to-state.ts +0 -24
  71. package/src/autoresearch/contract.ts +0 -288
  72. package/src/edit/modes/atom.lark +0 -29
  73. package/src/edit/modes/atom.ts +0 -1773
  74. package/src/prompts/tools/atom.md +0 -150
@@ -12,10 +12,13 @@ import type {
12
12
  import { formatShortSha } from "./gh-format";
13
13
  import {
14
14
  formatExpandHint,
15
+ formatMoreItems,
15
16
  formatStatusIcon,
16
17
  PREVIEW_LIMITS,
17
18
  replaceTabs,
18
19
  type ToolUIColor,
20
+ type ToolUIStatus,
21
+ TRUNCATE_LENGTHS,
19
22
  truncateToWidth as truncateVisualWidth,
20
23
  } from "./render-utils";
21
24
 
@@ -23,6 +26,10 @@ type GithubToolRenderArgs = {
23
26
  op?: string;
24
27
  run?: string;
25
28
  branch?: string;
29
+ repo?: string;
30
+ issue?: string;
31
+ pr?: string | string[];
32
+ query?: string;
26
33
  };
27
34
 
28
35
  const SUCCESS_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
@@ -31,6 +38,87 @@ const RUNNING_STATUSES = new Set(["in_progress"]);
31
38
  const PENDING_STATUSES = new Set(["queued", "requested", "waiting", "pending"]);
32
39
  const FALLBACK_WIDTH = 80;
33
40
 
41
+ const OP_TITLES: Record<string, string> = {
42
+ repo_view: "GitHub Repo",
43
+ issue_view: "GitHub Issue",
44
+ pr_view: "GitHub PR",
45
+ pr_diff: "GitHub PR Diff",
46
+ pr_checkout: "GitHub PR Checkout",
47
+ pr_push: "GitHub PR Push",
48
+ search_issues: "GitHub Search Issues",
49
+ search_prs: "GitHub Search PRs",
50
+ run_watch: "GitHub Run Watch",
51
+ };
52
+
53
+ function formatOpTitle(op: string | undefined): string {
54
+ if (op && OP_TITLES[op]) return OP_TITLES[op];
55
+ return "GitHub";
56
+ }
57
+
58
+ function extractIssueId(value: string | undefined): string | undefined {
59
+ if (!value) return undefined;
60
+ const trimmed = value.trim();
61
+ if (!trimmed) return undefined;
62
+ if (/^\d+$/.test(trimmed)) return `#${trimmed}`;
63
+ const match = trimmed.match(/\/(?:issues|pull)\/(\d+)/);
64
+ if (match) return `#${match[1]}`;
65
+ return truncateVisualWidth(trimmed, TRUNCATE_LENGTHS.SHORT);
66
+ }
67
+
68
+ function formatPrIdentifier(pr: string | string[] | undefined): string | undefined {
69
+ if (pr === undefined) return undefined;
70
+ if (Array.isArray(pr)) {
71
+ const parts = pr.map(p => extractIssueId(p)).filter((p): p is string => p !== undefined);
72
+ if (parts.length === 0) return undefined;
73
+ if (parts.length > 3) {
74
+ return `${parts.slice(0, 3).join(", ")}, +${parts.length - 3} more`;
75
+ }
76
+ return parts.join(", ");
77
+ }
78
+ return extractIssueId(pr);
79
+ }
80
+
81
+ function buildOpMeta(args: GithubToolRenderArgs): string[] {
82
+ const meta: string[] = [];
83
+ const op = args.op;
84
+ switch (op) {
85
+ case "issue_view": {
86
+ const id = extractIssueId(args.issue);
87
+ if (id) meta.push(id);
88
+ if (args.repo) meta.push(args.repo);
89
+ break;
90
+ }
91
+ case "pr_view":
92
+ case "pr_diff":
93
+ case "pr_checkout":
94
+ case "pr_push": {
95
+ const id = formatPrIdentifier(args.pr);
96
+ if (id) meta.push(id);
97
+ else if (args.branch) meta.push(args.branch);
98
+ if (args.repo) meta.push(args.repo);
99
+ break;
100
+ }
101
+ case "search_issues":
102
+ case "search_prs": {
103
+ if (args.query) meta.push(truncateVisualWidth(args.query, TRUNCATE_LENGTHS.CONTENT));
104
+ if (args.repo) meta.push(args.repo);
105
+ break;
106
+ }
107
+ case "repo_view": {
108
+ if (args.repo) meta.push(args.repo);
109
+ if (args.branch) meta.push(args.branch);
110
+ break;
111
+ }
112
+ case "run_watch":
113
+ break;
114
+ default: {
115
+ if (args.repo) meta.push(args.repo);
116
+ break;
117
+ }
118
+ }
119
+ return meta;
120
+ }
121
+
34
122
  function getWatchHeader(watch: GhRunWatchViewDetails): string {
35
123
  if (watch.mode === "run" && watch.run) {
36
124
  if (watch.state === "watching") {
@@ -183,7 +271,7 @@ function renderFailedLogs(
183
271
  return lines;
184
272
  }
185
273
 
186
- function buildRenderedLines(
274
+ function buildWatchLines(
187
275
  watch: GhRunWatchViewDetails,
188
276
  theme: Theme,
189
277
  options: RenderResultOptions,
@@ -215,92 +303,129 @@ function buildRenderedLines(
215
303
  return lines;
216
304
  }
217
305
 
218
- function renderFallbackText(
219
- result: { content: Array<{ type: string; text?: string }>; isError?: boolean },
220
- theme: Theme,
221
- ): Component {
222
- const text = result.content
306
+ function extractText(content: Array<{ type: string; text?: string }>): string {
307
+ return content
223
308
  .filter(part => part.type === "text")
224
309
  .map(part => part.text)
225
310
  .filter((value): value is string => typeof value === "string" && value.length > 0)
226
311
  .join("\n");
227
- if (text) {
228
- return new Text(replaceTabs(text), 0, 0);
229
- }
312
+ }
230
313
 
314
+ function renderFallbackComponent(
315
+ result: { content: Array<{ type: string; text?: string }>; isError?: boolean },
316
+ options: RenderResultOptions,
317
+ theme: Theme,
318
+ args: GithubToolRenderArgs,
319
+ ): Component {
320
+ const text = extractText(result.content);
321
+ const title = formatOpTitle(args.op);
322
+ const meta = buildOpMeta(args);
323
+ const isError = result.isError === true;
324
+ const status: ToolUIStatus = isError ? "error" : text ? "success" : "warning";
231
325
  const header = renderStatusLine(
232
326
  {
233
- icon: result.isError ? "error" : "warning",
234
- title: "GitHub Run Watch",
235
- description: result.isError ? "failed" : "no output",
327
+ icon: status,
328
+ title,
329
+ titleColor: isError ? "error" : "accent",
330
+ meta,
236
331
  },
237
332
  theme,
238
333
  );
239
- return new Text(header, 0, 0);
240
- }
241
334
 
242
- export const githubToolRenderer = {
243
- renderCall(args: GithubToolRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
244
- const lines: string[] = [];
335
+ if (!text) {
336
+ const empty = isError ? "request failed" : "no output";
337
+ return new Text(`${header}\n${theme.fg("dim", empty)}`, 0, 0);
338
+ }
245
339
 
246
- // Header with spinner reflecting the dispatched op
247
- const icon =
248
- options.spinnerFrame !== undefined
249
- ? formatStatusIcon("running", uiTheme, options.spinnerFrame)
250
- : formatStatusIcon("pending", uiTheme);
340
+ const allLines = replaceTabs(text).split("\n");
251
341
 
252
- // Build a target description that mirrors the result view style
253
- const runId = typeof args.run === "string" && args.run.trim().length > 0 ? args.run.trim() : undefined;
254
- const branch = typeof args.branch === "string" && args.branch.trim().length > 0 ? args.branch.trim() : undefined;
342
+ return {
343
+ render(width: number): string[] {
344
+ const lineWidth = Math.max(24, width || FALLBACK_WIDTH);
345
+ const expanded = options.expanded;
346
+ const limit = expanded ? allLines.length : Math.min(allLines.length, PREVIEW_LIMITS.OUTPUT_EXPANDED);
347
+ const visible = allLines.slice(0, limit);
348
+ const remaining = allLines.length - visible.length;
349
+
350
+ const out: string[] = [header];
351
+ for (const line of visible) {
352
+ const colored = isError ? theme.fg("error", line) : theme.fg("toolOutput", line);
353
+ out.push(truncateVisualWidth(colored, lineWidth));
354
+ }
355
+ if (!expanded && remaining > 0) {
356
+ const hint = formatExpandHint(theme, expanded, true);
357
+ const more = `${formatMoreItems(remaining, "line")}${hint ? ` ${hint}` : ""}`;
358
+ out.push(theme.fg("dim", more));
359
+ }
360
+ return out.map(line => truncateToWidth(line, lineWidth));
361
+ },
362
+ invalidate() {},
363
+ };
364
+ }
255
365
 
256
- const op = typeof args.op === "string" && args.op.trim().length > 0 ? args.op.trim() : undefined;
257
- if (op && op !== "run_watch") {
258
- const title = uiTheme.fg("accent", `GitHub ${op}`);
259
- lines.push(`${icon} ${title}`);
260
- return new Text(lines.join("\n"), 0, 0);
261
- }
366
+ function renderWatchCall(args: GithubToolRenderArgs, options: RenderResultOptions, theme: Theme): Component {
367
+ const icon =
368
+ options.spinnerFrame !== undefined
369
+ ? formatStatusIcon("running", theme, options.spinnerFrame)
370
+ : formatStatusIcon("pending", theme);
371
+
372
+ const runId = typeof args.run === "string" && args.run.trim().length > 0 ? args.run.trim() : undefined;
373
+ const branch = typeof args.branch === "string" && args.branch.trim().length > 0 ? args.branch.trim() : undefined;
374
+
375
+ const titleText = theme.fg("accent", "GitHub Run Watch");
376
+ let metaText: string;
377
+ if (runId) {
378
+ metaText = theme.fg("muted", `#${runId}`);
379
+ } else if (branch) {
380
+ metaText = theme.fg("text", branch);
381
+ } else {
382
+ metaText = theme.fg("muted", "current HEAD");
383
+ }
262
384
 
263
- if (runId) {
264
- // "⠋ GitHub Run Watch run #12345"
265
- const title = uiTheme.fg("accent", "GitHub Run Watch");
266
- const meta = uiTheme.fg("muted", `#${runId}`);
267
- lines.push(`${icon} ${title} ${meta}`);
268
- } else if (branch) {
269
- // "⠋ GitHub Run Watch feature-branch"
270
- const title = uiTheme.fg("accent", "GitHub Run Watch");
271
- const meta = uiTheme.fg("text", branch);
272
- lines.push(`${icon} ${title} ${meta}`);
273
- } else {
274
- // "⠋ GitHub Run Watch current HEAD"
275
- const title = uiTheme.fg("accent", "GitHub Run Watch");
276
- const meta = uiTheme.fg("muted", "current HEAD");
277
- lines.push(`${icon} ${title} ${meta}`);
278
- }
385
+ const header = `${icon} ${titleText} ${metaText}`;
386
+ const wait = theme.fg("dim", " waiting for workflow data...");
387
+ return new Text(`${header}\n${wait}`, 0, 0);
388
+ }
279
389
 
280
- lines.push(uiTheme.fg("dim", " waiting for workflow data..."));
390
+ export const githubToolRenderer = {
391
+ renderCall(args: GithubToolRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
392
+ const op = typeof args.op === "string" && args.op.trim().length > 0 ? args.op.trim() : undefined;
393
+ if (op === "run_watch") {
394
+ return renderWatchCall({ ...args, op }, options, uiTheme);
395
+ }
281
396
 
282
- return new Text(lines.join("\n"), 0, 0);
397
+ const status: ToolUIStatus = options.spinnerFrame !== undefined ? "running" : "pending";
398
+ const header = renderStatusLine(
399
+ {
400
+ icon: status,
401
+ spinnerFrame: options.spinnerFrame,
402
+ title: formatOpTitle(op),
403
+ meta: buildOpMeta({ ...args, op }),
404
+ },
405
+ uiTheme,
406
+ );
407
+ return new Text(header, 0, 0);
283
408
  },
284
409
 
285
410
  renderResult(
286
411
  result: { content: Array<{ type: string; text?: string }>; details?: GhToolDetails; isError?: boolean },
287
412
  options: RenderResultOptions,
288
413
  uiTheme: Theme,
414
+ args?: GithubToolRenderArgs,
289
415
  ): Component {
290
416
  const watch = result.details?.watch;
291
- if (!watch) {
292
- return renderFallbackText(result, uiTheme);
417
+ if (watch) {
418
+ return {
419
+ render(width: number): string[] {
420
+ const lineWidth = Math.max(24, width || FALLBACK_WIDTH);
421
+ return buildWatchLines(watch, uiTheme, options, lineWidth).map(line => truncateToWidth(line, lineWidth));
422
+ },
423
+ invalidate() {},
424
+ };
293
425
  }
294
426
 
295
- return {
296
- render(width: number): string[] {
297
- const lineWidth = Math.max(24, width || FALLBACK_WIDTH);
298
- return buildRenderedLines(watch, uiTheme, options, lineWidth).map(line => truncateToWidth(line, lineWidth));
299
- },
300
- invalidate() {},
301
- };
427
+ return renderFallbackComponent(result, options, uiTheme, args ?? {});
302
428
  },
303
429
 
304
430
  mergeCallAndResult: true,
305
- inline: true,
306
431
  };
@@ -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 looksLikeDelimitedPathToken(token: string): boolean {
416
- return (
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
- const pathItems = await splitDelimitedSearchInput(rawPath, cwd, value => parseSearchPath(value).basePath);
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 resolveMultiFindPattern(
569
- rawPattern: string,
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
- const patternItems = await splitDelimitedSearchInput(rawPattern, cwd, value => parseFindPattern(value).basePath);
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);