+ {model.tools && (
+
+
+ Tools
+
+
+ )}
{model.num_mmproj > 0 && (
@@ -165,10 +172,10 @@ export const ModelInfoHoverCard = ({
)}
- {model.tools && (
+ {model.num_mmproj > 0 && model.tools && (
- Tools
+ Proactive
)}
diff --git a/web-app/src/containers/__tests__/Capabilities.test.tsx b/web-app/src/containers/__tests__/Capabilities.test.tsx
new file mode 100644
index 000000000..a5e60c600
--- /dev/null
+++ b/web-app/src/containers/__tests__/Capabilities.test.tsx
@@ -0,0 +1,124 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import Capabilities from '../Capabilities'
+
+// Mock Tooltip components
+vi.mock('@/components/ui/tooltip', () => ({
+ Tooltip: ({ children }: { children: React.ReactNode }) =>
{children}
,
+ TooltipContent: ({ children }: { children: React.ReactNode }) =>
{children}
,
+ TooltipProvider: ({ children }: { children: React.ReactNode }) =>
{children}
,
+ TooltipTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
,
+}))
+
+// Mock Tabler icons
+vi.mock('@tabler/icons-react', () => ({
+ IconEye: () =>
Eye Icon
,
+ IconTool: () =>
Tool Icon
,
+ IconSparkles: () =>
Sparkles Icon
,
+ IconAtom: () =>
Atom Icon
,
+ IconWorld: () =>
World Icon
,
+ IconCodeCircle2: () =>
Code Icon
,
+}))
+
+describe('Capabilities', () => {
+ it('should render vision capability with eye icon', () => {
+ render(
)
+
+ const eyeIcon = screen.getByTestId('icon-eye')
+ expect(eyeIcon).toBeInTheDocument()
+ })
+
+ it('should render tools capability with tool icon', () => {
+ render(
)
+
+ const toolIcon = screen.getByTestId('icon-tool')
+ expect(toolIcon).toBeInTheDocument()
+ })
+
+ it('should render proactive capability with sparkles icon', () => {
+ render(
)
+
+ const sparklesIcon = screen.getByTestId('icon-sparkles')
+ expect(sparklesIcon).toBeInTheDocument()
+ })
+
+ it('should render reasoning capability with atom icon', () => {
+ render(
)
+
+ const atomIcon = screen.getByTestId('icon-atom')
+ expect(atomIcon).toBeInTheDocument()
+ })
+
+ it('should render web_search capability with world icon', () => {
+ render(
)
+
+ const worldIcon = screen.getByTestId('icon-world')
+ expect(worldIcon).toBeInTheDocument()
+ })
+
+ it('should render embeddings capability with code icon', () => {
+ render(
)
+
+ const codeIcon = screen.getByTestId('icon-code')
+ expect(codeIcon).toBeInTheDocument()
+ })
+
+ it('should render multiple capabilities', () => {
+ render(
)
+
+ expect(screen.getByTestId('icon-tool')).toBeInTheDocument()
+ expect(screen.getByTestId('icon-eye')).toBeInTheDocument()
+ expect(screen.getByTestId('icon-sparkles')).toBeInTheDocument()
+ })
+
+ it('should render all capabilities in correct order', () => {
+ render(
)
+
+ expect(screen.getByTestId('icon-tool')).toBeInTheDocument()
+ expect(screen.getByTestId('icon-eye')).toBeInTheDocument()
+ expect(screen.getByTestId('icon-sparkles')).toBeInTheDocument()
+ expect(screen.getByTestId('icon-atom')).toBeInTheDocument()
+ expect(screen.getByTestId('icon-world')).toBeInTheDocument()
+ expect(screen.getByTestId('icon-code')).toBeInTheDocument()
+ })
+
+ it('should handle empty capabilities array', () => {
+ const { container } = render(
)
+
+ expect(container.querySelector('[data-testid^="icon-"]')).not.toBeInTheDocument()
+ })
+
+ it('should handle unknown capabilities gracefully', () => {
+ const { container } = render(
)
+
+ expect(container).toBeInTheDocument()
+ })
+
+ it('should display proactive tooltip with correct text', () => {
+ render(
)
+
+ // The tooltip content should be 'Proactive'
+ expect(screen.getByTestId('icon-sparkles')).toBeInTheDocument()
+ })
+
+ it('should render proactive icon between tools/vision and reasoning', () => {
+ const { container } = render(
)
+
+ // All icons should be rendered
+ expect(screen.getByTestId('icon-tool')).toBeInTheDocument()
+ expect(screen.getByTestId('icon-eye')).toBeInTheDocument()
+ expect(screen.getByTestId('icon-sparkles')).toBeInTheDocument()
+ expect(screen.getByTestId('icon-atom')).toBeInTheDocument()
+
+ expect(container.querySelector('[data-testid="icon-sparkles"]')).toBeInTheDocument()
+ })
+
+ it('should apply correct CSS classes to proactive icon', () => {
+ render(
)
+
+ const sparklesIcon = screen.getByTestId('icon-sparkles')
+ expect(sparklesIcon).toBeInTheDocument()
+ // Icon should have size-3.5 class (same as tools, reasoning, etc.)
+ expect(sparklesIcon.parentElement).toBeInTheDocument()
+ })
+})
diff --git a/web-app/src/containers/__tests__/ChatInput.test.tsx b/web-app/src/containers/__tests__/ChatInput.test.tsx
index 642313ec7..a1c24d3e3 100644
--- a/web-app/src/containers/__tests__/ChatInput.test.tsx
+++ b/web-app/src/containers/__tests__/ChatInput.test.tsx
@@ -437,4 +437,31 @@ describe('ChatInput', () => {
expect(() => renderWithRouter()).not.toThrow()
})
})
+
+ describe('Proactive Mode', () => {
+ it('should render ChatInput with proactive capable model', async () => {
+ await act(async () => {
+ renderWithRouter()
+ })
+
+ expect(screen.getByTestId('chat-input')).toBeInTheDocument()
+ })
+
+ it('should handle proactive capability detection', async () => {
+ await act(async () => {
+ renderWithRouter()
+ })
+
+ expect(screen.getByTestId('chat-input')).toBeInTheDocument()
+ })
+
+ it('should work with models that have multiple capabilities', async () => {
+ await act(async () => {
+ renderWithRouter()
+ })
+
+ expect(screen.getByTestId('chat-input')).toBeInTheDocument()
+ })
+
+ })
})
diff --git a/web-app/src/containers/__tests__/EditModel.test.tsx b/web-app/src/containers/__tests__/EditModel.test.tsx
index 6c0dfd059..345bc91d6 100644
--- a/web-app/src/containers/__tests__/EditModel.test.tsx
+++ b/web-app/src/containers/__tests__/EditModel.test.tsx
@@ -82,6 +82,7 @@ vi.mock('@tabler/icons-react', () => ({
IconEye: () =>
,
IconTool: () =>
,
IconLoader2: () =>
,
+ IconSparkles: () =>
,
}))
describe('DialogEditModel - Basic Component Tests', () => {
@@ -189,7 +190,7 @@ describe('DialogEditModel - Basic Component Tests', () => {
{
id: 'test-model.gguf',
displayName: 'Test Model',
- capabilities: ['vision', 'tools'],
+ capabilities: ['vision', 'tools', 'proactive'],
},
],
settings: [],
@@ -226,7 +227,7 @@ describe('DialogEditModel - Basic Component Tests', () => {
{
id: 'test-model.gguf',
displayName: 'Test Model',
- capabilities: ['vision', 'tools', 'completion', 'embeddings', 'web_search', 'reasoning'],
+ capabilities: ['vision', 'tools', 'proactive', 'completion', 'embeddings', 'web_search', 'reasoning'],
},
],
settings: [],
@@ -240,7 +241,7 @@ describe('DialogEditModel - Basic Component Tests', () => {
)
// Component should render without errors even with extra capabilities
- // The capabilities helper should only extract vision and tools
+ // The capabilities helper should only extract vision, tools, and proactive
expect(container).toBeInTheDocument()
})
})
diff --git a/web-app/src/containers/dialogs/EditModel.tsx b/web-app/src/containers/dialogs/EditModel.tsx
index f7dec06eb..78f6e93c2 100644
--- a/web-app/src/containers/dialogs/EditModel.tsx
+++ b/web-app/src/containers/dialogs/EditModel.tsx
@@ -17,6 +17,7 @@ import {
IconTool,
IconAlertTriangle,
IconLoader2,
+ IconSparkles,
} from '@tabler/icons-react'
import { useState, useEffect } from 'react'
import { useTranslation } from '@/i18n/react-i18next-compat'
@@ -45,6 +46,7 @@ export const DialogEditModel = ({
const [capabilities, setCapabilities] = useState
>({
vision: false,
tools: false,
+ proactive: false,
})
// Initialize with the provided model ID or the first model if available
@@ -67,6 +69,7 @@ export const DialogEditModel = ({
const capabilitiesToObject = (capabilitiesList: string[]) => ({
vision: capabilitiesList.includes('vision'),
tools: capabilitiesList.includes('tools'),
+ proactive: capabilitiesList.includes('proactive'),
})
// Initialize capabilities and display name from selected model
@@ -268,6 +271,23 @@ export const DialogEditModel = ({
disabled={isLoading}
/>