From fc9c9b2ee9a482a2004d2cbbac4c78b7fdfc3ea5 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Thu, 23 Apr 2026 15:55:08 +0200 Subject: [PATCH 1/2] Keep Active Agents detail cards focused on scan-first signals Hide Provider from expanded session details and keep the regression suite aligned with the branch's compact Active Agents summaries, provider-first decorations, and grouped file sections. Constraint: This follow-up had to layer onto the branch's existing compact-tree refactor instead of restoring older verbose copy. Rejected: Update only the screenshot-facing row removal and ignore stale tests | would leave the focused proof surface red. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep provider identity in badges and tooltips, not duplicated as a session detail row. Tested: node --test test/vscode-active-agents-session-state.test.js Not-tested: Manual VS Code extension screenshot pass --- .../vscode/guardex-active-agents/extension.js | 251 +++++++++++++----- ...vscode-active-agents-session-state.test.js | 71 ++--- vscode/guardex-active-agents/extension.js | 251 +++++++++++++----- 3 files changed, 417 insertions(+), 156 deletions(-) diff --git a/templates/vscode/guardex-active-agents/extension.js b/templates/vscode/guardex-active-agents/extension.js index 17b391e..c7322ff 100644 --- a/templates/vscode/guardex-active-agents/extension.js +++ b/templates/vscode/guardex-active-agents/extension.js @@ -72,6 +72,70 @@ const SESSION_PROVIDER_BRANDS = { badge: 'CL', }, }; +const SESSION_FILE_GROUPS = [ + { key: 'code', label: 'Code', iconId: 'symbol-file' }, + { key: 'tests', label: 'Tests', iconId: 'beaker' }, + { key: 'openspec', label: 'OpenSpec', iconId: 'note' }, + { key: 'config', label: 'Config', iconId: 'settings-gear' }, + { key: 'other', label: 'Other', iconId: 'files' }, +]; +const CODE_FILE_EXTENSIONS = new Set([ + '.c', + '.cc', + '.cpp', + '.cs', + '.css', + '.go', + '.h', + '.hpp', + '.html', + '.java', + '.js', + '.jsx', + '.kt', + '.mjs', + '.cjs', + '.mdx', + '.php', + '.py', + '.rb', + '.rs', + '.scss', + '.sh', + '.sql', + '.swift', + '.ts', + '.tsx', + '.vue', +]); +const CONFIG_FILE_NAMES = new Set([ + '.editorconfig', + '.gitignore', + '.npmrc', + '.prettierignore', + '.prettierrc', + 'biome.json', + 'bunfig.toml', + 'dockerfile', + 'eslint.config.js', + 'eslint.config.mjs', + 'jsconfig.json', + 'package-lock.json', + 'package.json', + 'pnpm-lock.yaml', + 'pnpm-workspace.yaml', + 'prettier.config.js', + 'prettier.config.mjs', + 'tailwind.config.js', + 'tailwind.config.cjs', + 'tailwind.config.ts', + 'tsconfig.json', + 'turbo.json', + 'vite.config.js', + 'vite.config.ts', + 'webpack.config.js', + 'yarn.lock', +]); function iconColorId(iconId) { switch (iconId) { @@ -175,6 +239,10 @@ function formatCountLabel(count, singular, plural = `${singular}s`) { return `${count} ${count === 1 ? singular : plural}`; } +function formatShortCountLabel(count, singular, plural = singular) { + return `${count} ${count === 1 ? singular : plural}`; +} + function branchSegments(branch) { return String(branch || '') .split('/') @@ -293,7 +361,7 @@ function sessionSnapshotDecoration(session) { } function sessionIdentityDecoration(session) { - return sessionSnapshotDecoration(session) || sessionProviderDecoration(session); + return sessionProviderDecoration(session) || sessionSnapshotDecoration(session); } function stringListsEqual(left, right) { @@ -445,6 +513,68 @@ function summarizeCompactPaths(paths, maxCount = SESSION_TOP_FILE_COUNT) { return compactPaths.join(', '); } +function sessionChangeGroupKey(change) { + const relativePath = normalizeRelativePath(change?.relativePath).toLowerCase(); + if (!relativePath) { + return 'other'; + } + + const baseName = path.posix.basename(relativePath); + if (relativePath === 'openspec' || relativePath.startsWith('openspec/')) { + return 'openspec'; + } + if (/(^|\/)(__tests__|test|tests)(\/|$)/.test(relativePath) || /\.(test|spec)\.[^.\/]+$/.test(baseName)) { + return 'tests'; + } + if ( + relativePath.startsWith('.github/') + || relativePath.startsWith('.vscode/') + || relativePath.startsWith('.claude/') + || relativePath.startsWith('.codex/') + || relativePath.startsWith('.omx/') + || relativePath.startsWith('.omc/') + || baseName.startsWith('.env') + || CONFIG_FILE_NAMES.has(baseName) + ) { + return 'config'; + } + if ( + relativePath.startsWith('app/') + || relativePath.startsWith('bin/') + || relativePath.startsWith('lib/') + || relativePath.startsWith('scripts/') + || relativePath.startsWith('server/') + || relativePath.startsWith('src/') + || relativePath.startsWith('templates/') + || relativePath.startsWith('vscode/') + || CODE_FILE_EXTENSIONS.has(path.posix.extname(baseName)) + ) { + return 'code'; + } + return 'other'; +} + +function buildSessionFileGroupItems(session) { + const groups = new Map(SESSION_FILE_GROUPS.map(({ key }) => [key, []])); + for (const change of session?.touchedChanges || []) { + const groupKey = sessionChangeGroupKey(change); + groups.get(groupKey)?.push(change); + } + + return SESSION_FILE_GROUPS + .map(({ key, label, iconId }) => { + const groupChanges = groups.get(key) || []; + if (groupChanges.length === 0) { + return null; + } + return new SectionItem(label, buildChangeTreeNodes(groupChanges), { + description: String(groupChanges.length), + iconId, + }); + }) + .filter(Boolean); +} + function isProtectedBranchName(branch) { return branch === 'main' || branch === 'dev'; } @@ -578,6 +708,10 @@ function buildSessionHealthTooltip(session) { ].filter(Boolean).join('\n'); } +function sessionCompactTimeLabel(session) { + return session.lastActiveLabel || session.elapsedLabel || formatElapsedFrom(session.startedAt); +} + function buildSessionTopFiles(session) { return uniqueStringList((session?.worktreeChangedPaths || []) .map(normalizeRelativePath) @@ -617,48 +751,32 @@ function changeRiskBadges(change) { ].filter(Boolean)); } -function buildSessionCardDescription(session) { - const provider = resolveSessionProvider(session); - const statusAgentLabel = `${sessionStatusLabel(session)}: ${session.agentName || 'agent'}`; - const descriptionParts = [ - statusAgentLabel, - provider?.label ? `via ${provider.label}` : '', - sessionSnapshotDescription(session), - session.deltaLabel || '', - session.changeCount > 0 ? formatCountLabel(session.changeCount, 'changed file') : '', - session.lockCount > 0 ? formatCountLabel(session.lockCount, 'lock') : '', - buildSessionHealthCompactLabel(session), - session.freshnessLabel || '', - session.lastActiveLabel ? `${session.lastActiveLabel} ago` : '', - ].filter(Boolean); - return descriptionParts.join(' · '); -} - -function buildRawSessionDescription(session) { - const provider = resolveSessionProvider(session); +function buildSessionCompactDescription(session) { const descriptionParts = [sessionStatusLabel(session)]; const fileCountLabel = sessionFileCountLabel(session); if (fileCountLabel) { descriptionParts.push(fileCountLabel); } - if (provider?.label) { - descriptionParts.push(provider.label); - } - const snapshot = sessionSnapshotDescription(session); - if (snapshot) { - descriptionParts.push(snapshot); - } - descriptionParts.push(session.elapsedLabel || formatElapsedFrom(session.startedAt)); - const sessionHealthLabel = buildSessionHealthCompactLabel(session); - if (sessionHealthLabel) { - descriptionParts.push(sessionHealthLabel); + const timeLabel = sessionCompactTimeLabel(session); + if (timeLabel) { + descriptionParts.push(timeLabel); } - if (session.lockCount > 0) { + if (session.conflictCount > 0) { + descriptionParts.push(formatCountLabel(session.conflictCount, 'conflict')); + } else if (session.lockCount > 0) { descriptionParts.push(formatCountLabel(session.lockCount, 'lock')); } return descriptionParts.join(' · '); } +function buildSessionCardDescription(session) { + return buildSessionCompactDescription(session); +} + +function buildRawSessionDescription(session) { + return buildSessionCompactDescription(session); +} + function buildSessionTooltip(session, description) { const provider = resolveSessionProvider(session); const riskSummary = uniqueStringList([ @@ -699,33 +817,32 @@ function buildUnassignedChangeDescription(change) { function buildWorktreeBranchDescription(sessions) { const sessionList = Array.isArray(sessions) ? sessions : []; - const primarySession = sessionList[0] || null; - if (!primarySession) { + if (sessionList.length === 0) { return ''; } - const descriptionParts = [ - `${sessionStatusLabel(primarySession).toLowerCase()}: ${primarySession.agentName || 'agent'}`, - sessionSnapshotDescription(primarySession), - ]; - if (sessionList.length > 1) { - descriptionParts.push(formatCountLabel(sessionList.length, 'agent')); - } - return descriptionParts.filter(Boolean).join(' · '); + const changedCount = sessionList.reduce((total, session) => total + (session.changeCount || 0), 0); + return buildWorktreeDescription(sessionList, changedCount); } function buildOverviewDescription(summary) { return [ - formatCountLabel(summary?.workingCount || 0, 'working agent'), - formatCountLabel(summary?.idleCount || 0, 'idle agent'), - formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'), - formatCountLabel(summary?.lockedFileCount || 0, 'locked file'), - formatCountLabel(summary?.conflictCount || 0, 'conflict'), - ].join(' · '); + formatShortCountLabel(summary?.workingCount || 0, 'working'), + formatShortCountLabel(summary?.idleCount || 0, 'thinking'), + formatShortCountLabel(summary?.unassignedChangeCount || 0, 'unassigned'), + formatShortCountLabel(summary?.lockedFileCount || 0, 'locked'), + formatShortCountLabel(summary?.conflictCount || 0, 'conflict', 'conflicts'), + summary?.deadCount ? formatShortCountLabel(summary.deadCount, 'dead') : '', + ].filter(Boolean).join(' · '); } function buildRepoDescription(summary) { - return buildOverviewDescription(summary); + return [ + formatShortCountLabel(summary?.workingCount || 0, 'working'), + summary?.idleCount ? formatShortCountLabel(summary.idleCount, 'thinking') : '', + summary?.unassignedChangeCount ? formatShortCountLabel(summary.unassignedChangeCount, 'unassigned') : '', + summary?.conflictCount ? formatShortCountLabel(summary.conflictCount, 'conflict', 'conflicts') : '', + ].filter(Boolean).join(' · '); } function buildRepoTooltip(repoRoot, summary) { @@ -735,6 +852,29 @@ function buildRepoTooltip(repoRoot, summary) { ].join('\n'); } +function buildOverviewItems(summary) { + return [ + new DetailItem('Agents', [ + formatShortCountLabel(summary?.workingCount || 0, 'working'), + formatShortCountLabel(summary?.idleCount || 0, 'thinking'), + ].join(' · '), { + iconId: 'git-branch', + }), + new DetailItem('Files', [ + formatShortCountLabel(summary?.unassignedChangeCount || 0, 'unassigned'), + formatShortCountLabel(summary?.lockedFileCount || 0, 'locked'), + ].join(' · '), { + iconId: 'files', + }), + new DetailItem('State', [ + formatShortCountLabel(summary?.conflictCount || 0, 'conflict', 'conflicts'), + summary?.deadCount ? formatShortCountLabel(summary.deadCount, 'dead') : '', + ].filter(Boolean).join(' · '), { + iconId: summary?.conflictCount ? 'warning' : 'pulse', + }), + ]; +} + function sessionSnapshotKey(session) { return `${session?.repoRoot || ''}::${session?.branch || ''}`; } @@ -2353,7 +2493,6 @@ function commitWorktree(worktreePath, message) { } function buildSessionDetailItems(session) { - const provider = resolveSessionProvider(session); const snapshot = sessionSnapshotDisplayName(session); const projectRelativePath = resolveSessionProjectRelativePath(session); const badgeSummary = uniqueStringList([ @@ -2362,12 +2501,7 @@ function buildSessionDetailItems(session) { ].filter(Boolean)).join(', '); const sessionHealthSummary = buildSessionHealthSummary(session); const items = [ - new DetailItem('Recent change', session.recentChangeSummary || 'No recent change summary.', { - iconId: 'history', - }), - new DetailItem('Top files', session.topChangedFilesLabel || 'No tracked file edits.', { - iconId: 'list-flat', - }), + ...buildSessionFileGroupItems(session), ]; if (badgeSummary) { items.push(new DetailItem('Signals', badgeSummary, { @@ -2380,11 +2514,6 @@ function buildSessionDetailItems(session) { tooltip: buildSessionHealthTooltip(session) || sessionHealthSummary, })); } - if (provider?.label) { - items.push(new DetailItem('Provider', provider.label, { - iconId: 'sparkle', - })); - } if (snapshot) { items.push(new DetailItem('Snapshot', snapshot, { iconId: 'account', diff --git a/test/vscode-active-agents-session-state.test.js b/test/vscode-active-agents-session-state.test.js index 5a8add8..7f30ee7 100644 --- a/test/vscode-active-agents-session-state.test.js +++ b/test/vscode-active-agents-session-state.test.js @@ -1644,7 +1644,7 @@ test('active-agents extension groups live sessions under a repo node', async () const provider = registrations.providers[0].provider; const [repoItem] = await provider.getChildren(); assert.equal(repoItem.label, path.basename(tempRoot)); - assert.equal(repoItem.description, '0 working agents · 1 idle agent · 0 unassigned changes · 0 locked files · 0 conflicts'); + assert.equal(repoItem.description, '0 working · 1 thinking'); assert.deepEqual((await provider.getChildren(repoItem)).map((item) => item.label), [ 'Overview', @@ -1654,7 +1654,7 @@ test('active-agents extension groups live sessions under a repo node', async () const overviewSection = await getSectionByLabel(provider, repoItem, 'Overview'); const [summaryItem] = await provider.getChildren(overviewSection); assert.equal(summaryItem.label, 'Summary'); - assert.equal(summaryItem.description, repoItem.description); + assert.equal(summaryItem.description, '0 working · 1 thinking · 0 unassigned · 0 locked · 0 conflicts'); const idleSection = await getSectionByLabel(provider, repoItem, 'Idle / thinking'); assert.equal(idleSection.description, '1'); @@ -1663,7 +1663,7 @@ test('active-agents extension groups live sessions under a repo node', async () assert.equal(worktreeItem, null); assert.equal(sessionItem.label, 'live-task'); assert.equal(sessionItem.session.branch, 'agent/codex/live-task'); - assert.match(sessionItem.description, /^Idle: codex · via OpenAI/); + assert.match(sessionItem.description, /^Idle · /); assert.equal(sessionItem.iconPath.id, 'comment-discussion'); assert.equal(sessionItem.resourceUri.scheme, 'gitguardex-agent'); assert.equal( @@ -1672,7 +1672,7 @@ test('active-agents extension groups live sessions under a repo node', async () ); assert.deepEqual(registrations.treeViews[0].badge, { value: 1, - tooltip: repoItem.description, + tooltip: '0 working · 1 thinking · 0 unassigned · 0 locked · 0 conflicts', }); assert.equal(registrations.treeViews[0].message, undefined); assert.equal( @@ -1732,8 +1732,8 @@ test('active-agents extension shows provider and snapshot identity badges', asyn const idleSection = await getSectionByLabel(provider, repoItem, 'Idle / thinking'); const codexItem = await getSessionByBranch(provider, idleSection, 'agent/codex/provider-task'); const claudeItem = await getSessionByBranch(provider, idleSection, 'agent/claude/provider-task'); - assert.match(codexItem.description, /^Idle: codex · via OpenAI · snapshot nagyviktor@edixa\.com/); - assert.match(claudeItem.description, /^Idle: claude · via Claude/); + assert.match(codexItem.description, /^Idle · /); + assert.match(claudeItem.description, /^Idle · /); const decorationProvider = registrations.decorationProviders[0]; const codexDecoration = decorationProvider.provideFileDecoration(vscode.Uri.parse( @@ -1742,8 +1742,8 @@ test('active-agents extension shows provider and snapshot identity badges', asyn const claudeDecoration = decorationProvider.provideFileDecoration(vscode.Uri.parse( `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/claude/provider-task')}`, )); - assert.equal(codexDecoration.badge, 'N'); - assert.equal(codexDecoration.tooltip, 'Snapshot nagyviktor@edixa.com'); + assert.equal(codexDecoration.badge, 'AI'); + assert.equal(codexDecoration.tooltip, 'OpenAI session via codex'); assert.equal(claudeDecoration.badge, 'CL'); assert.equal(claudeDecoration.tooltip, 'Claude session via claude'); @@ -1965,7 +1965,7 @@ test('active-agents extension shows grouped repo changes beside active agents', const provider = registrations.providers[0].provider; const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.description, '1 working agent · 0 idle agents · 1 unassigned change · 0 locked files · 0 conflicts'); + assert.equal(repoItem.description, '1 working · 1 unassigned'); assert.deepEqual((await provider.getChildren(repoItem)).map((item) => item.label), [ 'Overview', 'Working now', @@ -1981,15 +1981,18 @@ test('active-agents extension shows grouped repo changes beside active agents', assert.equal(worktreeItem, null); assert.equal(sessionItem.label, latestTaskPreview); assert.equal(sessionItem.session.branch, 'agent/codex/live-task'); - assert.match(sessionItem.description, /^Working: codex · via OpenAI · 2 changed files/); + assert.match(sessionItem.description, /^Working · 2 files · /); assert.match(sessionItem.tooltip, /Recent Fix cave hivemind hero layout/); assert.equal(sessionItem.iconPath.id, 'loading~spin'); assert.equal(sessionItem.iconPath.color.id, 'gitDecoration.addedResourceForeground'); const sessionDetails = await provider.getChildren(sessionItem); - assert.equal(sessionDetails.find((item) => item.label === 'Top files')?.description, 'src/nested.js, tracked.txt'); + assert.equal(sessionDetails.some((item) => item.label === 'Top files'), false); + assert.equal(sessionDetails.some((item) => item.label === 'Provider'), false); + assert.ok(sessionDetails.find((item) => item.label === 'Branch')); + assert.ok(sessionDetails.find((item) => item.label === 'Worktree')); assert.deepEqual(registrations.treeViews[0].badge, { value: 1, - tooltip: repoItem.description, + tooltip: '1 working · 0 thinking · 1 unassigned · 0 locked · 0 conflicts', }); const [unassignedChangeItem] = await provider.getChildren(unassignedSection); @@ -2002,7 +2005,7 @@ test('active-agents extension shows grouped repo changes beside active agents', const rawWorkingSection = await getSectionByLabel(provider, activeAgentTree, 'WORKING NOW'); const [rawWorktreeItem] = await provider.getChildren(rawWorkingSection); assert.equal(rawWorktreeItem.label, latestTaskPreview); - assert.equal(rawWorktreeItem.description, 'working: codex'); + assert.equal(rawWorktreeItem.description, 'codex · 2 files'); const [rawSessionItem] = await provider.getChildren(rawWorktreeItem); assert.equal(rawSessionItem.label, latestTaskPreview); assert.match(rawSessionItem.description, /^Working · 2 files · /); @@ -2095,7 +2098,7 @@ test('active-agents extension surfaces live managed worktrees from AGENT.lock fa const provider = registrations.providers[0].provider; const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.description, '1 working agent · 0 idle agents · 0 unassigned changes · 0 locked files · 0 conflicts'); + assert.equal(repoItem.description, '1 working'); assert.deepEqual((await provider.getChildren(repoItem)).map((item) => item.label), [ 'Overview', @@ -2109,7 +2112,7 @@ test('active-agents extension surfaces live managed worktrees from AGENT.lock fa const [sessionItem] = await provider.getChildren(projectFolder); assert.equal(sessionItem.label, 'Implement live worktree telemetry'); assert.equal(sessionItem.session.branch, 'agent/codex/lock-visible-task'); - assert.match(sessionItem.description, /^Working: codex · via OpenAI · snapshot nagyviktor@edixa\.com · 1 changed file/); + assert.match(sessionItem.description, /^Working · 1 file · /); assert.equal(sessionItem.iconPath.color.id, 'gitDecoration.addedResourceForeground'); assert.equal(sessionItem.session.snapshotName, 'nagyviktor@edixa.com'); assert.match(sessionItem.tooltip, /Telemetry updated 2026-04-22T09:01:00.000Z/); @@ -2123,7 +2126,7 @@ test('active-agents extension surfaces live managed worktrees from AGENT.lock fa assert.equal(rawProjectFolder.description, '1 agent · 1 file'); const [rawWorktreeItem] = await provider.getChildren(rawProjectFolder); assert.equal(rawWorktreeItem.label, 'Implement live worktree telemetry'); - assert.equal(rawWorktreeItem.description, 'working: codex · snapshot nagyviktor@edixa.com'); + assert.equal(rawWorktreeItem.description, 'codex · 1 file'); assert.equal( rawWorktreeItem.resourceUri.toString(), `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/codex/lock-visible-task')}`, @@ -2135,8 +2138,8 @@ test('active-agents extension surfaces live managed worktrees from AGENT.lock fa const snapshotDecoration = registrations.decorationProviders[0].provideFileDecoration(vscode.Uri.parse( `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/codex/lock-visible-task')}`, )); - assert.equal(snapshotDecoration.badge, 'N'); - assert.equal(snapshotDecoration.tooltip, 'Snapshot nagyviktor@edixa.com'); + assert.equal(snapshotDecoration.badge, 'AI'); + assert.equal(snapshotDecoration.tooltip, 'OpenAI session via codex'); for (const subscription of context.subscriptions) { subscription.dispose?.(); @@ -2189,7 +2192,7 @@ test('active-agents extension shows session health from active-session records', const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); assert.equal(worktreeItem, null); - assert.match(sessionItem.description, /^Working: codex · via OpenAI · 1 changed file · 45\/100/); + assert.match(sessionItem.description, /^Working · 1 file · /); assert.match(sessionItem.tooltip, /Session health 45\/100 · Inefficient/); const sessionDetails = await provider.getChildren(sessionItem); const sessionHealthItem = sessionDetails.find((item) => item.label === 'Session health'); @@ -2269,7 +2272,7 @@ test('active-agents extension shows session health from AGENT.lock fallback tele const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); assert.equal(worktreeItem, null); - assert.match(sessionItem.description, /^Working: codex · via OpenAI · snapshot snapshot-a · 1 changed file · 45\/100/); + assert.match(sessionItem.description, /^Working · 1 file · /); assert.match(sessionItem.tooltip, /Session health 45\/100 · Inefficient/); const sessionDetails = await provider.getChildren(sessionItem); const sessionHealthItem = sessionDetails.find((item) => item.label === 'Session health'); @@ -2309,13 +2312,13 @@ test('active-agents extension surfaces plain managed worktrees from workspace fa const provider = registrations.providers[0].provider; const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.description, '1 working agent · 0 idle agents · 0 unassigned changes · 0 locked files · 0 conflicts'); + assert.equal(repoItem.description, '1 working'); const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); assert.equal(worktreeItem, null); assert.equal(sessionItem.session.branch, 'agent/codex/plain-visible-task'); - assert.match(sessionItem.description, /^Working: codex · via OpenAI · 1 changed file/); + assert.match(sessionItem.description, /^Working · 1 file · /); assert.match(sessionItem.tooltip, /Started /); for (const subscription of context.subscriptions) { @@ -2387,7 +2390,7 @@ test('active-agents extension decorates sessions and repo changes from the lock const provider = registrations.providers[0].provider; const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.description, '1 working agent · 0 idle agents · 1 unassigned change · 3 locked files · 2 conflicts'); + assert.equal(repoItem.description, '1 working · 1 unassigned · 2 conflicts'); const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); const unassignedSection = await getSectionByLabel(provider, repoItem, 'Unassigned changes'); const advancedSection = await getSectionByLabel(provider, repoItem, 'Advanced details'); @@ -2395,14 +2398,14 @@ test('active-agents extension decorates sessions and repo changes from the lock assert.equal(worktreeItem, null); assert.equal(sessionItem.label, 'live-task'); assert.equal(sessionItem.session.branch, branch); - assert.match(sessionItem.tooltip, /1 lock/); + assert.match(sessionItem.tooltip, /Locked/); assert.match(sessionItem.tooltip, /Conflicts 1/); const activeAgentTree = await getSectionByLabel(provider, advancedSection, 'Active agent tree'); const rawWorkingSection = await getSectionByLabel(provider, activeAgentTree, 'WORKING NOW'); const worktreeGroup = await getChildByLabel(provider, rawWorkingSection, 'live-task'); assert.equal(worktreeGroup.iconPath.id, 'git-branch'); - assert.equal(worktreeGroup.description, 'working: codex'); + assert.equal(worktreeGroup.description, 'codex · 1 file · 1 lock'); assert.equal(worktreeGroup.resourceUri.toString(), `gitguardex-agent://${sessionSchema.sanitizeBranchForFile(branch)}`); const [sessionGroup] = await provider.getChildren(worktreeGroup); assert.equal(sessionGroup.label, 'live-task'); @@ -2418,7 +2421,7 @@ test('active-agents extension decorates sessions and repo changes from the lock assert.match(changeItem.tooltip, /Locked by agent\/codex\/other-task/); assert.deepEqual(registrations.treeViews[0].badge, { value: 1, - tooltip: repoItem.description, + tooltip: '1 working · 0 thinking · 1 unassigned · 3 locked · 2 conflicts', }); assert.equal( registrations.executedCommands.some((entry) => ( @@ -2638,7 +2641,7 @@ test('active-agents extension groups blocked, working, idle, stalled, and dead s const provider = registrations.providers[0].provider; const [repoItem] = await provider.getChildren(); - assert.equal(repoItem.description, '2 working agents · 2 idle agents · 0 unassigned changes · 0 locked files · 0 conflicts'); + assert.equal(repoItem.description, '2 working · 2 thinking'); assert.deepEqual((await provider.getChildren(repoItem)).map((item) => item.label), [ 'Overview', @@ -2656,19 +2659,19 @@ test('active-agents extension groups blocked, working, idle, stalled, and dead s const idleItem = await getSessionByBranch(provider, idleThinkingSection, 'agent/codex/idle-task'); const stalledItem = await getSessionByBranch(provider, idleThinkingSection, 'agent/codex/stalled-task'); const deadItem = await getSessionByBranch(provider, idleThinkingSection, 'agent/codex/dead-task'); - assert.match(blockedItem.description, /^Blocked: codex · via OpenAI/); + assert.match(blockedItem.description, /^Blocked · /); assert.equal(blockedItem.iconPath.id, 'warning'); - assert.match(workingItem.description, /^Working: codex · via OpenAI · 1 changed file/); + assert.match(workingItem.description, /^Working · 1 file · /); assert.equal(workingItem.iconPath.id, 'loading~spin'); - assert.match(idleItem.description, /^Idle: codex · via OpenAI/); + assert.match(idleItem.description, /^Idle · /); assert.equal(idleItem.iconPath.id, 'comment-discussion'); - assert.match(stalledItem.description, /^Stale: codex · via OpenAI/); + assert.match(stalledItem.description, /^Stale · /); assert.equal(stalledItem.iconPath.id, 'clock'); - assert.match(deadItem.description, /^Dead: codex · via OpenAI/); + assert.match(deadItem.description, /^Dead · /); assert.equal(deadItem.iconPath.id, 'error'); assert.deepEqual(registrations.treeViews[0].badge, { value: 5, - tooltip: repoItem.description, + tooltip: '2 working · 2 thinking · 0 unassigned · 0 locked · 0 conflicts · 1 dead', }); for (const subscription of context.subscriptions) { @@ -2904,7 +2907,7 @@ test('active-agents extension decorates sessions and repo changes from the lock assert.equal(worktreeItem, null); assert.equal(sessionItem.label, 'live-task'); assert.equal(sessionItem.session.branch, branch); - assert.match(sessionItem.tooltip, /1 lock/); + assert.match(sessionItem.tooltip, /Locked/); const [changeItem] = await provider.getChildren(unassignedSection); assert.equal(changeItem.label, 'root-file.txt'); diff --git a/vscode/guardex-active-agents/extension.js b/vscode/guardex-active-agents/extension.js index 17b391e..c7322ff 100644 --- a/vscode/guardex-active-agents/extension.js +++ b/vscode/guardex-active-agents/extension.js @@ -72,6 +72,70 @@ const SESSION_PROVIDER_BRANDS = { badge: 'CL', }, }; +const SESSION_FILE_GROUPS = [ + { key: 'code', label: 'Code', iconId: 'symbol-file' }, + { key: 'tests', label: 'Tests', iconId: 'beaker' }, + { key: 'openspec', label: 'OpenSpec', iconId: 'note' }, + { key: 'config', label: 'Config', iconId: 'settings-gear' }, + { key: 'other', label: 'Other', iconId: 'files' }, +]; +const CODE_FILE_EXTENSIONS = new Set([ + '.c', + '.cc', + '.cpp', + '.cs', + '.css', + '.go', + '.h', + '.hpp', + '.html', + '.java', + '.js', + '.jsx', + '.kt', + '.mjs', + '.cjs', + '.mdx', + '.php', + '.py', + '.rb', + '.rs', + '.scss', + '.sh', + '.sql', + '.swift', + '.ts', + '.tsx', + '.vue', +]); +const CONFIG_FILE_NAMES = new Set([ + '.editorconfig', + '.gitignore', + '.npmrc', + '.prettierignore', + '.prettierrc', + 'biome.json', + 'bunfig.toml', + 'dockerfile', + 'eslint.config.js', + 'eslint.config.mjs', + 'jsconfig.json', + 'package-lock.json', + 'package.json', + 'pnpm-lock.yaml', + 'pnpm-workspace.yaml', + 'prettier.config.js', + 'prettier.config.mjs', + 'tailwind.config.js', + 'tailwind.config.cjs', + 'tailwind.config.ts', + 'tsconfig.json', + 'turbo.json', + 'vite.config.js', + 'vite.config.ts', + 'webpack.config.js', + 'yarn.lock', +]); function iconColorId(iconId) { switch (iconId) { @@ -175,6 +239,10 @@ function formatCountLabel(count, singular, plural = `${singular}s`) { return `${count} ${count === 1 ? singular : plural}`; } +function formatShortCountLabel(count, singular, plural = singular) { + return `${count} ${count === 1 ? singular : plural}`; +} + function branchSegments(branch) { return String(branch || '') .split('/') @@ -293,7 +361,7 @@ function sessionSnapshotDecoration(session) { } function sessionIdentityDecoration(session) { - return sessionSnapshotDecoration(session) || sessionProviderDecoration(session); + return sessionProviderDecoration(session) || sessionSnapshotDecoration(session); } function stringListsEqual(left, right) { @@ -445,6 +513,68 @@ function summarizeCompactPaths(paths, maxCount = SESSION_TOP_FILE_COUNT) { return compactPaths.join(', '); } +function sessionChangeGroupKey(change) { + const relativePath = normalizeRelativePath(change?.relativePath).toLowerCase(); + if (!relativePath) { + return 'other'; + } + + const baseName = path.posix.basename(relativePath); + if (relativePath === 'openspec' || relativePath.startsWith('openspec/')) { + return 'openspec'; + } + if (/(^|\/)(__tests__|test|tests)(\/|$)/.test(relativePath) || /\.(test|spec)\.[^.\/]+$/.test(baseName)) { + return 'tests'; + } + if ( + relativePath.startsWith('.github/') + || relativePath.startsWith('.vscode/') + || relativePath.startsWith('.claude/') + || relativePath.startsWith('.codex/') + || relativePath.startsWith('.omx/') + || relativePath.startsWith('.omc/') + || baseName.startsWith('.env') + || CONFIG_FILE_NAMES.has(baseName) + ) { + return 'config'; + } + if ( + relativePath.startsWith('app/') + || relativePath.startsWith('bin/') + || relativePath.startsWith('lib/') + || relativePath.startsWith('scripts/') + || relativePath.startsWith('server/') + || relativePath.startsWith('src/') + || relativePath.startsWith('templates/') + || relativePath.startsWith('vscode/') + || CODE_FILE_EXTENSIONS.has(path.posix.extname(baseName)) + ) { + return 'code'; + } + return 'other'; +} + +function buildSessionFileGroupItems(session) { + const groups = new Map(SESSION_FILE_GROUPS.map(({ key }) => [key, []])); + for (const change of session?.touchedChanges || []) { + const groupKey = sessionChangeGroupKey(change); + groups.get(groupKey)?.push(change); + } + + return SESSION_FILE_GROUPS + .map(({ key, label, iconId }) => { + const groupChanges = groups.get(key) || []; + if (groupChanges.length === 0) { + return null; + } + return new SectionItem(label, buildChangeTreeNodes(groupChanges), { + description: String(groupChanges.length), + iconId, + }); + }) + .filter(Boolean); +} + function isProtectedBranchName(branch) { return branch === 'main' || branch === 'dev'; } @@ -578,6 +708,10 @@ function buildSessionHealthTooltip(session) { ].filter(Boolean).join('\n'); } +function sessionCompactTimeLabel(session) { + return session.lastActiveLabel || session.elapsedLabel || formatElapsedFrom(session.startedAt); +} + function buildSessionTopFiles(session) { return uniqueStringList((session?.worktreeChangedPaths || []) .map(normalizeRelativePath) @@ -617,48 +751,32 @@ function changeRiskBadges(change) { ].filter(Boolean)); } -function buildSessionCardDescription(session) { - const provider = resolveSessionProvider(session); - const statusAgentLabel = `${sessionStatusLabel(session)}: ${session.agentName || 'agent'}`; - const descriptionParts = [ - statusAgentLabel, - provider?.label ? `via ${provider.label}` : '', - sessionSnapshotDescription(session), - session.deltaLabel || '', - session.changeCount > 0 ? formatCountLabel(session.changeCount, 'changed file') : '', - session.lockCount > 0 ? formatCountLabel(session.lockCount, 'lock') : '', - buildSessionHealthCompactLabel(session), - session.freshnessLabel || '', - session.lastActiveLabel ? `${session.lastActiveLabel} ago` : '', - ].filter(Boolean); - return descriptionParts.join(' · '); -} - -function buildRawSessionDescription(session) { - const provider = resolveSessionProvider(session); +function buildSessionCompactDescription(session) { const descriptionParts = [sessionStatusLabel(session)]; const fileCountLabel = sessionFileCountLabel(session); if (fileCountLabel) { descriptionParts.push(fileCountLabel); } - if (provider?.label) { - descriptionParts.push(provider.label); - } - const snapshot = sessionSnapshotDescription(session); - if (snapshot) { - descriptionParts.push(snapshot); - } - descriptionParts.push(session.elapsedLabel || formatElapsedFrom(session.startedAt)); - const sessionHealthLabel = buildSessionHealthCompactLabel(session); - if (sessionHealthLabel) { - descriptionParts.push(sessionHealthLabel); + const timeLabel = sessionCompactTimeLabel(session); + if (timeLabel) { + descriptionParts.push(timeLabel); } - if (session.lockCount > 0) { + if (session.conflictCount > 0) { + descriptionParts.push(formatCountLabel(session.conflictCount, 'conflict')); + } else if (session.lockCount > 0) { descriptionParts.push(formatCountLabel(session.lockCount, 'lock')); } return descriptionParts.join(' · '); } +function buildSessionCardDescription(session) { + return buildSessionCompactDescription(session); +} + +function buildRawSessionDescription(session) { + return buildSessionCompactDescription(session); +} + function buildSessionTooltip(session, description) { const provider = resolveSessionProvider(session); const riskSummary = uniqueStringList([ @@ -699,33 +817,32 @@ function buildUnassignedChangeDescription(change) { function buildWorktreeBranchDescription(sessions) { const sessionList = Array.isArray(sessions) ? sessions : []; - const primarySession = sessionList[0] || null; - if (!primarySession) { + if (sessionList.length === 0) { return ''; } - const descriptionParts = [ - `${sessionStatusLabel(primarySession).toLowerCase()}: ${primarySession.agentName || 'agent'}`, - sessionSnapshotDescription(primarySession), - ]; - if (sessionList.length > 1) { - descriptionParts.push(formatCountLabel(sessionList.length, 'agent')); - } - return descriptionParts.filter(Boolean).join(' · '); + const changedCount = sessionList.reduce((total, session) => total + (session.changeCount || 0), 0); + return buildWorktreeDescription(sessionList, changedCount); } function buildOverviewDescription(summary) { return [ - formatCountLabel(summary?.workingCount || 0, 'working agent'), - formatCountLabel(summary?.idleCount || 0, 'idle agent'), - formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'), - formatCountLabel(summary?.lockedFileCount || 0, 'locked file'), - formatCountLabel(summary?.conflictCount || 0, 'conflict'), - ].join(' · '); + formatShortCountLabel(summary?.workingCount || 0, 'working'), + formatShortCountLabel(summary?.idleCount || 0, 'thinking'), + formatShortCountLabel(summary?.unassignedChangeCount || 0, 'unassigned'), + formatShortCountLabel(summary?.lockedFileCount || 0, 'locked'), + formatShortCountLabel(summary?.conflictCount || 0, 'conflict', 'conflicts'), + summary?.deadCount ? formatShortCountLabel(summary.deadCount, 'dead') : '', + ].filter(Boolean).join(' · '); } function buildRepoDescription(summary) { - return buildOverviewDescription(summary); + return [ + formatShortCountLabel(summary?.workingCount || 0, 'working'), + summary?.idleCount ? formatShortCountLabel(summary.idleCount, 'thinking') : '', + summary?.unassignedChangeCount ? formatShortCountLabel(summary.unassignedChangeCount, 'unassigned') : '', + summary?.conflictCount ? formatShortCountLabel(summary.conflictCount, 'conflict', 'conflicts') : '', + ].filter(Boolean).join(' · '); } function buildRepoTooltip(repoRoot, summary) { @@ -735,6 +852,29 @@ function buildRepoTooltip(repoRoot, summary) { ].join('\n'); } +function buildOverviewItems(summary) { + return [ + new DetailItem('Agents', [ + formatShortCountLabel(summary?.workingCount || 0, 'working'), + formatShortCountLabel(summary?.idleCount || 0, 'thinking'), + ].join(' · '), { + iconId: 'git-branch', + }), + new DetailItem('Files', [ + formatShortCountLabel(summary?.unassignedChangeCount || 0, 'unassigned'), + formatShortCountLabel(summary?.lockedFileCount || 0, 'locked'), + ].join(' · '), { + iconId: 'files', + }), + new DetailItem('State', [ + formatShortCountLabel(summary?.conflictCount || 0, 'conflict', 'conflicts'), + summary?.deadCount ? formatShortCountLabel(summary.deadCount, 'dead') : '', + ].filter(Boolean).join(' · '), { + iconId: summary?.conflictCount ? 'warning' : 'pulse', + }), + ]; +} + function sessionSnapshotKey(session) { return `${session?.repoRoot || ''}::${session?.branch || ''}`; } @@ -2353,7 +2493,6 @@ function commitWorktree(worktreePath, message) { } function buildSessionDetailItems(session) { - const provider = resolveSessionProvider(session); const snapshot = sessionSnapshotDisplayName(session); const projectRelativePath = resolveSessionProjectRelativePath(session); const badgeSummary = uniqueStringList([ @@ -2362,12 +2501,7 @@ function buildSessionDetailItems(session) { ].filter(Boolean)).join(', '); const sessionHealthSummary = buildSessionHealthSummary(session); const items = [ - new DetailItem('Recent change', session.recentChangeSummary || 'No recent change summary.', { - iconId: 'history', - }), - new DetailItem('Top files', session.topChangedFilesLabel || 'No tracked file edits.', { - iconId: 'list-flat', - }), + ...buildSessionFileGroupItems(session), ]; if (badgeSummary) { items.push(new DetailItem('Signals', badgeSummary, { @@ -2380,11 +2514,6 @@ function buildSessionDetailItems(session) { tooltip: buildSessionHealthTooltip(session) || sessionHealthSummary, })); } - if (provider?.label) { - items.push(new DetailItem('Provider', provider.label, { - iconId: 'sparkle', - })); - } if (snapshot) { items.push(new DetailItem('Snapshot', snapshot, { iconId: 'account', From aecf6820dd6b9d00a840ef5d15d5da10156881a5 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Thu, 23 Apr 2026 16:22:10 +0200 Subject: [PATCH 2/2] Make blocked Active Agents rows easier to scan in VS Code The tree now surfaces overview metrics as chip-like items, compresses session rows into stronger summaries, and groups drill-down metadata under Status, Context, and Location so blocked sessions stop reading like flat property sheets. Constraint: VS Code tree views cannot render custom card backgrounds or true summary bars, so emphasis must come from badges, icon colors, grouped sections, and compact text Rejected: Keep the flat detail list and single summary sentence | still fragmented and weak for blocked sessions Confidence: high Scope-risk: moderate Directive: Keep card details inside the three grouped sections; move extra telemetry into those sections instead of reintroducing new top-level labels Tested: node --test test/vscode-active-agents-session-state.test.js Not-tested: Manual VS Code rendering against a live blocked rebase session --- .../vscode/guardex-active-agents/extension.js | 226 ++++++++++++++---- .../vscode/guardex-active-agents/package.json | 2 +- ...vscode-active-agents-session-state.test.js | 56 +++-- vscode/guardex-active-agents/extension.js | 226 ++++++++++++++---- vscode/guardex-active-agents/package.json | 2 +- 5 files changed, 388 insertions(+), 124 deletions(-) diff --git a/templates/vscode/guardex-active-agents/extension.js b/templates/vscode/guardex-active-agents/extension.js index c7322ff..cf030c5 100644 --- a/templates/vscode/guardex-active-agents/extension.js +++ b/templates/vscode/guardex-active-agents/extension.js @@ -189,8 +189,8 @@ function sessionIdleDecoration(session, now = Date.now()) { if (session.activityKind === 'blocked') { return { badge: '!', - tooltip: 'blocked', - color: new vscode.ThemeColor('list.warningForeground'), + tooltip: 'Blocked: needs attention', + color: new vscode.ThemeColor('list.errorForeground'), }; } if (session.activityKind === 'dead') { @@ -286,6 +286,12 @@ function uniqueStringList(values) { return result; } +function trimSummaryLabel(value) { + return typeof value === 'string' + ? value.trim().replace(/\.+$/, '') + : ''; +} + function normalizeSessionProviderToken(value) { return typeof value === 'string' ? value.trim().toLowerCase() : ''; } @@ -712,6 +718,10 @@ function sessionCompactTimeLabel(session) { return session.lastActiveLabel || session.elapsedLabel || formatElapsedFrom(session.startedAt); } +function sessionProviderLabel(session) { + return resolveSessionProvider(session)?.label || ''; +} + function buildSessionTopFiles(session) { return uniqueStringList((session?.worktreeChangedPaths || []) .map(normalizeRelativePath) @@ -742,6 +752,57 @@ function sessionRiskBadges(session) { ].filter(Boolean)); } +function buildSessionCauseSummary(session) { + const candidates = session?.activityKind === 'blocked' + ? [session?.activitySummary, session?.recentChangeSummary] + : [session?.recentChangeSummary, session?.activitySummary]; + return uniqueStringList(candidates.map(trimSummaryLabel).filter(Boolean)) + .find((value) => ( + value + && value !== trimSummaryLabel(sessionStatusLabel(session)) + && value !== trimSummaryLabel(sessionFreshnessLabel(session)) + )) || ''; +} + +function buildSessionStatusFacts(session) { + return uniqueStringList([ + sessionStatusLabel(session), + buildSessionCauseSummary(session), + sessionFreshnessLabel(session), + ].map(trimSummaryLabel).filter(Boolean)); +} + +function buildSessionStatusDescription(session) { + return buildSessionStatusFacts(session).join(' · '); +} + +function buildSessionContextFacts(session) { + const contextParts = []; + const providerLabel = sessionProviderLabel(session); + if (providerLabel) { + contextParts.push(providerLabel); + } + if (session.conflictCount > 0) { + contextParts.push(formatCountLabel(session.conflictCount, 'conflict')); + } else if (session.lockCount > 0) { + contextParts.push(formatCountLabel(session.lockCount, 'lock')); + } else { + const fileCountLabel = sessionFileCountLabel(session); + if (fileCountLabel) { + contextParts.push(fileCountLabel); + } + } + const timeLabel = sessionCompactTimeLabel(session); + if (timeLabel) { + contextParts.push(timeLabel); + } + return uniqueStringList(contextParts.filter(Boolean)); +} + +function buildSessionContextDescription(session) { + return buildSessionContextFacts(session).join(' · '); +} + function changeRiskBadges(change) { return uniqueStringList([ change?.protectedBranch ? 'Protected branch' : '', @@ -752,7 +813,15 @@ function changeRiskBadges(change) { } function buildSessionCompactDescription(session) { + if (session.activityKind === 'blocked') { + return buildSessionStatusDescription(session); + } + const descriptionParts = [sessionStatusLabel(session)]; + const providerLabel = sessionProviderLabel(session); + if (providerLabel) { + descriptionParts.push(providerLabel); + } const fileCountLabel = sessionFileCountLabel(session); if (fileCountLabel) { descriptionParts.push(fileCountLabel); @@ -853,26 +922,41 @@ function buildRepoTooltip(repoRoot, summary) { } function buildOverviewItems(summary) { - return [ - new DetailItem('Agents', [ - formatShortCountLabel(summary?.workingCount || 0, 'working'), - formatShortCountLabel(summary?.idleCount || 0, 'thinking'), - ].join(' · '), { - iconId: 'git-branch', + const items = [ + new DetailItem(`Working ${summary?.workingCount || 0}`, '', { + iconId: 'loading~spin', + tooltip: formatCountLabel(summary?.workingCount || 0, 'working agent'), }), - new DetailItem('Files', [ - formatShortCountLabel(summary?.unassignedChangeCount || 0, 'unassigned'), - formatShortCountLabel(summary?.lockedFileCount || 0, 'locked'), - ].join(' · '), { - iconId: 'files', + new DetailItem(`Idle ${summary?.idleCount || 0}`, '', { + iconId: 'comment-discussion', + tooltip: formatCountLabel(summary?.idleCount || 0, 'idle agent'), }), - new DetailItem('State', [ - formatShortCountLabel(summary?.conflictCount || 0, 'conflict', 'conflicts'), - summary?.deadCount ? formatShortCountLabel(summary.deadCount, 'dead') : '', - ].filter(Boolean).join(' · '), { - iconId: summary?.conflictCount ? 'warning' : 'pulse', + new DetailItem(`Locked ${summary?.lockedFileCount || 0}`, '', { + iconId: 'lock', + iconColorId: (summary?.lockedFileCount || 0) > 0 + ? 'gitDecoration.modifiedResourceForeground' + : 'descriptionForeground', + tooltip: formatCountLabel(summary?.lockedFileCount || 0, 'locked file'), + }), + new DetailItem(`Conflicts ${summary?.conflictCount || 0}`, '', { + iconId: 'warning', + iconColorId: (summary?.conflictCount || 0) > 0 + ? 'list.errorForeground' + : 'descriptionForeground', + tooltip: formatCountLabel(summary?.conflictCount || 0, 'conflict', 'conflicts'), + }), + new DetailItem(`Unassigned ${summary?.unassignedChangeCount || 0}`, '', { + iconId: 'files', + tooltip: formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'), }), ]; + if (summary?.deadCount) { + items.push(new DetailItem(`Dead ${summary.deadCount}`, '', { + iconId: 'error', + tooltip: formatCountLabel(summary.deadCount, 'dead agent'), + })); + } + return items; } function sessionSnapshotKey(session) { @@ -1255,8 +1339,9 @@ class SectionItem extends vscode.TreeItem { : vscode.TreeItemCollapsibleState.None; super(label, collapsibleState); this.items = items; - this.description = options.description - || (items.length > 0 ? String(items.length) : ''); + this.description = Object.prototype.hasOwnProperty.call(options, 'description') + ? options.description + : (items.length > 0 ? String(items.length) : ''); this.tooltip = options.tooltip || [label, this.description].filter(Boolean).join('\n'); this.iconPath = options.iconId ? themeIcon(options.iconId, options.iconColorId) : undefined; this.contextValue = 'gitguardex.section'; @@ -1325,7 +1410,12 @@ class SessionItem extends vscode.TreeItem { ? buildRawSessionDescription(session) : buildSessionCardDescription(session); this.tooltip = buildSessionTooltip(session, this.description); - this.iconPath = themeIcon(resolveSessionActivityIconId(session.activityKind)); + this.iconPath = themeIcon( + resolveSessionActivityIconId(session.activityKind), + session.activityKind === 'blocked' + ? 'list.errorForeground' + : undefined, + ); this.contextValue = 'gitguardex.session'; this.command = { command: 'gitguardex.activeAgents.openWorktree', @@ -2493,6 +2583,8 @@ function commitWorktree(worktreePath, message) { } function buildSessionDetailItems(session) { + const statusFacts = buildSessionStatusFacts(session); + const contextFacts = buildSessionContextFacts(session); const snapshot = sessionSnapshotDisplayName(session); const projectRelativePath = resolveSessionProjectRelativePath(session); const badgeSummary = uniqueStringList([ @@ -2500,38 +2592,72 @@ function buildSessionDetailItems(session) { session.deltaLabel || '', ].filter(Boolean)).join(', '); const sessionHealthSummary = buildSessionHealthSummary(session); - const items = [ - ...buildSessionFileGroupItems(session), - ]; - if (badgeSummary) { - items.push(new DetailItem('Signals', badgeSummary, { + const locationItems = [ + new DetailItem('Branch', session.branch, { + iconId: 'git-branch', + tooltip: session.branch, + }), + new DetailItem('Worktree', session.worktreePath, { + iconId: 'folder', + tooltip: session.worktreePath, + }), + ].filter((item) => Boolean(item.description)); + const items = []; + + if (statusFacts.length > 0) { + const statusItems = statusFacts.map((fact) => new DetailItem(fact)); + if (badgeSummary) { + statusItems.push(new DetailItem('Signals', badgeSummary, { + iconId: 'warning', + })); + } + items.push(new SectionItem('Status', statusItems, { + description: statusFacts.join(' · '), iconId: 'warning', + iconColorId: session.activityKind === 'blocked' + ? 'list.errorForeground' + : 'list.warningForeground', + collapsedState: vscode.TreeItemCollapsibleState.Collapsed, + tooltip: statusFacts.join('\n'), })); } - if (sessionHealthSummary) { - items.push(new DetailItem('Session health', sessionHealthSummary, { - iconId: 'pulse', - tooltip: buildSessionHealthTooltip(session) || sessionHealthSummary, - })); - } - if (snapshot) { - items.push(new DetailItem('Snapshot', snapshot, { + + if (contextFacts.length > 0) { + const contextItems = contextFacts.map((fact) => new DetailItem(fact)); + if (sessionHealthSummary) { + contextItems.push(new DetailItem('Session health', sessionHealthSummary, { + iconId: 'pulse', + tooltip: buildSessionHealthTooltip(session) || sessionHealthSummary, + })); + } + if (snapshot) { + contextItems.push(new DetailItem('Snapshot', snapshot, { + iconId: 'account', + })); + } + if (projectRelativePath) { + contextItems.push(new DetailItem('Project', projectRelativePath, { + iconId: 'folder', + tooltip: projectRelativePath, + })); + } + items.push(new SectionItem('Context', contextItems, { + description: contextFacts.join(' · '), iconId: 'account', + collapsedState: vscode.TreeItemCollapsibleState.Collapsed, + tooltip: contextFacts.join('\n'), })); } - if (projectRelativePath) { - items.push(new DetailItem('Project', projectRelativePath, { + + if (locationItems.length > 0) { + items.push(new SectionItem('Location', locationItems, { + description: compactBranchLabel(session.branch) || session.branch || '', iconId: 'folder', - tooltip: projectRelativePath, + collapsedState: vscode.TreeItemCollapsibleState.Collapsed, + tooltip: [session.branch, session.worktreePath].filter(Boolean).join('\n'), })); } - items.push(new DetailItem('Branch', session.branch, { - iconId: 'git-branch', - })); - items.push(new DetailItem('Worktree', session.worktreePath, { - iconId: 'folder', - tooltip: session.worktreePath, - })); + return items; } @@ -2802,14 +2928,12 @@ class ActiveAgentsProvider { async getChildren(element) { if (element instanceof RepoItem) { + const overviewItems = buildOverviewItems(element.overview); const sectionItems = [ - new SectionItem('Overview', [ - new DetailItem('Summary', buildOverviewDescription(element.overview), { - iconId: 'graph', - tooltip: buildRepoTooltip(element.repoRoot, element.overview), - }), - ], { - description: '1', + new SectionItem('Overview', overviewItems, { + description: '', + iconId: 'graph', + tooltip: buildRepoTooltip(element.repoRoot, element.overview), }), ]; diff --git a/templates/vscode/guardex-active-agents/package.json b/templates/vscode/guardex-active-agents/package.json index 1758ff0..ac121bd 100644 --- a/templates/vscode/guardex-active-agents/package.json +++ b/templates/vscode/guardex-active-agents/package.json @@ -3,7 +3,7 @@ "displayName": "GitGuardex Active Agents", "description": "Shows live Guardex sandbox sessions and repo changes in a dedicated VS Code Active Agents sidebar.", "publisher": "recodeee", - "version": "0.0.13", + "version": "0.0.16", "license": "MIT", "icon": "icon.png", "engines": { diff --git a/test/vscode-active-agents-session-state.test.js b/test/vscode-active-agents-session-state.test.js index 7f30ee7..067eff1 100644 --- a/test/vscode-active-agents-session-state.test.js +++ b/test/vscode-active-agents-session-state.test.js @@ -1652,9 +1652,13 @@ test('active-agents extension groups live sessions under a repo node', async () 'Advanced details', ]); const overviewSection = await getSectionByLabel(provider, repoItem, 'Overview'); - const [summaryItem] = await provider.getChildren(overviewSection); - assert.equal(summaryItem.label, 'Summary'); - assert.equal(summaryItem.description, '0 working · 1 thinking · 0 unassigned · 0 locked · 0 conflicts'); + assert.deepEqual((await provider.getChildren(overviewSection)).map((item) => item.label), [ + 'Working 0', + 'Idle 1', + 'Locked 0', + 'Conflicts 0', + 'Unassigned 0', + ]); const idleSection = await getSectionByLabel(provider, repoItem, 'Idle / thinking'); assert.equal(idleSection.description, '1'); @@ -1981,15 +1985,22 @@ test('active-agents extension shows grouped repo changes beside active agents', assert.equal(worktreeItem, null); assert.equal(sessionItem.label, latestTaskPreview); assert.equal(sessionItem.session.branch, 'agent/codex/live-task'); - assert.match(sessionItem.description, /^Working · 2 files · /); + assert.match(sessionItem.description, /^Working · OpenAI · 2 files · /); assert.match(sessionItem.tooltip, /Recent Fix cave hivemind hero layout/); assert.equal(sessionItem.iconPath.id, 'loading~spin'); assert.equal(sessionItem.iconPath.color.id, 'gitDecoration.addedResourceForeground'); const sessionDetails = await provider.getChildren(sessionItem); - assert.equal(sessionDetails.some((item) => item.label === 'Top files'), false); - assert.equal(sessionDetails.some((item) => item.label === 'Provider'), false); - assert.ok(sessionDetails.find((item) => item.label === 'Branch')); - assert.ok(sessionDetails.find((item) => item.label === 'Worktree')); + assert.deepEqual(sessionDetails.map((item) => item.label), [ + 'Status', + 'Context', + 'Location', + ]); + const statusSection = await getSectionByLabel(provider, sessionItem, 'Status'); + const contextSection = await getSectionByLabel(provider, sessionItem, 'Context'); + const locationSection = await getSectionByLabel(provider, sessionItem, 'Location'); + assert.match(statusSection.description, /^Working · Fix cave hivemind hero layout · /); + assert.match(contextSection.description, /^OpenAI · 2 files · /); + assert.equal(locationSection.description, 'codex/live-task'); assert.deepEqual(registrations.treeViews[0].badge, { value: 1, tooltip: '1 working · 0 thinking · 1 unassigned · 0 locked · 0 conflicts', @@ -2008,7 +2019,7 @@ test('active-agents extension shows grouped repo changes beside active agents', assert.equal(rawWorktreeItem.description, 'codex · 2 files'); const [rawSessionItem] = await provider.getChildren(rawWorktreeItem); assert.equal(rawSessionItem.label, latestTaskPreview); - assert.match(rawSessionItem.description, /^Working · 2 files · /); + assert.match(rawSessionItem.description, /^Working · OpenAI · 2 files · /); const rawPathTree = await getSectionByLabel(provider, advancedSection, 'Raw path tree'); const [worktreeGroup, repoRootGroup] = await provider.getChildren(rawPathTree); @@ -2018,7 +2029,7 @@ test('active-agents extension shows grouped repo changes beside active agents', const [sessionGroup] = await provider.getChildren(worktreeGroup); assert.equal(sessionGroup.label, latestTaskPreview); - assert.match(sessionGroup.description, /^Working · 2 files · /); + assert.match(sessionGroup.description, /^Working · OpenAI · 2 files · /); const [folderItem, trackedItem] = await provider.getChildren(sessionGroup); assert.equal(folderItem.label, 'src'); assert.equal(trackedItem.label, 'tracked.txt'); @@ -2112,7 +2123,7 @@ test('active-agents extension surfaces live managed worktrees from AGENT.lock fa const [sessionItem] = await provider.getChildren(projectFolder); assert.equal(sessionItem.label, 'Implement live worktree telemetry'); assert.equal(sessionItem.session.branch, 'agent/codex/lock-visible-task'); - assert.match(sessionItem.description, /^Working · 1 file · /); + assert.match(sessionItem.description, /^Working · OpenAI · 1 file · /); assert.equal(sessionItem.iconPath.color.id, 'gitDecoration.addedResourceForeground'); assert.equal(sessionItem.session.snapshotName, 'nagyviktor@edixa.com'); assert.match(sessionItem.tooltip, /Telemetry updated 2026-04-22T09:01:00.000Z/); @@ -2133,7 +2144,7 @@ test('active-agents extension surfaces live managed worktrees from AGENT.lock fa ); const [rawSessionItem] = await provider.getChildren(rawWorktreeItem); assert.equal(rawSessionItem.label, 'Implement live worktree telemetry'); - assert.match(rawSessionItem.description, /^Working · 1 file · /); + assert.match(rawSessionItem.description, /^Working · OpenAI · 1 file · /); const snapshotDecoration = registrations.decorationProviders[0].provideFileDecoration(vscode.Uri.parse( `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/codex/lock-visible-task')}`, @@ -2192,10 +2203,12 @@ test('active-agents extension shows session health from active-session records', const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); assert.equal(worktreeItem, null); - assert.match(sessionItem.description, /^Working · 1 file · /); + assert.match(sessionItem.description, /^Working · OpenAI · 1 file · /); assert.match(sessionItem.tooltip, /Session health 45\/100 · Inefficient/); const sessionDetails = await provider.getChildren(sessionItem); - const sessionHealthItem = sessionDetails.find((item) => item.label === 'Session health'); + const contextSection = await getSectionByLabel(provider, sessionItem, 'Context'); + const contextDetails = await provider.getChildren(contextSection); + const sessionHealthItem = contextDetails.find((item) => item.label === 'Session health'); assert.equal(sessionHealthItem?.description, '45/100 · Inefficient'); assert.match(sessionHealthItem?.tooltip || '', /Score 45\/100 — Inefficient\./); @@ -2272,10 +2285,12 @@ test('active-agents extension shows session health from AGENT.lock fallback tele const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); assert.equal(worktreeItem, null); - assert.match(sessionItem.description, /^Working · 1 file · /); + assert.match(sessionItem.description, /^Working · OpenAI · 1 file · /); assert.match(sessionItem.tooltip, /Session health 45\/100 · Inefficient/); const sessionDetails = await provider.getChildren(sessionItem); - const sessionHealthItem = sessionDetails.find((item) => item.label === 'Session health'); + const contextSection = await getSectionByLabel(provider, sessionItem, 'Context'); + const contextDetails = await provider.getChildren(contextSection); + const sessionHealthItem = contextDetails.find((item) => item.label === 'Session health'); assert.equal(sessionHealthItem?.description, '45/100 · Inefficient'); assert.match(sessionHealthItem?.tooltip || '', /Score 45\/100 — Inefficient\./); @@ -2318,7 +2333,7 @@ test('active-agents extension surfaces plain managed worktrees from workspace fa const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); assert.equal(worktreeItem, null); assert.equal(sessionItem.session.branch, 'agent/codex/plain-visible-task'); - assert.match(sessionItem.description, /^Working · 1 file · /); + assert.match(sessionItem.description, /^Working · OpenAI · 1 file · /); assert.match(sessionItem.tooltip, /Started /); for (const subscription of context.subscriptions) { @@ -2409,7 +2424,7 @@ test('active-agents extension decorates sessions and repo changes from the lock assert.equal(worktreeGroup.resourceUri.toString(), `gitguardex-agent://${sessionSchema.sanitizeBranchForFile(branch)}`); const [sessionGroup] = await provider.getChildren(worktreeGroup); assert.equal(sessionGroup.label, 'live-task'); - assert.match(sessionGroup.description, /^Working · 1 file · /); + assert.match(sessionGroup.description, /^Working · OpenAI · 1 file · /); const [sessionChangeItem] = await provider.getChildren(sessionGroup); assert.equal(sessionChangeItem.label, 'tracked.txt'); assert.equal(sessionChangeItem.iconPath.id, 'warning'); @@ -2659,9 +2674,10 @@ test('active-agents extension groups blocked, working, idle, stalled, and dead s const idleItem = await getSessionByBranch(provider, idleThinkingSection, 'agent/codex/idle-task'); const stalledItem = await getSessionByBranch(provider, idleThinkingSection, 'agent/codex/stalled-task'); const deadItem = await getSessionByBranch(provider, idleThinkingSection, 'agent/codex/dead-task'); - assert.match(blockedItem.description, /^Blocked · /); + assert.equal(blockedItem.description, 'Blocked · Merge in progress · Needs attention'); assert.equal(blockedItem.iconPath.id, 'warning'); - assert.match(workingItem.description, /^Working · 1 file · /); + assert.equal(blockedItem.iconPath.color.id, 'list.errorForeground'); + assert.match(workingItem.description, /^Working · OpenAI · 1 file · /); assert.equal(workingItem.iconPath.id, 'loading~spin'); assert.match(idleItem.description, /^Idle · /); assert.equal(idleItem.iconPath.id, 'comment-discussion'); diff --git a/vscode/guardex-active-agents/extension.js b/vscode/guardex-active-agents/extension.js index c7322ff..cf030c5 100644 --- a/vscode/guardex-active-agents/extension.js +++ b/vscode/guardex-active-agents/extension.js @@ -189,8 +189,8 @@ function sessionIdleDecoration(session, now = Date.now()) { if (session.activityKind === 'blocked') { return { badge: '!', - tooltip: 'blocked', - color: new vscode.ThemeColor('list.warningForeground'), + tooltip: 'Blocked: needs attention', + color: new vscode.ThemeColor('list.errorForeground'), }; } if (session.activityKind === 'dead') { @@ -286,6 +286,12 @@ function uniqueStringList(values) { return result; } +function trimSummaryLabel(value) { + return typeof value === 'string' + ? value.trim().replace(/\.+$/, '') + : ''; +} + function normalizeSessionProviderToken(value) { return typeof value === 'string' ? value.trim().toLowerCase() : ''; } @@ -712,6 +718,10 @@ function sessionCompactTimeLabel(session) { return session.lastActiveLabel || session.elapsedLabel || formatElapsedFrom(session.startedAt); } +function sessionProviderLabel(session) { + return resolveSessionProvider(session)?.label || ''; +} + function buildSessionTopFiles(session) { return uniqueStringList((session?.worktreeChangedPaths || []) .map(normalizeRelativePath) @@ -742,6 +752,57 @@ function sessionRiskBadges(session) { ].filter(Boolean)); } +function buildSessionCauseSummary(session) { + const candidates = session?.activityKind === 'blocked' + ? [session?.activitySummary, session?.recentChangeSummary] + : [session?.recentChangeSummary, session?.activitySummary]; + return uniqueStringList(candidates.map(trimSummaryLabel).filter(Boolean)) + .find((value) => ( + value + && value !== trimSummaryLabel(sessionStatusLabel(session)) + && value !== trimSummaryLabel(sessionFreshnessLabel(session)) + )) || ''; +} + +function buildSessionStatusFacts(session) { + return uniqueStringList([ + sessionStatusLabel(session), + buildSessionCauseSummary(session), + sessionFreshnessLabel(session), + ].map(trimSummaryLabel).filter(Boolean)); +} + +function buildSessionStatusDescription(session) { + return buildSessionStatusFacts(session).join(' · '); +} + +function buildSessionContextFacts(session) { + const contextParts = []; + const providerLabel = sessionProviderLabel(session); + if (providerLabel) { + contextParts.push(providerLabel); + } + if (session.conflictCount > 0) { + contextParts.push(formatCountLabel(session.conflictCount, 'conflict')); + } else if (session.lockCount > 0) { + contextParts.push(formatCountLabel(session.lockCount, 'lock')); + } else { + const fileCountLabel = sessionFileCountLabel(session); + if (fileCountLabel) { + contextParts.push(fileCountLabel); + } + } + const timeLabel = sessionCompactTimeLabel(session); + if (timeLabel) { + contextParts.push(timeLabel); + } + return uniqueStringList(contextParts.filter(Boolean)); +} + +function buildSessionContextDescription(session) { + return buildSessionContextFacts(session).join(' · '); +} + function changeRiskBadges(change) { return uniqueStringList([ change?.protectedBranch ? 'Protected branch' : '', @@ -752,7 +813,15 @@ function changeRiskBadges(change) { } function buildSessionCompactDescription(session) { + if (session.activityKind === 'blocked') { + return buildSessionStatusDescription(session); + } + const descriptionParts = [sessionStatusLabel(session)]; + const providerLabel = sessionProviderLabel(session); + if (providerLabel) { + descriptionParts.push(providerLabel); + } const fileCountLabel = sessionFileCountLabel(session); if (fileCountLabel) { descriptionParts.push(fileCountLabel); @@ -853,26 +922,41 @@ function buildRepoTooltip(repoRoot, summary) { } function buildOverviewItems(summary) { - return [ - new DetailItem('Agents', [ - formatShortCountLabel(summary?.workingCount || 0, 'working'), - formatShortCountLabel(summary?.idleCount || 0, 'thinking'), - ].join(' · '), { - iconId: 'git-branch', + const items = [ + new DetailItem(`Working ${summary?.workingCount || 0}`, '', { + iconId: 'loading~spin', + tooltip: formatCountLabel(summary?.workingCount || 0, 'working agent'), }), - new DetailItem('Files', [ - formatShortCountLabel(summary?.unassignedChangeCount || 0, 'unassigned'), - formatShortCountLabel(summary?.lockedFileCount || 0, 'locked'), - ].join(' · '), { - iconId: 'files', + new DetailItem(`Idle ${summary?.idleCount || 0}`, '', { + iconId: 'comment-discussion', + tooltip: formatCountLabel(summary?.idleCount || 0, 'idle agent'), }), - new DetailItem('State', [ - formatShortCountLabel(summary?.conflictCount || 0, 'conflict', 'conflicts'), - summary?.deadCount ? formatShortCountLabel(summary.deadCount, 'dead') : '', - ].filter(Boolean).join(' · '), { - iconId: summary?.conflictCount ? 'warning' : 'pulse', + new DetailItem(`Locked ${summary?.lockedFileCount || 0}`, '', { + iconId: 'lock', + iconColorId: (summary?.lockedFileCount || 0) > 0 + ? 'gitDecoration.modifiedResourceForeground' + : 'descriptionForeground', + tooltip: formatCountLabel(summary?.lockedFileCount || 0, 'locked file'), + }), + new DetailItem(`Conflicts ${summary?.conflictCount || 0}`, '', { + iconId: 'warning', + iconColorId: (summary?.conflictCount || 0) > 0 + ? 'list.errorForeground' + : 'descriptionForeground', + tooltip: formatCountLabel(summary?.conflictCount || 0, 'conflict', 'conflicts'), + }), + new DetailItem(`Unassigned ${summary?.unassignedChangeCount || 0}`, '', { + iconId: 'files', + tooltip: formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'), }), ]; + if (summary?.deadCount) { + items.push(new DetailItem(`Dead ${summary.deadCount}`, '', { + iconId: 'error', + tooltip: formatCountLabel(summary.deadCount, 'dead agent'), + })); + } + return items; } function sessionSnapshotKey(session) { @@ -1255,8 +1339,9 @@ class SectionItem extends vscode.TreeItem { : vscode.TreeItemCollapsibleState.None; super(label, collapsibleState); this.items = items; - this.description = options.description - || (items.length > 0 ? String(items.length) : ''); + this.description = Object.prototype.hasOwnProperty.call(options, 'description') + ? options.description + : (items.length > 0 ? String(items.length) : ''); this.tooltip = options.tooltip || [label, this.description].filter(Boolean).join('\n'); this.iconPath = options.iconId ? themeIcon(options.iconId, options.iconColorId) : undefined; this.contextValue = 'gitguardex.section'; @@ -1325,7 +1410,12 @@ class SessionItem extends vscode.TreeItem { ? buildRawSessionDescription(session) : buildSessionCardDescription(session); this.tooltip = buildSessionTooltip(session, this.description); - this.iconPath = themeIcon(resolveSessionActivityIconId(session.activityKind)); + this.iconPath = themeIcon( + resolveSessionActivityIconId(session.activityKind), + session.activityKind === 'blocked' + ? 'list.errorForeground' + : undefined, + ); this.contextValue = 'gitguardex.session'; this.command = { command: 'gitguardex.activeAgents.openWorktree', @@ -2493,6 +2583,8 @@ function commitWorktree(worktreePath, message) { } function buildSessionDetailItems(session) { + const statusFacts = buildSessionStatusFacts(session); + const contextFacts = buildSessionContextFacts(session); const snapshot = sessionSnapshotDisplayName(session); const projectRelativePath = resolveSessionProjectRelativePath(session); const badgeSummary = uniqueStringList([ @@ -2500,38 +2592,72 @@ function buildSessionDetailItems(session) { session.deltaLabel || '', ].filter(Boolean)).join(', '); const sessionHealthSummary = buildSessionHealthSummary(session); - const items = [ - ...buildSessionFileGroupItems(session), - ]; - if (badgeSummary) { - items.push(new DetailItem('Signals', badgeSummary, { + const locationItems = [ + new DetailItem('Branch', session.branch, { + iconId: 'git-branch', + tooltip: session.branch, + }), + new DetailItem('Worktree', session.worktreePath, { + iconId: 'folder', + tooltip: session.worktreePath, + }), + ].filter((item) => Boolean(item.description)); + const items = []; + + if (statusFacts.length > 0) { + const statusItems = statusFacts.map((fact) => new DetailItem(fact)); + if (badgeSummary) { + statusItems.push(new DetailItem('Signals', badgeSummary, { + iconId: 'warning', + })); + } + items.push(new SectionItem('Status', statusItems, { + description: statusFacts.join(' · '), iconId: 'warning', + iconColorId: session.activityKind === 'blocked' + ? 'list.errorForeground' + : 'list.warningForeground', + collapsedState: vscode.TreeItemCollapsibleState.Collapsed, + tooltip: statusFacts.join('\n'), })); } - if (sessionHealthSummary) { - items.push(new DetailItem('Session health', sessionHealthSummary, { - iconId: 'pulse', - tooltip: buildSessionHealthTooltip(session) || sessionHealthSummary, - })); - } - if (snapshot) { - items.push(new DetailItem('Snapshot', snapshot, { + + if (contextFacts.length > 0) { + const contextItems = contextFacts.map((fact) => new DetailItem(fact)); + if (sessionHealthSummary) { + contextItems.push(new DetailItem('Session health', sessionHealthSummary, { + iconId: 'pulse', + tooltip: buildSessionHealthTooltip(session) || sessionHealthSummary, + })); + } + if (snapshot) { + contextItems.push(new DetailItem('Snapshot', snapshot, { + iconId: 'account', + })); + } + if (projectRelativePath) { + contextItems.push(new DetailItem('Project', projectRelativePath, { + iconId: 'folder', + tooltip: projectRelativePath, + })); + } + items.push(new SectionItem('Context', contextItems, { + description: contextFacts.join(' · '), iconId: 'account', + collapsedState: vscode.TreeItemCollapsibleState.Collapsed, + tooltip: contextFacts.join('\n'), })); } - if (projectRelativePath) { - items.push(new DetailItem('Project', projectRelativePath, { + + if (locationItems.length > 0) { + items.push(new SectionItem('Location', locationItems, { + description: compactBranchLabel(session.branch) || session.branch || '', iconId: 'folder', - tooltip: projectRelativePath, + collapsedState: vscode.TreeItemCollapsibleState.Collapsed, + tooltip: [session.branch, session.worktreePath].filter(Boolean).join('\n'), })); } - items.push(new DetailItem('Branch', session.branch, { - iconId: 'git-branch', - })); - items.push(new DetailItem('Worktree', session.worktreePath, { - iconId: 'folder', - tooltip: session.worktreePath, - })); + return items; } @@ -2802,14 +2928,12 @@ class ActiveAgentsProvider { async getChildren(element) { if (element instanceof RepoItem) { + const overviewItems = buildOverviewItems(element.overview); const sectionItems = [ - new SectionItem('Overview', [ - new DetailItem('Summary', buildOverviewDescription(element.overview), { - iconId: 'graph', - tooltip: buildRepoTooltip(element.repoRoot, element.overview), - }), - ], { - description: '1', + new SectionItem('Overview', overviewItems, { + description: '', + iconId: 'graph', + tooltip: buildRepoTooltip(element.repoRoot, element.overview), }), ]; diff --git a/vscode/guardex-active-agents/package.json b/vscode/guardex-active-agents/package.json index 1758ff0..ac121bd 100644 --- a/vscode/guardex-active-agents/package.json +++ b/vscode/guardex-active-agents/package.json @@ -3,7 +3,7 @@ "displayName": "GitGuardex Active Agents", "description": "Shows live Guardex sandbox sessions and repo changes in a dedicated VS Code Active Agents sidebar.", "publisher": "recodeee", - "version": "0.0.13", + "version": "0.0.16", "license": "MIT", "icon": "icon.png", "engines": {