Merge remote-tracking branch 'origin/feat/agents-new' into feat/agents-new

This commit is contained in:
Vaayne 2025-09-23 12:16:58 +08:00
commit 1ec81f9e75
13 changed files with 577 additions and 534 deletions

View File

@ -5,15 +5,22 @@ import React from 'react'
export interface ModelLabelProps extends Omit<React.ComponentPropsWithRef<'div'>, 'children'> { export interface ModelLabelProps extends Omit<React.ComponentPropsWithRef<'div'>, 'children'> {
model?: ApiModel model?: ApiModel
classNames?: {
container?: string
avatar?: string
modelName?: string
divider?: string
providerName?: string
}
} }
export const ApiModelLabel: React.FC<ModelLabelProps> = ({ model, className, ...props }) => { export const ApiModelLabel: React.FC<ModelLabelProps> = ({ model, className, classNames, ...props }) => {
return ( return (
<div className={cn('flex items-center gap-1', className)} {...props}> <div className={cn('flex items-center gap-1', className, classNames?.container)} {...props}>
<Avatar src={model ? getModelLogo(model.id) : undefined} className="h-4 w-4" /> <Avatar src={model ? getModelLogo(model.id) : undefined} className={cn('h-4 w-4', classNames?.avatar)} />
<span> <span className={classNames?.modelName}>{model?.name}</span>
{model?.name} | {model?.provider_name} <span className={classNames?.divider}> | </span>
</span> <span className={classNames?.providerName}>{model?.provider_name}</span>
</div> </div>
) )
} }

View File

