feat: web add search button for extension (#6671)

* add search button for web extension

* change button color and behavior

* Update extensions-web/src/mcp-web/components/WebSearchButton.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Dinh Long Nguyen 2025-09-30 21:39:08 +07:00 committed by GitHub
parent b3fc64049a
commit f33c2c205a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 263 additions and 69 deletions

View File

@ -27,11 +27,13 @@
"devDependencies": {
"@npmcli/arborist": "^7.1.0",
"@types/node": "^22.10.0",
"@types/react": "19.1.2",
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.8",
"eslint": "8.57.0",
"happy-dom": "^15.11.6",
"pacote": "^21.0.0",
"react": "19.0.0",
"request": "^2.88.2",
"request-progress": "^3.0.0",
"rimraf": "^6.0.1",
@ -44,5 +46,8 @@
"rxjs": "^7.8.1",
"ulidx": "^2.3.0"
},
"peerDependencies": {
"react": "19.0.0"
},
"packageManager": "yarn@4.5.3"
}

View File

@ -10,7 +10,7 @@ export default defineConfig([
sourcemap: true,
},
platform: 'browser',
external: ['path'],
external: ['path', 'react', 'react-dom', 'react/jsx-runtime'],
define: {
NODE: JSON.stringify(`${pkgJson.name}/${pkgJson.node}`),
VERSION: JSON.stringify(pkgJson.version),

View File

@ -1,5 +1,6 @@
import { MCPInterface, MCPTool, MCPToolCallResult } from '../../types'
import { MCPInterface, MCPTool, MCPToolCallResult, MCPToolComponentProps } from '../../types'
import { BaseExtension, ExtensionTypeEnum } from '../extension'
import type { ComponentType } from 'react'
/**
* MCP (Model Context Protocol) extension for managing tools and server communication.
@ -18,4 +19,10 @@ export abstract class MCPExtension extends BaseExtension implements MCPInterface
abstract getConnectedServers(): Promise<string[]>
abstract refreshTools(): Promise<void>
abstract isHealthy(): Promise<boolean>
/**
* Optional method to provide a custom UI component for tools
* @returns A React component or null if no custom component is provided
*/
getToolComponent?(): ComponentType<MCPToolComponentProps> | null
}

View File

@ -21,4 +21,18 @@ export interface MCPServerInfo {
name: string
connected: boolean
tools?: MCPTool[]
}
/**
* Props for MCP tool UI components
*/
export interface MCPToolComponentProps {
/** List of available MCP tools */
tools: MCPTool[]
/** Function to check if a specific tool is currently enabled */
isToolEnabled: (toolName: string) => boolean
/** Function to toggle a tool's enabled/disabled state */
onToolToggle: (toolName: string, enabled: boolean) => void
}

View File

@ -22,6 +22,9 @@
},
"devDependencies": {
"@janhq/core": "workspace:*",
"@tabler/icons-react": "^3.34.0",
"@types/react": "19.1.2",
"react": "19.0.0",
"typescript": "5.9.2",
"vite": "5.4.20",
"vitest": "2.1.9",
@ -29,6 +32,8 @@
},
"peerDependencies": {
"@janhq/core": "*",
"@tabler/icons-react": "*",
"react": "19.0.0",
"zustand": "5.0.3"
},
"dependencies": {

View File

@ -0,0 +1,54 @@
import { useMemo, useCallback } from 'react'
import { IconWorld } from '@tabler/icons-react'
import { MCPToolComponentProps } from '@janhq/core'
// List of tool names considered as web search tools
const WEB_SEARCH_TOOL_NAMES = ['google_search', 'scrape'];
export const WebSearchButton = ({
tools,
isToolEnabled,
onToolToggle,
}: MCPToolComponentProps) => {
const webSearchTools = useMemo(
() => tools.filter((tool) => WEB_SEARCH_TOOL_NAMES.includes(tool.name)),
[tools]
)
// Early return if no web search tools available
if (webSearchTools.length === 0) {
return null
}
// Check if all web search tools are enabled
const isEnabled = useMemo(
() => webSearchTools.every((tool) => isToolEnabled(tool.name)),
[webSearchTools, isToolEnabled]
)
const handleToggle = useCallback(() => {
// Toggle all web search tools at once
const newState = !isEnabled
webSearchTools.forEach((tool) => {
onToolToggle(tool.name, newState)
})
}, [isEnabled, webSearchTools, onToolToggle])
return (
<button
onClick={handleToggle}
className={`h-7 px-2 py-1 flex items-center justify-center rounded-md transition-all duration-200 ease-in-out gap-1 cursor-pointer ml-0.5 border-0 ${
isEnabled
? 'bg-accent/20 text-accent'
: 'bg-transparent text-main-view-fg/70 hover:bg-main-view-fg/5'
}`}
title={isEnabled ? 'Disable Web Search' : 'Enable Web Search'}
>
<IconWorld
size={16}
className={isEnabled ? 'text-accent' : 'text-main-view-fg/70'}
/>
<span className={`text-sm font-medium ${isEnabled ? 'text-accent' : ''}`}>Search</span>
</button>
)
}

View File

@ -0,0 +1 @@
export { WebSearchButton } from './WebSearchButton'

View File

@ -4,11 +4,13 @@
* Uses official MCP TypeScript SDK with proper session handling
*/
import { MCPExtension, MCPTool, MCPToolCallResult } from '@janhq/core'
import { MCPExtension, MCPTool, MCPToolCallResult, MCPToolComponentProps } from '@janhq/core'
import { getSharedAuthService, JanAuthService } from '../shared'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { JanMCPOAuthProvider } from './oauth-provider'
import { WebSearchButton } from './components'
import type { ComponentType } from 'react'
// JAN_API_BASE is defined in vite.config.ts (defaults to 'https://api-dev.jan.ai/jan/v1')
declare const JAN_API_BASE: string
@ -232,4 +234,12 @@ export default class MCPExtensionWeb extends MCPExtension {
throw error
}
}
/**
* Provides a custom UI component for web search tools
* @returns The WebSearchButton component
*/
getToolComponent(): ComponentType<MCPToolComponentProps> | null {
return WebSearchButton
}
}

