@oh-my-pi/pi-coding-agent 14.6.5 → 14.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/examples/hooks/handoff.ts +1 -1
  3. package/examples/hooks/qna.ts +1 -1
  4. package/examples/sdk/03-custom-prompt.ts +7 -4
  5. package/examples/sdk/README.md +1 -1
  6. package/package.json +7 -7
  7. package/src/autoresearch/index.ts +48 -44
  8. package/src/cli/read-cli.ts +58 -0
  9. package/src/cli.ts +1 -0
  10. package/src/commands/read.ts +40 -0
  11. package/src/commit/agentic/agent.ts +1 -1
  12. package/src/commit/analysis/conventional.ts +1 -1
  13. package/src/commit/analysis/summary.ts +1 -1
  14. package/src/commit/changelog/generate.ts +1 -1
  15. package/src/commit/map-reduce/map-phase.ts +1 -1
  16. package/src/commit/map-reduce/reduce-phase.ts +1 -1
  17. package/src/config/settings-schema.ts +39 -0
  18. package/src/edit/line-hash.ts +34 -4
  19. package/src/edit/modes/hashline.ts +221 -7
  20. package/src/edit/streaming.ts +4 -1
  21. package/src/export/html/index.ts +1 -1
  22. package/src/extensibility/extensions/runner.ts +3 -3
  23. package/src/extensibility/extensions/types.ts +4 -4
  24. package/src/main.ts +3 -3
  25. package/src/memories/index.ts +1 -1
  26. package/src/modes/components/agent-dashboard.ts +1 -1
  27. package/src/modes/components/custom-editor.ts +4 -5
  28. package/src/modes/components/read-tool-group.ts +4 -9
  29. package/src/modes/components/tool-execution.ts +4 -0
  30. package/src/modes/controllers/event-controller.ts +2 -0
  31. package/src/modes/controllers/input-controller.ts +3 -1
  32. package/src/modes/interactive-mode.ts +24 -0
  33. package/src/modes/rpc/rpc-types.ts +1 -1
  34. package/src/modes/utils/context-usage.ts +12 -5
  35. package/src/modes/utils/ui-helpers.ts +1 -0
  36. package/src/prompts/system/project-prompt.md +36 -0
  37. package/src/prompts/system/system-prompt.md +0 -29
  38. package/src/prompts/tools/github.md +1 -0
  39. package/src/prompts/tools/hashline.md +24 -6
  40. package/src/prompts/tools/read.md +15 -14
  41. package/src/sdk.ts +29 -28
  42. package/src/session/agent-session.ts +20 -12
  43. package/src/session/compaction/branch-summarization.ts +1 -1
  44. package/src/session/compaction/compaction.ts +3 -3
  45. package/src/session/session-dump-format.ts +10 -5
  46. package/src/session/session-manager.ts +57 -0
  47. package/src/session/streaming-output.ts +1 -1
  48. package/src/system-prompt.ts +35 -3
  49. package/src/task/executor.ts +4 -3
  50. package/src/tools/fetch.ts +4 -4
  51. package/src/tools/gh.ts +187 -0
  52. package/src/tools/image-gen.ts +3 -1
  53. package/src/tools/inspect-image.ts +1 -1
  54. package/src/tools/output-meta.ts +1 -1
  55. package/src/tools/path-utils.ts +11 -0
  56. package/src/tools/read.ts +388 -204
  57. package/src/tools/search.ts +1 -1
  58. package/src/tools/sqlite-reader.ts +1 -1
  59. package/src/utils/commit-message-generator.ts +1 -1
  60. package/src/utils/title-generator.ts +1 -1
  61. package/src/web/search/providers/anthropic.ts +1 -1
  62. package/src/workspace-tree.ts +396 -0
@@ -13,7 +13,9 @@ import type { SkillsSettings } from "./config/settings";
13
13
  import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
14
14
  import { loadSkills, type Skill } from "./extensibility/skills";
15
15
  import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
