From 5f201bb4ba6d2df5ee7664cc24319947df410c26 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sat, 29 Aug 2026 17:02:05 +0300 Subject: [PATCH] fix(chat): scope command, skill, and file autocomplete to the chat's own directory Commands and skills for the composer were read from the ambient store, which follows the project selected in the sidebar; opening a managed chat or a chat draft never changes that selection, so the previous project's commands, skills, and (for drafts) files leaked into chats. The autocompletes now load and read commands and skills for the effective directory, and a chat draft resolves its effective directory to the prepared chat directory or the Chats root instead of the last project. Claude-Session: https://claude.ai/code/session_017TK5JAYDfT3Fotc23UEg98 --- .../components/chat/CommandAutocomplete.tsx | 19 +++++++++++++------ .../src/components/chat/SkillAutocomplete.tsx | 14 +++++++++----- .../ui/src/hooks/useEffectiveDirectory.ts | 12 +++++++++++- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/components/chat/CommandAutocomplete.tsx b/packages/ui/src/components/chat/CommandAutocomplete.tsx index 6cf85536..355eca35 100644 --- a/packages/ui/src/components/chat/CommandAutocomplete.tsx +++ b/packages/ui/src/components/chat/CommandAutocomplete.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { cn, fuzzyMatch } from '@/lib/utils'; import { useSessionUIStore } from '@/sync/session-ui-store'; -import { useCommandsStore } from '@/stores/useCommandsStore'; -import { useSkillsStore } from '@/stores/useSkillsStore'; +import { selectCommandsForDirectory, useCommandsStore } from '@/stores/useCommandsStore'; +import { selectSkillsForDirectory, useSkillsStore } from '@/stores/useSkillsStore'; +import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; @@ -73,10 +74,16 @@ export const CommandAutocomplete = React.forwardRef([]); const [loading, setLoading] = React.useState(false); - const commandsWithMetadata = useCommandsStore((s) => s.commands); - const refreshCommands = useCommandsStore((s) => s.loadCommands); - const skills = useSkillsStore((s) => s.skills); - const refreshSkills = useSkillsStore((s) => s.loadSkills); + // Commands and skills belong to the directory the composer sends to — the + // session's own directory, or the Chats root for a chat draft — not to the + // project the app was on last. + const effectiveDirectory = useEffectiveDirectory(); + const commandsWithMetadata = useCommandsStore((s) => selectCommandsForDirectory(s, effectiveDirectory)); + const loadCommandsForDirectory = useCommandsStore((s) => s.loadCommands); + const skills = useSkillsStore((s) => selectSkillsForDirectory(s, effectiveDirectory)); + const loadSkillsForDirectory = useSkillsStore((s) => s.loadSkills); + const refreshCommands = React.useCallback(() => loadCommandsForDirectory(effectiveDirectory), [effectiveDirectory, loadCommandsForDirectory]); + const refreshSkills = React.useCallback(() => loadSkillsForDirectory(effectiveDirectory), [effectiveDirectory, loadSkillsForDirectory]); const [selectedIndex, setSelectedIndex] = React.useState(0); const selectedIndexRef = React.useRef(0); const keyboardNavigationRef = React.useRef(false); diff --git a/packages/ui/src/components/chat/SkillAutocomplete.tsx b/packages/ui/src/components/chat/SkillAutocomplete.tsx index a5d05e4a..9a8d6088 100644 --- a/packages/ui/src/components/chat/SkillAutocomplete.tsx +++ b/packages/ui/src/components/chat/SkillAutocomplete.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { cn, fuzzyMatch } from '@/lib/utils'; -import { useSkillsStore } from '@/stores/useSkillsStore'; +import { selectSkillsForDirectory, useSkillsStore } from '@/stores/useSkillsStore'; +import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; import { useUIStore } from '@/stores/useUIStore'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { useMobileAutocompleteMaxHeight } from './useMobileAutocompleteMaxHeight'; @@ -38,13 +39,16 @@ export const SkillAutocomplete = React.forwardRef([]); const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]); - const skills = useSkillsStore((s) => s.skills); + // Skills of the directory the composer sends to (session directory, or the + // Chats root for a chat draft), not of the project the app was on last. + const effectiveDirectory = useEffectiveDirectory(); + const skills = useSkillsStore((s) => selectSkillsForDirectory(s, effectiveDirectory)); const loadSkills = useSkillsStore((s) => s.loadSkills); React.useEffect(() => { - // Always trigger loadSkills when autocomplete opens to ensure project context is fresh - void loadSkills(); - }, [loadSkills]); + // Always trigger loadSkills when autocomplete opens to ensure the directory's skills are fresh + void loadSkills(effectiveDirectory); + }, [effectiveDirectory, loadSkills]); React.useEffect(() => { const normalizedQuery = searchQuery.trim(); diff --git a/packages/ui/src/hooks/useEffectiveDirectory.ts b/packages/ui/src/hooks/useEffectiveDirectory.ts index 1b783536..d5c6de23 100644 --- a/packages/ui/src/hooks/useEffectiveDirectory.ts +++ b/packages/ui/src/hooks/useEffectiveDirectory.ts @@ -3,6 +3,7 @@ import { useSessionWorktreeStore } from '@/sync/session-worktree-store'; import { getAttachedSessionDirectory } from '@/sync/session-worktree-contract'; import { useSessionDirectory } from '@/sync/sync-context'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; +import { getChatsRootForHome } from '@/lib/chatDirectories'; /** * Hook that resolves the effective working directory for tabs (Git, Diff, Files, Terminal). @@ -11,7 +12,10 @@ import { useDirectoryStore } from '@/stores/useDirectoryStore'; * 1. Worktree metadata path (for worktree sessions) * 2. Session directory (for active sessions) * 3. Draft session directoryOverride (when creating a new session) - * 4. Fallback directory from DirectoryStore + * 4. For a Chat draft, the prepared chat directory or the managed Chats root — + * never the project the app was on before, which would leak that + * project's files, commands, and skills into the chat + * 5. Fallback directory from DirectoryStore * * This ensures that tabs show content from the correct project directory * even when a draft session is being created. @@ -23,6 +27,7 @@ export const useEffectiveDirectory = (): string | undefined => { const worktreeAttachment = useSessionWorktreeStore((s) => currentSessionId ? s.getAttachment(currentSessionId) : undefined); const worktreeMap = useSessionUIStore((s) => s.worktreeMetadata); const fallbackDirectory = useDirectoryStore((s) => s.currentDirectory); + const homeDirectory = useDirectoryStore((s) => s.homeDirectory); // If we have an active session, use its directory if (currentSessionId) { @@ -44,6 +49,11 @@ export const useEffectiveDirectory = (): string | undefined => { return (newSessionDraft.bootstrapPendingDirectory || newSessionDraft.directoryOverride) ?? undefined; } + if (newSessionDraft?.open && newSessionDraft.target === 'chat') { + const chatDirectory = newSessionDraft.preparedChatDirectory ?? getChatsRootForHome(homeDirectory); + if (chatDirectory) return chatDirectory; + } + // Fall back to the global directory return fallbackDirectory ?? undefined; };