Skip to content

Project Info Panel v2 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: Enhance the VSCode extension's Project Info panel from a flat list into 7 collapsible groups with rich project stats.

Architecture: Add 6 new fetch functions to githubData.js (issues, PRs, CI, reviews, branches, activity). Refactor ProjectInfoProvider.js to use a parent/child tree with InfoGroup (collapsible) and InfoItem (leaf) classes. All fetches run in parallel via Promise.all.

Tech Stack: VS Code TreeView API, GitHub CLI (gh), GitHub Search API, local git commands.


Task 1: Add InfoGroup class to ProjectInfoProvider โ€‹

Files:

  • Modify: tooling/vscode-extension/src/providers/ProjectInfoProvider.js:7-24

  • [ ] Step 1: Add InfoGroup class above InfoItem

Add the collapsible parent class right after the existing imports:

javascript
class InfoGroup extends vscode.TreeItem {
  constructor(label, { description, tooltip, icon, url, children }) {
    super(label, vscode.TreeItemCollapsibleState.Collapsed)
    this.description  = description || ''
    this.tooltip      = tooltip || ''
    this.iconPath     = new vscode.ThemeIcon(icon)
    this.contextValue = 'projectInfoGroup'
    this.children     = children || []

    if (url) {
      this.command = {
        command:   'vscode.open',
        title:     'Open in Browser',
        arguments: [vscode.Uri.parse(url)],
      }
    }
  }
}
  • [ ] Step 2: Update getChildren to support parent/child

Replace the existing getChildren() method:

javascript
getChildren(element) {
  if (element instanceof InfoGroup) {
    return element.children
  }
  return this._buildTree()
}
  • [ ] Step 3: Extract current getChildren body into _buildTree

Rename the current getChildren() body to _buildTree():

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

  const items = []

  // (existing items will be replaced in later tasks)
  // ... keep existing code for now ...

  return items
}
  • [ ] Step 4: Verify extension still loads

Open VS Code, check the Lantern Control sidebar loads without errors. The panel should look identical to before.

  • [ ] Step 5: Commit
bash
git add tooling/vscode-extension/src/providers/ProjectInfoProvider.js
git commit -m "refactor(vscode-ext): add InfoGroup class for collapsible tree items"

Task 2: Add fetchIssueCounts and fetchPRCounts to githubData โ€‹

Files:

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

  • [ ] Step 1: Add a helper for GitHub search count queries

Add this after the existing execAsync function:

javascript
/**
 * Run a GitHub search query and return the total_count.
 * @param {string} cwd โ€” workspace root
 * @param {string} query โ€” search query (without repo: prefix, that's added automatically)
 * @returns {Promise<number|null>}
 */
async function searchCount(cwd, query) {
  const raw = await execAsync('gh', [
    'api', `search/issues?q=repo:{owner}/{repo}+${query}`,
    '--jq', '.total_count',
  ], { cwd })

  if (!raw) return null
  const n = parseInt(raw.trim(), 10)
  return Number.isNaN(n) ? null : n
}
  • [ ] Step 2: Add fetchIssueCounts
javascript
/**
 * Fetch issue counts from GitHub search API.
 * @param {string} cwd โ€” workspace root
 * @returns {Promise<{ open: number, closed: number, unassigned: number } | null>}
 */
async function fetchIssueCounts(cwd) {
  const [open, closed, unassigned] = await Promise.all([
    searchCount(cwd, 'type:issue+state:open'),
    searchCount(cwd, 'type:issue+state:closed'),
    searchCount(cwd, 'type:issue+state:open+no:assignee'),
  ])

  if (open === null && closed === null) return null
  return { open: open ?? 0, closed: closed ?? 0, unassigned: unassigned ?? 0 }
}
  • [ ] Step 3: Add fetchPRCounts
javascript
/**
 * Fetch pull request counts from GitHub search API.
 * @param {string} cwd โ€” workspace root
 * @returns {Promise<{ open: number, merged: number, closed: number, draft: number } | null>}
 */
async function fetchPRCounts(cwd) {
  const [open, merged, closed, draft] = await Promise.all([
    searchCount(cwd, 'type:pr+state:open'),
    searchCount(cwd, 'type:pr+is:merged'),
    searchCount(cwd, 'type:pr+state:closed+-is:merged'),
    searchCount(cwd, 'type:pr+state:open+draft:true'),
  ])

  if (open === null && merged === null) return null
  return { open: open ?? 0, merged: merged ?? 0, closed: closed ?? 0, draft: draft ?? 0 }
}
  • [ ] Step 4: Update exports
