Skip to content

Project Info Panel Implementation Plan โ€‹

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a read-only "Project Info" dashboard view to the Lantern Control VS Code extension sidebar showing milestone progress, branch, PR status, and app version.

Architecture: New ProjectInfoProvider tree view provider backed by a githubData.js utility that wraps gh CLI calls. Data fetched on activation and on branch change. Graceful fallback when gh is unavailable.

Tech Stack: VS Code Extension API (TreeDataProvider), gh CLI via child_process.execSync

Spec: docs/superpowers/specs/2026-03-28-project-info-panel-design.md

Note on execSync: The VS Code extension already uses execSync in src/lib/gitBranch.js for shell commands. The gh CLI calls in githubData.js follow the same pattern โ€” all commands are hardcoded strings with no user-supplied input interpolation, so shell injection is not a concern. The execFileNoThrow utility in the main app is not available in the extension context.


File Map โ€‹

ActionFileResponsibility
Createtooling/vscode-extension/src/lib/githubData.jsWraps gh CLI for milestone + PR data
Createtooling/vscode-extension/src/providers/ProjectInfoProvider.jsTreeDataProvider for the Project Info view
Modifytooling/vscode-extension/package.jsonRegister view, command, menu
Modifytooling/vscode-extension/src/extension.jsWire up provider, refresh command, branch-change hook

Task 1: Create githubData.js โ€” GitHub CLI wrapper โ€‹

Files:

  • Create: tooling/vscode-extension/src/lib/githubData.js

  • [ ] Step 1: Create githubData.js with fetchActiveMilestone

js
const { execSync } = require('child_process')

/**
 * Fetch the active milestone from GitHub.
 * "Active" = open milestone with the highest total issues that still has open issues.
 * @param {string} cwd โ€” workspace root (for gh CLI context)
 * @returns {{ title: string, open: number, closed: number, total: number, percent: number, url: string } | null}
 */
function fetchActiveMilestone(cwd) {
  try {
    const raw = execSync(
      'gh api repos/{owner}/{repo}/milestones --jq "[.[] | {title, open_issues, closed_issues, html_url}]"',
      { cwd, encoding: 'utf-8', timeout: 10000 }
    )
    const milestones = JSON.parse(raw)

    // Pick the milestone with the most total issues that still has open work
    const active = milestones
      .filter(m => m.open_issues > 0)
      .sort((a, b) => (b.open_issues + b.closed_issues) - (a.open_issues + a.closed_issues))[0]

    if (!active) return null

    const total   = active.open_issues + active.closed_issues
    const percent = total > 0 ? Math.round((active.closed_issues / total) * 100) : 0

    return {
      title:   active.title,
      open:    active.open_issues,
      closed:  active.closed_issues,
      total,
      percent,
      url:     active.html_url,
    }
  } catch {
    return null
  }
}

module.exports = { fetchActiveMilestone }
  • [ ] Step 2: Add fetchBranchPR to the same file

Add this function after fetchActiveMilestone, before module.exports:

js
/**
 * Fetch the open PR for a given branch.
 * @param {string} cwd โ€” workspace root
 * @param {string} branch โ€” branch name
 * @returns {{ number: number, title: string, url: string, state: string } | null}
 */
function fetchBranchPR(cwd, branch) {
  try {
    const raw = execSync(
      `gh pr list --head "${branch}" --json number,title,url,state --limit 1`,
      { cwd, encoding: 'utf-8', timeout: 10000 }
    )
    const prs = JSON.parse(raw)
    return prs[0] || null
  } catch {
    return null
  }
}

Update exports:

js
module.exports = { fetchActiveMilestone, fetchBranchPR }
  • [ ] Step 3: Commit
bash
git add tooling/vscode-extension/src/lib/githubData.js
git commit -m "feat(vscode-ext): add githubData.js โ€” gh CLI wrapper for milestone and PR data"

Task 2: Create ProjectInfoProvider.js โ€” Tree view provider โ€‹

Files:

  • Create: tooling/vscode-extension/src/providers/ProjectInfoProvider.js

  • [ ] Step 1: Create the provider file

js
const vscode = require('vscode')
const { getCurrentBranch }    = require('../lib/gitBranch')
const { readPackageJson }     = require('../lib/packageReader')
const { fetchActiveMilestone, fetchBranchPR } = require('../lib/githubData')

