Files
oh-my-pi/packages/coding-agent/src/modes/github-ref-autocomplete.ts
T
oldschoola d8f52e8a9b fix(coding-agent): require token boundary for #<number>; add pr/issue qualifier
Address review feedback on #3224 (roboomp + Codex): #<number> now requires a token boundary before # (start/whitespace/quote/paren/</=), matching the internal-URL boundary set, so owner/repo#N, foo#N, C#12, and URL fragments no longer match and corrupt on accept. Also detect a pr/pull/issue qualifier word before the number (pr #3164 / issue #3164); when present, only that kind is offered and the qualifier is consumed on accept.
2026-06-21 18:36:20 -07:00

76 lines
3.3 KiB
TypeScript

/**
* Autocomplete for GitHub issue/PR references typed as `#<number>` (e.g. `#3164`).
*
* Mirrors the `@` file-reference and `scheme://` internal-url conventions: the
* token is rewritten to an internal URL (`pr://3164` or `issue://3164`) plus a
* trailing space, and the existing tool-mediated pipeline (the `read` tool →
* InternalUrlRouter → `gh`) resolves it from the session cwd's git remote.
*
* No network at suggestion time — candidates are generated locally. GitHub
* shares the issue/PR number space and there is no cheap way to tell which a
* given number is while typing, so both a PR and an Issue candidate are offered
* by default. Naming the type first (`pr #3164` / `issue #3164`) constrains the
* candidates to that kind. Anything that is not a standalone `#<number>` token
* keeps falling through to the existing prompt-action menu.
*/
import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
/** Candidate kinds, in default display order. */
const GITHUB_REF_KINDS = [
{ qualifier: "pr", scheme: "pr", label: "PR", description: "GitHub pull request" },
{ qualifier: "issue", scheme: "issue", label: "Issue", description: "GitHub issue" },
] as const;
export interface GithubRefContext {
/** Text to replace on accept: `#3164`, or `pr #3164` when a qualifier precedes it. */
prefix: string;
/** Type the user named (`pr`/`pull` → `pr`, `issue` → `issue`), or null to offer both. */
qualifier: "pr" | "issue" | null;
/** The numeric reference, e.g. `3164`. */
number: string;
}
/**
* A standalone `#<positive-number>` token ending at the cursor. The `#` must be
* preceded by a token boundary (start, whitespace, or an opening quote/paren/`<`/`=`,
* matching the internal-URL boundary set) so embedded hashes like `owner/repo#N`,
* `foo#N`, `C#12`, or a URL fragment do not match. An optional `pr`/`pull`/`issue`
* qualifier word (case-insensitive) immediately before the `#` constrains the kind.
*/
const GITHUB_REF_TOKEN_RE = /(?:^|[\s"'`(<=])(?:(pr|pull|issue)(\s+))?#([1-9]\d*)$/i;
export function getGithubRefContext(textBeforeCursor: string): GithubRefContext | null {
const match = textBeforeCursor.match(GITHUB_REF_TOKEN_RE);
if (!match) return null;
const qualifierWord = match[1];
const whitespace = match[2] ?? "";
const number = match[3] ?? "";
return {
prefix: qualifierWord ? `${qualifierWord}${whitespace}#${number}` : `#${number}`,
qualifier: !qualifierWord ? null : qualifierWord.toLowerCase() === "issue" ? "issue" : "pr",
number,
};
}
/**
* Suggestions for a `#<number>` token. Both kinds are offered unless the user
* named a type (`pr #3164` / `issue #3164`), in which case only that kind is
* offered. Returns `null` when the text before the cursor is not a standalone
* `#<number>` token.
*/
export function getGithubRefSuggestions(
textBeforeCursor: string,
): { items: AutocompleteItem[]; prefix: string } | null {
const context = getGithubRefContext(textBeforeCursor);
if (!context) return null;
const kinds = context.qualifier
? GITHUB_REF_KINDS.filter(kind => kind.qualifier === context.qualifier)
: GITHUB_REF_KINDS;
const items: AutocompleteItem[] = kinds.map(kind => ({
value: `${kind.scheme}://${context.number}`,
label: `${kind.label} #${context.number}`,
description: kind.description,
}));
return { items, prefix: context.prefix };
}