javascript
module.exports = { fetchActiveMilestone, fetchBranchPR, fetchIssueCounts, fetchPRCounts }
  • [ ] Step 5: Commit
bash
git add tooling/vscode-extension/src/lib/githubData.js
git commit -m "feat(vscode-ext): add fetchIssueCounts and fetchPRCounts via GitHub search API"

Task 3: Add fetchCIStatus and fetchReviewRequests to githubData โ€‹

Files:

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

  • [ ] Step 1: Add fetchCIStatus

javascript
/**
 * Fetch CI workflow run status for a branch.
 * Returns the latest run per unique workflow name.
 * @param {string} cwd โ€” workspace root
 * @param {string} branch โ€” branch name
 * @returns {Promise<{ overall: string, runs: Array<{ name: string, conclusion: string, status: string, url: string, createdAt: string }> } | null>}
 */
async function fetchCIStatus(cwd, branch) {
  const raw = await execAsync('gh', [
    'run', 'list',
    '--branch', branch,
    '--limit', '20',
    '--json', 'name,status,conclusion,createdAt,url',
  ], { cwd })

  if (!raw) return null

  try {
    const allRuns = JSON.parse(raw)
    if (!allRuns.length) return { overall: 'none', runs: [] }

    // Dedupe: keep only the latest run per workflow name
    const seen = new Set()
    const runs = []
    for (const run of allRuns) {
      if (!seen.has(run.name)) {
        seen.add(run.name)
        runs.push(run)
      }
    }

    // Derive overall status
    let overall = 'passing'
    if (runs.some(r => r.status === 'in_progress' || r.status === 'queued')) {
      overall = 'running'
    }
    if (runs.some(r => r.conclusion === 'failure')) {
      overall = 'failing'
    }

    return { overall, runs }
  } catch {
    return null
  }
}
  • [ ] Step 2: Add fetchReviewRequests
javascript
/**
 * Fetch open PRs requesting review from the authenticated user.
 * @param {string} cwd โ€” workspace root
 * @returns {Promise<Array<{ number: number, title: string, url: string, createdAt: string }> | null>}
 */
async function fetchReviewRequests(cwd) {
  const raw = await execAsync('gh', [
    'api', 'search/issues?q=repo:{owner}/{repo}+type:pr+state:open+review-requested:@me',
    '--jq', '[.items[] | {number, title, url: .html_url, createdAt: .created_at}]',
  ], { cwd })

  if (!raw) return null

  try {
    return JSON.parse(raw)
  } catch {
    return null
  }
}
  • [ ] Step 3: Update exports
javascript
module.exports = {
  fetchActiveMilestone, fetchBranchPR, fetchIssueCounts, fetchPRCounts,
  fetchCIStatus, fetchReviewRequests,
}
  • [ ] Step 4: Commit
bash
git add tooling/vscode-extension/src/lib/githubData.js
git commit -m "feat(vscode-ext): add fetchCIStatus and fetchReviewRequests"

Task 4: Add fetchBranchStats and fetchActivityStats to githubData โ€‹

Files:

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

  • [ ] Step 1: Add fetchBranchStats

javascript
/**
 * Fetch branch statistics from local git.
 * @param {string} cwd โ€” workspace root
 * @returns {Promise<{ local: number, remote: number, gone: number, stale: 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

  // Count branches with [gone] upstream
  const gone = vvRaw
    ? vvRaw.trim().split('\n').filter(l => /\[.*gone\]/.test(l)).length
    : 0

  // Count stale branches (no commits in 30+ days)
  const thirtyDaysAgo = Math.floor(Date.now() / 1000) - (30 * 24 * 60 * 60)
  let stale = 0
  if (refRaw) {
    for (const line of refRaw.trim().split('\n')) {
      const parts = line.trim().split(' ')
      const timestamp = parseInt(parts[parts.length - 1], 10)
      if (timestamp && timestamp < thirtyDaysAgo) stale++
    }
  }

  return { local, remote, gone, stale }
}
  • [ ] Step 2: Add fetchActivityStats
javascript
/**
 * Fetch commit activity from local git.
 * @param {string} cwd โ€” workspace root
 * @returns {Promise<{ weekCommits: number, totalCommits: number, contributors: number, lastCommit: Date|null } | null>}
 */