// โ”€โ”€โ”€ Tree item โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

class InfoItem extends vscode.TreeItem {
  constructor(label, { description, tooltip, icon, url }) {
    super(label, vscode.TreeItemCollapsibleState.None)
    this.description  = description || ''
    this.tooltip      = tooltip || ''
    this.iconPath     = new vscode.ThemeIcon(icon)
    this.contextValue = 'projectInfo'

    if (url) {
      this.command = {
        command:   'vscode.open',
        title:     'Open in Browser',
        arguments: [vscode.Uri.parse(url)],
      }
    }
  }
}

// โ”€โ”€โ”€ Provider โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

class ProjectInfoProvider {
  constructor(workspaceRoot) {
    this._root   = workspaceRoot
    this._change = new vscode.EventEmitter()
    this.onDidChangeTreeData = this._change.event

    this._milestone  = null
    this._pr         = null
    this._lastFetch  = null
  }

  refresh() {
    const branch = getCurrentBranch(this._root)

    this._milestone = fetchActiveMilestone(this._root)
    this._pr        = branch ? fetchBranchPR(this._root, branch) : null
    this._lastFetch = new Date()

    this._change.fire()
  }

  getTreeItem(element) { return element }

  getChildren() {
    const branch  = getCurrentBranch(this._root)
    const pkg     = readPackageJson(this._root)
    const version = pkg.version || 'unknown'

    const items = []

    // Milestone
    if (this._milestone) {
      const m = this._milestone
      items.push(new InfoItem(`Milestone: ${m.title}`, {
        description: `${m.percent}% (${m.closed}/${m.total})`,
        tooltip:     `${m.title} โ€” ${m.closed} closed, ${m.open} open of ${m.total} issues`,
        icon:        'milestone',
        url:         m.url,
      }))
    } else {
      items.push(new InfoItem('Milestone: unavailable', {
        description: 'could not fetch from GitHub',
        tooltip:     'Run refresh or check that gh CLI is authenticated',
        icon:        'milestone',
      }))
    }

    // Branch
    items.push(new InfoItem(`Branch: ${branch || 'unknown'}`, {
      icon: 'git-branch',
    }))

    // PR
    if (this._pr) {
      items.push(new InfoItem(`PR: #${this._pr.number}`, {
        description: `${this._pr.title}   ${this._pr.state}`,
        tooltip:     `#${this._pr.number} โ€” ${this._pr.title}\n${this._pr.url}`,
        icon:        'git-pull-request',
        url:         this._pr.url,
      }))
    } else {
      items.push(new InfoItem('PR: No open PR', {
        description: branch ? `for ${branch}` : '',
        tooltip:     'No open pull request found for this branch',
        icon:        'git-pull-request',
      }))
    }

    // Version
    items.push(new InfoItem(`Version: ${version}`, {
      icon: 'tag',
    }))

    // Last refreshed
    if (this._lastFetch) {
      const ago = formatRelativeTime(this._lastFetch)
      items.push(new InfoItem(`Last refreshed: ${ago}`, {
        tooltip: this._lastFetch.toLocaleString(),
        icon:    'clock',
      }))
    }

    return items
  }
}

// โ”€โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function formatRelativeTime(date) {
  const seconds = Math.round((Date.now() - date.getTime()) / 1000)
  if (seconds < 60)  return 'just now'
  const minutes = Math.round(seconds / 60)
  if (minutes < 60)  return `${minutes} min ago`
  const hours = Math.round(minutes / 60)
  return `${hours}h ago`
}

module.exports = ProjectInfoProvider
  • [ ] Step 2: Commit
bash
git add tooling/vscode-extension/src/providers/ProjectInfoProvider.js
git commit -m "feat(vscode-ext): add ProjectInfoProvider โ€” read-only project dashboard"

Task 3: Register the view and command in package.json โ€‹

Files:

  • Modify: tooling/vscode-extension/package.json

  • [ ] Step 1: Add lanternControl.projectInfo as the first view

In package.json under contributes.views.lanternControl, add the new view at position 0 (before the existing quickstart entry):

json
{ "id": "lanternControl.projectInfo", "name": "Project Info" }

The full array becomes:

