diff --git a/src/components/AiPanel.test.tsx b/src/components/AiPanel.test.tsx index 6da94f03..c9657102 100644 --- a/src/components/AiPanel.test.tsx +++ b/src/components/AiPanel.test.tsx @@ -1,7 +1,8 @@ import { describe, it, expect, vi } from 'vitest' -import { render, screen, fireEvent, act } from '@testing-library/react' +import { render as rtlRender, screen, fireEvent, act } from '@testing-library/react' import { AiPanel } from './AiPanel' import { UNSUPPORTED_INLINE_PASTE_MESSAGE } from './InlineWikilinkInput' +import { TooltipProvider } from '@/components/ui/tooltip' import type { VaultEntry } from '../types' import { queueAiPrompt } from '../utils/aiPromptBridge' import { bindVaultConfigStore, getVaultConfig, resetVaultConfigStore } from '../utils/vaultConfigStore' @@ -67,6 +68,10 @@ const makeEntry = (overrides: Partial = {}): VaultEntry => ({ ...overrides, }) +function render(ui: Parameters[0]) { + return rtlRender(ui, { wrapper: TooltipProvider }) +} + describe('AiPanel', () => { beforeEach(() => { mockMessages = [] @@ -127,7 +132,7 @@ describe('AiPanel', () => { }] render() - fireEvent.click(screen.getByRole('button', { name: 'Power User' })) + fireEvent.click(screen.getByRole('radio', { name: 'Power User' })) expect(getVaultConfig().ai_agent_permission_mode).toBe('power_user') expect(save).toHaveBeenLastCalledWith(expect.objectContaining({ @@ -143,8 +148,26 @@ describe('AiPanel', () => { render() - expect(screen.getByRole('button', { name: 'Vault Safe' })).toBeDisabled() - expect(screen.getByRole('button', { name: 'Power User' })).toBeDisabled() + expect(screen.getByRole('radio', { name: 'Vault Safe' })).toBeDisabled() + expect(screen.getByRole('radio', { name: 'Power User' })).toBeDisabled() + }) + + it('renders the permission mode toggle with high contrast selected state and explanatory tooltip', async () => { + render() + + expect(screen.getByTestId('ai-permission-mode-toggle')).toHaveClass('border', 'bg-muted') + + const safeMode = screen.getByRole('radio', { name: 'Vault Safe' }) + const powerUserMode = screen.getByRole('radio', { name: 'Power User' }) + expect(safeMode).toHaveAttribute('aria-checked', 'true') + expect(safeMode).toHaveClass('bg-background', 'text-foreground', 'shadow-xs') + expect(powerUserMode).toHaveClass('text-muted-foreground') + + fireEvent.focus(safeMode) + + expect(await screen.findByTestId('ai-permission-mode-tooltip')).toHaveTextContent( + 'Vault Safe keeps agents to file, search, and edit tools. Power User also allows local shell commands for this vault.', + ) }) it('renders data-testid ai-panel', () => { diff --git a/src/components/AiPanelChrome.tsx b/src/components/AiPanelChrome.tsx index 2172145e..ffdab2e5 100644 --- a/src/components/AiPanelChrome.tsx +++ b/src/components/AiPanelChrome.tsx @@ -3,6 +3,8 @@ import { Robot, X, PaperPlaneRight, Plus, Link } from '@phosphor-icons/react' import { Copy } from 'lucide-react' import { AiMessage } from './AiMessage' import { Button } from '@/components/ui/button' +import { ActionTooltip } from '@/components/ui/action-tooltip' +import { TooltipProvider } from '@/components/ui/tooltip' import { WikilinkChatInput } from './WikilinkChatInput' import { extractInlineWikilinkReferences } from './inlineWikilinkText' import { @@ -14,6 +16,10 @@ import type { AiAgentReadiness } from '../lib/aiAgents' import type { NoteReference } from '../utils/ai-context' import type { VaultEntry } from '../types' +const AI_PERMISSION_MODE_TOOLTIP = { + label: 'Vault Safe keeps agents to file, search, and edit tools. Power User also allows local shell commands for this vault.', +} + interface AiPanelHeaderProps { agentLabel: string agentReadiness: AiAgentReadiness @@ -210,30 +216,43 @@ function AiPermissionModeToggle({ onChange: (mode: AiAgentPermissionMode) => void }) { return ( -
- {(['safe', 'power_user'] as const).map((mode) => { - const selected = value === mode - return ( - - ) - })} -
+ +
+ {(['safe', 'power_user'] as const).map((mode) => { + const selected = value === mode + return ( + + + + ) + })} +
+
) }