async function fetchActivityStats(cwd) {
  const [weekRaw, totalRaw, contribRaw, lastRaw] = await Promise.all([
    execAsync('git', ['rev-list', '--count', '--since=7 days ago', '--all'], { cwd }),
    execAsync('git', ['rev-list', '--count', '--all'], { cwd }),
    execAsync('git', ['log', '--format=%ae', '--since=7 days ago', '--all'], { cwd }),
    execAsync('git', ['log', '-1', '--format=%ci'], { cwd }),
  ])

  if (!totalRaw) return null

  const weekCommits  = weekRaw  ? parseInt(weekRaw.trim(), 10)  : 0
  const totalCommits = parseInt(totalRaw.trim(), 10)

  // Count unique contributors
  const contributors = contribRaw
    ? new Set(contribRaw.trim().split('\n').filter(l => l.trim())).size
    : 0

  // Parse last commit date
  const lastCommit = lastRaw ? new Date(lastRaw.trim()) : null

  return { weekCommits, totalCommits, contributors, lastCommit }
}
  • [ ] Step 3: Update exports
javascript
module.exports = {
  fetchActiveMilestone, fetchBranchPR, fetchIssueCounts, fetchPRCounts,
  fetchCIStatus, fetchReviewRequests, fetchBranchStats, fetchActivityStats,
}
  • [ ] Step 4: Commit
bash
git add tooling/vscode-extension/src/lib/githubData.js
git commit -m "feat(vscode-ext): add fetchBranchStats and fetchActivityStats (local git)"

Task 5: Refactor ProjectInfoProvider refresh() to fetch all data โ€‹

Files:

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

  • [ ] Step 1: Update imports

Replace the existing import line:

javascript
const { fetchActiveMilestone, fetchBranchPR } = require('../lib/githubData')

With:

javascript
const {
  fetchActiveMilestone, fetchBranchPR, fetchIssueCounts, fetchPRCounts,
  fetchCIStatus, fetchReviewRequests, fetchBranchStats, fetchActivityStats,
} = require('../lib/githubData')
  • [ ] Step 2: Add new state fields to constructor

Replace the constructor body:

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

  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 3: Replace refresh() to fetch all in parallel
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),
  ])

  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 "refactor(vscode-ext): fetch all project info data in parallel"

Task 6: Build the grouped tree in _buildTree() โ€‹

Files:

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

  • [ ] Step 1: Replace _buildTree() with full grouped implementation

