131 lines
5.5 KiB
TypeScript
131 lines
5.5 KiB
TypeScript
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<Props> = ({ 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 (
|
||
|
|
<WorkStatusCollapsibleSection
|
||
|
|
id="context-sources"
|
||
|
|
title={t('chat.workStatus.section.contextBreakdown')}
|
||
|
|
icon="stack"
|
||
|
|
summary={summaryParts.join(' · ')}
|
||
|
|
>
|
||
|
|
{/* Attached threads first: they are specific to this session, while the
|
||
|
|
counts below describe the workspace. */}
|
||
|
|
{linked.map((entry) => (
|
||
|
|
<WorkStatusRow
|
||
|
|
key={entry.id}
|
||
|
|
leading={entry.authorAvatarUrl ? (
|
||
|
|
<img src={entry.authorAvatarUrl} alt="" className="size-4 shrink-0 rounded-full" loading="lazy" />
|
||
|
|
) : (
|
||
|
|
<Icon
|
||
|
|
name={entry.kind === 'pull' ? 'git-pull-request' : 'error-warning'}
|
||
|
|
className="size-4 shrink-0 text-muted-foreground"
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
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={<WorkStatusValue tone="muted">{`#${entry.number}`}</WorkStatusValue>}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
|
||
|
|
<WorkStatusRow
|
||
|
|
muted
|
||
|
|
label={t('chat.workStatus.breakdown.skills')}
|
||
|
|
value={<WorkStatusValue>{skills.length}</WorkStatusValue>}
|
||
|
|
/>
|
||
|
|
<WorkStatusRow
|
||
|
|
muted
|
||
|
|
label={t('chat.workStatus.breakdown.mcp')}
|
||
|
|
value={<WorkStatusValue>{mcpCount}</WorkStatusValue>}
|
||
|
|
/>
|
||
|
|
</WorkStatusCollapsibleSection>
|
||
|
|
);
|
||
|
|
};
|