View File

@ -3,6 +3,7 @@
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"strict": true,

View File

@ -9,7 +9,7 @@ export default defineConfig({
fileName: 'index'
},
rollupOptions: {
external: ['@janhq/core', 'zustand']
external: ['@janhq/core', 'zustand', 'react', 'react-dom', 'react/jsx-runtime', '@tabler/icons-react']
},
emptyOutDir: false // Don't clean the output directory
},

View File

@ -38,6 +38,9 @@ import { useTools } from '@/hooks/useTools'
import { TokenCounter } from '@/components/TokenCounter'
import { useMessages } from '@/hooks/useMessages'
import { useShallow } from 'zustand/react/shallow'
import { McpExtensionToolLoader } from './McpExtensionToolLoader'
import { ExtensionTypeEnum, MCPExtension } from '@janhq/core'
import { ExtensionManager } from '@/lib/extension'
type ChatInputProps = {
className?: string
@ -171,6 +174,12 @@ const ChatInput = ({
// Check if there are active MCP servers
const hasActiveMCPServers = connectedServers.length > 0 || tools.length > 0
// Get MCP extension and its custom component
const extensionManager = ExtensionManager.getInstance()
const mcpExtension = extensionManager.get<MCPExtension>(ExtensionTypeEnum.MCP)
const MCPToolComponent = mcpExtension?.getToolComponent?.()
const handleSendMesage = (prompt: string) => {
if (!selectedModel) {
setMessage('Please select a model to start chatting.')
@ -719,60 +728,72 @@ const ChatInput = ({
{selectedModel?.capabilities?.includes('tools') &&
hasActiveMCPServers && (
<TooltipProvider>
<Tooltip
open={tooltipToolsAvailable}
onOpenChange={setTooltipToolsAvailable}
>
<TooltipTrigger
asChild
disabled={dropdownToolsAvailable}
MCPToolComponent ? (
// Use custom MCP component
<McpExtensionToolLoader
tools={tools}
hasActiveMCPServers={hasActiveMCPServers}
selectedModelHasTools={selectedModel?.capabilities?.includes('tools') ?? false}
initialMessage={initialMessage}
MCPToolComponent={MCPToolComponent}
/>
) : (
// Use default tools dropdown
<TooltipProvider>
<Tooltip
open={tooltipToolsAvailable}
onOpenChange={setTooltipToolsAvailable}
>
<div
onClick={(e) => {
setDropdownToolsAvailable(false)
e.stopPropagation()
}}
<TooltipTrigger
asChild
disabled={dropdownToolsAvailable}
>
<DropdownToolsAvailable
initialMessage={initialMessage}
onOpenChange={(isOpen) => {
setDropdownToolsAvailable(isOpen)
if (isOpen) {
setTooltipToolsAvailable(false)
}
<div
onClick={(e) => {
setDropdownToolsAvailable(false)
e.stopPropagation()
}}
>
{(isOpen, toolsCount) => {
return (
<div
className={cn(
'h-7 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1 cursor-pointer relative',
isOpen && 'bg-main-view-fg/10'
)}
>
<IconTool
size={18}
className="text-main-view-fg/50"
/>
{toolsCount > 0 && (
<div className="absolute -top-2 -right-2 bg-accent text-accent-fg text-xs rounded-full size-5 flex items-center justify-center font-medium">
<span className="leading-0 text-xs">
{toolsCount > 99 ? '99+' : toolsCount}
</span>
</div>
)}
</div>
)
}}
</DropdownToolsAvailable>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{t('tools')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownToolsAvailable
initialMessage={initialMessage}
onOpenChange={(isOpen) => {
setDropdownToolsAvailable(isOpen)
if (isOpen) {
setTooltipToolsAvailable(false)
}
}}
>
{(isOpen, toolsCount) => {
return (
<div
className={cn(
'h-7 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1 cursor-pointer relative',
isOpen && 'bg-main-view-fg/10'
)}
>
<IconTool
size={18}
className="text-main-view-fg/50"
/>
{toolsCount > 0 && (
<div className="absolute -top-2 -right-2 bg-accent text-accent-fg text-xs rounded-full size-5 flex items-center justify-center font-medium">
<span className="leading-0 text-xs">
{toolsCount > 99 ? '99+' : toolsCount}
</span>
</div>
)}
</div>
)
}}
</DropdownToolsAvailable>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{t('tools')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
)}
{selectedModel?.capabilities?.includes('web_search') && (
<TooltipProvider>

View File

@ -0,0 +1,61 @@
import { ComponentType } from 'react'
import { MCPTool, MCPToolComponentProps } from '@janhq/core'
import { useToolAvailable } from '@/hooks/useToolAvailable'
import { useThreads } from '@/hooks/useThreads'
interface McpExtensionToolLoaderProps {
tools: MCPTool[]
hasActiveMCPServers: boolean
selectedModelHasTools: boolean
initialMessage?: boolean
MCPToolComponent?: ComponentType<MCPToolComponentProps> | null
}
export const McpExtensionToolLoader = ({
tools,
hasActiveMCPServers,
selectedModelHasTools,
initialMessage,
MCPToolComponent,
}: McpExtensionToolLoaderProps) => {
// Get tool management hooks
const { isToolDisabled, setToolDisabledForThread, setDefaultDisabledTools, getDefaultDisabledTools } = useToolAvailable()
const { getCurrentThread } = useThreads()
const currentThread = getCurrentThread()
// Handle tool toggle for custom component
const handleToolToggle = (toolName: string, enabled: boolean) => {
if (initialMessage) {
const currentDefaults = getDefaultDisabledTools()
if (enabled) {
setDefaultDisabledTools(currentDefaults.filter((name) => name !== toolName))
} else {
setDefaultDisabledTools([...currentDefaults, toolName])
}
} else if (currentThread?.id) {
setToolDisabledForThread(currentThread.id, toolName, enabled)
}
}
const isToolEnabled = (toolName: string): boolean => {
if (initialMessage) {
return !getDefaultDisabledTools().includes(toolName)
} else if (currentThread?.id) {
return !isToolDisabled(currentThread.id, toolName)
}
return false
}
// Only render if we have the custom MCP component and conditions are met
if (!selectedModelHasTools || !hasActiveMCPServers || !MCPToolComponent) {
return null
}
return (
<MCPToolComponent
tools={tools}
isToolEnabled={isToolEnabled}
onToolToggle={handleToolToggle}
/>
)
}

View File

@ -3453,45 +3453,40 @@ __metadata:
languageName: node
linkType: hard
"@jan/extensions-web@link:../extensions-web::locator=%40janhq%2Fweb-app%40workspace%3Aweb-app":
version: 0.0.0-use.local
resolution: "@jan/extensions-web@link:../extensions-web::locator=%40janhq%2Fweb-app%40workspace%3Aweb-app"
languageName: node
linkType: soft
"@jan/extensions-web@workspace:extensions-web":
"@jan/extensions-web@workspace:*, @jan/extensions-web@workspace:extensions-web":
version: 0.0.0-use.local
resolution: "@jan/extensions-web@workspace:extensions-web"
dependencies:
"@janhq/core": "workspace:*"
"@modelcontextprotocol/sdk": "npm:1.17.5"
"@tabler/icons-react": "npm:^3.34.0"
"@types/react": "npm:19.1.2"
react: "npm:19.0.0"
typescript: "npm:5.9.2"
vite: "npm:5.4.20"
vitest: "npm:2.1.9"
zustand: "npm:5.0.8"
peerDependencies:
"@janhq/core": "*"
"@tabler/icons-react": "*"
react: 19.0.0
zustand: 5.0.3
languageName: unknown
linkType: soft
"@janhq/core@link:../core::locator=%40janhq%2Fweb-app%40workspace%3Aweb-app":
version: 0.0.0-use.local
resolution: "@janhq/core@link:../core::locator=%40janhq%2Fweb-app%40workspace%3Aweb-app"
languageName: node
linkType: soft
"@janhq/core@workspace:*, @janhq/core@workspace:core":
version: 0.0.0-use.local
resolution: "@janhq/core@workspace:core"
dependencies:
"@npmcli/arborist": "npm:^7.1.0"
"@types/node": "npm:^22.10.0"
"@types/react": "npm:19.1.2"
"@vitest/coverage-v8": "npm:^2.1.8"
"@vitest/ui": "npm:^2.1.8"
eslint: "npm:8.57.0"
happy-dom: "npm:^15.11.6"
pacote: "npm:^21.0.0"
react: "npm:19.0.0"
request: "npm:^2.88.2"
request-progress: "npm:^3.0.0"
rimraf: "npm:^6.0.1"
@ -3501,6 +3496,8 @@ __metadata:
typescript: "npm:^5.8.3"
ulidx: "npm:^2.3.0"
vitest: "npm:^2.1.8"
peerDependencies:
react: 19.0.0
languageName: unknown
linkType: soft
@ -3512,8 +3509,8 @@ __metadata:
"@dnd-kit/modifiers": "npm:9.0.0"
"@dnd-kit/sortable": "npm:10.0.0"
"@eslint/js": "npm:8.57.0"
"@jan/extensions-web": "link:../extensions-web"
"@janhq/core": "link:../core"
"@jan/extensions-web": "workspace:*"
"@janhq/core": "workspace:*"
"@radix-ui/react-accordion": "npm:1.2.11"
"@radix-ui/react-avatar": "npm:1.1.10"
"@radix-ui/react-dialog": "npm:1.1.15"
@ -7022,6 +7019,17 @@ __metadata:
languageName: node
linkType: hard
"@tabler/icons-react@npm:^3.34.0":
version: 3.35.0
resolution: "@tabler/icons-react@npm:3.35.0"
dependencies:
"@tabler/icons": "npm:3.35.0"
peerDependencies:
react: ">= 16"
checksum: 10c0/8d280fcdae00916b001142ba0800ea05d8fa2acdcbd82f88a299b4141fb941237be2e826b86b1af710e038b4f8bb6f76f452c3309c29fd62398b4d5789c2b3e0
languageName: node
linkType: hard
"@tabler/icons@npm:3.34.0":
version: 3.34.0
resolution: "@tabler/icons@npm:3.34.0"
@ -7029,6 +7037,13 @@ __metadata:
languageName: node
linkType: hard
"@tabler/icons@npm:3.35.0":
version: 3.35.0
resolution: "@tabler/icons@npm:3.35.0"
checksum: 10c0/93098828128ffed2cf412b39bd78992f93f25b22349a4e04523d2a018b7fe376ddeff105babcc3efedd707aa00b705425c7d9f598d6987552a563c62125795a2
languageName: node
linkType: hard
"@tailwindcss/node@npm:4.1.4":
version: 4.1.4
resolution: "@tailwindcss/node@npm:4.1.4"