Replace the entire _buildTree() method with the following. This builds all 7 collapsible groups plus 2 flat items at the bottom:

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

  const items = []

  // โ”€โ”€ Milestone group โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  if (this._milestone) {
    const m = this._milestone
    items.push(new InfoGroup(`Milestone: ${m.title}`, {
      description: `${m.percent}% (${m.closed}/${m.total})`,
      tooltip:     `${m.title} โ€” ${m.closed} closed, ${m.open} open of ${m.total}`,
      icon:        'milestone',
      url:         m.url,
      children: [
        new InfoItem('Open', { description: `${m.open}`, icon: 'circle-filled' }),
        new InfoItem('Closed', { description: `${m.closed}`, icon: 'pass' }),
      ],
    }))
  } 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',
    }))
  }

  // โ”€โ”€ Issues group โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  if (this._issues) {
    const i = this._issues
    items.push(new InfoGroup('Issues', {
      description: `${i.open} open, ${i.closed} closed`,
      tooltip:     `${i.open} open, ${i.closed} closed, ${i.unassigned} unassigned`,
      icon:        'issues',
      url:         `https://github.com/${this._getRepo()}/issues`,
      children: [
        new InfoItem('Open', { description: `${i.open}`, icon: 'circle-filled' }),
        new InfoItem('Closed', { description: `${i.closed}`, icon: 'pass' }),
        new InfoItem('Unassigned', { description: `${i.unassigned}`, icon: 'warning' }),
      ],
    }))
  } else {
    items.push(new InfoItem('Issues: unavailable', {
      description: 'could not fetch from GitHub',
      icon:        'issues',
    }))
  }

  // โ”€โ”€ Pull Requests group โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  if (this._prCounts) {
    const p = this._prCounts
    const prChildren = [
      new InfoItem('Open', { description: `${p.open}`, icon: 'circle-filled' }),
      new InfoItem('Merged', { description: `${p.merged}`, icon: 'git-merge' }),
      new InfoItem('Closed (unmerged)', { description: `${p.closed}`, icon: 'circle-slash' }),
      new InfoItem('Draft', { description: `${p.draft}`, icon: 'edit' }),
    ]

    if (this._pr) {
      prChildren.push(new InfoItem(`โ†’ Current: #${this._pr.number}`, {
        description: this._pr.title,
        tooltip:     `#${this._pr.number} โ€” ${this._pr.title}\n${this._pr.url}`,
        icon:        'git-pull-request',
        url:         this._pr.url,
      }))
    }

    items.push(new InfoGroup('Pull Requests', {
      description: `${p.open} open, ${p.merged} merged`,
      tooltip:     `${p.open} open, ${p.merged} merged, ${p.closed} closed, ${p.draft} draft`,
      icon:        'git-pull-request',
      url:         `https://github.com/${this._getRepo()}/pulls`,
      children:    prChildren,
    }))
  } else {
    items.push(new InfoItem('Pull Requests: unavailable', {
      description: 'could not fetch from GitHub',
      icon:        'git-pull-request',
    }))
  }

  // โ”€โ”€ CI Status group โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  if (this._ci) {
    const statusIcon = this._ci.overall === 'failing' ? 'error'
      : this._ci.overall === 'running' ? 'sync~spin'
      : this._ci.overall === 'none' ? 'circle-outline'
      : 'pass-filled'
    const statusLabel = this._ci.overall === 'failing' ? 'โœ— failing'
      : this._ci.overall === 'running' ? 'โ— running'
      : this._ci.overall === 'none' ? 'no runs'
      : 'โœ“ passing'

    const ciChildren = this._ci.runs.map(run => {
      const runIcon = run.conclusion === 'failure' ? 'error'
        : run.conclusion === 'success' ? 'pass'
        : run.status === 'in_progress' ? 'sync~spin'
        : 'circle-outline'
      const ago = formatRelativeTime(new Date(run.createdAt))
      const result = run.conclusion || run.status
      return new InfoItem(run.name, {
        description: `${result} (${ago})`,
        tooltip:     `${run.name} โ€” ${result}\n${run.url}`,
        icon:        runIcon,
        url:         run.url,
      })
    })

    items.push(new InfoGroup('CI Status', {
      description: statusLabel,
      tooltip:     `${this._ci.runs.length} workflows for ${branch}`,
      icon:        statusIcon,
      url:         `https://github.com/${this._getRepo()}/actions?query=branch:${encodeURIComponent(branch || '')}`,
      children:    ciChildren,
    }))
  } else {
    items.push(new InfoItem('CI Status: unavailable', {
      description: 'could not fetch from GitHub',
      icon:        'circle-outline',
    }))
  }

  // โ”€โ”€ Review Requests group โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  if (this._reviews) {
    const reviewChildren = this._reviews.map(pr => {
      const daysAgo = Math.floor((Date.now() - new Date(pr.createdAt).getTime()) / (1000 * 60 * 60 * 24))
      return new InfoItem(`#${pr.number} โ€” ${pr.title}`, {
        description: `waiting ${daysAgo} day${daysAgo !== 1 ? 's' : ''}`,
        tooltip:     `#${pr.number} โ€” ${pr.title}\n${pr.url}`,
        icon:        'request-changes',
        url:         pr.url,
      })
    })

    items.push(new InfoGroup('Review Requests', {
      description: `${this._reviews.length} awaiting review`,
      tooltip:     `${this._reviews.length} PRs requesting your review`,
      icon:        'eye',
      url:         `https://github.com/${this._getRepo()}/pulls/review-requested/@me`,
      children:    reviewChildren,
    }))
  } else {
    items.push(new InfoItem('Review Requests: unavailable', {
      description: 'could not fetch from GitHub',
      icon:        'eye',
    }))
  }

  // โ”€โ”€ 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.gone > 0) {
      branchChildren.push(new InfoItem('Gone (remote deleted)', {
        description: `${b.gone}`,
        tooltip:     `${b.gone} branch${b.gone !== 1 ? 'es' : ''} with deleted remote โ€” consider running clean_gone`,
        icon:        'warning',
      }))
    }
    if (b.stale > 0) {
      branchChildren.push(new InfoItem('Stale (30+ days)', {
        description: `${b.stale}`,
        tooltip:     `${b.stale} branch${b.stale !== 1 ? 'es' : ''} with no commits in 30+ days`,
        icon:        'history',
      }))
    }

    items.push(new InfoGroup('Branches', {
      description: `${b.local} local / ${b.remote} remote`,
      tooltip:     `${b.local} local, ${b.remote} remote, ${b.gone} gone, ${b.stale} stale`,
      icon:        'git-branch',
      children:    branchChildren,
    }))
  } else {
    items.push(new InfoItem('Branches: unavailable', {
      description: 'could not read git data',
      icon:        'git-branch',
    }))
  }

  // โ”€โ”€ Activity group โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  if (this._activity) {
    const a = this._activity
    const lastCommitAgo = a.lastCommit ? formatRelativeTime(a.lastCommit) : 'unknown'

    items.push(new InfoGroup('Activity', {
      description: `${a.weekCommits} this week, ${a.totalCommits.toLocaleString()} total`,
      tooltip:     `${a.weekCommits} commits in last 7 days, ${a.totalCommits.toLocaleString()} all time`,
      icon:        'pulse',
      children: [
        new InfoItem('Commits (7d)', { description: `${a.weekCommits}`, icon: 'git-commit' }),
        new InfoItem('Commits (all time)', { description: `${a.totalCommits.toLocaleString()}`, icon: 'git-commit' }),
        new InfoItem('Contributors (7d)', { description: `${a.contributors}`, icon: 'person' }),
        new InfoItem('Last commit', { description: lastCommitAgo, icon: 'history' }),
      ],
    }))
  } else {
    items.push(new InfoItem('Activity: unavailable', {
      description: 'could not read git data',
      icon:        'pulse',
    }))
  }

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

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

  return items
}
  • [ ] Step 2: Add _getRepo() helper method to the class

