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 โ
| Action | File | Responsibility |
|---|---|---|
| Create | tooling/vscode-extension/src/lib/githubData.js | Wraps gh CLI for milestone + PR data |
| Create | tooling/vscode-extension/src/providers/ProjectInfoProvider.js | TreeDataProvider for the Project Info view |
| Modify | tooling/vscode-extension/package.json | Register view, command, menu |
| Modify | tooling/vscode-extension/src/extension.js | Wire 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.jswithfetchActiveMilestone
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
fetchBranchPRto the same file
Add this function after fetchActiveMilestone, before module.exports:
/**
* 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:
module.exports = { fetchActiveMilestone, fetchBranchPR }- [ ] Step 3: Commit
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
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
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.projectInfoas the first view
In package.json under contributes.views.lanternControl, add the new view at position 0 (before the existing quickstart entry):
{ "id": "lanternControl.projectInfo", "name": "Project Info" }The full array becomes:
"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:
{ "command": "lanternControl.refreshProjectInfo", "title": "Refresh", "icon": "$(refresh)" }- [ ] Step 3: Add the menu entry for the refresh button
In contributes.menus.view/title, add:
{ "command": "lanternControl.refreshProjectInfo", "when": "view == lanternControl.projectInfo", "group": "navigation" }- [ ] Step 4: Commit
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:
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:
const projectInfoProvider = new ProjectInfoProvider(workspaceRoot)Inside the context.subscriptions.push( block that registers tree views (starting at line 122), add as the first entry:
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:
vscode.commands.registerCommand('lanternControl.refreshProjectInfo', () => {
projectInfoProvider.refresh()
}),- [ ] Step 4: Fetch data on activation
After registering all commands (but still inside activate()), add:
// 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:
// 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
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
cd tooling/vscode-extension && npm run compileExpected: Clean build, no errors. Output written to out/extension.js.
- [ ] Step 2: Package the extension
cd tooling/vscode-extension && npm run packageExpected: lantern-control.vsix generated without errors.
- [ ] Step 3: Commit the built artifact
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
code --install-extension tooling/vscode-extension/lantern-control.vsix --forceAfter reloading VS Code:
- Open the Lantern Control sidebar
- Verify "Project Info" appears as the first section
- Verify it shows: Milestone with progress, Branch name, PR info (or "No open PR"), Version, Last refreshed
- Click the refresh button โ data should update
- Verify clicking the PR item opens the PR URL in the browser
- Verify clicking the Milestone item opens the milestone URL in the browser