{"slug": "pi-coding-agent-extensions-debug-guardrails-review-todos-emacs-bridge-web", "title": "Pi Coding Agent Extensions - Debug, Guardrails, Review, Todos, Emacs Bridge, Web Browser", "summary": "Pi Coding Agent has released a set of extensions including a code review extension that provides `/review`, `/post-review`, `/resume-review`, and `/end-review` commands. The extension integrates with GitHub via the `gh` CLI, supports project-specific guidelines from REVIEW_GUIDELINES.md, and allows filtering by priority and type. It also includes debug, guardrails, todos, an Emacs bridge, and a web browser extension.", "body_md": "|\n/** |\n|\n* Code Review Extension |\n|\n* |\n|\n* Provides a `/review` command focused on: |\n|\n* 1. Validating code correctness |\n|\n* 2. Surfacing important design decisions |\n|\n* |\n|\n* Usage: |\n|\n* - `/review` - show interactive selector |\n|\n* - `/review uncommitted` - review uncommitted changes |\n|\n* - `/review branch main` - review against main branch |\n|\n* - `/review commit abc123` - review specific commit |\n|\n* - `/review pr 123` - review PR #123 (checks out locally) |\n|\n* - `/review file src/foo.ts` - review specific files (snapshot) |\n|\n* - `/review custom \"focus on error handling\"` - custom focus |\n|\n* - `/post-review` - post findings to GitHub PR (interactive selection) |\n|\n* - `/post-review 123` - post findings to specific PR |\n|\n* - `/post-review P1` - only post P1 priority findings |\n|\n* - `/post-review P0 P1` - only post P0 and P1 findings |\n|\n* - `/post-review design` - only post design findings |\n|\n* - `/post-review 123 P1 correctness` - combine PR number and filters |\n|\n* - `/resume-review` - restore review session after /reload |\n|\n* - `/end-review` - complete review and return to original position |\n|\n* |\n|\n* Project-specific guidelines: |\n|\n* - If REVIEW_GUIDELINES.md exists in project root, it's included in the prompt. |\n|\n* |\n|\n* GitHub Integration: |\n|\n* - Uses `gh` CLI for PR operations (checkout, post comments) |\n|\n* - `/post-review` parses review findings and posts as PR review with inline comments |\n|\n*/ |\n|\n|\n|\nimport type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from \"@mariozechner/pi-coding-agent\"; |\n|\nimport { Text } from \"@mariozechner/pi-tui\"; |\n|\nimport path from \"node:path\"; |\n|\nimport { promises as fs } from \"node:fs\"; |\n|\n|\n|\n// State for fresh session review tracking |\n|\nlet reviewOriginId: string | undefined = undefined; |\n|\nlet currentPrNumber: number | undefined = undefined; |\n|\nlet currentPrBaseBranch: string | undefined = undefined; |\n|\nconst REVIEW_STATE_TYPE = \"review-session\"; |\n|\n|\n|\ntype ReviewSessionState = { |\n|\nactive: boolean; |\n|\noriginId?: string; |\n|\nprNumber?: number; |\n|\nprBaseBranch?: string; |\n|\n}; |\n|\n|\n|\n// Types for PR review comments |\n|\ntype ReviewFinding = { |\n|\ntype: \"correctness\" | \"design\"; |\n|\npriority?: \"P0\" | \"P1\" | \"P2\" | \"P3\"; |\n|\nfile: string; |\n|\nline?: number; |\n|\nissue: string; |\n|\ndetails: string; |\n|\nsuggestion?: string; |\n|\n}; |\n|\n|\n|\ntype ParsedReview = { |\n|\nfindings: ReviewFinding[]; |\n|\nsummary?: string; |\n|\n}; |\n|\n|\n|\nfunction setReviewWidget(ctx: ExtensionContext, active: boolean) { |\n|\nif (!ctx.hasUI) return; |\n|\nif (!active) { |\n|\nctx.ui.setWidget(\"review\", undefined); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nctx.ui.setWidget(\"review\", (_tui, theme) => { |\n|\nconst text = new Text(theme.fg(\"warning\", \"Review session active — use /end-review to return\"), 0, 0); |\n|\nreturn { |\n|\nrender(width: number) { return text.render(width); }, |\n|\ninvalidate() { text.invalidate(); }, |\n|\n}; |\n|\n}); |\n|\n} |\n|\n|\n|\nfunction getReviewState(ctx: ExtensionContext): ReviewSessionState | undefined { |\n|\nlet state: ReviewSessionState | undefined; |\n|\nfor (const entry of ctx.sessionManager.getBranch()) { |\n|\nif (entry.type === \"custom\" && entry.customType === REVIEW_STATE_TYPE) { |\n|\nstate = entry.data as ReviewSessionState | undefined; |\n|\n} |\n|\n} |\n|\nreturn state; |\n|\n} |\n|\n|\n|\nfunction applyReviewState(ctx: ExtensionContext) { |\n|\nconst state = getReviewState(ctx); |\n|\nif (state?.active && state.originId) { |\n|\nreviewOriginId = state.originId; |\n|\ncurrentPrNumber = state.prNumber; |\n|\ncurrentPrBaseBranch = state.prBaseBranch; |\n|\nsetReviewWidget(ctx, true); |\n|\nreturn; |\n|\n} |\n|\nreviewOriginId = undefined; |\n|\ncurrentPrNumber = undefined; |\n|\ncurrentPrBaseBranch = undefined; |\n|\nsetReviewWidget(ctx, false); |\n|\n} |\n|\n|\n|\n// Parse review findings from assistant messages |\n|\nfunction parseReviewFindings(messages: string[]): ParsedReview { |\n|\nconst findings: ReviewFinding[] = []; |\n|\nlet summary: string | undefined; |\n|\n|\n|\nconst combinedText = messages.join(\"\\n\\n\"); |\n|\n|\n|\n// Extract findings using regex patterns matching the review output format |\n|\n// Format: **[CORRECTNESS]** or **[DESIGN]** followed by **File:** `path:line` |\n|\nconst findingPattern = /\\*\\*\\[(CORRECTNESS|DESIGN)\\]\\*\\*(?:\\s*\\*\\*\\[(P[0-3])\\]\\*\\*)?\\s*\\n+\\*\\*File:\\*\\*\\s*`([^`]+)`\\s*\\n+\\*\\*Issue:\\*\\*\\s*([^\\n]+)\\s*\\n+\\*\\*Details:\\*\\*\\s*([\\s\\S]*?)(?=\\*\\*Suggestion:\\*\\*|\\*\\*\\[(?:CORRECTNESS|DESIGN)\\]\\*\\*|## Summary|$)\\s*(?:\\*\\*Suggestion:\\*\\*\\s*([\\s\\S]*?))?(?=\\*\\*\\[(?:CORRECTNESS|DESIGN)\\]\\*\\*|## Summary|$)/gi; |\n|\n|\n|\nlet match; |\n|\nwhile ((match = findingPattern.exec(combinedText)) !== null) { |\n|\nconst [, type, priority, filePath, issue, details, suggestion] = match; |\n|\n|\n|\n// Parse file:line format |\n|\nconst fileMatch = filePath.match(/^(.+?)(?::(\\d+))?$/); |\n|\nif (fileMatch) { |\n|\nfindings.push({ |\n|\ntype: type.toLowerCase() as \"correctness\" | \"design\", |\n|\npriority: priority as \"P0\" | \"P1\" | \"P2\" | \"P3\" | undefined, |\n|\nfile: fileMatch[1].trim(), |\n|\nline: fileMatch[2] ? parseInt(fileMatch[2], 10) : undefined, |\n|\nissue: issue.trim(), |\n|\ndetails: details.trim(), |\n|\nsuggestion: suggestion?.trim(), |\n|\n}); |\n|\n} |\n|\n} |\n|\n|\n|\n// Extract summary section |\n|\nconst summaryMatch = combinedText.match(/## Summary\\s*([\\s\\S]*?)(?=## Key Questions|$)/i); |\n|\nif (summaryMatch) { |\n|\nsummary = summaryMatch[1].trim(); |\n|\n} |\n|\n|\n|\nreturn { findings, summary }; |\n|\n} |\n|\n|\n|\n// Get the latest assistant messages from the session for parsing |\n|\nfunction getAssistantMessages(ctx: ExtensionContext, searchAll = false): string[] { |\n|\nconst messages: string[] = []; |\n|\nconst allAssistantMessages: string[] = []; |\n|\nconst entries = ctx.sessionManager.getBranch(); |\n|\n|\n|\n// Get messages from current review session (after the review prompt) |\n|\nlet foundReviewPrompt = false; |\n|\nfor (const entry of entries) { |\n|\nif (entry.type === \"message\") { |\n|\nconst msg = entry.message; |\n|\n|\n|\n// Collect all assistant messages as fallback |\n|\nif (msg.role === \"assistant\") { |\n|\nconst extractText = (content: any): string[] => { |\n|\nif (Array.isArray(content)) { |\n|\nreturn content.filter(p => p.type === \"text\").map(p => p.text); |\n|\n} else if (typeof content === \"string\") { |\n|\nreturn [content]; |\n|\n} |\n|\nreturn []; |\n|\n}; |\n|\nallAssistantMessages.push(...extractText(msg.content)); |\n|\n} |\n|\n|\n|\nif (msg.role === \"user\") { |\n|\nconst content = typeof msg.content === \"string\" ? msg.content : |\n|\nArray.isArray(msg.content) ? msg.content.filter(p => p.type === \"text\").map(p => p.text).join(\"\\n\") : \"\"; |\n|\nif (content.includes(\"# Code Review Guidelines\") || content.includes(\"Review the\") && content.includes(\"changes\")) { |\n|\nfoundReviewPrompt = true; |\n|\ncontinue; |\n|\n} |\n|\n} |\n|\nif (foundReviewPrompt && msg.role === \"assistant\") { |\n|\nif (Array.isArray(msg.content)) { |\n|\nfor (const part of msg.content) { |\n|\nif (part.type === \"text\") { |\n|\nmessages.push(part.text); |\n|\n} |\n|\n} |\n|\n} else if (typeof msg.content === \"string\") { |\n|\nmessages.push(msg.content); |\n|\n} |\n|\n} |\n|\n} |\n|\n} |\n|\n|\n|\n// If we didn't find a review prompt or searchAll is true, return all assistant messages |\n|\n// that contain review-like content |\n|\nif (messages.length === 0 || searchAll) { |\n|\n// Filter to only messages that look like they contain review findings |\n|\nconst reviewMessages = allAssistantMessages.filter(m => |\n|\nm.includes(\"[CORRECTNESS]\") || |\n|\nm.includes(\"[DESIGN]\") || |\n|\nm.includes(\"**File:**\") || |\n|\nm.includes(\"## Summary\") |\n|\n); |\n|\nif (reviewMessages.length > 0) { |\n|\nreturn reviewMessages; |\n|\n} |\n|\n// Last resort: return all assistant messages |\n|\nreturn allAssistantMessages; |\n|\n} |\n|\n|\n|\nreturn messages; |\n|\n} |\n|\n|\n|\n// Format a finding as a GitHub review comment body |\n|\nfunction formatFindingAsComment(finding: ReviewFinding): string { |\n|\nlet body = \"\"; |\n|\n|\n|\n// Header with type and priority |\n|\nconst typeEmoji = finding.type === \"correctness\" ? \"🐛\" : \"💡\"; |\n|\nconst priorityBadge = finding.priority ? ` **[${finding.priority}]**` : \"\"; |\n|\nbody += `${typeEmoji} **${finding.type.toUpperCase()}**${priorityBadge}\\n\\n`; |\n|\n|\n|\n// Issue |\n|\nbody += `**Issue:** ${finding.issue}\\n\\n`; |\n|\n|\n|\n// Details |\n|\nbody += `${finding.details}\\n`; |\n|\n|\n|\n// Suggestion |\n|\nif (finding.suggestion) { |\n|\nbody += `\\n**Suggestion:** ${finding.suggestion}`; |\n|\n} |\n|\n|\n|\nreturn body; |\n|\n} |\n|\n|\n|\n// Get the diff position for a file:line in the PR diff |\n|\nasync function getDiffPosition( |\n|\npi: ExtensionAPI, |\n|\nprNumber: number, |\n|\nfile: string, |\n|\nline: number |\n|\n): Promise<number | null> { |\n|\n// Get the PR diff |\n|\nconst { stdout, code } = await pi.exec(\"gh\", [ |\n|\n\"api\", |\n|\n`repos/{owner}/{repo}/pulls/${prNumber}`, |\n|\n\"-H\", \"Accept: application/vnd.github.diff\", |\n|\n]); |\n|\n|\n|\nif (code !== 0) return null; |\n|\n|\n|\n// Parse diff to find position |\n|\n// The position is the line number in the diff, not the file |\n|\nconst lines = stdout.split(\"\\n\"); |\n|\nlet currentFile = \"\"; |\n|\nlet position = 0; |\n|\nlet inFile = false; |\n|\nlet newLineNumber = 0; |\n|\n|\n|\nfor (const diffLine of lines) { |\n|\nif (diffLine.startsWith(\"diff --git\")) { |\n|\n// Extract filename from diff header |\n|\nconst match = diffLine.match(/b\\/(.+)$/); |\n|\ncurrentFile = match ? match[1] : \"\"; |\n|\ninFile = currentFile === file || file.endsWith(currentFile) || currentFile.endsWith(file); |\n|\nposition = 0; |\n|\nnewLineNumber = 0; |\n|\n} else if (diffLine.startsWith(\"@@\") && inFile) { |\n|\n// Parse hunk header: @@ -old,count +new,count @@ |\n|\nconst hunkMatch = diffLine.match(/@@ -\\d+(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@/); |\n|\nif (hunkMatch) { |\n|\nnewLineNumber = parseInt(hunkMatch[1], 10) - 1; |\n|\n} |\n|\nposition++; |\n|\n} else if (inFile && (diffLine.startsWith(\"+\") || diffLine.startsWith(\"-\") || diffLine.startsWith(\" \"))) { |\n|\nposition++; |\n|\nif (!diffLine.startsWith(\"-\")) { |\n|\nnewLineNumber++; |\n|\nif (newLineNumber === line) { |\n|\nreturn position; |\n|\n} |\n|\n} |\n|\n} |\n|\n} |\n|\n|\n|\nreturn null; |\n|\n} |\n|\n|\n|\n// Review targets |\n|\ntype ReviewTarget = |\n|\n| { type: \"uncommitted\" } |\n|\n| { type: \"baseBranch\"; branch: string } |\n|\n| { type: \"commit\"; sha: string; title?: string } |\n|\n| { type: \"pullRequest\"; prNumber: number; baseBranch: string; title: string } |\n|\n| { type: \"file\"; paths: string[] } |\n|\n| { type: \"custom\"; instructions: string }; |\n|\n|\n|\n// The review rubric focused on correctness and design decisions |\n|\nconst REVIEW_RUBRIC = `# Code Review Guidelines |\n|\n|\n|\nYou are reviewing code changes with two primary goals: |\n|\n1. **Validate correctness** — Find bugs, logic errors, and potential runtime issues |\n|\n2. **Surface design decisions** — Identify architectural choices that deserve discussion |\n|\n|\n|\n## What to Look For |\n|\n|\n|\n### Correctness Issues (MUST flag) |\n|\n- Logic errors, off-by-one bugs, incorrect conditions |\n|\n- Null/undefined handling gaps |\n|\n- Race conditions, deadlocks, resource leaks |\n|\n- Error handling that swallows or mishandles errors |\n|\n- Type mismatches or unsafe casts |\n|\n- Security vulnerabilities (SQL injection, XSS, path traversal, etc.) |\n|\n- Incorrect API usage or protocol violations |\n|\n- Edge cases that would cause failures |\n|\n|\n|\n### Design Decisions (SHOULD surface) |\n|\n- New abstractions, interfaces, or patterns introduced |\n|\n- Changes to data models or schemas |\n|\n- New dependencies or significant library choices |\n|\n- Performance trade-offs (time vs space, caching strategies) |\n|\n- Error handling strategies (fail-fast vs graceful degradation) |\n|\n- API design choices (naming, signatures, contracts) |\n|\n- Coupling/cohesion changes |\n|\n- Backwards compatibility implications |\n|\n- Testing strategy choices |\n|\n|\n|\n## Review Process |\n|\n|\n|\n1. **Understand the change** — What is this trying to accomplish? |\n|\n2. **Trace the data flow** — Follow inputs through transformations to outputs |\n|\n3. **Consider edge cases** — What happens with empty, null, huge, or malformed inputs? |\n|\n4. **Check error paths** — Are errors handled correctly and propagated appropriately? |\n|\n5. **Evaluate the design** — Is this the right abstraction? Will it scale? Is it maintainable? |\n|\n|\n|\n## Output Format |\n|\n|\n|\n### For each finding, provide: |\n|\n|\n|\n**[CORRECTNESS]** or **[DESIGN]** tag |\n|\n|\n|\n**File:** \\`path/to/file.ts:line\\` |\n|\n|\n|\n**Issue:** One-sentence summary |\n|\n|\n|\n**Details:** Brief explanation (1-2 paragraphs max) |\n|\n- Why this is a problem (for correctness) or why it deserves discussion (for design) |\n|\n- Specific scenario where it would fail (for correctness) |\n|\n|\n|\n**Suggestion:** How to fix it or what to consider |\n|\n|\n|\n--- |\n|\n|\n|\n### Priority Levels (for correctness issues only): |\n|\n- **[P0]** — Will cause data loss, security breach, or crash in production |\n|\n- **[P1]** — Will cause incorrect behavior in common scenarios |\n|\n- **[P2]** — Will cause incorrect behavior in edge cases |\n|\n- **[P3]** — Code smell or potential future issue |\n|\n|\n|\n### At the end, provide: |\n|\n|\n|\n## Summary |\n|\n|\n|\n**Correctness:** [PASS / NEEDS FIXES] — Brief assessment |\n|\n**Design:** [List 2-3 most important design decisions that should be discussed] |\n|\n|\n|\n## Key Questions for the Author |\n|\n1. [Question about a design decision] |\n|\n2. [Question about intended behavior] |\n|\n3. [Question about trade-off made] |\n|\n|\n|\n--- |\n|\n|\n|\nFocus on findings the author would want to know about. Skip trivial style issues unless they impact readability or correctness.`; |\n|\n|\n|\n// Prompt templates |\n|\nconst UNCOMMITTED_PROMPT = `Review the current uncommitted changes (staged, unstaged, and untracked files). |\n|\n|\n|\nRun \\`git status\\` and \\`git diff\\` to see the changes, then review them.`; |\n|\n|\n|\nconst BASE_BRANCH_PROMPT = `Review the code changes against the base branch '{branch}'. |\n|\n|\n|\nFirst find the merge base: \\`git merge-base HEAD {branch}\\` |\n|\nThen review the diff: \\`git diff <merge-base-sha>\\` |\n|\n|\n|\nFocus on what would be merged into {branch}.`; |\n|\n|\n|\nconst COMMIT_PROMPT = `Review commit {sha}{titlePart}. |\n|\n|\n|\nRun \\`git show {sha}\\` to see the changes, then review them.`; |\n|\n|\n|\nconst PR_PROMPT = `Review pull request #{prNumber} \"{title}\" against {baseBranch}. |\n|\n|\n|\nFirst find the merge base: \\`git merge-base HEAD {baseBranch}\\` |\n|\nThen review the diff: \\`git diff <merge-base-sha>\\` |\n|\n|\n|\nThis represents what would be merged.`; |\n|\n|\n|\nconst FILE_PROMPT = `Review the code in these files (snapshot review, not a diff): |\n|\n|\n|\n{paths} |\n|\n|\n|\nRead each file and review for correctness issues and design patterns.`; |\n|\n|\n|\nasync function loadProjectGuidelines(cwd: string): Promise<string | null> { |\n|\nconst guidelinesPath = path.join(cwd, \"REVIEW_GUIDELINES.md\"); |\n|\ntry { |\n|\nconst content = await fs.readFile(guidelinesPath, \"utf8\"); |\n|\nreturn content.trim() || null; |\n|\n} catch { |\n|\nreturn null; |\n|\n} |\n|\n} |\n|\n|\n|\nasync function getLocalBranches(pi: ExtensionAPI): Promise<string[]> { |\n|\nconst { stdout, code } = await pi.exec(\"git\", [\"branch\", \"--format=%(refname:short)\"]); |\n|\nif (code !== 0) return []; |\n|\nreturn stdout.trim().split(\"\\n\").filter(b => b.trim()); |\n|\n} |\n|\n|\n|\nasync function getRecentCommits(pi: ExtensionAPI, limit = 15): Promise<Array<{ sha: string; title: string }>> { |\n|\nconst { stdout, code } = await pi.exec(\"git\", [\"log\", \"--oneline\", `-n`, `${limit}`]); |\n|\nif (code !== 0) return []; |\n|\nreturn stdout.trim().split(\"\\n\").filter(line => line.trim()).map(line => { |\n|\nconst [sha, ...rest] = line.trim().split(\" \"); |\n|\nreturn { sha, title: rest.join(\" \") }; |\n|\n}); |\n|\n} |\n|\n|\n|\nasync function hasUncommittedChanges(pi: ExtensionAPI): Promise<boolean> { |\n|\nconst { stdout, code } = await pi.exec(\"git\", [\"status\", \"--porcelain\"]); |\n|\nreturn code === 0 && stdout.trim().length > 0; |\n|\n} |\n|\n|\n|\nasync function hasPendingChanges(pi: ExtensionAPI): Promise<boolean> { |\n|\nconst { stdout, code } = await pi.exec(\"git\", [\"status\", \"--porcelain\"]); |\n|\nif (code !== 0) return false; |\n|\nconst lines = stdout.trim().split(\"\\n\").filter(line => line.trim()); |\n|\nreturn lines.filter(line => !line.startsWith(\"??\")).length > 0; |\n|\n} |\n|\n|\n|\nasync function getCurrentBranch(pi: ExtensionAPI): Promise<string | null> { |\n|\nconst { stdout, code } = await pi.exec(\"git\", [\"branch\", \"--show-current\"]); |\n|\nreturn code === 0 && stdout.trim() ? stdout.trim() : null; |\n|\n} |\n|\n|\n|\nasync function getDefaultBranch(pi: ExtensionAPI): Promise<string> { |\n|\nconst { stdout, code } = await pi.exec(\"git\", [\"symbolic-ref\", \"refs/remotes/origin/HEAD\", \"--short\"]); |\n|\nif (code === 0 && stdout.trim()) { |\n|\nreturn stdout.trim().replace(\"origin/\", \"\"); |\n|\n} |\n|\nconst branches = await getLocalBranches(pi); |\n|\nif (branches.includes(\"main\")) return \"main\"; |\n|\nif (branches.includes(\"master\")) return \"master\"; |\n|\nreturn \"main\"; |\n|\n} |\n|\n|\n|\nfunction parsePrReference(ref: string): number | null { |\n|\nconst trimmed = ref.trim(); |\n|\nconst num = parseInt(trimmed, 10); |\n|\nif (!isNaN(num) && num > 0) return num; |\n|\nconst urlMatch = trimmed.match(/github\\.com\\/[^/]+\\/[^/]+\\/pull\\/(\\d+)/); |\n|\nif (urlMatch) return parseInt(urlMatch[1], 10); |\n|\nreturn null; |\n|\n} |\n|\n|\n|\nasync function getPrInfo(pi: ExtensionAPI, prNumber: number): Promise<{ baseBranch: string; title: string; headBranch: string } | null> { |\n|\nconst { stdout, code } = await pi.exec(\"gh\", [\"pr\", \"view\", String(prNumber), \"--json\", \"baseRefName,title,headRefName\"]); |\n|\nif (code !== 0) return null; |\n|\ntry { |\n|\nconst data = JSON.parse(stdout); |\n|\nreturn { baseBranch: data.baseRefName, title: data.title, headBranch: data.headRefName }; |\n|\n} catch { |\n|\nreturn null; |\n|\n} |\n|\n} |\n|\n|\n|\nasync function checkoutPr(pi: ExtensionAPI, prNumber: number): Promise<{ success: boolean; error?: string }> { |\n|\nconst { stdout, stderr, code } = await pi.exec(\"gh\", [\"pr\", \"checkout\", String(prNumber)]); |\n|\nif (code !== 0) return { success: false, error: stderr || stdout || \"Failed to checkout PR\" }; |\n|\nreturn { success: true }; |\n|\n} |\n|\n|\n|\nfunction buildPrompt(target: ReviewTarget): string { |\n|\nswitch (target.type) { |\n|\ncase \"uncommitted\": |\n|\nreturn UNCOMMITTED_PROMPT; |\n|\ncase \"baseBranch\": |\n|\nreturn BASE_BRANCH_PROMPT.replace(/{branch}/g, target.branch); |\n|\ncase \"commit\": { |\n|\nconst titlePart = target.title ? ` (\"${target.title}\")` : \"\"; |\n|\nreturn COMMIT_PROMPT.replace(\"{sha}\", target.sha).replace(\"{titlePart}\", titlePart); |\n|\n} |\n|\ncase \"pullRequest\": |\n|\nreturn PR_PROMPT |\n|\n.replace(\"{prNumber}\", String(target.prNumber)) |\n|\n.replace(\"{title}\", target.title) |\n|\n.replace(\"{baseBranch}\", target.baseBranch); |\n|\ncase \"file\": |\n|\nreturn FILE_PROMPT.replace(\"{paths}\", target.paths.map(p => `- ${p}`).join(\"\\n\")); |\n|\ncase \"custom\": |\n|\nreturn target.instructions; |\n|\n} |\n|\n} |\n|\n|\n|\nfunction getTargetDescription(target: ReviewTarget): string { |\n|\nswitch (target.type) { |\n|\ncase \"uncommitted\": return \"uncommitted changes\"; |\n|\ncase \"baseBranch\": return `changes vs ${target.branch}`; |\n|\ncase \"commit\": return `commit ${target.sha.slice(0, 7)}${target.title ? `: ${target.title}` : \"\"}`; |\n|\ncase \"pullRequest\": return `PR #${target.prNumber}: ${target.title}`; |\n|\ncase \"file\": return `files: ${target.paths.join(\", \")}`; |\n|\ncase \"custom\": return target.instructions.slice(0, 40) + (target.instructions.length > 40 ? \"...\" : \"\"); |\n|\n} |\n|\n} |\n|\n|\n|\nconst REVIEW_PRESETS = [ |\n|\n{ value: \"uncommitted\", label: \"Review uncommitted changes\", description: \"\" }, |\n|\n{ value: \"baseBranch\", label: \"Review against a branch\", description: \"(PR-style diff)\" }, |\n|\n{ value: \"commit\", label: \"Review a specific commit\", description: \"\" }, |\n|\n{ value: \"pullRequest\", label: \"Review a GitHub PR\", description: \"(checks out locally)\" }, |\n|\n{ value: \"file\", label: \"Review specific files\", description: \"(snapshot)\" }, |\n|\n{ value: \"custom\", label: \"Custom review focus\", description: \"\" }, |\n|\n] as const; |\n|\n|\n|\nexport default function reviewExtension(pi: ExtensionAPI) { |\n|\n// Restore review state on session events |\n|\npi.on(\"session_start\", (_event, ctx) => applyReviewState(ctx)); |\n|\npi.on(\"session_switch\", (_event, ctx) => applyReviewState(ctx)); |\n|\npi.on(\"session_tree\", (_event, ctx) => applyReviewState(ctx)); |\n|\n|\n|\nasync function getSmartDefault(): Promise<string> { |\n|\nif (await hasUncommittedChanges(pi)) return \"uncommitted\"; |\n|\nconst current = await getCurrentBranch(pi); |\n|\nconst defaultBranch = await getDefaultBranch(pi); |\n|\nif (current && current !== defaultBranch) return \"baseBranch\"; |\n|\nreturn \"commit\"; |\n|\n} |\n|\n|\n|\nasync function showSelector(ctx: ExtensionContext): Promise<ReviewTarget | null> { |\n|\nconst smartDefault = await getSmartDefault(); |\n|\n|\n|\n// Sort presets with smart default first |\n|\nconst sortedPresets = REVIEW_PRESETS |\n|\n.slice() |\n|\n.sort((a, b) => { |\n|\nif (a.value === smartDefault) return -1; |\n|\nif (b.value === smartDefault) return 1; |\n|\nreturn 0; |\n|\n}); |\n|\n|\n|\n// Use simple ctx.ui.select() for better compatibility with non-TUI environments (e.g., Emacs RPC mode) |\n|\nconst options = sortedPresets.map(p => p.label); |\n|\n|\n|\nwhile (true) { |\n|\nconst selected = await ctx.ui.select(\"Select review type:\", options); |\n|\nif (selected === undefined) return null; |\n|\n|\n|\n// Find the preset by label |\n|\nconst preset = sortedPresets.find(p => p.label === selected); |\n|\nif (!preset) return null; |\n|\n|\n|\nswitch (preset.value) { |\n|\ncase \"uncommitted\": |\n|\nreturn { type: \"uncommitted\" }; |\n|\n|\n|\ncase \"baseBranch\": { |\n|\nconst branches = await getLocalBranches(pi); |\n|\nconst defaultBranch = await getDefaultBranch(pi); |\n|\nconst sorted = branches.sort((a, b) => { |\n|\nif (a === defaultBranch) return -1; |\n|\nif (b === defaultBranch) return 1; |\n|\nreturn a.localeCompare(b); |\n|\n}); |\n|\n|\n|\n// Add \"(default)\" suffix to default branch for visibility |\n|\nconst branchOptions = sorted.map(b => b === defaultBranch ? `${b} (default)` : b); |\n|\n|\n|\nconst selectedBranch = await ctx.ui.select(\"Select base branch:\", branchOptions); |\n|\nif (selectedBranch === undefined) continue; // Go back to main menu |\n|\n|\n|\n// Strip \"(default)\" suffix if present |\n|\nconst branch = selectedBranch.replace(\" (default)\", \"\"); |\n|\nreturn { type: \"baseBranch\", branch }; |\n|\n} |\n|\n|\n|\ncase \"commit\": { |\n|\nconst commits = await getRecentCommits(pi, 20); |\n|\nif (!commits.length) { |\n|\nctx.ui.notify(\"No commits found\", \"error\"); |\n|\ncontinue; |\n|\n} |\n|\n|\n|\nconst commitOptions = commits.map(c => `${c.sha.slice(0, 7)} ${c.title}`); |\n|\n|\n|\nconst selectedCommit = await ctx.ui.select(\"Select commit:\", commitOptions); |\n|\nif (selectedCommit === undefined) continue; // Go back to main menu |\n|\n|\n|\n// Find the commit by matching the option string |\n|\nconst commit = commits.find(c => `${c.sha.slice(0, 7)} ${c.title}` === selectedCommit); |\n|\nif (commit) return { type: \"commit\", sha: commit.sha, title: commit.title }; |\n|\ncontinue; |\n|\n} |\n|\n|\n|\ncase \"pullRequest\": { |\n|\nif (await hasPendingChanges(pi)) { |\n|\nctx.ui.notify(\"Cannot checkout PR: uncommitted changes exist. Commit or stash first.\", \"error\"); |\n|\ncontinue; |\n|\n} |\n|\n|\n|\nconst prRef = await ctx.ui.input(\"PR number or URL:\", \"123\"); |\n|\nif (!prRef?.trim()) continue; |\n|\n|\n|\nconst prNumber = parsePrReference(prRef); |\n|\nif (!prNumber) { |\n|\nctx.ui.notify(\"Invalid PR reference\", \"error\"); |\n|\ncontinue; |\n|\n} |\n|\n|\n|\nctx.ui.notify(`Fetching PR #${prNumber}...`, \"info\"); |\n|\nconst prInfo = await getPrInfo(pi, prNumber); |\n|\nif (!prInfo) { |\n|\nctx.ui.notify(`Could not find PR #${prNumber}. Is gh authenticated?`, \"error\"); |\n|\ncontinue; |\n|\n} |\n|\n|\n|\nctx.ui.notify(`Checking out PR #${prNumber}...`, \"info\"); |\n|\nconst checkout = await checkoutPr(pi, prNumber); |\n|\nif (!checkout.success) { |\n|\nctx.ui.notify(`Checkout failed: ${checkout.error}`, \"error\"); |\n|\ncontinue; |\n|\n} |\n|\n|\n|\nctx.ui.notify(`Checked out PR #${prNumber}`, \"info\"); |\n|\nreturn { type: \"pullRequest\", prNumber, baseBranch: prInfo.baseBranch, title: prInfo.title }; |\n|\n} |\n|\n|\n|\ncase \"file\": { |\n|\nconst input = await ctx.ui.input(\"Files/folders to review (space-separated):\", \"src/\"); |\n|\nif (!input?.trim()) continue; |\n|\nconst paths = input.trim().split(/\\s+/).filter(p => p); |\n|\nif (paths.length) return { type: \"file\", paths }; |\n|\ncontinue; |\n|\n} |\n|\n|\n|\ncase \"custom\": { |\n|\nconst instructions = await ctx.ui.editor(\"Review focus:\", \"Check for security issues and race conditions\"); |\n|\nif (instructions?.trim()) return { type: \"custom\", instructions: instructions.trim() }; |\n|\ncontinue; |\n|\n} |\n|\n} |\n|\n} |\n|\n} |\n|\n|\n|\nfunction parseArgs(args: string | undefined): ReviewTarget | { type: \"pr\"; ref: string } | null { |\n|\nif (!args?.trim()) return null; |\n|\nconst parts = args.trim().split(/\\s+/); |\n|\nconst cmd = parts[0]?.toLowerCase(); |\n|\n|\n|\nswitch (cmd) { |\n|\ncase \"uncommitted\": |\n|\nreturn { type: \"uncommitted\" }; |\n|\ncase \"branch\": { |\n|\nconst branch = parts[1]; |\n|\nreturn branch ? { type: \"baseBranch\", branch } : null; |\n|\n} |\n|\ncase \"commit\": { |\n|\nconst sha = parts[1]; |\n|\nif (!sha) return null; |\n|\nconst title = parts.slice(2).join(\" \") || undefined; |\n|\nreturn { type: \"commit\", sha, title }; |\n|\n} |\n|\ncase \"pr\": { |\n|\nconst ref = parts[1]; |\n|\nreturn ref ? { type: \"pr\", ref } : null; |\n|\n} |\n|\ncase \"file\": { |\n|\nconst paths = parts.slice(1); |\n|\nreturn paths.length ? { type: \"file\", paths } : null; |\n|\n} |\n|\ncase \"custom\": { |\n|\nconst instructions = parts.slice(1).join(\" \"); |\n|\nreturn instructions ? { type: \"custom\", instructions } : null; |\n|\n} |\n|\ndefault: |\n|\nreturn null; |\n|\n} |\n|\n} |\n|\n|\n|\nasync function handlePrCheckout(ctx: ExtensionContext, ref: string): Promise<ReviewTarget | null> { |\n|\nif (await hasPendingChanges(pi)) { |\n|\nctx.ui.notify(\"Cannot checkout PR: uncommitted changes. Commit or stash first.\", \"error\"); |\n|\nreturn null; |\n|\n} |\n|\n|\n|\nconst prNumber = parsePrReference(ref); |\n|\nif (!prNumber) { |\n|\nctx.ui.notify(\"Invalid PR reference\", \"error\"); |\n|\nreturn null; |\n|\n} |\n|\n|\n|\nctx.ui.notify(`Fetching PR #${prNumber}...`, \"info\"); |\n|\nconst prInfo = await getPrInfo(pi, prNumber); |\n|\nif (!prInfo) { |\n|\nctx.ui.notify(`Could not find PR #${prNumber}`, \"error\"); |\n|\nreturn null; |\n|\n} |\n|\n|\n|\nctx.ui.notify(`Checking out PR #${prNumber}...`, \"info\"); |\n|\nconst checkout = await checkoutPr(pi, prNumber); |\n|\nif (!checkout.success) { |\n|\nctx.ui.notify(`Checkout failed: ${checkout.error}`, \"error\"); |\n|\nreturn null; |\n|\n} |\n|\n|\n|\nctx.ui.notify(`Checked out PR #${prNumber}`, \"info\"); |\n|\nreturn { type: \"pullRequest\", prNumber, baseBranch: prInfo.baseBranch, title: prInfo.title }; |\n|\n} |\n|\n|\n|\nasync function executeReview(ctx: ExtensionCommandContext, target: ReviewTarget, useFreshSession: boolean): Promise<void> { |\n|\nif (reviewOriginId) { |\n|\nctx.ui.notify(\"Already in a review. Use /end-review first.\", \"warning\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nif (useFreshSession) { |\n|\nconst originId = ctx.sessionManager.getLeafId() ?? undefined; |\n|\nif (!originId) { |\n|\nctx.ui.notify(\"Failed to determine origin. Try from a session with messages.\", \"error\"); |\n|\nreturn; |\n|\n} |\n|\nreviewOriginId = originId; |\n|\nconst lockedOriginId = originId; |\n|\n|\n|\nconst entries = ctx.sessionManager.getEntries(); |\n|\nconst firstUserMessage = entries.find(e => e.type === \"message\" && e.message.role === \"user\"); |\n|\nif (!firstUserMessage) { |\n|\nctx.ui.notify(\"No user message found in session\", \"error\"); |\n|\nreviewOriginId = undefined; |\n|\nreturn; |\n|\n} |\n|\n|\n|\ntry { |\n|\nconst result = await ctx.navigateTree(firstUserMessage.id, { summarize: false, label: \"code-review\" }); |\n|\nif (result.cancelled) { |\n|\nreviewOriginId = undefined; |\n|\nreturn; |\n|\n} |\n|\n} catch (error) { |\n|\nreviewOriginId = undefined; |\n|\nctx.ui.notify(`Failed to start review: ${error instanceof Error ? error.message : String(error)}`, \"error\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nreviewOriginId = lockedOriginId; |\n|\nctx.ui.setEditorText(\"\"); |\n|\nsetReviewWidget(ctx, true); |\n|\n|\n|\n// Track PR info if this is a PR review |\n|\nif (target.type === \"pullRequest\") { |\n|\ncurrentPrNumber = target.prNumber; |\n|\ncurrentPrBaseBranch = target.baseBranch; |\n|\npi.appendEntry(REVIEW_STATE_TYPE, { |\n|\nactive: true, |\n|\noriginId: lockedOriginId, |\n|\nprNumber: target.prNumber, |\n|\nprBaseBranch: target.baseBranch, |\n|\n}); |\n|\n} else { |\n|\ncurrentPrNumber = undefined; |\n|\ncurrentPrBaseBranch = undefined; |\n|\npi.appendEntry(REVIEW_STATE_TYPE, { active: true, originId: lockedOriginId }); |\n|\n} |\n|\n} |\n|\n|\n|\nconst prompt = buildPrompt(target); |\n|\nconst projectGuidelines = await loadProjectGuidelines(ctx.cwd); |\n|\n|\n|\nlet fullPrompt = `${REVIEW_RUBRIC}\\n\\n---\\n\\n${prompt}`; |\n|\nif (projectGuidelines) { |\n|\nfullPrompt += `\\n\\n---\\n\\n## Project-Specific Guidelines\\n\\n${projectGuidelines}`; |\n|\n} |\n|\n|\n|\nconst desc = getTargetDescription(target); |\n|\nconst mode = useFreshSession ? \" (fresh session)\" : \"\"; |\n|\nctx.ui.notify(`Starting review: ${desc}${mode}`, \"info\"); |\n|\n|\n|\npi.sendUserMessage(fullPrompt); |\n|\n} |\n|\n|\n|\n// Register /review command |\n|\npi.registerCommand(\"review\", { |\n|\ndescription: \"Review code for correctness and design decisions\", |\n|\nhandler: async (args, ctx) => { |\n|\nif (!ctx.hasUI) { |\n|\nctx.ui.notify(\"Review requires interactive mode\", \"error\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nif (reviewOriginId) { |\n|\nctx.ui.notify(\"Already in a review. Use /end-review first.\", \"warning\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nconst { code } = await pi.exec(\"git\", [\"rev-parse\", \"--git-dir\"]); |\n|\nif (code !== 0) { |\n|\nctx.ui.notify(\"Not a git repository\", \"error\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nlet target: ReviewTarget | null = null; |\n|\nlet fromSelector = false; |\n|\nconst parsed = parseArgs(args); |\n|\n|\n|\nif (parsed) { |\n|\nif (parsed.type === \"pr\") { |\n|\ntarget = await handlePrCheckout(ctx, parsed.ref); |\n|\n} else { |\n|\ntarget = parsed; |\n|\n} |\n|\n} |\n|\n|\n|\nif (!target) fromSelector = true; |\n|\n|\n|\nwhile (true) { |\n|\nif (!target && fromSelector) { |\n|\ntarget = await showSelector(ctx); |\n|\n} |\n|\n|\n|\nif (!target) { |\n|\nctx.ui.notify(\"Review cancelled\", \"info\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nconst entries = ctx.sessionManager.getEntries(); |\n|\nconst messageCount = entries.filter(e => e.type === \"message\").length; |\n|\n|\n|\nlet useFreshSession = false; |\n|\nif (messageCount > 0) { |\n|\nconst choice = await ctx.ui.select(\"Start review in:\", [\"Empty branch\", \"Current session\"]); |\n|\nif (choice === undefined) { |\n|\nif (fromSelector) { |\n|\ntarget = null; |\n|\ncontinue; |\n|\n} |\n|\nctx.ui.notify(\"Review cancelled\", \"info\"); |\n|\nreturn; |\n|\n} |\n|\nuseFreshSession = choice === \"Empty branch\"; |\n|\n} |\n|\n|\n|\nawait executeReview(ctx, target, useFreshSession); |\n|\nreturn; |\n|\n} |\n|\n}, |\n|\n}); |\n|\n|\n|\n// Register /resume-review command to restore state after reload |\n|\npi.registerCommand(\"resume-review\", { |\n|\ndescription: \"Resume a review session (restores state after /reload)\", |\n|\nhandler: async (args, ctx) => { |\n|\nif (!ctx.hasUI) { |\n|\nctx.ui.notify(\"Requires interactive mode\", \"error\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\n// Check if already in a review |\n|\nif (reviewOriginId) { |\n|\nctx.ui.notify(\"Already in an active review session\", \"info\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\n// Try to restore state from session |\n|\nconst state = getReviewState(ctx); |\n|\nif (state?.active) { |\n|\nreviewOriginId = state.originId; |\n|\ncurrentPrNumber = state.prNumber; |\n|\ncurrentPrBaseBranch = state.prBaseBranch; |\n|\nsetReviewWidget(ctx, true); |\n|\n|\n|\nlet msg = \"Review session restored\"; |\n|\nif (currentPrNumber) { |\n|\nmsg += ` (PR #${currentPrNumber})`; |\n|\n} |\n|\nctx.ui.notify(msg, \"success\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\n// No active review state, but check if there are review findings in the session |\n|\n// Search all messages since old reviews didn't save state |\n|\nconst messages = getAssistantMessages(ctx, true); |\n|\nif (messages.length > 0) { |\n|\nconst parsed = parseReviewFindings(messages); |\n|\nif (parsed.findings.length > 0) { |\n|\nctx.ui.notify( |\n|\n`Found ${parsed.findings.length} review findings in conversation. Use /post-review <pr-number> to post them.`, |\n|\n\"success\" |\n|\n); |\n|\nreturn; |\n|\n} |\n|\n} |\n|\n|\n|\nctx.ui.notify(\"No review findings found in this session. Make sure you're in the right session branch.\", \"warning\"); |\n|\n}, |\n|\n}); |\n|\n|\n|\n// Review summary prompt |\n|\nconst REVIEW_SUMMARY_PROMPT = `Summarize this code review for future reference. |\n|\n|\n|\nInclude: |\n|\n1. What was reviewed (scope, files) |\n|\n2. Key findings: |\n|\n- Correctness issues found (with priority) |\n|\n- Design decisions surfaced |\n|\n3. Overall verdict |\n|\n4. Action items |\n|\n|\n|\nFormat as a structured summary that can be acted upon later.`; |\n|\n|\n|\n// Register /end-review command |\n|\npi.registerCommand(\"end-review\", { |\n|\ndescription: \"Complete review and return to original position\", |\n|\nhandler: async (args, ctx) => { |\n|\nif (!ctx.hasUI) { |\n|\nctx.ui.notify(\"Requires interactive mode\", \"error\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nif (!reviewOriginId) { |\n|\nconst state = getReviewState(ctx); |\n|\nif (state?.active && state.originId) { |\n|\nreviewOriginId = state.originId; |\n|\n} else if (state?.active) { |\n|\nsetReviewWidget(ctx, false); |\n|\npi.appendEntry(REVIEW_STATE_TYPE, { active: false }); |\n|\nctx.ui.notify(\"Review state cleared\", \"warning\"); |\n|\nreturn; |\n|\n} else { |\n|\nctx.ui.notify(\"Not in a review session\", \"info\"); |\n|\nreturn; |\n|\n} |\n|\n} |\n|\n|\n|\nconst choice = await ctx.ui.select(\"Summarize review?\", [\"Summarize\", \"No summary\"]); |\n|\nif (choice === undefined) { |\n|\nctx.ui.notify(\"Cancelled. Use /end-review to try again.\", \"info\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nconst wantsSummary = choice === \"Summarize\"; |\n|\nconst originId = reviewOriginId; |\n|\n|\n|\nif (wantsSummary) { |\n|\n// Show notification instead of custom loader UI for better compatibility with non-TUI environments |\n|\nctx.ui.notify(\"Summarizing review...\", \"info\"); |\n|\n|\n|\ntry { |\n|\nconst result = await ctx.navigateTree(originId!, { |\n|\nsummarize: true, |\n|\ncustomInstructions: REVIEW_SUMMARY_PROMPT, |\n|\nreplaceInstructions: true, |\n|\n}); |\n|\n|\n|\nsetReviewWidget(ctx, false); |\n|\nreviewOriginId = undefined; |\n|\ncurrentPrNumber = undefined; |\n|\ncurrentPrBaseBranch = undefined; |\n|\npi.appendEntry(REVIEW_STATE_TYPE, { active: false }); |\n|\n|\n|\nif (result.cancelled) { |\n|\nctx.ui.notify(\"Navigation cancelled\", \"info\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\ntry { |\n|\nif (!ctx.ui.getEditorText().trim()) { |\n|\nctx.ui.setEditorText(\"Address the review findings\"); |\n|\n} |\n|\n} catch { |\n|\n// getEditorText/setEditorText may not be available in all environments |\n|\n} |\n|\n|\n|\nctx.ui.notify(\"Review complete!\", \"info\"); |\n|\n} catch (error) { |\n|\nctx.ui.notify(`Failed: ${error instanceof Error ? error.message : String(error)}`, \"error\"); |\n|\n} |\n|\n} else { |\n|\ntry { |\n|\nconst result = await ctx.navigateTree(originId!, { summarize: false }); |\n|\nif (result.cancelled) { |\n|\nctx.ui.notify(\"Navigation cancelled\", \"info\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nsetReviewWidget(ctx, false); |\n|\nreviewOriginId = undefined; |\n|\ncurrentPrNumber = undefined; |\n|\ncurrentPrBaseBranch = undefined; |\n|\npi.appendEntry(REVIEW_STATE_TYPE, { active: false }); |\n|\nctx.ui.notify(\"Review complete!\", \"info\"); |\n|\n} catch (error) { |\n|\nctx.ui.notify(`Failed: ${error instanceof Error ? error.message : String(error)}`, \"error\"); |\n|\n} |\n|\n} |\n|\n}, |\n|\n}); |\n|\n|\n|\n// Register /post-review command for posting findings to GitHub PR |\n|\npi.registerCommand(\"post-review\", { |\n|\ndescription: \"Post review findings as GitHub PR comments. Use /post-review [PR#] [P0|P1|P2|P3|design|correctness]\", |\n|\nhandler: async (args, ctx) => { |\n|\nif (!ctx.hasUI) { |\n|\nctx.ui.notify(\"Requires interactive mode\", \"error\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\n// Try to restore state from session if not in memory (e.g., after /reload) |\n|\nif (!currentPrNumber) { |\n|\nconst state = getReviewState(ctx); |\n|\nif (state?.prNumber) { |\n|\ncurrentPrNumber = state.prNumber; |\n|\ncurrentPrBaseBranch = state.prBaseBranch; |\n|\nreviewOriginId = state.originId; |\n|\nif (state.active) { |\n|\nsetReviewWidget(ctx, true); |\n|\n} |\n|\n} |\n|\n} |\n|\n|\n|\n// Parse args: [PR number] [priority/type filters...] |\n|\n// e.g., \"/post-review 123 P1 P0\" or \"/post-review P1\" or \"/post-review design\" |\n|\nlet prNumber = currentPrNumber; |\n|\nlet baseBranch = currentPrBaseBranch; |\n|\nconst priorityFilters: Set<string> = new Set(); |\n|\nconst typeFilters: Set<string> = new Set(); |\n|\n|\n|\nif (args?.trim()) { |\n|\nconst parts = args.trim().split(/\\s+/); |\n|\nfor (const part of parts) { |\n|\nconst upper = part.toUpperCase(); |\n|\nif (/^P[0-3]$/i.test(part)) { |\n|\npriorityFilters.add(upper); |\n|\n} else if (part.toLowerCase() === \"design\") { |\n|\ntypeFilters.add(\"design\"); |\n|\n} else if (part.toLowerCase() === \"correctness\") { |\n|\ntypeFilters.add(\"correctness\"); |\n|\n} else { |\n|\n// Try to parse as PR number |\n|\nconst parsed = parsePrReference(part); |\n|\nif (parsed) { |\n|\nprNumber = parsed; |\n|\nconst prInfo = await getPrInfo(pi, prNumber); |\n|\nif (prInfo) { |\n|\nbaseBranch = prInfo.baseBranch; |\n|\n} |\n|\n} |\n|\n} |\n|\n} |\n|\n} |\n|\n|\n|\nif (!prNumber) { |\n|\nctx.ui.notify(\"No PR specified. Use /post-review <pr-number> or run from a PR review session.\", \"error\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\n// Check gh CLI is available |\n|\nconst { code: ghCode } = await pi.exec(\"gh\", [\"auth\", \"status\"]); |\n|\nif (ghCode !== 0) { |\n|\nctx.ui.notify(\"GitHub CLI not authenticated. Run: gh auth login\", \"error\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\n// Parse review findings from session |\n|\nlet messages = getAssistantMessages(ctx); |\n|\nlet parsed = parseReviewFindings(messages); |\n|\n|\n|\n// If no findings found, try searching all messages (for old reviews without state) |\n|\nif (parsed.findings.length === 0) { |\n|\nmessages = getAssistantMessages(ctx, true); |\n|\nparsed = parseReviewFindings(messages); |\n|\n} |\n|\n|\n|\nif (messages.length === 0) { |\n|\nctx.ui.notify(\"No review messages found in session\", \"error\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nif (parsed.findings.length === 0) { |\n|\nctx.ui.notify(\"No structured findings found. Make sure the review output includes **[CORRECTNESS]** or **[DESIGN]** tags.\", \"warning\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\n// Apply priority/type filters |\n|\nlet filteredFindings = parsed.findings; |\n|\nif (priorityFilters.size > 0 || typeFilters.size > 0) { |\n|\nfilteredFindings = parsed.findings.filter(f => { |\n|\nconst matchesPriority = priorityFilters.size === 0 || (f.priority && priorityFilters.has(f.priority)); |\n|\nconst matchesType = typeFilters.size === 0 || typeFilters.has(f.type); |\n|\nreturn matchesPriority && matchesType; |\n|\n}); |\n|\n} |\n|\n|\n|\nif (filteredFindings.length === 0) { |\n|\nconst filterDesc = [...priorityFilters, ...typeFilters].join(\", \") || \"specified filters\"; |\n|\nctx.ui.notify(`No findings match ${filterDesc}`, \"warning\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nctx.ui.notify(`Found ${filteredFindings.length} findings (of ${parsed.findings.length} total)`, \"info\"); |\n|\n|\n|\n// Let user select which findings to post |\n|\nconst findingOptions = filteredFindings.map((f, i) => { |\n|\nconst priority = f.priority ? `[${f.priority}]` : \"\"; |\n|\nconst type = f.type === \"correctness\" ? \"🐛\" : \"💡\"; |\n|\nconst loc = f.line ? `${f.file}:${f.line}` : f.file; |\n|\nreturn `${type} ${priority} ${loc}: ${f.issue.slice(0, 50)}${f.issue.length > 50 ? \"...\" : \"\"}`; |\n|\n}); |\n|\n|\n|\n// Add \"All\" and \"Select individually\" options |\n|\nconst selectionMode = await ctx.ui.select( |\n|\n`Post ${filteredFindings.length} findings:`, |\n|\n[\"Post all\", \"Select which to post\", \"Cancel\"] |\n|\n); |\n|\n|\n|\nif (selectionMode === undefined || selectionMode === \"Cancel\") { |\n|\nctx.ui.notify(\"Cancelled\", \"info\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nlet selectedFindings: ReviewFinding[] = []; |\n|\n|\n|\nif (selectionMode === \"Post all\") { |\n|\nselectedFindings = filteredFindings; |\n|\n} else { |\n|\n// Let user pick individual findings |\n|\nfor (let i = 0; i < filteredFindings.length; i++) { |\n|\nconst finding = filteredFindings[i]; |\n|\nconst preview = formatFindingAsComment(finding); |\n|\nconst loc = finding.line ? `${finding.file}:${finding.line}` : finding.file; |\n|\n|\n|\nconst action = await ctx.ui.select( |\n|\n`[${i + 1}/${filteredFindings.length}] ${loc}`, |\n|\n[\"Include\", \"Edit & include\", \"Skip\"] |\n|\n); |\n|\n|\n|\nif (action === undefined) { |\n|\nctx.ui.notify(\"Cancelled\", \"info\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nif (action === \"Skip\") { |\n|\ncontinue; |\n|\n} |\n|\n|\n|\nif (action === \"Edit & include\") { |\n|\n// Let user edit the comment |\n|\nconst edited = await ctx.ui.editor( |\n|\n`Edit comment for ${loc}:`, |\n|\npreview |\n|\n); |\n|\n|\n|\nif (edited === undefined) { |\n|\ncontinue; // Skip this one |\n|\n} |\n|\n|\n|\n// Create a modified finding with the edited content |\n|\nselectedFindings.push({ |\n|\n...finding, |\n|\n// Store the edited body directly - we'll use it later |\n|\n_editedBody: edited.trim(), |\n|\n} as ReviewFinding & { _editedBody?: string }); |\n|\n} else { |\n|\nselectedFindings.push(finding); |\n|\n} |\n|\n} |\n|\n} |\n|\n|\n|\nif (selectedFindings.length === 0) { |\n|\nctx.ui.notify(\"No findings selected\", \"info\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\n// Ask for review action |\n|\nconst reviewAction = await ctx.ui.select(\"Post review as:\", [ |\n|\n\"Comment (no approval status)\", |\n|\n\"Request changes\", |\n|\n\"Approve with comments\", |\n|\n]); |\n|\n|\n|\nif (reviewAction === undefined) { |\n|\nctx.ui.notify(\"Cancelled\", \"info\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nconst event = reviewAction === \"Request changes\" ? \"REQUEST_CHANGES\" |\n|\n: reviewAction === \"Approve with comments\" ? \"APPROVE\" |\n|\n: \"COMMENT\"; |\n|\n|\n|\n// Optionally edit the review summary |\n|\nlet reviewBody = \"\"; |\n|\nif (parsed.summary) { |\n|\nreviewBody = `## Review Summary\\n\\n${parsed.summary}`; |\n|\n} |\n|\n|\n|\nconst editSummary = await ctx.ui.confirm( |\n|\n\"Edit summary?\", |\n|\n\"Do you want to edit the review summary before posting?\" |\n|\n); |\n|\n|\n|\nif (editSummary) { |\n|\nconst editedSummary = await ctx.ui.editor( |\n|\n\"Edit review summary:\", |\n|\nreviewBody || `Code review with ${selectedFindings.length} findings.` |\n|\n); |\n|\nif (editedSummary !== undefined) { |\n|\nreviewBody = editedSummary.trim(); |\n|\n} |\n|\n} |\n|\n|\n|\nif (!reviewBody) { |\n|\nreviewBody = `Code review completed with ${selectedFindings.length} findings.`; |\n|\n} |\n|\n|\n|\n// Build review comments |\n|\nconst comments: Array<{ path: string; line?: number; body: string }> = []; |\n|\nconst generalComments: string[] = []; |\n|\n|\n|\nfor (const finding of selectedFindings) { |\n|\n// Use edited body if available, otherwise format normally |\n|\nconst body = (finding as any)._editedBody || formatFindingAsComment(finding); |\n|\n|\n|\nif (finding.line) { |\n|\ncomments.push({ |\n|\npath: finding.file, |\n|\nline: finding.line, |\n|\nbody, |\n|\n}); |\n|\n} else { |\n|\n// No line number - add to general comments |\n|\ngeneralComments.push(`**${finding.file}**\\n\\n${body}`); |\n|\n} |\n|\n} |\n|\n|\n|\n// Add general comments to review body |\n|\nif (generalComments.length > 0) { |\n|\nreviewBody += (reviewBody ? \"\\n\\n---\\n\\n\" : \"\") + \"## Additional Findings\\n\\n\" + generalComments.join(\"\\n\\n---\\n\\n\"); |\n|\n} |\n|\n|\n|\n// Post using gh api |\n|\n// First, we need to get the latest commit SHA for the PR |\n|\nconst { stdout: prData, code: prCode } = await pi.exec(\"gh\", [ |\n|\n\"pr\", \"view\", String(prNumber), \"--json\", \"headRefOid\" |\n|\n]); |\n|\n|\n|\nif (prCode !== 0) { |\n|\nctx.ui.notify(`Failed to get PR info: ${prData}`, \"error\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\nlet commitSha: string; |\n|\ntry { |\n|\nconst prJson = JSON.parse(prData); |\n|\ncommitSha = prJson.headRefOid; |\n|\n} catch { |\n|\nctx.ui.notify(\"Failed to parse PR info\", \"error\"); |\n|\nreturn; |\n|\n} |\n|\n|\n|\n// Build the review payload |\n|\n// For line comments, we need to use the pull request review API |\n|\n// which requires the commit_id and the position in the diff (not line number) |\n|\n|\n|\n// For simplicity, we'll post line-specific comments using the |\n|\n// newer \"line\" parameter which works with multi-line comments API |\n|\nconst reviewPayload: { |\n|\nbody: string; |\n|\nevent: string; |\n|\ncommit_id: string; |\n|\ncomments?: Array<{ path: string; line: number; body: string; side: string }>; |\n|\n} = { |\n|\nbody: reviewBody, |\n|\nevent, |\n|\ncommit_id: commitSha, |\n|\n}; |\n|\n|\n|\n// Add line comments if we have any |\n|\nif (comments.length > 0) { |\n|\nreviewPayload.comments = comments |\n|\n.filter(c => c.line !== undefined) |\n|\n.map(c => ({ |\n|\npath: c.path, |\n|\nline: c.line!, |\n|\nbody: c.body, |\n|\nside: \"RIGHT\", // Comment on the new version of the file |\n|\n})); |\n|\n} |\n|\n|\n|\n// Post the review |\n|\nctx.ui.notify(\"Posting review to GitHub...\", \"info\"); |\n|\n|\n|\nconst payloadJson = JSON.stringify(reviewPayload); |\n|\n|\n|\n// Write payload to temp file since pi.exec doesn't support stdin |\n|\nconst tempFile = `/tmp/pi-review-${Date.now()}.json`; |\n|\nawait fs.writeFile(tempFile, payloadJson); |\n|\n|\n|\nconst { stdout: reviewResult, stderr: reviewError, code: reviewCode } = await pi.exec(\"gh\", [ |\n|\n\"api\", |\n|\n\"--method\", \"POST\", |\n|\n`repos/{owner}/{repo}/pulls/${prNumber}/reviews`, |\n|\n\"--input\", tempFile, |\n|\n]); |\n|\n|\n|\n// Clean up temp file |\n|\nawait fs.unlink(tempFile).catch(() => {}); |\n|\n|\n|\nif (reviewCode !== 0) { |\n|\n// Try to extract error message |\n|\nconst errorMsg = reviewError || reviewResult || \"Unknown error\"; |\n|\n|\n|\n// Common error: line not part of diff |\n|\nif (errorMsg.includes(\"pull_request_review_thread.line\") || errorMsg.includes(\"not part of the diff\")) { |\n|\nctx.ui.notify(\"Some line comments failed (lines not in diff). Posting as general review...\", \"warning\"); |\n|\n|\n|\n// Retry without line comments |\n|\nconst fallbackPayload = { |\n|\nbody: reviewBody + \"\\n\\n---\\n\\n## Line-specific Findings\\n\\n\" + |\n|\ncomments.map(c => `**${c.path}:${c.line}**\\n\\n${c.body}`).join(\"\\n\\n---\\n\\n\"), |\n|\nevent, |\n|\ncommit_id: commitSha, |\n|\n}; |\n|\n|\n|\nconst fallbackFile = `/tmp/pi-review-fallback-${Date.now()}.json`; |\n|\nawait fs.writeFile(fallbackFile, JSON.stringify(fallbackPayload)); |\n|\n|\n|\nconst { code: fallbackCode } = await pi.exec(\"gh\", [ |\n|\n\"api\", |\n|\n\"--method\", \"POST\", |\n|\n`repos/{owner}/{repo}/pulls/${prNumber}/reviews`, |\n|\n\"--input\", fallbackFile, |\n|\n]); |\n|\n|\n|\nawait fs.unlink(fallbackFile).catch(() => {}); |\n|\n|\n|\nif (fallbackCode !== 0) { |\n|\nctx.ui.notify(\"Failed to post review. Check gh auth and permissions.\", \"error\"); |\n|\nreturn; |\n|\n} |\n|\n} else { |\n|\nctx.ui.notify(`Failed to post review: ${errorMsg}`, \"error\"); |\n|\nreturn; |\n|\n} |\n|\n} |\n|\n|\n|\nconst commentCount = reviewPayload.comments?.length || 0; |\n|\nctx.ui.notify( |\n|\n`✓ Posted review to PR #${prNumber} (${commentCount} inline comments, ${event.toLowerCase().replace(\"_\", \" \")})`, |\n|\n\"success\" |\n|\n); |\n|\n}, |\n|\n}); |\n|\n} |", "url": "https://wpnews.pro/news/pi-coding-agent-extensions-debug-guardrails-review-todos-emacs-bridge-web", "canonical_source": "https://gist.github.com/FJSampedro/c666d0acf424ba34dbb7353ab22e4f40", "published_at": "2026-07-21 12:24:27+00:00", "updated_at": "2026-07-21 12:28:54.730744+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "large-language-models"], "entities": ["Pi Coding Agent", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/pi-coding-agent-extensions-debug-guardrails-review-todos-emacs-bridge-web", "markdown": "https://wpnews.pro/news/pi-coding-agent-extensions-debug-guardrails-review-todos-emacs-bridge-web.md", "text": "https://wpnews.pro/news/pi-coding-agent-extensions-debug-guardrails-review-todos-emacs-bridge-web.txt", "jsonld": "https://wpnews.pro/news/pi-coding-agent-extensions-debug-guardrails-review-todos-emacs-bridge-web.jsonld"}}