|
|
|
@@ -8,8 +8,13 @@ import { getLinkedIssues } from '@/lib/linkedIssues';
|
|
|
|
|
import { fetchSessionKnowledgeSummary, setSessionProjectContextPin, type SessionKnowledgeSummary } from '@/lib/sessionKnowledgeApi';
|
|
|
|
|
import { useProjectContextStore } from '@/stores/useProjectContextStore';
|
|
|
|
|
import { useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
|
|
|
|
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
|
|
|
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
|
|
|
|
import { resolveProjectForSessionDirectory } from '@/lib/projectResolution';
|
|
|
|
|
import { resolveProjectContextId } from '@/lib/projectContextApi';
|
|
|
|
|
import { WorkStatusCollapsibleSection, WorkStatusRow, WorkStatusValue } from './WorkStatusPrimitives';
|
|
|
|
|
import { useReportWorkStatusPresence } from './presenceContext';
|
|
|
|
|
import { resolveDraftPinnedKnowledge } from './draftKnowledge';
|
|
|
|
|
|
|
|
|
|
type Props = {
|
|
|
|
|
sessionId: string | null;
|
|
|
|
@@ -29,6 +34,11 @@ export const WorkStatusContextSection: React.FC<Props> = ({ sessionId, directory
|
|
|
|
|
const { t } = useI18n();
|
|
|
|
|
|
|
|
|
|
const session = useSession(sessionId ?? '', directory ?? undefined);
|
|
|
|
|
const newSessionDraft = useSessionUIStore((state) => state.newSessionDraft);
|
|
|
|
|
const setDraftProjectContextPin = useSessionUIStore((state) => state.setDraftProjectContextPin);
|
|
|
|
|
const availableWorktreesByProject = useSessionUIStore((state) => state.availableWorktreesByProject);
|
|
|
|
|
const projects = useProjectsStore((state) => state.projects);
|
|
|
|
|
const isDraft = sessionId === null && newSessionDraft.open;
|
|
|
|
|
const skills = useSkillsStore((state) => state.skills);
|
|
|
|
|
const mcpStatus = useMcpStore(
|
|
|
|
|
React.useCallback((state) => state.getStatusForDirectory(directory), [directory]),
|
|
|
|
@@ -57,9 +67,31 @@ export const WorkStatusContextSection: React.FC<Props> = ({ sessionId, directory
|
|
|
|
|
|
|
|
|
|
// Re-read when source content or memory changes, not only when the session does.
|
|
|
|
|
const contextEntries = useProjectContextStore((state) => state.entries);
|
|
|
|
|
const loadProjectContext = useProjectContextStore((state) => state.load);
|
|
|
|
|
const memoryProject = useAgentMemoryStore((state) => state.project);
|
|
|
|
|
const memoryGlobal = useAgentMemoryStore((state) => state.global);
|
|
|
|
|
|
|
|
|
|
const draftProject = React.useMemo(() => {
|
|
|
|
|
if (!isDraft) return null;
|
|
|
|
|
const selected = newSessionDraft.selectedProjectId
|
|
|
|
|
? projects.find((project) => project.id === newSessionDraft.selectedProjectId) ?? null
|
|
|
|
|
: null;
|
|
|
|
|
return selected ?? resolveProjectForSessionDirectory(
|
|
|
|
|
projects,
|
|
|
|
|
availableWorktreesByProject,
|
|
|
|
|
newSessionDraft.directoryOverride ?? directory,
|
|
|
|
|
);
|
|
|
|
|
}, [availableWorktreesByProject, directory, isDraft, newSessionDraft.directoryOverride, newSessionDraft.selectedProjectId, projects]);
|
|
|
|
|
|
|
|
|
|
const draftContextEntry = draftProject
|
|
|
|
|
? contextEntries[resolveProjectContextId({ id: draftProject.id, path: draftProject.path })]
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (!isDraft || !draftProject) return;
|
|
|
|
|
void loadProjectContext({ id: draftProject.id, path: draftProject.path });
|
|
|
|
|
}, [draftProject, isDraft, loadProjectContext]);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
void fetchSessionKnowledgeSummary(directory, sessionId).then((summary) => {
|
|
|
|
@@ -68,24 +100,42 @@ export const WorkStatusContextSection: React.FC<Props> = ({ sessionId, directory
|
|
|
|
|
return () => { cancelled = true; };
|
|
|
|
|
}, [directory, sessionId, session, contextEntries, memoryProject, memoryGlobal]);
|
|
|
|
|
|
|
|
|
|
const visibleKnowledge = React.useMemo<SessionKnowledgeSummary>(() => {
|
|
|
|
|
if (!isDraft) return knowledge;
|
|
|
|
|
const pinned = resolveDraftPinnedKnowledge(
|
|
|
|
|
draftContextEntry?.notes ?? [],
|
|
|
|
|
draftContextEntry?.plans ?? [],
|
|
|
|
|
newSessionDraft.projectContextPins ?? { notes: [], plans: [] },
|
|
|
|
|
);
|
|
|
|
|
return { ...knowledge, ...pinned };
|
|
|
|
|
}, [draftContextEntry?.notes, draftContextEntry?.plans, isDraft, knowledge, newSessionDraft.projectContextPins]);
|
|
|
|
|
|
|
|
|
|
// Unpinning from here, like the pinned-messages section: a panel that says
|
|
|
|
|
// what is attached should be able to detach it, or the user has to go find
|
|
|
|
|
// the surface that can.
|
|
|
|
|
const unpinNote = React.useCallback((noteId: string) => {
|
|
|
|
|
if (isDraft) {
|
|
|
|
|
setDraftProjectContextPin('note', noteId, false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!directory || !sessionId) return;
|
|
|
|
|
void setSessionProjectContextPin(directory, sessionId, 'note', noteId, false).then((pins) => {
|
|
|
|
|
if (pins) setKnowledge((current) => ({ ...current, notes: current.notes.filter((note) => note.id !== noteId) }));
|
|
|
|
|
});
|
|
|
|
|
}, [directory, sessionId]);
|
|
|
|
|
}, [directory, isDraft, sessionId, setDraftProjectContextPin]);
|
|
|
|
|
const unpinPlan = React.useCallback((planId: string) => {
|
|
|
|
|
if (isDraft) {
|
|
|
|
|
setDraftProjectContextPin('plan', planId, false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!directory || !sessionId) return;
|
|
|
|
|
void setSessionProjectContextPin(directory, sessionId, 'plan', planId, false).then((pins) => {
|
|
|
|
|
if (pins) setKnowledge((current) => ({ ...current, plans: current.plans.filter((plan) => plan.id !== planId) }));
|
|
|
|
|
});
|
|
|
|
|
}, [directory, sessionId]);
|
|
|
|
|
}, [directory, isDraft, sessionId, setDraftProjectContextPin]);
|
|
|
|
|
|
|
|
|
|
const memoryCount = knowledge.memory.global + knowledge.memory.project;
|
|
|
|
|
const pinnedCount = knowledge.notes.length + knowledge.plans.length;
|
|
|
|
|
const memoryCount = visibleKnowledge.memory.global + visibleKnowledge.memory.project;
|
|
|
|
|
const pinnedCount = visibleKnowledge.notes.length + visibleKnowledge.plans.length;
|
|
|
|
|
|
|
|
|
|
const linked = React.useMemo(() => getLinkedIssues(session), [session]);
|
|
|
|
|
// Connected servers only. A disabled server contributes nothing to the
|
|
|
|
@@ -174,14 +224,14 @@ export const WorkStatusContextSection: React.FC<Props> = ({ sessionId, directory
|
|
|
|
|
{/* The pin is the control, exactly as in the pinned-messages section
|
|
|
|
|
above: same icon, same placement, same behaviour. Two pins that look
|
|
|
|
|
different in one panel would read as two different things. */}
|
|
|
|
|
{knowledge.notes.map((note) => (
|
|
|
|
|
{visibleKnowledge.notes.map((note) => (
|
|
|
|
|
<WorkStatusRow
|
|
|
|
|
key={note.id}
|
|
|
|
|
muted
|
|
|
|
|
leading={(
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={!sessionId || !directory}
|
|
|
|
|
disabled={!isDraft && (!sessionId || !directory)}
|
|
|
|
|
aria-label={t('chat.workStatus.breakdown.unpin')}
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
event.stopPropagation();
|
|
|
|
@@ -196,14 +246,14 @@ export const WorkStatusContextSection: React.FC<Props> = ({ sessionId, directory
|
|
|
|
|
value={<WorkStatusValue tone="muted">{t('chat.workStatus.breakdown.pinnedNote')}</WorkStatusValue>}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
{knowledge.plans.map((plan) => (
|
|
|
|
|
{visibleKnowledge.plans.map((plan) => (
|
|
|
|
|
<WorkStatusRow
|
|
|
|
|
key={plan.id}
|
|
|
|
|
muted
|
|
|
|
|
leading={(
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={!sessionId || !directory}
|
|
|
|
|
disabled={!isDraft && (!sessionId || !directory)}
|
|
|
|
|
aria-label={t('chat.workStatus.breakdown.unpin')}
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|