diff --git a/internal/gateway/methods/teams_crud.go b/internal/gateway/methods/teams_crud.go index 392a882a..d29f021a 100644 --- a/internal/gateway/methods/teams_crud.go +++ b/internal/gateway/methods/teams_crud.go @@ -260,8 +260,10 @@ func (m *TeamsMethods) handleTaskActiveBySession(ctx context.Context, client *ga // --- Update (settings) --- type teamsUpdateParams struct { - TeamID string `json:"teamId"` - Settings map[string]any `json:"settings"` + TeamID string `json:"teamId"` + Name string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Settings map[string]any `json:"settings"` } func (m *TeamsMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) { @@ -333,6 +335,12 @@ func (m *TeamsMethods) handleUpdate(ctx context.Context, client *gateway.Client, cleaned, _ := json.Marshal(access) updates := map[string]any{"settings": json.RawMessage(cleaned)} + if params.Name != "" { + updates["name"] = params.Name + } + if params.Description != nil { + updates["description"] = *params.Description + } if err := m.teamStore.UpdateTeam(ctx, teamID, updates); err != nil { client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToUpdate, "team", err.Error()))) return diff --git a/ui/desktop/frontend/src/components/chat/ChatCanvas.tsx b/ui/desktop/frontend/src/components/chat/ChatCanvas.tsx index ba4e896d..9f57f47b 100644 --- a/ui/desktop/frontend/src/components/chat/ChatCanvas.tsx +++ b/ui/desktop/frontend/src/components/chat/ChatCanvas.tsx @@ -1,22 +1,27 @@ -import { useEffect, useRef, useCallback, useMemo } from 'react' +import { useEffect, useRef, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useChat } from '../../hooks/use-chat' import { useAgents } from '../../hooks/use-agents' +import { useTeamTasks } from '../../hooks/use-team-tasks' import { useSessionStore } from '../../stores/session-store' import { ChatTopBar } from './ChatTopBar' import { MessageBubble } from './MessageBubble' import { ActivityIndicator } from './ActivityIndicator' import { InputBar, type AttachedFile } from './InputBar' import { TaskPanel } from './TaskPanel' +import { TaskDetailModal } from '../teams/TaskDetailModal' +import type { TeamTaskData } from '../../types/team' export function ChatCanvas() { const { t } = useTranslation('common') const { messages, isRunning, activity, sendMessage } = useChat() const { selectedAgent } = useAgents() + const { members, fetchTaskDetail } = useTeamTasks() const activeSessionKey = useSessionStore((s) => s.activeSessionKey) const messagesEndRef = useRef(null) const scrollAreaRef = useRef(null) const userScrolledUp = useRef(false) + const [selectedTask, setSelectedTask] = useState(null) // Find last assistant message ID for streaming cursor const lastAssistantId = useMemo(() => { @@ -85,7 +90,7 @@ export function ChatCanvas() { {/* Team task panel */} - + {/* Input bar */} + + {/* Task detail modal (from TaskPanel click) */} + {selectedTask && ( + setSelectedTask(null)} + onAssign={async () => {}} + onDelete={async () => {}} + onFetchDetail={fetchTaskDetail} + /> + )} ) } diff --git a/ui/desktop/frontend/src/components/common/Icons.tsx b/ui/desktop/frontend/src/components/common/Icons.tsx new file mode 100644 index 00000000..8f716be2 --- /dev/null +++ b/ui/desktop/frontend/src/components/common/Icons.tsx @@ -0,0 +1,116 @@ +/** + * Shared SVG icon components — replaces inline SVGs across the desktop UI. + * All icons use currentColor and accept className for sizing/color overrides. + */ + +interface IconProps { + size?: number + className?: string +} + +function svg(size: number, className: string | undefined, children: React.ReactNode, extra?: Record) { + return ( + + {children} + + ) +} + +export function IconClose({ size = 18, className }: IconProps) { + return svg(size, className, <> + + + ) +} + +export function IconChevronDown({ size = 14, className }: IconProps) { + return svg(size, className, ) +} + +export function IconChevronLeft({ size = 16, className }: IconProps) { + return svg(size, className, ) +} + +export function IconPlus({ size = 14, className }: IconProps) { + return svg(size, className, <> + + + , { strokeWidth: 2.5 }) +} + +export function IconGear({ size = 14, className }: IconProps) { + return svg(size, className, <> + + + ) +} + +export function IconTrash({ size = 14, className }: IconProps) { + return svg(size, className, <> + + + ) +} + +export function IconDocument({ size = 16, className }: IconProps) { + return svg(size, className, <> + + + + + + ) +} + +export function IconCheckCircle({ size = 16, className }: IconProps) { + return svg(size, className, <> + + + ) +} + +export function IconCheck({ size = 14, className }: IconProps) { + return svg(size, className, ) +} + +export function IconBlocked({ size = 14, className }: IconProps) { + return svg(size, className, <> + + + ) +} + +export function IconChat({ size = 20, className }: IconProps) { + return svg(size, className, ) +} + +export function IconUser({ size = 16, className }: IconProps) { + return svg(size, className, <> + + + ) +} + +export function IconPaperclip({ size = 10, className }: IconProps) { + return svg(size, className, ) +} + +export function IconSpinner({ size = 14, className }: IconProps) { + return ( +
+ ) +} diff --git a/ui/desktop/frontend/src/components/teams/KanbanCard.tsx b/ui/desktop/frontend/src/components/teams/KanbanCard.tsx index f004d4b9..81052dc8 100644 --- a/ui/desktop/frontend/src/components/teams/KanbanCard.tsx +++ b/ui/desktop/frontend/src/components/teams/KanbanCard.tsx @@ -1,6 +1,7 @@ import { motion } from 'framer-motion' +import { IconBlocked, IconChat, IconPaperclip } from '../common/Icons' +import { isTaskLocked, TERMINAL_STATUSES } from '../../types/team' import type { TeamTaskData } from '../../types/team' -import { isTaskLocked } from '../../types/team' /** Priority: plain text color, matching web kanban-card.tsx */ const PRIORITY_STYLE: Record = { @@ -10,7 +11,6 @@ const PRIORITY_STYLE: Record = { 3: { label: 'P-3', color: 'text-red-500' }, } -const TERMINAL = new Set(['completed', 'failed', 'cancelled']) interface KanbanCardProps { task: TeamTaskData @@ -24,7 +24,7 @@ export function KanbanCard({ task, ownerName, ownerEmoji, onClick }: KanbanCardP const blocked = task.status === 'blocked' const prio = PRIORITY_STYLE[task.priority] ?? PRIORITY_STYLE[0] const hasBlockers = task.blocked_by && task.blocked_by.length > 0 - const isTerminal = TERMINAL.has(task.status) + const isTerminal = TERMINAL_STATUSES.has(task.status) return ( - - - + {task.blocked_by!.map((id) => id.slice(0, 8)).join(', ')}

)} - {/* Bottom: owner */} + {/* Bottom: owner + counts */}
{ownerEmoji && {ownerEmoji}} - + {ownerName || task.owner_agent_key || 'Unassigned'} + {(task.comment_count ?? 0) > 0 && ( + + + {task.comment_count} + + )} + {(task.attachment_count ?? 0) > 0 && ( + + + {task.attachment_count} + + )}
{/* Progress bar */} diff --git a/ui/desktop/frontend/src/components/teams/TaskCreateDialog.tsx b/ui/desktop/frontend/src/components/teams/TaskCreateDialog.tsx index 3a508d4c..fb47420d 100644 --- a/ui/desktop/frontend/src/components/teams/TaskCreateDialog.tsx +++ b/ui/desktop/frontend/src/components/teams/TaskCreateDialog.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Combobox } from '../common/Combobox' +import { IconClose } from '../common/Icons' import type { TeamMemberData } from '../../types/team' interface TaskCreateDialogProps { @@ -56,9 +57,7 @@ export function TaskCreateDialog({ teamId, members, onClose, onCreate }: TaskCre

{t('createTask', 'Create Task')}

diff --git a/ui/desktop/frontend/src/components/teams/TaskDetailModal.tsx b/ui/desktop/frontend/src/components/teams/TaskDetailModal.tsx index 23f4751b..1271f062 100644 --- a/ui/desktop/frontend/src/components/teams/TaskDetailModal.tsx +++ b/ui/desktop/frontend/src/components/teams/TaskDetailModal.tsx @@ -1,10 +1,12 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { MarkdownRenderer } from '../chat/MarkdownRenderer' import { Combobox } from '../common/Combobox' import { ConfirmDialog } from '../common/ConfirmDialog' -import { STATUS_BADGE, PRIORITY_BADGE, isTaskLocked } from '../../types/team' -import type { TeamTaskData, TeamMemberData, TaskStatus } from '../../types/team' +import { IconClose, IconChevronDown, IconDocument, IconCheckCircle, IconTrash } from '../common/Icons' +import { getApiClient } from '../../lib/api' +import { STATUS_BADGE, PRIORITY_BADGE, isTaskLocked, TERMINAL_STATUSES } from '../../types/team' +import type { TeamTaskData, TeamMemberData, TeamTaskAttachment } from '../../types/team' interface TaskDetailModalProps { task: TeamTaskData @@ -12,10 +14,9 @@ interface TaskDetailModalProps { onClose: () => void onAssign: (taskId: string, agentKey: string) => Promise onDelete: (taskId: string) => Promise + onFetchDetail?: (teamId: string, taskId: string) => Promise<{ task: TeamTaskData; attachments: TeamTaskAttachment[] } | null> } -const TERMINAL: Set = new Set(['completed', 'failed', 'cancelled']) - /** Metadata label + value pair */ function MetaItem({ label, children }: { label: string; children: React.ReactNode }) { return ( @@ -40,9 +41,7 @@ function CollapsibleSection({ title, icon, defaultOpen = true, children }: { > {icon} {title} - - - + {open && (
@@ -53,14 +52,30 @@ function CollapsibleSection({ title, icon, defaultOpen = true, children }: { ) } -export function TaskDetailModal({ task, members, onClose, onAssign, onDelete }: TaskDetailModalProps) { +/** Format bytes to human-readable size */ +function formatFileSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` +} + +export function TaskDetailModal({ task, members, onClose, onAssign, onDelete, onFetchDetail }: TaskDetailModalProps) { const { t } = useTranslation('teams') const [confirmDelete, setConfirmDelete] = useState(false) + const [attachments, setAttachments] = useState([]) + + // Fetch full detail (with attachments) on mount + useEffect(() => { + if (!onFetchDetail) return + onFetchDetail(task.team_id, task.id).then((res) => { + if (res) setAttachments(res.attachments) + }) + }, [task.id, task.team_id, onFetchDetail]) const prio = PRIORITY_BADGE[task.priority] ?? PRIORITY_BADGE[3] const statusCls = STATUS_BADGE[task.status] ?? '' const locked = isTaskLocked(task) - const isTerminal = TERMINAL.has(task.status) + const isTerminal = TERMINAL_STATUSES.has(task.status) const member = task.owner_agent_id ? members.find((m) => m.agent_id === task.owner_agent_id) : undefined const memberOptions = members.map((m) => ({ @@ -105,9 +120,7 @@ export function TaskDetailModal({ task, members, onClose, onAssign, onDelete }:

{task.subject}

@@ -182,7 +195,7 @@ export function TaskDetailModal({ task, members, onClose, onAssign, onDelete }: {task.description && ( } + icon={} >
@@ -194,13 +207,52 @@ export function TaskDetailModal({ task, members, onClose, onAssign, onDelete }: {task.result && ( } + icon={} >
)} + + {/* Attachments section */} + {attachments.length > 0 && ( + } + > +
+ {attachments.map((a) => { + const fileName = a.path?.split('/').pop() || 'file' + const baseUrl = getApiClient()?.getBaseUrl() || '' + const fullUrl = a.download_url?.startsWith('http') ? a.download_url : `${baseUrl}${a.download_url}` + return ( + + ) + })} +
+
+ )}
{/* ── Footer ── */} @@ -221,9 +273,7 @@ export function TaskDetailModal({ task, members, onClose, onAssign, onDelete }: onClick={() => setConfirmDelete(true)} className="flex items-center gap-1.5 text-sm text-error hover:text-error/80 px-4 py-2 rounded-lg border border-error/30 hover:bg-error/10 transition-colors cursor-pointer" > - - - + {t('delete', 'Delete')} )} diff --git a/ui/desktop/frontend/src/components/teams/TeamBoard.tsx b/ui/desktop/frontend/src/components/teams/TeamBoard.tsx index 5066a874..c9014ad0 100644 --- a/ui/desktop/frontend/src/components/teams/TeamBoard.tsx +++ b/ui/desktop/frontend/src/components/teams/TeamBoard.tsx @@ -3,10 +3,12 @@ import { useTranslation } from 'react-i18next' import { useUiStore } from '../../stores/ui-store' import { useTeamTasks } from '../../hooks/use-team-tasks' import { RefreshButton } from '../common/RefreshButton' +import { IconChevronLeft, IconGear, IconChevronDown, IconPlus, IconChat } from '../common/Icons' import { KanbanColumn } from './KanbanColumn' import { TaskDetailModal } from './TaskDetailModal' -import { KANBAN_STATUSES, groupByStatus } from '../../types/team' -import type { TeamTaskData, TaskStatus } from '../../types/team' +import { TeamSettingsModal } from './TeamSettingsModal' +import { KANBAN_STATUSES, TERMINAL_STATUSES, groupByStatus } from '../../types/team' +import type { TeamTaskData } from '../../types/team' type ViewMode = 'kanban' | 'list' const FILTER_OPTIONS = [ @@ -19,13 +21,15 @@ export function TeamBoard() { const { t } = useTranslation('teams') const activeTeamId = useUiStore((s) => s.activeTeamId) const closeSettings = useUiStore((s) => s.closeSettings) - const { teams, tasks, members, loading, fetchTeams, fetchTasks, assignTask, deleteTask, deleteBulk } = useTeamTasks() + const { teams, tasks, members, loading, fetchTeams, fetchTasks, fetchTaskDetail, assignTask, deleteTask, deleteBulk } = useTeamTasks() const [viewMode, setViewMode] = useState('kanban') const [statusFilter, setStatusFilter] = useState('') const [filterOpen, setFilterOpen] = useState(false) const [selectedTask, setSelectedTask] = useState(null) const [selected, setSelected] = useState>(new Set()) + const [settingsOpen, setSettingsOpen] = useState(false) + const [infoOpen, setInfoOpen] = useState(false) const team = teams.find((t) => t.id === activeTeamId) @@ -49,8 +53,6 @@ export function TeamBoard() { } }, [tasks, selectedTask]) - const terminalStatuses = new Set(['completed', 'failed', 'cancelled']) - const handleBulkDelete = async () => { if (selected.size === 0) return await deleteBulk(Array.from(selected)) @@ -72,9 +74,7 @@ export function TeamBoard() { {/* Header */}
@@ -82,6 +82,14 @@ export function TeamBoard() {

{team?.name || t('team', 'Team')}

+ {/* Gear — team settings */} +
@@ -108,9 +116,7 @@ export function TeamBoard() { className="flex items-center gap-1.5 text-[11px] bg-surface-tertiary border border-border rounded-lg px-2.5 py-1.5 text-text-secondary hover:border-accent/30 transition-colors cursor-pointer" > {t(currentFilterLabel)} - - - + {filterOpen && ( <> @@ -134,6 +140,15 @@ export function TeamBoard() { { handleRefresh() }} /> + + {/* "+" info button */} + {/* Kanban view */} @@ -169,10 +184,10 @@ export function TeamBoard() { 0 && selected.size === tasks.filter((t) => terminalStatuses.has(t.status)).length} + checked={selected.size > 0 && selected.size === tasks.filter((t) => TERMINAL_STATUSES.has(t.status)).length} onChange={(e) => { if (e.target.checked) { - setSelected(new Set(tasks.filter((t) => terminalStatuses.has(t.status)).map((t) => t.id))) + setSelected(new Set(tasks.filter((t) => TERMINAL_STATUSES.has(t.status)).map((t) => t.id))) } else { setSelected(new Set()) } @@ -189,7 +204,7 @@ export function TeamBoard() { {tasks.map((task) => { const member = task.owner_agent_id ? members.find((m) => m.agent_id === task.owner_agent_id) : undefined - const canSelect = terminalStatuses.has(task.status) + const canSelect = TERMINAL_STATUSES.has(task.status) return ( setSelectedTask(null)} onAssign={assignTask} onDelete={deleteTask} + onFetchDetail={fetchTaskDetail} /> )} + {/* Team settings modal */} + {settingsOpen && activeTeamId && ( + setSettingsOpen(false)} + onSaved={() => { fetchTeams(); if (activeTeamId) fetchTasks(activeTeamId, statusFilter || undefined) }} + /> + )} + + {/* Task create info modal */} + {infoOpen && ( +
setInfoOpen(false)}> +
e.stopPropagation()} className="bg-surface-primary border border-border rounded-xl shadow-xl max-w-sm mx-4 p-6 text-center space-y-3"> +
+ +
+

{t('taskCreateInfo.title', 'How tasks are created')}

+

+ {t('taskCreateInfo.body', 'Tasks are created by chatting with the team leader agent. Start a conversation with {{leader}} to create and manage tasks.', { + leader: team?.lead_display_name || team?.lead_agent_key || 'the leader', + })} +

+ +
+
+ )} + ) } diff --git a/ui/desktop/frontend/src/components/teams/TeamCreateDialog.tsx b/ui/desktop/frontend/src/components/teams/TeamCreateDialog.tsx index a1f33ca8..fbcd91bb 100644 --- a/ui/desktop/frontend/src/components/teams/TeamCreateDialog.tsx +++ b/ui/desktop/frontend/src/components/teams/TeamCreateDialog.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import { getWsClient } from '../../lib/ws' import { toast } from '../../stores/toast-store' import { Combobox } from '../common/Combobox' +import { IconClose, IconCheck } from '../common/Icons' import type { TeamData } from '../../types/team' interface Agent { @@ -64,9 +65,7 @@ export function TeamCreateDialog({ agents, onClose, onCreated }: TeamCreateDialo

{t('createTeam', 'Create Team')}

@@ -119,11 +118,7 @@ export function TeamCreateDialog({ agents, onClose, onCreated }: TeamCreateDialo 'w-4 h-4 rounded flex items-center justify-center shrink-0 transition-colors', checked ? 'bg-accent' : 'border border-border bg-surface-tertiary', ].join(' ')}> - {checked && ( - - - - )} + {checked && } {opt.label} diff --git a/ui/desktop/frontend/src/components/teams/TeamSettingsModal.tsx b/ui/desktop/frontend/src/components/teams/TeamSettingsModal.tsx new file mode 100644 index 00000000..63a8bbf4 --- /dev/null +++ b/ui/desktop/frontend/src/components/teams/TeamSettingsModal.tsx @@ -0,0 +1,321 @@ +import { useState, useEffect, useMemo, useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { Combobox } from '../common/Combobox' +import { IconClose, IconPlus, IconUser, IconSpinner } from '../common/Icons' +import { useAgents } from '../../hooks/use-agents' +import { useTeamManage } from '../../hooks/use-team-manage' +import type { TeamData, TeamMemberData, TeamNotifyConfig } from '../../types/team' + +interface TeamSettingsModalProps { + teamId: string + onClose: () => void + /** Called after successful save so parent can refresh */ + onSaved?: () => void +} + +const ROLE_COLORS: Record = { + lead: 'bg-amber-500/15 text-amber-600 dark:text-amber-400', + reviewer: 'bg-orange-500/15 text-orange-600 dark:text-orange-400', + member: 'bg-surface-tertiary text-text-muted', +} + +const NOTIFY_KEYS = ['dispatched', 'progress', 'failed', 'completed', 'new_task'] as const +type NotifyKey = typeof NOTIFY_KEYS[number] + +export function TeamSettingsModal({ teamId, onClose, onSaved }: TeamSettingsModalProps) { + const { t } = useTranslation('teams') + const { fetchTeamDetail, updateTeam, addMember, removeMember } = useTeamManage() + const { agents, refreshAgents } = useAgents() + + const [team, setTeam] = useState(null) + const [members, setMembers] = useState([]) + const [loading, setLoading] = useState(true) + + // Editable fields + const [name, setName] = useState('') + const [description, setDescription] = useState('') + + // Notification settings + const [notify, setNotify] = useState>({ + dispatched: true, progress: true, failed: true, completed: true, new_task: true, + }) + const [notifyMode, setNotifyMode] = useState<'direct' | 'leader'>('direct') + + // Add member + const [showAdd, setShowAdd] = useState(false) + const [addAgent, setAddAgent] = useState('') + const [adding, setAdding] = useState(false) + const [removing, setRemoving] = useState(null) + const [saving, setSaving] = useState(false) + + // Load team detail (once on mount) + useEffect(() => { + refreshAgents() + fetchTeamDetail(teamId).then((res) => { + if (!res) return + setTeam(res.team) + setMembers(res.members ?? []) + setName(res.team.name) + setDescription(res.team.description ?? '') + // Parse notification settings + const settings = (res.team.settings ?? {}) as Record + const n = (settings.notifications ?? {}) as Record + setNotify({ + dispatched: n.dispatched !== false, + progress: n.progress !== false, + failed: n.failed !== false, + completed: n.completed !== false, + new_task: n.new_task !== false, + }) + setNotifyMode((n.mode as 'direct' | 'leader') || 'direct') + setLoading(false) + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [teamId]) + + const leadMember = members.find((m) => m.role === 'lead') + + const sorted = useMemo( + () => [...members].sort((a, b) => { + if (a.role === 'lead' && b.role !== 'lead') return -1 + if (b.role === 'lead' && a.role !== 'lead') return 1 + return (a.display_name || a.agent_key || '').localeCompare(b.display_name || b.agent_key || '') + }), + [members], + ) + + const memberIds = useMemo(() => new Set(members.map((m) => m.agent_id)), [members]) + const availableAgents = useMemo( + () => agents + .filter((a) => !memberIds.has(a.id)) + .map((a) => ({ value: a.id, label: `${a.emoji || ''} ${a.name}`.trim() })), + [agents, memberIds], + ) + + const toggleNotify = useCallback((key: NotifyKey) => { + setNotify((prev) => ({ ...prev, [key]: !prev[key] })) + }, []) + + const handleAddMember = async () => { + if (!addAgent) return + setAdding(true) + try { + await addMember(teamId, addAgent) + const res = await fetchTeamDetail(teamId) + if (res) setMembers(res.members ?? []) + setAddAgent('') + setShowAdd(false) + } catch { /* toast in hook */ } finally { setAdding(false) } + } + + const handleRemoveMember = async (agentId: string) => { + setRemoving(agentId) + try { + await removeMember(teamId, agentId) + setMembers((prev) => prev.filter((m) => m.agent_id !== agentId)) + } catch { /* toast in hook */ } finally { setRemoving(null) } + } + + const handleSave = async () => { + setSaving(true) + try { + const notifications: TeamNotifyConfig = { ...notify, mode: notifyMode } + const settings = { ...(team?.settings ?? {}), notifications } as Record + await updateTeam(teamId, { + name: name !== team?.name ? name : undefined, + description: description !== (team?.description ?? '') ? description : undefined, + settings, + }) + onSaved?.() + } catch { /* toast in hook */ } finally { setSaving(false) } + } + + if (loading) { + return ( +
+
+ +
+
+ ) + } + + return ( +
+
e.stopPropagation()} + className="bg-surface-primary border border-border rounded-xl shadow-xl w-[95vw] max-w-2xl max-h-[85vh] flex flex-col mx-4" + > + {/* Header */} +
+

{t('teamSettings', 'Team Settings')}

+ +
+ + {/* Scrollable body */} +
+ + {/* ── Section 1: Team Info ── */} +
+

{t('settings.teamInfo', 'Team Info')}

+
+ + setName(e.target.value)} + className="w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent/50" + /> +
+
+ +