json
"lanternControl": [
  { "id": "lanternControl.projectInfo",    "name": "Project Info"   },
  { "id": "lanternControl.quickstart",     "name": "Quickstart"     },
  { "id": "lanternControl.notifications", "name": "Notifications"  },
  { "id": "lanternControl.devTools",       "name": "Dev Tools"     },
  { "id": "lanternControl.firebase",       "name": "Firebase"      },
  { "id": "lanternControl.scripts",        "name": "All Scripts"   },
  { "id": "lanternControl.docs",           "name": "Docs"          }
]
  • [ ] Step 2: Add the refresh command

In contributes.commands, add:

json
{ "command": "lanternControl.refreshProjectInfo", "title": "Refresh", "icon": "$(refresh)" }
  • [ ] Step 3: Add the menu entry for the refresh button

In contributes.menus.view/title, add:

json
{ "command": "lanternControl.refreshProjectInfo", "when": "view == lanternControl.projectInfo", "group": "navigation" }
  • [ ] Step 4: Commit
bash
git add tooling/vscode-extension/package.json
git commit -m "feat(vscode-ext): register Project Info view and refresh command"

Task 4: Wire up the provider in extension.js โ€‹

Files:

  • Modify: tooling/vscode-extension/src/extension.js

  • [ ] Step 1: Add the import

At the top of extension.js, after the existing provider imports (line 9, after const QuickstartProvider), add:

js
const ProjectInfoProvider    = require('./providers/ProjectInfoProvider')
  • [ ] Step 2: Instantiate the provider and register the tree view

Inside activate(), after the existing provider instantiations (after line 119 const notificationsProvider), add:

js
const projectInfoProvider    = new ProjectInfoProvider(workspaceRoot)

Inside the context.subscriptions.push( block that registers tree views (starting at line 122), add as the first entry:

js
vscode.window.createTreeView('lanternControl.projectInfo', { treeDataProvider: projectInfoProvider }),
  • [ ] Step 3: Register the refresh command

Inside the context.subscriptions.push( block that registers commands (starting at line 208), add:

js
vscode.commands.registerCommand('lanternControl.refreshProjectInfo', () => {
  projectInfoProvider.refresh()
}),
  • [ ] Step 4: Fetch data on activation

After registering all commands (but still inside activate()), add:

js
// Fetch project info on activation
projectInfoProvider.refresh()
  • [ ] Step 5: Hook into branch polling for auto-refresh on branch change

After the startBranchPolling call (line 679), add a branch-change detector:

js
// Refresh project info when branch changes
let _lastKnownBranch = getCurrentBranch(workspaceRoot)
const branchChangeTimer = setInterval(() => {
  const current = getCurrentBranch(workspaceRoot)
  if (current !== _lastKnownBranch) {
    _lastKnownBranch = current
    projectInfoProvider.refresh()
  }
}, 10000)
context.subscriptions.push({ dispose: () => clearInterval(branchChangeTimer) })
  • [ ] Step 6: Commit
bash
git add tooling/vscode-extension/src/extension.js
git commit -m "feat(vscode-ext): wire ProjectInfoProvider into extension activation"

Task 5: Build and verify โ€‹

Files:

  • None (build + manual verification)

  • [ ] Step 1: Compile the extension

bash
cd tooling/vscode-extension && npm run compile

Expected: Clean build, no errors. Output written to out/extension.js.

  • [ ] Step 2: Package the extension
bash
cd tooling/vscode-extension && npm run package

Expected: lantern-control.vsix generated without errors.

  • [ ] Step 3: Commit the built artifact
bash
git add tooling/vscode-extension/out/extension.js tooling/vscode-extension/lantern-control.vsix
git commit -m "build(vscode-ext): rebuild extension with Project Info panel"
  • [ ] Step 4: Install and verify in VS Code
bash
code --install-extension tooling/vscode-extension/lantern-control.vsix --force

After reloading VS Code:

  1. Open the Lantern Control sidebar
  2. Verify "Project Info" appears as the first section
  3. Verify it shows: Milestone with progress, Branch name, PR info (or "No open PR"), Version, Last refreshed
  4. Click the refresh button โ€” data should update
  5. Verify clicking the PR item opens the PR URL in the browser
  6. Verify clicking the Milestone item opens the milestone URL in the browser

Built with VitePress