fix(knowledge): scope pins to sessions
This commit is contained in:
@@ -5,12 +5,9 @@ import { useSkillsStore } from '@/stores/useSkillsStore';
|
||||
import { useMcpStore } from '@/stores/useMcpStore';
|
||||
import { useSession } from '@/sync/sync-context';
|
||||
import { getLinkedIssues } from '@/lib/linkedIssues';
|
||||
import { fetchSessionKnowledgeSummary, type SessionKnowledgeSummary } from '@/lib/sessionKnowledgeApi';
|
||||
import { resolveProjectForSessionDirectory } from '@/lib/projectResolution';
|
||||
import { fetchSessionKnowledgeSummary, setSessionProjectContextPin, type SessionKnowledgeSummary } from '@/lib/sessionKnowledgeApi';
|
||||
import { useProjectContextStore } from '@/stores/useProjectContextStore';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { WorkStatusCollapsibleSection, WorkStatusRow, WorkStatusValue } from './WorkStatusPrimitives';
|
||||
import { useReportWorkStatusPresence } from './presenceContext';
|
||||
|
||||
@@ -50,7 +47,7 @@ export const WorkStatusContextSection: React.FC<Props> = ({ sessionId, directory
|
||||
}, [directory, loadSkills]);
|
||||
|
||||
/**
|
||||
* What the project sends along with every message. Read from the server
|
||||
* What this session carries. Read from the server
|
||||
* rather than from the notes panel's store, because this must be right
|
||||
* whether or not that panel has ever been opened.
|
||||
*/
|
||||
@@ -58,41 +55,34 @@ export const WorkStatusContextSection: React.FC<Props> = ({ sessionId, directory
|
||||
{ notes: [], plans: [], memory: { global: 0, project: 0 } },
|
||||
);
|
||||
|
||||
// Re-read whenever the stores that own pins or memory change, not only when
|
||||
// the directory does. Unpinning is a write those stores make, and a panel
|
||||
// that keeps listing what was just unpinned tells the user it is still going
|
||||
// to the agent when it is not.
|
||||
// Re-read when source content or memory changes, not only when the session does.
|
||||
const contextEntries = useProjectContextStore((state) => state.entries);
|
||||
const memoryProject = useAgentMemoryStore((state) => state.project);
|
||||
const memoryGlobal = useAgentMemoryStore((state) => state.global);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
void fetchSessionKnowledgeSummary(directory).then((summary) => {
|
||||
void fetchSessionKnowledgeSummary(directory, sessionId).then((summary) => {
|
||||
if (!cancelled) setKnowledge(summary);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [directory, contextEntries, memoryProject, memoryGlobal]);
|
||||
|
||||
const projects = useProjectsStore((state) => state.projects);
|
||||
const availableWorktreesByProject = useSessionUIStore((state) => state.availableWorktreesByProject);
|
||||
const setNotePinned = useProjectContextStore((state) => state.setNotePinned);
|
||||
const setPlanPinned = useProjectContextStore((state) => state.setPlanPinned);
|
||||
|
||||
const projectRef = React.useMemo(() => {
|
||||
const resolved = resolveProjectForSessionDirectory(projects, availableWorktreesByProject, directory ?? '');
|
||||
return resolved ? { id: resolved.id, path: resolved.path } : null;
|
||||
}, [availableWorktreesByProject, directory, projects]);
|
||||
}, [directory, sessionId, session, contextEntries, memoryProject, memoryGlobal]);
|
||||
|
||||
// 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 (projectRef) void setNotePinned(projectRef, noteId, false);
|
||||
}, [projectRef, setNotePinned]);
|
||||
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]);
|
||||
const unpinPlan = React.useCallback((planId: string) => {
|
||||
if (projectRef) void setPlanPinned(projectRef, planId, false);
|
||||
}, [projectRef, setPlanPinned]);
|
||||
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]);
|
||||
|
||||
const memoryCount = knowledge.memory.global + knowledge.memory.project;
|
||||
const pinnedCount = knowledge.notes.length + knowledge.plans.length;
|
||||
@@ -131,9 +121,7 @@ export const WorkStatusContextSection: React.FC<Props> = ({ sessionId, directory
|
||||
? t('chat.workStatus.breakdown.prCountSingle', { count: prCount })
|
||||
: t('chat.workStatus.breakdown.prCountPlural', { count: prCount }));
|
||||
}
|
||||
// Pinned knowledge outranks the ambient counts in the summary: it is
|
||||
// something the user chose for this project, not something that happens to
|
||||
// be installed.
|
||||
// Pinned knowledge outranks ambient counts because the user chose it for this session.
|
||||
if (summaryParts.length === 0 && pinnedCount > 0) {
|
||||
summaryParts.push(pinnedCount === 1
|
||||
? t('chat.workStatus.breakdown.pinnedKnowledgeSingle', { count: pinnedCount })
|
||||
@@ -182,8 +170,7 @@ export const WorkStatusContextSection: React.FC<Props> = ({ sessionId, directory
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Named individually: a count alone would not tell the user which note
|
||||
is riding along with every message they send. */}
|
||||
{/* Named individually: a count alone would not identify this session's context. */}
|
||||
{/* 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. */}
|
||||
@@ -194,7 +181,7 @@ export const WorkStatusContextSection: React.FC<Props> = ({ sessionId, directory
|
||||
leading={(
|
||||
<button
|
||||
type="button"
|
||||
disabled={!projectRef}
|
||||
disabled={!sessionId || !directory}
|
||||
aria-label={t('chat.workStatus.breakdown.unpin')}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
@@ -216,7 +203,7 @@ export const WorkStatusContextSection: React.FC<Props> = ({ sessionId, directory
|
||||
leading={(
|
||||
<button
|
||||
type="button"
|
||||
disabled={!projectRef}
|
||||
disabled={!sessionId || !directory}
|
||||
aria-label={t('chat.workStatus.breakdown.unpin')}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
Reference in New Issue
Block a user