From f9d1ded81a3909f46db896335b8822b4c865c093 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 19 Aug 2026 00:07:57 +0300 Subject: [PATCH] fix(knowledge): scope pins to sessions --- CHANGELOG.md | 4 +- .../work-status/WorkStatusContextSection.tsx | 51 ++++++-------- .../session/project-context/DOCUMENTATION.md | 19 +++--- .../session/project-context/NotesSection.tsx | 25 ++++--- .../session/project-context/PlansSection.tsx | 19 +++--- .../project-context/ProjectNotesTodoPanel.tsx | 43 ++++++++++++ packages/ui/src/lib/sessionKnowledgeApi.ts | 32 ++++++++- .../ui/src/sync/__tests__/issue-2039.test.ts | 21 ++++++ packages/ui/src/sync/session-ui-store.ts | 38 ++++++++--- .../server/lib/context-obligatory/runtime.js | 8 ++- .../lib/context-obligatory/runtime.test.js | 27 ++++++-- .../lib/project-context/DOCUMENTATION.md | 9 +-- .../lib/session-knowledge/DOCUMENTATION.md | 11 ++- .../server/lib/session-knowledge/routes.js | 22 +++++- .../server/lib/session-knowledge/runtime.js | 68 ++++++++++++++++--- .../lib/session-knowledge/runtime.test.js | 49 +++++++++++-- 16 files changed, 340 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76315f42..826d88ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,9 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - **Settings/Integrations:** a new Integrations settings page lists Claude Code, Command Code, and Cursor plugins with install, update, setup, and remove actions, plus Discord and Telegram Coming soon placeholders. -- **Project knowledge:** the Project notes panel is now Project knowledge, with notes, todos, plans and their search in a resizable sidebar. Notes are cards you expand by clicking anywhere on them, plans open and edit in the panel itself instead of a separate tab, and notes and plans can be pinned as project context. +- **Project knowledge:** the Project notes panel is now Project knowledge, with notes, todos, plans and their search in a resizable sidebar. Notes are cards you expand by clicking anywhere on them, plans open and edit in the panel itself instead of a separate tab, and notes and plans can be attached to the current session or its new-session draft without affecting other sessions. - **Files:** drag files onto the Files sidebar to upload them into the project or a specific folder; existing files require confirmation before replacement, and open previews refresh after an upload (thanks to @makeittech, @alanzchen). -- Work status: the Context sources section now names each pinned note and plan available as standing project context, and its pin button unpins them from there. +- Work status: the Context sources section now names each note and plan attached to that session, and its pin button detaches them from there. - Settings: OpenChamber no longer replaces a full OpenCode config with an empty `$schema`-only stub when the file uses JSON5-style unquoted keys; Settings changes now fail instead of wiping plugins, MCP servers, and providers (thanks to @makeittech). - Chat: an open conversation no longer keeps re-coloring the same code blocks in the background, so browsing files with a chat open stops pinning a CPU core and spinning up the fans (thanks to @makeittech). - Stability/Proxy: the local server now reuses its connection to OpenCode instead of opening a new one for every API request. Under sustained traffic the old behavior could use up every outgoing network port on the machine, at which point nothing on the computer could open a new connection until the traffic stopped and the ports were released (thanks to @alohaninja). diff --git a/packages/ui/src/components/chat/work-status/WorkStatusContextSection.tsx b/packages/ui/src/components/chat/work-status/WorkStatusContextSection.tsx index 73671442..f5800f04 100644 --- a/packages/ui/src/components/chat/work-status/WorkStatusContextSection.tsx +++ b/packages/ui/src/components/chat/work-status/WorkStatusContextSection.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ sessionId, directory leading={(