Files
awesome-copilot/cookbook/copilot-sdk/go/pr-visualization.md
James Montemagno 33f544c71d Align Copilot SDK documentation with permission handling requirements (#1107)
* Apply permission handler requirements across Copilot SDK docs

Co-authored-by: jamesmontemagno <1676321+jamesmontemagno@users.noreply.github.com>
Agent-Logs-Url: https://github.com/jamesmontemagno/awesome-copilot/sessions/adf27a88-92f8-4ca6-b3fe-1204e3bb9963

* Polish permission update formatting in SDK examples

Co-authored-by: jamesmontemagno <1676321+jamesmontemagno@users.noreply.github.com>
Agent-Logs-Url: https://github.com/jamesmontemagno/awesome-copilot/sessions/adf27a88-92f8-4ca6-b3fe-1204e3bb9963

* Fix review comments on SDK permission handling PR

Address 5 review comments from PR #1103:

1. Fix invalid object literal syntax (stray comma) in resumeSession
   example in copilot-sdk-nodejs.instructions.md

2. Replace unused PermissionHandler import with actual usage in
   cookbook/copilot-sdk/python/recipe/ralph_loop.py (was using
   inline lambda instead)

3. Replace unused approveAll import with actual usage in
   cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts (was using
   inline handler instead)

4. Add missing PermissionHandler import to 4 Python code snippets
   in skills/copilot-sdk/SKILL.md that reference it without importing

5. Add missing approveAll import to 3 TypeScript code snippets
   in skills/copilot-sdk/SKILL.md that reference it without importing

* Refactor session creation to improve code formatting and consistency across SDK examples

* Fix formatting: split multi-property lines and put closing braces on own lines

Address review comments on PR #1107:
- Split OnPermissionRequest + Model onto separate lines in Go, C#, TypeScript
- Put closing }); on its own line consistently across all examples
- Fix indentation in SKILL.md Quick Start, CLI URL, Error Handling sections
- Fix cookbook Go multiple-sessions and error-handling formatting
- Fix ralph-loop.md TypeScript indentation

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jamesmontemagno <1676321+jamesmontemagno@users.noreply.github.com>
2026-03-23 11:11:19 +11:00

7.3 KiB

Generating PR Age Charts

Build an interactive CLI tool that visualizes pull request age distribution for a GitHub repository using Copilot's built-in capabilities.

Runnable example: recipe/pr-visualization.go

# Auto-detect from current git repo
go run recipe/pr-visualization.go

# Specify a repo explicitly
go run recipe/pr-visualization.go -repo github/copilot-sdk

Example scenario

You want to understand how long PRs have been open in a repository. This tool detects the current Git repo or accepts a repo as input, then lets Copilot fetch PR data via the GitHub MCP Server and generate a chart image.

Prerequisites

go get github.com/github/copilot-sdk/go

Usage

# Auto-detect from current git repo
go run pr-visualization.go

# Specify a repo explicitly
go run pr-visualization.go -repo github/copilot-sdk

Full example: pr-visualization.go

package main

import (
    "bufio"
    "context"
    "flag"
    "fmt"
    "log"
    "os"
    "os/exec"
    "regexp"
    "strings"
    copilot "github.com/github/copilot-sdk/go"
)

// ============================================================================
// Git & GitHub Detection
// ============================================================================

func isGitRepo() bool {
    cmd := exec.Command("git", "rev-parse", "--git-dir")
    return cmd.Run() == nil
}

func getGitHubRemote() string {
    cmd := exec.Command("git", "remote", "get-url", "origin")
    output, err := cmd.Output()
    if err != nil {
        return ""
    }

    remoteURL := strings.TrimSpace(string(output))

    // Handle SSH: git@github.com:owner/repo.git
    sshRe := regexp.MustCompile(`git@github\.com:(.+/.+?)(?:\.git)?$`)
    if matches := sshRe.FindStringSubmatch(remoteURL); matches != nil {
        return matches[1]
    }

    // Handle HTTPS: https://github.com/owner/repo.git
    httpsRe := regexp.MustCompile(`https://github\.com/(.+/.+?)(?:\.git)?$`)
    if matches := httpsRe.FindStringSubmatch(remoteURL); matches != nil {
        return matches[1]
    }

    return ""
}

func promptForRepo() string {
    reader := bufio.NewReader(os.Stdin)
    fmt.Print("Enter GitHub repo (owner/repo): ")
    repo, _ := reader.ReadString('\n')
    return strings.TrimSpace(repo)
}

// ============================================================================
// Main Application
// ============================================================================