16
+ import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
16
17
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
18
+ import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
17
19
 
18
20
  interface AlwaysApplyRule {
19
21
  name: string;
@@ -409,12 +411,20 @@ export interface BuildSystemPromptOptions {
409
411
  secretsEnabled?: boolean;
410
412
  /** Pre-loaded AGENTS.md search (skips discovery if provided). May be a Promise to allow early kick-off. */
411
413
  agentsMdSearch?: AgentsMdSearch | Promise<AgentsMdSearch>;
414
+ /** Pre-loaded workspace tree (skips discovery if provided). May be a Promise to allow early kick-off. */
415
+ workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
416
+ }
417
+
418
+ /** Result of building provider-facing system prompt messages. */
419
+ export interface BuildSystemPromptResult {
420
+ /** Ordered system prompt blocks. Providers should preserve entries as distinct messages/blocks. */
421
+ systemPrompt: string[];
412
422
  }
413
423
 
414
424
  /** Build the system prompt with tools, guidelines, and context */
415
- export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<string> {
425
+ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<BuildSystemPromptResult> {
416
426
  if ($env.NULL_PROMPT === "true") {
417
- return "";
427
+ return { systemPrompt: [] };
418
428
  }
419
429
 
420
430
  const {
@@ -435,6 +445,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
435
445
  eagerTasks = false,
436
446
  secretsEnabled = false,
437
447
  agentsMdSearch: providedAgentsMdSearch,
448
+ workspaceTree: providedWorkspaceTree,
438
449
  } = options;
439
450
  const resolvedCwd = cwd ?? getProjectDir();
440
451
 
@@ -449,6 +460,10 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
449
460
  providedAgentsMdSearch !== undefined
450
461
  ? Promise.resolve(providedAgentsMdSearch)
451
462
  : logger.time("buildAgentsMdSearch", buildAgentsMdSearch, resolvedCwd);
463
+ const workspaceTreePromise =
464
+ providedWorkspaceTree !== undefined
465
+ ? Promise.resolve(providedWorkspaceTree)
466
+ : logger.time("buildWorkspaceTree", buildWorkspaceTree, resolvedCwd);
452
467
  const skillsPromise: Promise<Skill[]> =
453
468
  providedSkills !== undefined
454
469
  ? Promise.resolve(providedSkills)
@@ -463,6 +478,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
463
478
  contextFilesPromise,
464
479
  agentsMdSearchPromise,
465
480
  skillsPromise,
481
+ workspaceTreePromise,
466
482
  ]).then(
467
483
  ([
468
484
  resolvedCustomPrompt,
@@ -471,6 +487,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
471
487
  contextFiles,
472
488
  agentsMdSearch,
473
489
  skills,
490
+ workspaceTree,
474
491
  ]) => ({
475
492
  resolvedCustomPrompt,
476
493
  resolvedAppendPrompt,
@@ -478,6 +495,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
478
495
  contextFiles,
479
496
  agentsMdSearch,
480
497
  skills,
498
+ workspaceTree,
481
499
  }),
482
500
  );
483
501
  })();
@@ -501,6 +519,12 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
501
519
  pattern: `AGENTS.md depth ${AGENTS_MD_MIN_DEPTH}-${AGENTS_MD_MAX_DEPTH}`,
502
520
  files: [],
503
521
  };
522
+ let workspaceTree: WorkspaceTree = {
523
+ rootPath: resolvedCwd,
524
+ rendered: "",
525
+ truncated: false,
526
+ totalLines: 0,
527
+ };
504
528
  let skills: Skill[] = providedSkills ?? [];
505
529
 
506
530
  if (prepResult.type === "timeout") {
@@ -524,6 +548,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
524
548
  contextFiles = dedupeExactContextFiles(prepResult.value.contextFiles);
525
549
  agentsMdSearch = prepResult.value.agentsMdSearch;
526
550
  skills = prepResult.value.skills;
551
+ workspaceTree = prepResult.value.workspaceTree;
527
552
  }
