From 41b531e899c5f068cf2e83bd70aa78eca4a1e40e Mon Sep 17 00:00:00 2001 From: lucaronin Date: Wed, 27 May 2026 19:23:06 +0200 Subject: [PATCH] fix: avoid inactive AI chat context work --- src/components/AiWorkspace.test.tsx | 133 ++++++++++++++++++++++++---- src/components/AiWorkspace.tsx | 86 +++++++++--------- 2 files changed, 159 insertions(+), 60 deletions(-) diff --git a/src/components/AiWorkspace.test.tsx b/src/components/AiWorkspace.test.tsx index 2fc049a4..92d1c0cd 100644 --- a/src/components/AiWorkspace.test.tsx +++ b/src/components/AiWorkspace.test.tsx @@ -11,29 +11,34 @@ import { import type { AiModelProvider } from '../lib/aiTargets' import type { AgentStatus } from '../hooks/useCliAiAgent' import { resetVaultConfigStore } from '../utils/vaultConfigStore' +import type { VaultEntry } from '../types' let mockedAgentStatus: AgentStatus = 'idle' +let controllerCalls: unknown[] = [] vi.mock('./useAiPanelController', () => ({ - useAiPanelController: () => ({ - agent: { - messages: [], - status: mockedAgentStatus, - sendMessage: vi.fn(), - clearConversation: vi.fn(), - addLocalMarker: vi.fn(), - }, - input: '', - setInput: vi.fn(), - linkedEntries: [], - hasContext: false, - isActive: false, - permissionMode: 'safe', - handleSend: vi.fn(), - handleNavigateWikilink: vi.fn(), - handlePermissionModeChange: vi.fn(), - handleNewChat: vi.fn(), - }), + useAiPanelController: (args: unknown) => { + controllerCalls.push(args) + return { + agent: { + messages: [], + status: mockedAgentStatus, + sendMessage: vi.fn(), + clearConversation: vi.fn(), + addLocalMarker: vi.fn(), + }, + input: '', + setInput: vi.fn(), + linkedEntries: [], + hasContext: false, + isActive: false, + permissionMode: 'safe', + handleSend: vi.fn(), + handleNavigateWikilink: vi.fn(), + handlePermissionModeChange: vi.fn(), + handleNewChat: vi.fn(), + } + }, })) vi.mock('./AiPanel', () => ({ @@ -90,9 +95,53 @@ const providers: AiModelProvider[] = [ }, ] +function makeEntry(overrides: Partial = {}): VaultEntry { + return { + aliases: [], + archived: false, + belongsTo: [], + color: null, + createdAt: 1700000000, + favorite: false, + favoriteIndex: null, + fileSize: 100, + filename: 'active.md', + hasH1: false, + icon: null, + isA: 'Note', + listPropertiesDisplay: [], + modifiedAt: 1700000000, + order: null, + organized: false, + outgoingLinks: [], + path: '/tmp/vault/active.md', + properties: {}, + relatedTo: [], + relationships: {}, + sidebarLabel: null, + snippet: '', + sort: null, + status: null, + template: null, + title: 'Active', + view: null, + visible: null, + wordCount: 0, + ...overrides, + } +} + +function contextReadyControllerCalls(): unknown[] { + return controllerCalls.filter((args) => { + const call = args as { activeEntry?: unknown; entries?: unknown[] } + return call.activeEntry !== null && call.activeEntry !== undefined && Array.isArray(call.entries) + }) +} + describe('AiWorkspace', () => { beforeEach(() => { mockedAgentStatus = 'idle' + controllerCalls = [] resetVaultConfigStore() }) @@ -170,6 +219,52 @@ describe('AiWorkspace', () => { expect(screen.getByTestId('ai-workspace-session-archived-chat')).toHaveClass('hidden') }) + it('passes vault context only to the active conversation controller', () => { + const entries = [ + makeEntry({ path: '/tmp/vault/active.md', filename: 'active.md', title: 'Active' }), + makeEntry({ path: '/tmp/vault/related.md', filename: 'related.md', title: 'Related' }), + ] + const singleConversation = render( + , + ) + const activeContextCallCount = contextReadyControllerCalls().length + singleConversation.unmount() + controllerCalls = [] + + render( + , + ) + + expect(screen.getByTestId('ai-workspace-session-active-chat')).toHaveClass('flex') + expect(screen.getByTestId('ai-workspace-session-inactive-chat')).toHaveClass('hidden') + expect(contextReadyControllerCalls()).toHaveLength(activeContextCallCount) + }) + it('shows grouped target choices without missing agents', async () => { render() diff --git a/src/components/AiWorkspace.tsx b/src/components/AiWorkspace.tsx index 0f8de1a7..d7f1f5c1 100644 --- a/src/components/AiWorkspace.tsx +++ b/src/components/AiWorkspace.tsx @@ -642,6 +642,40 @@ function WorkspaceHeader({ ) } +type ConversationSessionProps = { + active: boolean + activeEntry?: VaultEntry | null + activeNoteContent?: string | null + aiAgentsStatus: AiAgentsStatus + conversation: AiConversation + defaultAiAgentReady: boolean + entries?: VaultEntry[] + groups: AiWorkspaceTargetGroups + locale: AppLocale + mode: 'docked' | 'window' + noteList?: NoteListItem[] + noteListFilter?: { type: string | null; query: string } + onArchive: () => void + onClose: () => void + onDock?: () => void + onFileCreated?: (relativePath: string) => void + onFileModified?: (relativePath: string) => void + onOpenAiSettings?: () => void + onOpenNote?: (path: string) => void + onPopOut?: () => void + onRestoreVaultAiGuidance?: () => void + onSelectTarget: (targetId: string) => void + onStatusChange: (id: string, status: AgentStatus) => void + onTitleFromPrompt: (id: string, prompt: string) => void + onUnsupportedAiPaste?: (message: string) => void + onVaultChanged?: () => void + openTabs?: VaultEntry[] + target: AiTarget + vaultAiGuidanceStatus?: VaultAiGuidanceStatus + vaultPath: string + vaultPaths?: string[] +} + function ConversationSession({ active, activeEntry, @@ -674,39 +708,9 @@ function ConversationSession({ vaultAiGuidanceStatus, vaultPath, vaultPaths, -}: { - active: boolean - activeEntry?: VaultEntry | null - activeNoteContent?: string | null - aiAgentsStatus: AiAgentsStatus - conversation: AiConversation - defaultAiAgentReady: boolean - entries?: VaultEntry[] - groups: AiWorkspaceTargetGroups - locale: AppLocale - mode: 'docked' | 'window' - noteList?: NoteListItem[] - noteListFilter?: { type: string | null; query: string } - onArchive: () => void - onClose: () => void - onDock?: () => void - onFileCreated?: (relativePath: string) => void - onFileModified?: (relativePath: string) => void - onOpenAiSettings?: () => void - onOpenNote?: (path: string) => void - onPopOut?: () => void - onRestoreVaultAiGuidance?: () => void - onSelectTarget: (targetId: string) => void - onStatusChange: (id: string, status: AgentStatus) => void - onTitleFromPrompt: (id: string, prompt: string) => void - onUnsupportedAiPaste?: (message: string) => void - onVaultChanged?: () => void - openTabs?: VaultEntry[] - target: AiTarget - vaultAiGuidanceStatus?: VaultAiGuidanceStatus - vaultPath: string - vaultPaths?: string[] -}) { +}: ConversationSessionProps) { + const contextActiveEntry = active ? activeEntry : null + const contextEntries = active ? entries : undefined const readiness = agentReadinessForTarget(target, aiAgentsStatus) const controller = useAiPanelController({ vaultPath, @@ -715,12 +719,12 @@ function ConversationSession({ defaultAiTarget: target, defaultAiAgentReady: target.kind === 'api_model' || defaultAiAgentReady, defaultAiAgentReadiness: readiness, - activeEntry, - activeNoteContent, - entries, - openTabs, - noteList, - noteListFilter, + activeEntry: contextActiveEntry, + activeNoteContent: active ? activeNoteContent : null, + entries: contextEntries, + openTabs: active ? openTabs : undefined, + noteList: active ? noteList : undefined, + noteListFilter: active ? noteListFilter : undefined, locale, onOpenNote, onFileCreated, @@ -774,8 +778,8 @@ function ConversationSession({ defaultAiAgentReadiness={readiness} defaultAiAgentReady={aiTargetReady(target, aiAgentsStatus)} defaultAiTarget={target} - entries={entries} - activeEntry={activeEntry} + entries={contextEntries} + activeEntry={contextActiveEntry} composerControls={composerControls} interactive={active} locale={locale}