func main() {
    ctx := context.Background()
    repoFlag := flag.String("repo", "", "GitHub repository (owner/repo)")
    flag.Parse()

    fmt.Println("🔍 PR Age Chart Generator\n")

    // Determine the repository
    var repo string

    if *repoFlag != "" {
        repo = *repoFlag
        fmt.Printf("📦 Using specified repo: %s\n", repo)
    } else if isGitRepo() {
        detected := getGitHubRemote()
        if detected != "" {
            repo = detected
            fmt.Printf("📦 Detected GitHub repo: %s\n", repo)
        } else {
            fmt.Println("⚠️  Git repo found but no GitHub remote detected.")
            repo = promptForRepo()
        }
    } else {
        fmt.Println("📁 Not in a git repository.")
        repo = promptForRepo()
    }

    if repo == "" || !strings.Contains(repo, "/") {
        log.Fatal("❌ Invalid repo format. Expected: owner/repo")
    }

    parts := strings.SplitN(repo, "/", 2)
    owner, repoName := parts[0], parts[1]

    // Create Copilot client
    client := copilot.NewClient(nil)

    if err := client.Start(ctx); err != nil {
        log.Fatal(err)
    }
    defer client.Stop()

    cwd, _ := os.Getwd()
    session, err := client.CreateSession(ctx, &copilot.SessionConfig{
    	OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
        Model: "gpt-5",
        SystemMessage: &copilot.SystemMessageConfig{
            Content: fmt.Sprintf(`
<context>
You are analyzing pull requests for the GitHub repository: %s/%s
The current working directory is: %s
</context>

<instructions>
- Use the GitHub MCP Server tools to fetch PR data
- Use your file and code execution tools to generate charts
- Save any generated images to the current working directory
- Be concise in your responses
</instructions>
`, owner, repoName, cwd),
        },
    })
    if err != nil {
        log.Fatal(err)
    }
    defer session.Destroy()

    // Set up event handling
    session.On(func(event copilot.SessionEvent) {
        switch event.Type {
        case "assistant.message":
            if event.Data.Content != nil {
                fmt.Printf("\n🤖 %s\n\n", *event.Data.Content)
            }
        case "tool.execution_start":
            if event.Data.ToolName != nil {
                fmt.Printf("  ⚙️  %s\n", *event.Data.ToolName)
            }
        }
    })

    // Initial prompt - let Copilot figure out the details
    fmt.Println("\n📊 Starting analysis...\n")

    prompt := fmt.Sprintf(`
      Fetch the open pull requests for %s/%s from the last week.
      Calculate the age of each PR in days.
      Then generate a bar chart image showing the distribution of PR ages
      (group them into sensible buckets like <1 day, 1-3 days, etc.).
      Save the chart as "pr-age-chart.png" in the current directory.
      Finally, summarize the PR health - average age, oldest PR, and how many might be considered stale.
    `, owner, repoName)

    if _, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt}); err != nil {
        log.Fatal(err)
    }

    // Interactive loop
    fmt.Println("\n💡 Ask follow-up questions or type \"exit\" to quit.\n")
    fmt.Println("Examples:")
    fmt.Println("  - \"Expand to the last month\"")
    fmt.Println("  - \"Show me the 5 oldest PRs\"")
    fmt.Println("  - \"Generate a pie chart instead\"")
    fmt.Println("  - \"Group by author instead of age\"")
    fmt.Println()

    reader := bufio.NewReader(os.Stdin)
    for {
        fmt.Print("You: ")
        input, _ := reader.ReadString('\n')
        input = strings.TrimSpace(input)

        if input == "" {
            continue
        }
        if strings.ToLower(input) == "exit" || strings.ToLower(input) == "quit" {
            fmt.Println("👋 Goodbye!")
            break
        }

        if _, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: input}); err != nil {
            log.Printf("Error: %v", err)
        }
    }
}

How it works

  1. Repository detection: Checks --repo flag → git remote → prompts user
  2. No custom tools: Relies entirely on Copilot CLI's built-in capabilities:
    • GitHub MCP Server - Fetches PR data from GitHub
    • File tools - Saves generated chart images
    • Code execution - Generates charts using Python/matplotlib or other methods
  3. Interactive session: After initial analysis, user can ask for adjustments

Why this approach?

Aspect Custom Tools Built-in Copilot
Code complexity High Minimal
Maintenance You maintain Copilot maintains
Flexibility Fixed logic AI decides best approach
Chart types What you coded Any type Copilot can generate
Data grouping Hardcoded buckets Intelligent grouping