528
553
 
529
554
  const date = new Date().toISOString().slice(0, 10);
@@ -578,6 +603,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
578
603
  environment,
579
604
  contextFiles,
580
605
  agentsMdSearch,
606
+ workspaceTree,
581
607
  skills: filteredSkills,
582
608
  rules: rules ?? [],
583
609
  alwaysApplyRules: injectedAlwaysApplyRules,
@@ -599,5 +625,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
599
625
  rendered += `\n\n<critical>\nThe \`${reportToolIssueToolName}\` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call \`${reportToolIssueToolName}\` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.\n</critical>`;
600
626
  }
601
627
 
602
- return rendered;
628
+ const systemPrompt = [rendered];
629
+ const projectPrompt = resolvedCustomPrompt ? "" : prompt.render(projectPromptTemplate, data).trim();
630
+ if (projectPrompt) {
631
+ systemPrompt.push(projectPrompt);
632
+ }
633
+
634
+ return { systemPrompt };
603
635
  }
@@ -967,9 +967,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
967
967
  contextFiles: options.contextFiles,
968
968
  skills: options.skills,
969
969
  promptTemplates: options.promptTemplates,
970
- systemPrompt: defaultPrompt =>
970
+ systemPrompt: defaultPrompt => [
971
971
  prompt.render(subagentSystemPromptTemplate, {
972
- base: defaultPrompt,
972
+ base: defaultPrompt.join("\n\n"),
973
973
  agent: agent.systemPrompt,
974
974
  worktree: worktree ?? "",
975
975
  outputSchema: normalizedOutputSchema,
@@ -977,6 +977,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
977
977
  ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
978
978
  ircSelfId: ircEnabled ? id : "",
979
979
  }),
980
+ ],
980
981
  sessionManager,
981
982
  hasUI: false,
982
983
  spawns: spawnsEnv,
@@ -1016,7 +1017,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1016
1017
  }
1017
1018
 