@ -1,6 +1,5 @@
import { import {
Button, Button,
Chip,
cn, cn,
Form, Form,
Input, Input,
@ -18,6 +17,7 @@ import {
} from '@heroui/react' } from '@heroui/react'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import type { Selection } from '@react-types/shared' import type { Selection } from '@react-types/shared'
import { AllowedToolsSelect } from '@renderer/components/agent'
import { getModelLogo } from '@renderer/config/models' import { getModelLogo } from '@renderer/config/models'
import { useAgent } from '@renderer/hooks/agents/useAgent' import { useAgent } from '@renderer/hooks/agents/useAgent'
import { useApiModels } from '@renderer/hooks/agents/useModels' import { useApiModels } from '@renderer/hooks/agents/useModels'
@ -198,21 +198,6 @@ export const SessionModal: React.FC<Props> = ({
[availableTools] [availableTools]
) )
const renderSelectedTools = useCallback((items: SelectedItems<Tool>) => {
if (!items.length) {
return null
}
return (
<div className="flex flex-wrap gap-2">
{items.map((item) => (
<Chip key={item.key} size="sm" variant="flat" className="max-w-[160px] truncate">
{item.data?.name ?? item.textValue ?? item.key}
</Chip>
))}
</div>
)
}, [])
const modelOptions = useMemo(() => { const modelOptions = useMemo(() => {
// mocked data. not final version // mocked data. not final version
return (models ?? []).map((model) => ({ return (models ?? []).map((model) => ({
@ -363,32 +348,11 @@ export const SessionModal: React.FC<Props> = ({
value={form.description ?? ''} value={form.description ?? ''}
onValueChange={onDescChange} onValueChange={onDescChange}
/> />
<Select <AllowedToolsSelect
selectionMode="multiple" items={availableTools}
isMultiline
selectedKeys={selectedToolKeys} selectedKeys={selectedToolKeys}
onSelectionChange={onAllowedToolsChange} onSelectionChange={onAllowedToolsChange}
label={t('agent.session.allowed_tools.label')} />
placeholder={t('agent.session.allowed_tools.placeholder')}
description={
availableTools.length
? t('agent.session.allowed_tools.helper')
: t('agent.session.allowed_tools.empty')
}
isDisabled={!availableTools.length}
items={availableTools}
renderValue={renderSelectedTools}>
{(tool) => (
<SelectItem key={tool.id} textValue={tool.name}>
<div className="flex flex-col">
<span className="font-medium text-sm">{tool.name}</span>
{tool.description ? (
<span className="text-foreground-500 text-xs">{tool.description}</span>
) : null}
</div>
</SelectItem>
)}
</Select>
<Textarea label={t('common.prompt')} value={form.instructions ?? ''} onValueChange={onInstChange} /> <Textarea label={t('common.prompt')} value={form.instructions ?? ''} onValueChange={onInstChange} />
</ModalBody> </ModalBody>
<ModalFooter className="w-full"> <ModalFooter className="w-full">

View File

@ -0,0 +1,54 @@
import { Chip, cn, Select, SelectedItems, SelectItem, SelectProps } from '@heroui/react'
import { Tool } from '@renderer/types'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
export interface AllowedToolsSelectProps extends Omit<SelectProps, 'children'> {
items: Tool[]
}
export const AllowedToolsSelect: React.FC<AllowedToolsSelectProps> = (props) => {
const { t } = useTranslation()
const { items: availableTools, className, ...rest } = props
const renderSelectedTools = useCallback((items: SelectedItems<Tool>) => {
if (!items.length) {
return null
}
return (
<div className="flex flex-wrap gap-2">
{items.map((item) => (
<Chip key={item.key} size="sm" variant="flat" className="max-w-[160px] truncate">
{item.data?.name ?? item.textValue ?? item.key}
</Chip>
))}
</div>
)
}, [])
return (
<Select
aria-label={t('agent.session.allowed_tools.label')}
selectionMode="multiple"
isMultiline
label={t('agent.session.allowed_tools.label')}
placeholder={t('agent.session.allowed_tools.placeholder')}
description={
availableTools.length ? t('agent.session.allowed_tools.helper') : t('agent.session.allowed_tools.empty')
}
isDisabled={!availableTools.length}
items={availableTools}
renderValue={renderSelectedTools}
className={cn('max-w-xl', className)}
{...rest}>
{(tool) => (
<SelectItem key={tool.id} textValue={tool.name}>
<div className="flex flex-col">
<span className="font-medium text-sm">{tool.name}</span>
{tool.description ? <span className="text-foreground-500 text-xs">{tool.description}</span> : null}
</div>
</SelectItem>
)}
</Select>
)
}

View File

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

View File

@ -1,10 +1,13 @@
import { ApiModelsFilter } from '@renderer/types'
import { useApiModels } from './useModels' import { useApiModels } from './useModels'
export type UseModelProps = { export type UseModelProps = {
id: string id?: string
filter?: ApiModelsFilter
} }
export const useApiModel = (id?: string) => { export const useApiModel = ({ id, filter }: UseModelProps) => {
const { models } = useApiModels() const { models } = useApiModels(filter)
return models.find((model) => model.id === id) return models.find((model) => model.id === id)
} }

View File

@ -100,6 +100,7 @@
"title": "[to be translated]:高级设置" "title": "[to be translated]:高级设置"
}, },
"essential": "Βασικές Ρυθμίσεις", "essential": "Βασικές Ρυθμίσεις",
"mcps": "Διακομιστής MCP",
"prompt": "Ρυθμίσεις Προτροπής", "prompt": "Ρυθμίσεις Προτροπής",
"tooling": { "tooling": {
"mcp": { "mcp": {
@ -172,12 +173,12 @@
"tab": "[to be translated]:Tooling & permissions" "tab": "[to be translated]:Tooling & permissions"
}, },
"tools": { "tools": {
"approved": "[to be translated]:approved", "approved": "εγκεκριμένο",
"caution": "[to be translated]:Pre-approved tools bypass human review. Enable only trusted tools.", "caution": "Εργαλεία προεγκεκριμένα παρακάμπτουν την ανθρώπινη αξιολόγηση. Ενεργοποιήστε μόνο έμπιστα εργαλεία.",
"description": "[to be translated]:Choose which tools can run without manual approval.", "description": "Επιλέξτε ποια εργαλεία μπορούν να εκτελούνται χωρίς χειροκίνητη έγκριση.",
"requiresPermission": "[to be translated]:Requires permission when not pre-approved.", "requiresPermission": "Απαιτείται άδεια όταν δεν έχει προεγκριθεί.",
"tab": "[to be translated]:Pre-approved tools", "tab": "Προεγκεκριμένα εργαλεία",
"title": "[to be translated]:Pre-approved tools", "title": "Προεγκεκριμένα εργαλεία",
"toggle": "{{defaultValue}}" "toggle": "{{defaultValue}}"
} }
}, },

View File

@ -100,6 +100,7 @@
"title": "[to be translated]:高级设置" "title": "[to be translated]:高级设置"
}, },
"essential": "Configuraciones esenciales", "essential": "Configuraciones esenciales",
"mcps": "Servidor MCP",
"prompt": "Configuración de indicaciones", "prompt": "Configuración de indicaciones",
"tooling": { "tooling": {
"mcp": { "mcp": {
@ -172,12 +173,12 @@
"tab": "[to be translated]:Tooling & permissions" "tab": "[to be translated]:Tooling & permissions"
}, },
"tools": { "tools": {
"approved": "[to be translated]:approved", "approved": "aprobado",
"caution": "[to be translated]:Pre-approved tools bypass human review. Enable only trusted tools.", "caution": "Herramientas preaprobadas omiten la revisión humana. Habilita solo herramientas de confianza.",
"description": "[to be translated]:Choose which tools can run without manual approval.", "description": "Elige qué herramientas pueden ejecutarse sin aprobación manual.",
"requiresPermission": "[to be translated]:Requires permission when not pre-approved.", "requiresPermission": "Requiere permiso cuando no está preaprobado.",
"tab": "[to be translated]:Pre-approved tools", "tab": "Herramientas preaprobadas",
"title": "[to be translated]:Pre-approved tools", "title": "Herramientas preaprobadas",
"toggle": "{{defaultValue}}" "toggle": "{{defaultValue}}"
} }
}, },

View File

@ -100,6 +100,7 @@
"title": "[to be translated]:高级设置" "title": "[to be translated]:高级设置"
}, },
"essential": "Paramètres essentiels", "essential": "Paramètres essentiels",
"mcps": "Serveur MCP",
"prompt": "Paramètres de l'invite", "prompt": "Paramètres de l'invite",
"tooling": { "tooling": {
"mcp": { "mcp": {
@ -172,12 +173,12 @@
"tab": "[to be translated]:Tooling & permissions" "tab": "[to be translated]:Tooling & permissions"
}, },
"tools": { "tools": {
"approved": "[to be translated]:approved", "approved": "approuvé",
"caution": "[to be translated]:Pre-approved tools bypass human review. Enable only trusted tools.", "caution": "Outils pré-approuvés contournent la révision humaine. Activez uniquement les outils de confiance.",
"description": "[to be translated]:Choose which tools can run without manual approval.", "description": "Choisissez quels outils peuvent s'exécuter sans approbation manuelle.",
"requiresPermission": "[to be translated]:Requires permission when not pre-approved.", "requiresPermission": "Nécessite une autorisation lorsqu'elle n'est pas préapprouvée.",
"tab": "[to be translated]:Pre-approved tools", "tab": "Outils pré-approuvés",
"title": "[to be translated]:Pre-approved tools", "title": "Outils pré-approuvés",
"toggle": "{{defaultValue}}" "toggle": "{{defaultValue}}"
} }
}, },

View File

@ -100,6 +100,7 @@
"title": "[to be translated]:高级设置" "title": "[to be translated]:高级设置"
}, },
"essential": "必須設定", "essential": "必須設定",
"mcps": "MCPサーバー",
"prompt": "プロンプト設定", "prompt": "プロンプト設定",
"tooling": { "tooling": {
"mcp": { "mcp": {

View File

@ -100,6 +100,7 @@
"title": "[to be translated]:高级设置" "title": "[to be translated]:高级设置"
}, },
"essential": "Configurações Essenciais", "essential": "Configurações Essenciais",
"mcps": "Servidor MCP",
"prompt": "Configurações de Prompt", "prompt": "Configurações de Prompt",
"tooling": { "tooling": {
"mcp": { "mcp": {
@ -172,12 +173,12 @@
"tab": "[to be translated]:Tooling & permissions" "tab": "[to be translated]:Tooling & permissions"
}, },
"tools": { "tools": {
"approved": "[to be translated]:approved", "approved": "aprovado",
"caution": "[to be translated]:Pre-approved tools bypass human review. Enable only trusted tools.", "caution": "Ferramentas pré-aprovadas ignoram a revisão humana. Ative apenas ferramentas confiáveis.",
"description": "[to be translated]:Choose which tools can run without manual approval.", "description": "Escolha quais ferramentas podem ser executadas sem aprovação manual.",
"requiresPermission": "[to be translated]:Requires permission when not pre-approved.", "requiresPermission": "Requer permissão quando não pré-aprovado.",
"tab": "[to be translated]:Pre-approved tools", "tab": "Ferramentas pré-aprovadas",
"title": "[to be translated]:Pre-approved tools", "title": "Ferramentas pré-aprovadas",
"toggle": "{{defaultValue}}" "toggle": "{{defaultValue}}"
} }
}, },

View File

@ -100,6 +100,7 @@
"title": "[to be translated]:高级设置" "title": "[to be translated]:高级设置"
}, },
"essential": "Основные настройки", "essential": "Основные настройки",
"mcps": "MCP сервер",
"prompt": "Настройки подсказки", "prompt": "Настройки подсказки",
"tooling": { "tooling": {
"mcp": { "mcp": {
@ -172,12 +173,12 @@
"tab": "[to be translated]:Tooling & permissions" "tab": "[to be translated]:Tooling & permissions"
}, },
"tools": { "tools": {
"approved": "[to be translated]:approved", "approved": "одобрено",
"caution": "[to be translated]:Pre-approved tools bypass human review. Enable only trusted tools.", "caution": "Предварительно одобренные инструменты обходят проверку человеком. Включайте только доверенные инструменты.",
"description": "[to be translated]:Choose which tools can run without manual approval.", "description": "Выберите, какие инструменты могут запускаться без ручного подтверждения.",
"requiresPermission": "[to be translated]:Requires permission when not pre-approved.", "requiresPermission": "Требуется разрешение, если не предварительно одобрено.",
"tab": "[to be translated]:Pre-approved tools", "tab": "Предварительно одобренные инструменты",
"title": "[to be translated]:Pre-approved tools", "title": "Предварительно одобренные инструменты",
"toggle": "{{defaultValue}}" "toggle": "{{defaultValue}}"
} }
}, },

View File

@ -41,7 +41,8 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
const { chat } = useRuntime() const { chat } = useRuntime()
const { activeTopicOrSession, activeAgentId } = chat const { activeTopicOrSession, activeAgentId } = chat
const { agent } = useAgent(activeAgentId) const { agent } = useAgent(activeAgentId)
const agentModel = useApiModel(agent?.model) // TODO: filter is temporally for agent since it cannot get all models once
const agentModel = useApiModel({ id: agent?.model, filter: { providerType: 'anthropic' } })
useShortcut('toggle_show_assistants', toggleShowAssistants) useShortcut('toggle_show_assistants', toggleShowAssistants)
@ -104,7 +105,9 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
{activeTopicOrSession === 'topic' && <SelectModelButton assistant={assistant} />} {activeTopicOrSession === 'topic' && <SelectModelButton assistant={assistant} />}
{/* TODO: Show a select model button for agent. */} {/* TODO: Show a select model button for agent. */}
{/* FIXME: models endpoint doesn't return all models, so cannot found. */} {/* FIXME: models endpoint doesn't return all models, so cannot found. */}
{activeTopicOrSession === 'session' && <ApiModelLabel model={agentModel} />} {activeTopicOrSession === 'session' && (
<ApiModelLabel classNames={{ container: 'text-xs' }} model={agentModel} />
)}
</HStack> </HStack>
<HStack alignItems="center" gap={8}> <HStack alignItems="center" gap={8}>
<UpdateAppButton /> <UpdateAppButton />

View File

@ -1,13 +1,11 @@
import { Button, Tooltip } from '@heroui/react' import { Button, Input, Select, SelectedItems, SelectItem, Tooltip } from '@heroui/react'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { ApiModelLabel } from '@renderer/components/ApiModelLabel' import { ApiModelLabel } from '@renderer/components/ApiModelLabel'
import { useApiModels } from '@renderer/hooks/agents/useModels' import { useApiModels } from '@renderer/hooks/agents/useModels'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent' import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
import { GetAgentResponse, UpdateAgentForm } from '@renderer/types' import { ApiModel, GetAgentResponse, UpdateAgentForm } from '@renderer/types'
import { Input, Select } from 'antd'
import { DefaultOptionType } from 'antd/es/select'
import { Plus } from 'lucide-react' import { Plus } from 'lucide-react'
import { FC, useCallback, useMemo, useState } from 'react' import { FC, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AgentLabel, SettingsContainer, SettingsItem, SettingsTitle } from './shared' import { AgentLabel, SettingsContainer, SettingsItem, SettingsTitle } from './shared'
@ -42,13 +40,6 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
[agent, update] [agent, update]
) )
const modelOptions = useMemo(() => {
return models.map((model) => ({
value: model.id,
label: <ApiModelLabel model={model} />
})) satisfies DefaultOptionType[]
}, [models])
const addAccessiblePath = useCallback(async () => { const addAccessiblePath = useCallback(async () => {
if (!agent) return if (!agent) return
@ -83,6 +74,12 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
[agent, t, updateAccessiblePaths] [agent, t, updateAccessiblePaths]
) )
const renderModels = useCallback((items: SelectedItems<ApiModel>) => {
return items.map((item) => {
const model = item.data ?? undefined
return <ApiModelLabel key={model?.id} model={model} />
})
}, [])
if (!agent) return null if (!agent) return null
@ -97,7 +94,7 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
<Input <Input
placeholder={t('common.agent_one') + t('common.name')} placeholder={t('common.agent_one') + t('common.name')}
value={name} value={name}
onChange={(e) => setName(e.target.value)} onValueChange={(value) => setName(value)}
onBlur={() => { onBlur={() => {
if (name !== agent.name) { if (name !== agent.name) {
updateName(name) updateName(name)
@ -107,16 +104,24 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
/> />
</SettingsItem> </SettingsItem>
<SettingsItem inline className="gap-8"> <SettingsItem inline className="gap-8">
<SettingsTitle>{t('common.model')}</SettingsTitle> <SettingsTitle id="model">{t('common.model')}</SettingsTitle>
<Select <Select
options={modelOptions} selectionMode="single"
value={agent.model} aria-labelledby="model"
onChange={(value) => { items={models}
updateModel(value) selectedKeys={[agent.model]}
onSelectionChange={(keys) => {
updateModel(keys.currentKey)
}} }}
className="max-w-80 flex-1" className="max-w-80 flex-1"
placeholder={t('common.placeholders.select.model')} placeholder={t('common.placeholders.select.model')}
/> renderValue={renderModels}>
{(model) => (
<SelectItem textValue={model.id}>
<ApiModelLabel model={model} />
</SelectItem>
)}
</Select>
</SettingsItem> </SettingsItem>
<SettingsItem> <SettingsItem>
<SettingsTitle <SettingsTitle