Project Info โ Interactive Actions & CI Failure Badges 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 interactive branch cleanup (expandable stale/gone branches with delete actions + bulk Quick Pick) and CI failure badges with notification push.
Architecture: Update fetchBranchStats to return branch name arrays. Refactor the Branches group to use nested InfoGroup items for gone/stale with individual branch sub-items. Add branch deletion commands with confirmation dialogs. Wire CI failure detection to badge updates and the existing Notifications panel via state transition tracking.
Tech Stack: VS Code TreeView API (badge, contextValue, menus), GitHub CLI, local git commands.
Task 1: Update fetchBranchStats to return branch names โ
Files:
Modify:
tooling/vscode-extension/src/lib/githubData.js:197-227[ ] Step 1: Replace
fetchBranchStatswith version that returns branch arrays
Replace the entire fetchBranchStats function (lines 197-227):
/**
* Fetch branch statistics from local git.
* @param {string} cwd โ workspace root
* @returns {Promise<{ local: number, remote: number, goneBranches: Array<{ name: string }>, staleBranches: Array<{ name: string, daysAgo: number }> } | null>}
*/
async function fetchBranchStats(cwd) {
const [localRaw, remoteRaw, vvRaw, refRaw] = await Promise.all([
execAsync('git', ['branch', '--list'], { cwd }),
execAsync('git', ['branch', '-r', '--list'], { cwd }),
execAsync('git', ['branch', '-vv'], { cwd }),
execAsync('git', ['for-each-ref', '--format=%(refname:short) %(committerdate:unix)', 'refs/heads/'], { cwd }),
])
if (!localRaw && !remoteRaw) return null
const local = localRaw ? localRaw.trim().split('\n').filter(l => l.trim()).length : 0
const remote = remoteRaw ? remoteRaw.trim().split('\n').filter(l => l.trim()).length : 0
// Collect branches with [gone] upstream
const goneBranches = []
if (vvRaw) {
for (const line of vvRaw.trim().split('\n')) {
if (/\[.*gone\]/.test(line)) {
const name = line.trim().replace(/^\*\s*/, '').split(/\s+/)[0]
if (name) goneBranches.push({ name })
}
}
}
// Collect stale branches (no commits in 30+ days)
const now = Math.floor(Date.now() / 1000)
const thirtyDaysAgo = now - (30 * 24 * 60 * 60)
const staleBranches = []
if (refRaw) {
for (const line of refRaw.trim().split('\n')) {
const parts = line.trim().split(' ')
const branchName = parts.slice(0, -1).join(' ')
const timestamp = parseInt(parts[parts.length - 1], 10)
if (timestamp && timestamp < thirtyDaysAgo) {
const daysAgo = Math.floor((now - timestamp) / (24 * 60 * 60))
staleBranches.push({ name: branchName, daysAgo })
}
}
}
// Sort stale branches by staleness (most stale first)
staleBranches.sort((a, b) => b.daysAgo - a.daysAgo)
return { local, remote, goneBranches, staleBranches }
}- [ ] Step 2: Commit
git add tooling/vscode-extension/src/lib/githubData.js
git commit -m "feat(vscode-ext): fetchBranchStats returns branch names for gone/stale"Task 2: Update Branches group in ProjectInfoProvider to show individual branches โ
Files:
Modify:
tooling/vscode-extension/src/providers/ProjectInfoProvider.js:253-287[ ] Step 1: Replace the Branches group section in
_buildTree()
Replace the entire // โโ Branches group section (lines 253-287) with:
// โโ Branches group โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (this._branches) {
const b = this._branches
const branchChildren = [
new InfoItem(`โ
Current: ${branch || 'unknown'}`, { icon: 'star-full' }),
new InfoItem('Local', { description: `${b.local}`, icon: 'git-branch' }),
new InfoItem('Remote', { description: `${b.remote}`, icon: 'cloud' }),
]
if (b.goneBranches.length > 0) {
const goneChildren = b.goneBranches.map(gb => {
const item = new InfoItem(gb.name, { icon: 'git-branch' })
item.contextValue = 'deletableBranch'
item.branchName = gb.name
return item
})
const goneGroup = new InfoGroup('Gone (remote deleted)', {
description: `${b.goneBranches.length}`,
tooltip: `${b.goneBranches.length} branch${b.goneBranches.length !== 1 ? 'es' : ''} with deleted remote โ click trash to clean up`,
icon: 'warning',
children: goneChildren,
})
goneGroup.contextValue = 'branchCleanupGone'
goneGroup.branches = b.goneBranches
branchChildren.push(goneGroup)
}
if (b.staleBranches.length > 0) {
const staleChildren = b.staleBranches.map(sb => {
const item = new InfoItem(sb.name, {
description: `${sb.daysAgo} days`,
icon: 'git-branch',
})
item.contextValue = 'deletableBranch'
item.branchName = sb.name
return item
})
const staleGroup = new InfoGroup('Stale (30+ days)', {
description: `${b.staleBranches.length}`,
tooltip: `${b.staleBranches.length} branch${b.staleBranches.length !== 1 ? 'es' : ''} with no commits in 30+ days`,
icon: 'history',
children: staleChildren,
})
staleGroup.contextValue = 'branchCleanupStale'
staleGroup.branches = b.staleBranches
branchChildren.push(staleGroup)
}
items.push(new InfoGroup('Branches', {
description: `${b.local} local / ${b.remote} remote`,
tooltip: `${b.local} local, ${b.remote} remote, ${b.goneBranches.length} gone, ${b.staleBranches.length} stale`,
icon: 'git-branch',
children: branchChildren,
}))
} else {
items.push(new InfoItem('Branches: unavailable', {
description: 'could not read git data',
icon: 'git-branch',
}))
}- [ ] Step 2: Commit
git add tooling/vscode-extension/src/providers/ProjectInfoProvider.js
git commit -m "feat(vscode-ext): expandable gone/stale branch lists with contextValues"Task 3: Add CI failure badge and notification support to ProjectInfoProvider โ
Files:
Modify:
tooling/vscode-extension/src/providers/ProjectInfoProvider.js[ ] Step 1: Update constructor to accept notificationsProvider and add badge emitter
Replace the constructor (lines 51-66):
constructor(workspaceRoot, notificationsProvider) {
this._root = workspaceRoot
this._change = new vscode.EventEmitter()
this.onDidChangeTreeData = this._change.event
this._notificationsProvider = notificationsProvider || null
this._milestone = null
this._pr = null
this._branch = null
this._issues = null
this._prCounts = null
this._ci = null
this._reviews = null
this._branches = null
this._activity = null
this._lastFetch = null
}- [ ] Step 2: Add
ciFailureCountgetter
Add this getter right after the constructor (after line 66, before async refresh()):
/**
* Number of failing CI workflows. Used by extension.js for badge updates.
* @returns {number}
*/
get ciFailureCount() {
if (!this._ci || this._ci.overall !== 'failing') return 0
return this._ci.runs.filter(r => r.conclusion === 'failure').length
}- [ ] Step 3: Add CI failure transition detection in
refresh()
In the refresh() method, add transition detection after the Promise.all block and before assigning this._ci = ci. Insert this code right after this._pr = pr (after the existing assignments at line 83, before this._ci = ci):
Replace the entire refresh() method:
async refresh() {
const branch = getCurrentBranch(this._root)
this._branch = branch
const [milestone, pr, issues, prCounts, ci, reviews, branches, activity] = await Promise.all([
fetchActiveMilestone(this._root),
branch ? fetchBranchPR(this._root, branch) : null,
fetchIssueCounts(this._root),
fetchPRCounts(this._root),
branch ? fetchCIStatus(this._root, branch) : null,
fetchReviewRequests(this._root),
fetchBranchStats(this._root),
fetchActivityStats(this._root),
])
// โโ CI failure transition detection โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const wasFailing = this._ci?.overall === 'failing'
const nowFailing = ci?.overall === 'failing'
if (nowFailing && !wasFailing && this._notificationsProvider && branch) {
const failedCount = ci.runs.filter(r => r.conclusion === 'failure').length
const repo = this._getRepo()
this._notificationsProvider.push(
`CI: ${failedCount} workflow${failedCount !== 1 ? 's' : ''} failing on ${branch}`,
'error',
{
command: 'vscode.open',
title: 'Open CI',
arguments: [vscode.Uri.parse(`https://github.com/${repo}/actions?query=branch:${encodeURIComponent(branch)}`)],
}
)
}
this._milestone = milestone
this._pr = pr
this._issues = issues
this._prCounts = prCounts
this._ci = ci
this._reviews = reviews
this._branches = branches
this._activity = activity
this._lastFetch = new Date()
this._change.fire()
}- [ ] Step 4: Commit
git add tooling/vscode-extension/src/providers/ProjectInfoProvider.js
git commit -m "feat(vscode-ext): CI failure badge getter and notification push on transition"Task 4: Wire up extension.js โ badge, notifications, and branch commands โ
Files:
Modify:
tooling/vscode-extension/src/extension.js[ ] Step 1: Pass notificationsProvider to ProjectInfoProvider constructor
Replace line 130:
const projectInfoProvider = new ProjectInfoProvider(workspaceRoot)With:
const projectInfoProvider = new ProjectInfoProvider(workspaceRoot, notificationsProvider)- [ ] Step 2: Store the tree view reference for badge updates
Replace line 133:
vscode.window.createTreeView('lanternControl.projectInfo', { treeDataProvider: projectInfoProvider }),With:
projectInfoView,And add this line BEFORE the context.subscriptions.push( block (before line 132):
const projectInfoView = vscode.window.createTreeView('lanternControl.projectInfo', { treeDataProvider: projectInfoProvider })- [ ] Step 3: Add a badge update helper function
Add this function inside activate(), right after the projectInfoView declaration:
function updateCIBadge() {
const count = projectInfoProvider.ciFailureCount
projectInfoView.badge = count > 0
? { value: count, tooltip: `${count} CI workflow${count !== 1 ? 's' : ''} failing` }
: undefined
}- [ ] Step 4: Call badge update after every refresh
The refresh happens in two places. Update both.
In the branch polling callback (around line 708), replace:
startBranchPolling(workspaceRoot, env, 10000, () => {
projectInfoProvider.refresh()
})With:
startBranchPolling(workspaceRoot, env, 10000, async () => {
await projectInfoProvider.refresh()
updateCIBadge()
})After the activation refresh (around line 714), replace:
projectInfoProvider.refresh()With:
projectInfoProvider.refresh().then(() => updateCIBadge())Also update the manual refresh command (around line 295):
vscode.commands.registerCommand('lanternControl.refreshProjectInfo', () => {
projectInfoProvider.refresh()
}),With:
vscode.commands.registerCommand('lanternControl.refreshProjectInfo', async () => {
await projectInfoProvider.refresh()
updateCIBadge()
}),- [ ] Step 5: Register the 3 branch cleanup commands
Add these command registrations inside the existing context.subscriptions.push( block, right after the lanternControl.refreshProjectInfo command:
// โโ Branch cleanup commands โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
vscode.commands.registerCommand('lanternControl.deleteBranch', async (item) => {
if (!item?.branchName) return
const choice = await vscode.window.showWarningMessage(
`Delete local branch "${item.branchName}"?`,
{ modal: true },
'Delete'
)
if (choice !== 'Delete') return
const { execFile } = require('child_process')
execFile('git', ['branch', '-D', item.branchName], { cwd: workspaceRoot }, (error) => {
if (error) {
vscode.window.showErrorMessage(`Failed to delete branch: ${error.message}`)
} else {
vscode.window.showInformationMessage(`Deleted branch "${item.branchName}".`)
projectInfoProvider.refresh().then(() => updateCIBadge())
}
})
}),
vscode.commands.registerCommand('lanternControl.cleanupGoneBranches', async (item) => {
if (!item?.branches?.length) return
const picks = item.branches.map(b => ({
label: b.name,
description: 'remote deleted',
picked: true,
}))
const selected = await vscode.window.showQuickPick(picks, {
title: 'Delete gone branches (remote deleted)',
canPickMany: true,
placeHolder: 'Select branches to delete',
})
if (!selected?.length) return
const { execFile } = require('child_process')
let deleted = 0
let failed = 0
for (const pick of selected) {
await new Promise((resolve) => {
execFile('git', ['branch', '-D', pick.label], { cwd: workspaceRoot }, (error) => {
if (error) failed++
else deleted++
resolve()
})
})
}
if (failed > 0) {
vscode.window.showWarningMessage(`Deleted ${deleted} branch${deleted !== 1 ? 'es' : ''}, ${failed} failed.`)
} else {
vscode.window.showInformationMessage(`Deleted ${deleted} branch${deleted !== 1 ? 'es' : ''}.`)
}
projectInfoProvider.refresh().then(() => updateCIBadge())
}),
vscode.commands.registerCommand('lanternControl.cleanupStaleBranches', async (item) => {
if (!item?.branches?.length) return
const picks = item.branches.map(b => ({
label: b.name,
description: `${b.daysAgo} days inactive`,
picked: false,
}))
const selected = await vscode.window.showQuickPick(picks, {
title: 'Delete stale branches (30+ days inactive)',
canPickMany: true,
placeHolder: 'Select branches to delete',
})
if (!selected?.length) return
const { execFile } = require('child_process')
let deleted = 0
let failed = 0
for (const pick of selected) {
await new Promise((resolve) => {
execFile('git', ['branch', '-D', pick.label], { cwd: workspaceRoot }, (error) => {
if (error) failed++
else deleted++
resolve()
})
})
}
if (failed > 0) {
vscode.window.showWarningMessage(`Deleted ${deleted} branch${deleted !== 1 ? 'es' : ''}, ${failed} failed.`)
} else {
vscode.window.showInformationMessage(`Deleted ${deleted} branch${deleted !== 1 ? 'es' : ''}.`)
}
projectInfoProvider.refresh().then(() => updateCIBadge())
}),- [ ] Step 6: Commit
git add tooling/vscode-extension/src/extension.js
git commit -m "feat(vscode-ext): wire up CI badge, notifications, and branch cleanup commands"Task 5: Add commands and menus to package.json โ
Files:
Modify:
tooling/vscode-extension/package.json[ ] Step 1: Add the 3 new commands
Add these entries to the commands array (after the lanternControl.refreshProjectInfo entry, before the closing ]):
{ "command": "lanternControl.deleteBranch", "title": "Delete Branch", "icon": "$(trash)" },
{ "command": "lanternControl.cleanupGoneBranches", "title": "Clean Up", "icon": "$(trash)" },
{ "command": "lanternControl.cleanupStaleBranches", "title": "Clean Up", "icon": "$(trash)" }- [ ] Step 2: Add menu entries for the new commands
Add these entries to the view/item/context array (after the last lanternControl.cleanInstall entry, before the closing ]):
,
{
"command": "lanternControl.deleteBranch",
"when": "viewItem == deletableBranch",
"group": "inline@1"
},
{
"command": "lanternControl.cleanupGoneBranches",
"when": "viewItem == branchCleanupGone",
"group": "inline@1"
},
{
"command": "lanternControl.cleanupStaleBranches",
"when": "viewItem == branchCleanupStale",
"group": "inline@1"
}- [ ] Step 3: Commit
git add tooling/vscode-extension/package.json
git commit -m "feat(vscode-ext): add branch cleanup commands and menu entries"Task 6: Rebuild the extension and verify โ
Files:
Modify:
tooling/vscode-extension/(rebuild)[ ] Step 1: Build the extension
cd tooling/vscode-extension && npm run package- [ ] Step 2: Manual verification checklist
Open VS Code with the Lantern Control sidebar and verify:
- Branches group โ expands to show Current, Local, Remote, plus Gone and Stale sub-groups
- Gone sub-group โ expands to show individual branch names, each with a trash icon
- Stale sub-group โ expands to show individual branch names with "N days" description, each with trash icon
- Gone "Clean Up" button โ opens Quick Pick with all gone branches pre-selected
- Stale "Clean Up" button โ opens Quick Pick with stale branches (not pre-selected)
- Individual delete โ clicking trash on a single branch shows confirmation dialog, deletes on confirm
- Bulk delete โ selecting multiple in Quick Pick and confirming deletes them all, shows count message
- Refresh after delete โ panel updates counts after branch deletion
- CI badge โ if any CI workflow is failing, a number badge appears on the Project Info view header
- CI notification โ on first detection of CI failure, a notification appears in the Notifications panel with error level
- No duplicate notifications โ refreshing again while still failing does NOT push another notification
- Badge clears โ when CI passes again, badge disappears
- [ ] Step 3: Commit built output
git add tooling/vscode-extension/
git commit -m "build(vscode-ext): rebuild with branch actions and CI badges"