import React from 'react'; import { useI18n } from '@/lib/i18n'; import { Icon } from '@/components/icon/Icon'; import { useSkillsStore } from '@/stores/useSkillsStore'; import { useMcpStore } from '@/stores/useMcpStore'; import { useSession } from '@/sync/sync-context'; import { getLinkedIssues } from '@/lib/linkedIssues'; import { WorkStatusCollapsibleSection, WorkStatusRow, WorkStatusValue } from './WorkStatusPrimitives'; import { useReportWorkStatusPresence } from './presenceContext'; type Props = { sessionId: string | null; directory: string | null; }; /** * What is loaded into the agent's context: the GitHub threads this session was * pointed at, plus how much ambient material is available. * * Agents are deliberately absent — an agent is who does the work, not material * the work is done with. Tools are absent for want of an honest source: * `Agent.tools` is a per-agent override map, not a registry, so its size would * report something other than "tools available". */ export const WorkStatusContextSection: React.FC = ({ sessionId, directory }) => { const { t } = useI18n(); const session = useSession(sessionId ?? '', directory ?? undefined); const skills = useSkillsStore((state) => state.skills); const mcpStatus = useMcpStore( React.useCallback((state) => state.getStatusForDirectory(directory), [directory]), ); // Skills were previously fetched only when the composer's slash autocomplete // opened, so this row reported whatever count happened to be cached — often // none — until the user typed "/". The panel states a count, so it is the // panel's business to have one. Re-run per directory because skills are // discovered relative to the active project. No background-network wrap // here: `loadSkills` already gates its own fetch, and wrapping it again // would hold a second slot idle for the length of the first. const loadSkills = useSkillsStore((state) => state.loadSkills); React.useEffect(() => { void loadSkills(); }, [directory, loadSkills]); const linked = React.useMemo(() => getLinkedIssues(session), [session]); // Connected servers only. A disabled server contributes nothing to the // context, so counting it here contradicts the MCP section right above, // which shows the same servers switched off. const mcpCount = React.useMemo( () => Object.values(mcpStatus ?? {}).filter((entry) => entry?.status === 'connected').length, [mcpStatus], ); useReportWorkStatusPresence('context-sources', linked.length > 0 || skills.length > 0 || mcpCount > 0); if (linked.length === 0 && skills.length === 0 && mcpCount === 0) return null; // The heading names what is distinctive about this session when there is // something — an attached thread — and falls back to the ambient counts // when there is not. `1 · 33 · 2` said nothing without opening the section. const issueCount = linked.filter((entry) => entry.kind === 'issue').length; const prCount = linked.length - issueCount; const summaryParts: string[] = []; if (issueCount > 0) { summaryParts.push(issueCount === 1 ? t('chat.workStatus.breakdown.issueCountSingle', { count: issueCount }) : t('chat.workStatus.breakdown.issueCountPlural', { count: issueCount })); } if (prCount > 0) { summaryParts.push(prCount === 1 ? t('chat.workStatus.breakdown.prCountSingle', { count: prCount }) : t('chat.workStatus.breakdown.prCountPlural', { count: prCount })); } if (summaryParts.length === 0) { if (skills.length > 0) { summaryParts.push(skills.length === 1 ? t('chat.workStatus.breakdown.skillCountSingle', { count: skills.length }) : t('chat.workStatus.breakdown.skillCountPlural', { count: skills.length })); } if (mcpCount > 0) { summaryParts.push(mcpCount === 1 ? t('chat.workStatus.breakdown.mcpCountSingle', { count: mcpCount }) : t('chat.workStatus.breakdown.mcpCountPlural', { count: mcpCount })); } } return ( {/* Attached threads first: they are specific to this session, while the counts below describe the workspace. */} {linked.map((entry) => ( ) : ( )} label={entry.title} muted // The stored snapshot is enough to render; the live thread only ever // exists on github.com. onClick={() => window.open(entry.url, '_blank', 'noopener,noreferrer')} ariaLabel={t('chat.workStatus.linkedIssues.open', { number: entry.number })} value={{`#${entry.number}`}} /> ))} {skills.length}} /> {mcpCount}} /> ); };