Add this method to the ProjectInfoProvider class, right after _buildTree(). It extracts the owner/repo slug from the git remote URL for building GitHub links:

javascript
/**
 * Get the owner/repo string from git remote.
 * Cached after first call.
 */
_getRepo() {
  if (this._repoSlug) return this._repoSlug

  try {
    const { execSync } = require('child_process')
    const url = execSync('git remote get-url origin', { cwd: this._root, encoding: 'utf-8' }).trim()
    // Handle both HTTPS and SSH URLs
    const match = url.match(/github\.com[:/](.+?)(?:\.git)?$/)
    this._repoSlug = match ? match[1] : 'owner/repo'
  } catch {
    this._repoSlug = 'owner/repo'
  }
  return this._repoSlug
}
  • [ ] Step 3: Verify extension loads with all groups

Open VS Code, check the Lantern Control sidebar. You should see 7 collapsible groups plus Version and Last Refreshed at the bottom. Click each group to expand and verify sub-items appear.

  • [ ] Step 4: Commit
bash
git add tooling/vscode-extension/src/providers/ProjectInfoProvider.js
git commit -m "feat(vscode-ext): build grouped tree with all 7 sections in Project Info panel"

Task 7: Rebuild the extension and verify โ€‹

Files:

  • Modify: tooling/vscode-extension/ (rebuild)

  • [ ] Step 1: Build the extension

bash
cd tooling/vscode-extension && npm run build 2>/dev/null || npx vsce package 2>/dev/null || echo "Manual build โ€” check package.json for build script"
  • [ ] Step 2: Manual verification checklist

Open VS Code with the Lantern Control sidebar and verify:

  1. Milestone โ€” shows title, percent, open/closed sub-items, click opens GitHub
  2. Issues โ€” shows open/closed/unassigned counts
  3. Pull Requests โ€” shows open/merged/closed/draft, current branch PR if exists
  4. CI Status โ€” shows overall status, per-workflow runs with relative times
  5. Review Requests โ€” shows count and individual PRs with wait times
  6. Branches โ€” shows local/remote counts, gone and stale if any
  7. Activity โ€” shows 7d and all-time commits, contributors, last commit
  8. Version โ€” still shows correctly
  9. Last refreshed โ€” updates on refresh
  10. Refresh button โ€” triggers full data reload
  11. Collapsed view โ€” each group shows summary description when collapsed
  12. Error graceful degradation โ€” disconnect from internet, refresh, verify groups show "unavailable" while local groups (Branches, Activity) still work
  • [ ] Step 3: Commit built output
bash
git add tooling/vscode-extension/
git commit -m "build(vscode-ext): rebuild with Project Info panel v2"

Built with VitePress