1018
1019
  session.sessionManager.appendSessionInit({
1019
- systemPrompt: session.agent.state.systemPrompt,
1020
+ systemPrompt: session.agent.state.systemPrompt.join("\n\n"),
1020
1021
  task,
1021
1022
  tools: session.getActiveToolNames(),
1022
1023
  outputSchema,
@@ -148,20 +148,20 @@ export interface ParsedReadUrlTarget {
148
148
  limit?: number;
149
149
  }
150
150
 
151
- export function parseReadUrlTarget(readPath: string, sel?: string): ParsedReadUrlTarget | null {
152
- const embedded = sel ? undefined : tryExtractEmbeddedUrlSelector(readPath);
151
+ export function parseReadUrlTarget(readPath: string): ParsedReadUrlTarget | null {
152
+ const embedded = tryExtractEmbeddedUrlSelector(readPath);
153
153
  const urlPath = embedded?.path ?? readPath;
154
154
  if (!isReadableUrlPath(urlPath)) {
155
155
  return null;
156
156
  }
157
157
 
158
- const selector = sel ?? embedded?.sel;
158
+ const selector = embedded?.sel;
159
159
  const raw = selector === "raw";
160
160
  const lineMatch = selector ? URL_LINE_RANGE_RE.exec(selector) : null;
161
161
  if (lineMatch) {
162
162
  const startLine = Number.parseInt(lineMatch[1]!, 10);
163
163
  if (startLine < 1) {
164
- throw new ToolError("sel=0 is invalid; lines are 1-indexed. Use sel=1.");
164
+ throw new ToolError("URL line selector 0 is invalid; lines are 1-indexed. Use :L1.");
165
165
  }
166
166
  const sep = lineMatch[2];
167
167
  const rhs = lineMatch[3] ? Number.parseInt(lineMatch[3], 10) : undefined;
package/src/tools/gh.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
2
3
  import * as path from "node:path";
3
4
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
5
  import { StringEnum } from "@oh-my-pi/pi-ai";
@@ -151,6 +152,7 @@ const githubSchema = Type.Object({
151
152
  [
152
153
  "repo_view",
153
154
  "issue_view",
155
+ "pr_create",
154
156
  "pr_view",
155
157
  "pr_diff",
156
158
  "pr_checkout",
@@ -205,6 +207,53 @@ const githubSchema = Type.Object({
205
207
  ),
206
208
  force: Type.Optional(Type.Boolean({ description: "reset existing local branch (pr_checkout)" })),
207
209
  forceWithLease: Type.Optional(Type.Boolean({ description: "force-with-lease push (pr_push)" })),
210
+ title: Type.Optional(
211
+ Type.String({
212
+ description: "PR title (pr_create)",
213
+ examples: ["Fix login bug"],
214
+ }),
215
+ ),
216
+ body: Type.Optional(
217
+ Type.String({
218
+ description: "PR body markdown (pr_create); mutually exclusive with fill",
219
+ }),
220
+ ),
221
+ base: Type.Optional(
222
+ Type.String({
223
+ description: "PR base branch (pr_create); defaults to repo default branch",
224
+ examples: ["main"],
225
+ }),
226
+ ),
227
+ head: Type.Optional(
228
+ Type.String({
229
+ description: "PR head branch (pr_create); defaults to current branch",
230
+ examples: ["feature/foo"],
231
+ }),
232
+ ),
233
+ draft: Type.Optional(Type.Boolean({ description: "open PR as draft (pr_create)" })),
234
+ fill: Type.Optional(
235
+ Type.Boolean({
236
+ description: "auto-fill PR title/body from commits (pr_create); mutually exclusive with title/body",
237
+ }),
238
+ ),
239
+ reviewer: Type.Optional(
240
+ Type.Array(Type.String(), {
241
+ description: "reviewers to request (pr_create); accepts users or org/team",
242
+ examples: [["octocat", "myorg/team"]],
243
+ }),
244
+ ),
245
+ assignee: Type.Optional(
246
+ Type.Array(Type.String(), {
247
+ description: "assignees (pr_create); use @me for the authenticated user",
248
+ examples: [["@me"]],
249
+ }),
250
+ ),
251
+ label: Type.Optional(
252
+ Type.Array(Type.String(), {
253
+ description: "labels to apply (pr_create)",
254
+ examples: [["bug", "enhancement"]],
255
+ }),
256
+ ),
208
257
  query: Type.Optional(
209
258
  Type.String({
210
259
  description: "search query (search_issues, search_prs, search_code, search_commits, search_repos)",
@@ -2063,6 +2112,8 @@ export class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails>
2063
2112
  return executeRepoView(this.session, params, signal);
2064
2113
  case "issue_view":
2065
2114
  return executeIssueView(this.session, params, signal);
2115
+ case "pr_create":
2116
+ return executePrCreate(this.session, params, signal);
2066
2117
  case "pr_view":
2067
2118
  return executePrView(this.session, params, signal);
2068
2119
  case "pr_diff":
@@ -2442,6 +2493,142 @@ async function executePrPush(
2442
2493
  );
2443
2494
  }
2444
2495
 
2496
+ async function executePrCreate(
2497
+ session: ToolSession,
2498
+ params: GithubInput,
2499
+ signal: AbortSignal | undefined,
2500
+ ): Promise<AgentToolResult<GhToolDetails>> {
2501
+ const repo = normalizeOptionalString(params.repo);
2502
+ const title = normalizeOptionalString(params.title);
2503
+ const body = params.body;
2504
+ const base = normalizeOptionalString(params.base);
2505
+ const head = normalizeOptionalString(params.head);
2506
+ const draft = params.draft ?? false;
2507
+ const fill = params.fill ?? false;
2508
+ const reviewers = normalizePrIdentifierList(params.reviewer);
2509
+ const assignees = normalizePrIdentifierList(params.assignee);
2510
+ const labels = normalizePrIdentifierList(params.label);
2511
+
2512
+ if (!fill && !title) {
2513
+ throw new ToolError("title is required unless fill is true");
2514
+ }
2515
+ if (fill && (title || body !== undefined)) {
2516
+ throw new ToolError("fill is mutually exclusive with title and body");
2517
+ }
2518
+
2519
+ const args = ["pr", "create"];
2520
+ appendRepoFlag(args, repo);
2521
+ if (title) args.push("--title", title);
2522
+ if (base) args.push("--base", base);
2523
+ if (head) args.push("--head", head);
2524
+ if (draft) args.push("--draft");
2525
+ if (fill) args.push("--fill");
2526
+ for (const reviewer of reviewers) args.push("--reviewer", reviewer);
2527
+ for (const assignee of assignees) args.push("--assignee", assignee);
2528
+ for (const label of labels) args.push("--label", label);
2529
+
2530
+ let bodyDir: string | undefined;
2531
+ try {
2532
+ if (!fill) {
2533
+ if (body !== undefined && body.length > 0) {
2534
+ // Route through a temp file so multi-KB bodies stay clear of any
2535
+ // argv-length limits and shell-quoting hazards on uncommon platforms.
2536
+ bodyDir = await fs.mkdtemp(path.join(os.tmpdir(), "gh-pr-body-"));
2537
+ const bodyFile = path.join(bodyDir, "body.md");
2538
+ await Bun.write(bodyFile, body);
2539
+ args.push("--body-file", bodyFile);
2540
+ } else {
2541
+ // Avoid gh dropping into an interactive editor when no body is given.
2542
+ args.push("--body", "");
2543
+ }
2544
+ }
2545
+
2546
+ const output = await git.github.text(session.cwd, args, signal, {
2547
+ repoProvided: Boolean(repo),
2548
+ });
2549
+ const url =
2550
+ output
2551
+ .split("\n")
2552
+ .map(line => line.trim())
2553
+ .find(line => line.startsWith("https://github.com/")) ?? output.trim();
2554
+ const parsed = parsePullRequestUrl(url);
2555
+ const resolvedRepo = repo ?? parsed.repo;
2556
+
2557
+ let prView: GhPrViewData | undefined;
2558
+ if (resolvedRepo && parsed.prNumber !== undefined) {
2559
+ try {
2560
+ prView = await git.github.json<GhPrViewData>(
2561
+ session.cwd,
2562
+ [
2563
+ "pr",
2564
+ "view",
2565
+ String(parsed.prNumber),
2566
+ "--repo",
2567
+ resolvedRepo,
2568
+ "--json",
2569
+ GH_PR_FIELDS_NO_COMMENTS.join(","),
2570
+ ],
2571
+ signal,
2572
+ { repoProvided: true },
2573
+ );
2574
+ } catch {
2575
+ // Best-effort summary; PR creation already succeeded.
2576
+ }
2577
+ }
2578
+
2579
+ const text = formatPrCreateResult({
2580
+ url,
2581
+ prNumber: parsed.prNumber,
2582
+ data: prView,
2583
+ title,
2584
+ base,
2585
+ head,
2586
+ draft,
2587
+ });
2588
+ return buildTextResult(text, url || prView?.url);
2589
+ } finally {
2590
+ if (bodyDir) {
2591
+ await fs.rm(bodyDir, { recursive: true, force: true }).catch(() => {});
2592
+ }
2593
+ }
2594
+ }
2595
+
2596
+ function formatPrCreateResult(options: {
2597
+ url: string;
2598
+ prNumber?: number;
2599
+ data?: GhPrViewData;
2600
+ title?: string;
2601
+ base?: string;
2602
+ head?: string;
2603
+ draft?: boolean;
2604
+ }): string {
2605
+ const number = options.prNumber ?? options.data?.number;
2606
+ const headerTitle = options.data?.title ?? options.title ?? "Untitled";
2607
+ const header =
2608
+ number !== undefined
2609
+ ? `# Created Pull Request #${number}: ${headerTitle}`
2610
+ : `# Created Pull Request: ${headerTitle}`;
2611
+ const lines: string[] = [header, ""];
2612
+ pushLine(lines, "URL", options.url || options.data?.url);
2613
+ pushLine(lines, "State", options.data?.state);
2614
+ pushLine(lines, "Draft", options.data?.isDraft ?? options.draft);
2615
+ pushLine(lines, "Base", options.data?.baseRefName ?? options.base);
2616
+ pushLine(lines, "Head", options.data?.headRefName ?? options.head);
2617
+ pushLine(lines, "Author", formatAuthor(options.data?.author));
2618
+ pushLine(lines, "Created", options.data?.createdAt);
2619
+ pushLine(lines, "Labels", formatLabels(options.data?.labels));
2620
+
2621
+ const bodyText = normalizeText(options.data?.body);
2622
+ if (bodyText) {
2623
+ lines.push("");
2624
+ lines.push("## Body");
2625
+ lines.push("");
2626
+ lines.push(bodyText);
2627
+ }
2628
+
2629
+ return lines.join("\n").trim();
2630
+ }
2631
+
2445
2632
  async function executeSearchIssues(
2446
2633
  session: ToolSession,
2447
2634
  params: GithubInput,
@@ -1125,7 +1125,9 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1125
1125
  headers: {
1126
1126
  "Content-Type": "application/json",
1127
1127
  Authorization: `Bearer ${apiKey.apiKey}`,
1128
- "X-Title": "Oh-My-Pi",
1128
+ "HTTP-Referer": "https://github.com/can1357/oh-my-pi",
1129
+ "X-OpenRouter-Title": "Oh-My-Pi",
1130
+ "X-OpenRouter-Categories": "cli-agent",
1129
1131
  },
1130
1132
  body: JSON.stringify(requestBody),
1131
1133
  signal: requestSignal,
@@ -127,7 +127,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
127
127
  const response = await this.completeImageRequest(
128
128
  model,
129
129
  {
130
- systemPrompt: prompt.render(inspectImageSystemPromptTemplate),
130
+ systemPrompt: [prompt.render(inspectImageSystemPromptTemplate)],
131
131
  messages: [
132
132
  {
133
133
  role: "user",
@@ -337,7 +337,7 @@ export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
337
337
  }
338
338
 
339
339
  if (truncation.nextOffset != null) {
340
- notice += `. Use sel=${truncation.nextOffset} to continue`;
340
+ notice += `. Use :${truncation.nextOffset} to continue`;
341
341
  }
342
342
 
343
343
  if (truncation.artifactId != null) {
@@ -5,6 +5,7 @@ import * as url from "node:url";
5
5
  import { isEnoent } from "@oh-my-pi/pi-utils";
6
6
 
7
7
  const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
8
+ const FILE_LINE_RANGE_RE = /^(?:L?\d+(?:[-+]L?\d+)?|raw)$/i;
8
9
  const NARROW_NO_BREAK_SPACE = "\u202F";
9
10
  const TOP_LEVEL_INTERNAL_URL_PREFIXES = [
10
11
  "agent://",
@@ -102,6 +103,16 @@ export function expandPath(filePath: string): string {
102
103
  return expandTilde(normalized);
103
104
  }
104
105
 
106
+ export function splitPathAndSel(rawPath: string): { path: string; sel?: string } {
107
+ const colon = rawPath.lastIndexOf(":");
108
+ if (colon <= 0) return { path: rawPath };
109
+
110
+ const candidate = rawPath.slice(colon + 1);
111
+ if (!FILE_LINE_RANGE_RE.test(candidate)) return { path: rawPath };
112
+
113
+ return { path: rawPath.slice(0, colon), sel: candidate };
114
+ }
115
+
105
116
  function assertNotInternalUrl(expanded: string, original: string): void {
106
117
  for (const prefix of TOP_LEVEL_INTERNAL_URL_PREFIXES) {
107
118
  if (expanded.startsWith(prefix)) {