fix: avoid inactive AI chat context work

This commit is contained in:
lucaronin
2026-05-27 19:23:06 +02:00
parent 03a667661e
commit 41b531e899
2 changed files with 159 additions and 60 deletions

View File

@@ -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> = {}): 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(
<AiWorkspace
open
mode="docked"
aiAgentsStatus={installedStatuses()}
aiModelProviders={providers}
activeEntry={entries[0]}
entries={entries}
conversationSettings={[
{ id: 'active-chat', title: 'Live Chat', target_id: null, archived: false },
]}
vaultPath="/tmp/vault"
onClose={vi.fn()}
/>,
)
const activeContextCallCount = contextReadyControllerCalls().length
singleConversation.unmount()
controllerCalls = []
render(
<AiWorkspace
open
mode="docked"
aiAgentsStatus={installedStatuses()}
aiModelProviders={providers}
activeEntry={entries[0]}
entries={entries}
conversationSettings={[
{ id: 'active-chat', title: 'Live Chat', target_id: null, archived: false },
{ id: 'inactive-chat', title: 'Later Chat', target_id: null, archived: false },
]}
vaultPath="/tmp/vault"
onClose={vi.fn()}
/>,
)
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(<AiWorkspace open mode="docked" aiAgentsStatus={installedStatuses()} aiModelProviders={providers} vaultPath="/tmp/vault" onClose={vi.fn()} />)

View File

@@ -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}