fix(ui): scope agent memory to project context owner
This commit is contained in:
@@ -13,12 +13,10 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { resolveProjectForSessionDirectory } from '@/lib/projectResolution';
|
||||
import { subscribeOpenchamberEvents } from '@/lib/openchamberEvents';
|
||||
import { useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useProjectContextOwner } from '@/hooks/useProjectContextOwner';
|
||||
|
||||
/**
|
||||
* The directory is a parameter rather than read from `useEffectiveDirectory`,
|
||||
@@ -29,18 +27,9 @@ export const useAgentMemorySync = (directory: string | null): void => {
|
||||
const enabled = useUIStore((state) => (
|
||||
state.agentMemoryFeatureAvailable && state.agentMemoryToolEnabled
|
||||
));
|
||||
const projects = useProjectsStore((state) => state.projects);
|
||||
const availableWorktreesByProject = useSessionUIStore((state) => state.availableWorktreesByProject);
|
||||
const effectiveDirectory = directory ?? '';
|
||||
const load = useAgentMemoryStore((state) => state.load);
|
||||
|
||||
const projectPath = React.useMemo(() => {
|
||||
if (!effectiveDirectory) {
|
||||
return null;
|
||||
}
|
||||
const resolved = resolveProjectForSessionDirectory(projects, availableWorktreesByProject, effectiveDirectory);
|
||||
return resolved?.path ?? null;
|
||||
}, [availableWorktreesByProject, effectiveDirectory, projects]);
|
||||
const owner = useProjectContextOwner(directory);
|
||||
const projectPath = owner?.path ?? null;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!enabled) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { CHAT_DRAFT_PROJECT_ID } from '@/lib/chatDirectories';
|
||||
import { resolveProjectContextOwner } from './useProjectContextOwner';
|
||||
|
||||
const projects = [
|
||||
{ id: 'openchamber', path: '/workspace/openchamber', label: 'OpenChamber' },
|
||||
];
|
||||
|
||||
describe('resolveProjectContextOwner', () => {
|
||||
test('resolves a managed chat directory to the Chats root instead of the active project', () => {
|
||||
const owner = resolveProjectContextOwner({
|
||||
projects,
|
||||
worktreesByProject: new Map(),
|
||||
directory: '/Users/test/.config/openchamber/chats/2026-08-27/session-a',
|
||||
activeProjectId: 'openchamber',
|
||||
chatDraftOpen: false,
|
||||
chatDraftTarget: 'project',
|
||||
homeDirectory: '/Users/test',
|
||||
});
|
||||
|
||||
expect(owner).toEqual({
|
||||
id: CHAT_DRAFT_PROJECT_ID,
|
||||
path: '/Users/test/.config/openchamber/chats',
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves a worktree session to its owning project', () => {
|
||||
const owner = resolveProjectContextOwner({
|
||||
projects,
|
||||
worktreesByProject: new Map([
|
||||
['/workspace/openchamber', [{
|
||||
path: '/workspace/openchamber-feature',
|
||||
projectDirectory: '/workspace/openchamber',
|
||||
branch: 'feature',
|
||||
label: 'feature',
|
||||
}]],
|
||||
]),
|
||||
directory: '/workspace/openchamber-feature',
|
||||
activeProjectId: null,
|
||||
chatDraftOpen: false,
|
||||
chatDraftTarget: 'project',
|
||||
homeDirectory: '/Users/test',
|
||||
});
|
||||
|
||||
expect(owner).toEqual({ id: 'openchamber', path: '/workspace/openchamber' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
|
||||
import { CHAT_DRAFT_PROJECT_ID, getChatsRootForHome, getChatsRootFromDirectory } from '@/lib/chatDirectories';
|
||||
import { normalizePath } from '@/lib/pathNormalization';
|
||||
import { resolveProjectForSessionDirectory } from '@/lib/projectResolution';
|
||||
import type { ProjectRef } from '@/lib/projectContextApi';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import type { WorktreeMetadata } from '@/types/worktree';
|
||||
import type { ProjectEntry } from '@/lib/api/types';
|
||||
|
||||
interface ProjectContextOwnerInput {
|
||||
projects: ProjectEntry[];
|
||||
worktreesByProject: Map<string, WorktreeMetadata[]>;
|
||||
directory: string | null;
|
||||
activeProjectId: string | null;
|
||||
chatDraftOpen: boolean;
|
||||
chatDraftTarget: 'chat' | 'project';
|
||||
homeDirectory: string | null;
|
||||
}
|
||||
|
||||
export const resolveProjectContextOwner = ({
|
||||
projects,
|
||||
worktreesByProject,
|
||||
directory,
|
||||
activeProjectId,
|
||||
chatDraftOpen,
|
||||
chatDraftTarget,
|
||||
homeDirectory,
|
||||
}: ProjectContextOwnerInput): ProjectRef | null => {
|
||||
const chatsRoot = getChatsRootFromDirectory(directory) ?? getChatsRootForHome(homeDirectory);
|
||||
const normalizedDirectory = normalizePath(directory);
|
||||
const normalizedChatsRoot = normalizePath(chatsRoot);
|
||||
const ownsChats = chatDraftOpen
|
||||
? chatDraftTarget === 'chat'
|
||||
: Boolean(normalizedDirectory && normalizedChatsRoot && (
|
||||
normalizedDirectory === normalizedChatsRoot || normalizedDirectory.startsWith(`${normalizedChatsRoot}/`)
|
||||
));
|
||||
|
||||
if (ownsChats && chatsRoot) {
|
||||
return { id: CHAT_DRAFT_PROJECT_ID, path: chatsRoot };
|
||||
}
|
||||
|
||||
const sessionProject = resolveProjectForSessionDirectory(projects, worktreesByProject, directory);
|
||||
if (sessionProject) {
|
||||
return { id: sessionProject.id, path: sessionProject.path };
|
||||
}
|
||||
|
||||
const activeProject = projects.find((project) => project.id === activeProjectId) ?? projects[0] ?? null;
|
||||
return activeProject ? { id: activeProject.id, path: activeProject.path } : null;
|
||||
};
|
||||
|
||||
/** The single owner used by Project knowledge and agent-memory synchronization. */
|
||||
export const useProjectContextOwner = (directory: string | null): ProjectRef | null => {
|
||||
const projects = useProjectsStore((state) => state.projects);
|
||||
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
|
||||
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
|
||||
const worktreesByProject = useSessionUIStore((state) => state.availableWorktreesByProject);
|
||||
const chatDraftOpen = useSessionUIStore((state) => state.newSessionDraft.open);
|
||||
const chatDraftTarget = useSessionUIStore((state) => state.newSessionDraft.target);
|
||||
|
||||
return React.useMemo(() => resolveProjectContextOwner({
|
||||
projects,
|
||||
worktreesByProject,
|
||||
directory,
|
||||
activeProjectId,
|
||||
chatDraftOpen,
|
||||
chatDraftTarget,
|
||||
homeDirectory,
|
||||
}), [
|
||||
activeProjectId,
|
||||
chatDraftOpen,
|
||||
chatDraftTarget,
|
||||
directory,
|
||||
homeDirectory,
|
||||
projects,
|
||||
worktreesByProject,
|
||||
]);
|
||||
};
|
||||
Reference in New Issue
Block a user