Skip to content

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 fetchBranchStats with version that returns branch arrays

Replace the entire fetchBranchStats function (lines 197-227):

javascript
/**
 * 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
bash
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:

javascript
    // โ”€โ”€ 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
bash
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):

javascript
  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 ciFailureCount getter

Add this getter right after the constructor (after line 66, before async refresh()):

javascript
  /**
   * 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:

javascript
  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
bash
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:

javascript
  const projectInfoProvider    = new ProjectInfoProvider(workspaceRoot)

With:

javascript
  const projectInfoProvider    = new ProjectInfoProvider(workspaceRoot, notificationsProvider)
  • [ ] Step 2: Store the tree view reference for badge updates

Replace line 133:

javascript
    vscode.window.createTreeView('lanternControl.projectInfo',    { treeDataProvider: projectInfoProvider }),

With:

javascript
    projectInfoView,

And add this line BEFORE the context.subscriptions.push( block (before line 132):

javascript
  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:

javascript
  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:

javascript
  startBranchPolling(workspaceRoot, env, 10000, () => {
    projectInfoProvider.refresh()
  })

With:

javascript
  startBranchPolling(workspaceRoot, env, 10000, async () => {
    await projectInfoProvider.refresh()
    updateCIBadge()
  })

After the activation refresh (around line 714), replace:

javascript
  projectInfoProvider.refresh()

With:

javascript
  projectInfoProvider.refresh().then(() => updateCIBadge())

Also update the manual refresh command (around line 295):

javascript
    vscode.commands.registerCommand('lanternControl.refreshProjectInfo', () => {
      projectInfoProvider.refresh()
    }),

With:

javascript
    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:

javascript
    // โ”€โ”€ 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
bash
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 ]):

json
      { "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 ]):

json
        ,
        {
          "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
bash
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

bash
cd tooling/vscode-extension && npm run package
  • [ ] Step 2: Manual verification checklist

Open VS Code with the Lantern Control sidebar and verify:

  1. Branches group โ€” expands to show Current, Local, Remote, plus Gone and Stale sub-groups
  2. Gone sub-group โ€” expands to show individual branch names, each with a trash icon
  3. Stale sub-group โ€” expands to show individual branch names with "N days" description, each with trash icon
  4. Gone "Clean Up" button โ€” opens Quick Pick with all gone branches pre-selected
  5. Stale "Clean Up" button โ€” opens Quick Pick with stale branches (not pre-selected)
  6. Individual delete โ€” clicking trash on a single branch shows confirmation dialog, deletes on confirm
  7. Bulk delete โ€” selecting multiple in Quick Pick and confirming deletes them all, shows count message
  8. Refresh after delete โ€” panel updates counts after branch deletion
  9. CI badge โ€” if any CI workflow is failing, a number badge appears on the Project Info view header
  10. CI notification โ€” on first detection of CI failure, a notification appears in the Notifications panel with error level
  11. No duplicate notifications โ€” refreshing again while still failing does NOT push another notification
  12. Badge clears โ€” when CI passes again, badge disappears
  • [ ] Step 3: Commit built output
bash
git add tooling/vscode-extension/
git commit -m "build(vscode-ext): rebuild with branch actions and CI badges"

Built with VitePress