diff --git a/README.md b/README.md index ad85aec..6784471 100644 --- a/README.md +++ b/README.md @@ -5,23 +5,46 @@ React frontend for interacting with the [composable-agents](https://github.com/s ## Features - **Agent management** — Create, view, configure, and delete agents via YAML file upload -- **Real-time chat** — Stream AI responses via SSE with typed events (thinking, content, structured response) +- **Real-time chat with trace timeline** — Stream AI responses via SSE as `TraceEvent`s and render a per-turn timeline (thinking, content, tool calls, subagent panels) - **Human-in-the-loop** — Review and approve/reject tool calls before execution - **Thread history** -- Conversation threads grouped by agent in a sidebar -- **RAG file browser** -- Browse MinIO folders and files with breadcrumb navigation and file metadata display -- **Material Design 3** -- Inspired design system with shadcn/ui components +- **RAG file browser** -- Browse MinIO folders and files with breadcrumb navigation and file metadata display. Create new folders (`CreateFolderDialog` triggered by the "New folder" button in the RAG browse tab) and delete files or folders via a trash-icon row action confirmed through a `ConfirmDeleteDialog`. Folder deletion is recursive and targets the MinIO prefix; file deletion targets a single object. Both calls hit the `POST /api/v1/files/folders` and `DELETE /api/v1/files` / `DELETE /api/v1/files/folders` endpoints exposed by `mcp-raganything`. Multi-file upload with drag-and-drop and folder upload support (see [File upload](#file-upload)). +- **Tetris design system** -- Dark arcade visual language aligned with the Open Design `composable` maquette: magenta accent, Press Start 2P applied to all text (display, body, labels, buttons, nav, chat), block shadows, and `steps(2, end)` easing. Light theme toggle available from the Settings page. +- **Settings** -- A dedicated `/settings` page (4 cards: Theme, Chat, LLM Provider, Reset) backed by a persisted Zustand store and aligned to the Open Design maquette via `data-od-id` QA attributes. ## Tech Stack - **Runtime**: Bun - **Framework**: React 19 + TypeScript - **Build**: Vite 8 -- **Styling**: Tailwind CSS 4 + shadcn/ui + Framer Motion +- **Styling**: Tailwind CSS 4 + shadcn/ui + Framer Motion + lucide-react icons - **State**: Zustand (local) + TanStack React Query (server) - **Forms**: React Hook Form + Zod validation - **Testing**: Vitest + Testing Library - **Linting**: ESLint + Prettier +## Design System + +The UI implements the **Tetris** design system derived from the [Open Design](https://opencode.ai) `composable` project maquette. Source-of-truth artifacts live under the Open Design project (`DESIGN.md`, `composable-tokens.css`, `app.html`, `styles.css`). + +### Visual language + +- **Theme**: Dark arcade (default) with a light alternative. Toggle from the Settings page (`/settings` > Theme card). The active toggle persists to `localStorage["composable-ui-settings"]`; the legacy `composable-ui-theme` key is still written by `useThemeStore` for anti-FOUC during boot but is no longer driven by the UI. +- **Colors**: near-black ink background (`#050816`), ink-blue surfaces (`#10162a`), one electric-magenta accent (`#ff00ff`), terminal-style semantic colors (`--success` green, `--warn` yellow, `--danger` red). Light theme uses the same token names with accessible light values. +- **Typography**: `Press Start 2P` is now applied to all text — display, body, labels, buttons, nav, and chat — bringing the UI to full parity with the Open Design maquette. Mono uppercase with `0.1em` tracking for badges/tags. +- **Shape**: sharp corners (`0px` radius) everywhere; icon buttons are now `rounded-none` (previously `rounded-full`) to match the maquette. The only round elements are the Switch toggle and the sidebar avatar (`--radius-pill`). +- **Elevation**: hard block shadows (`4px 4px 0` black for buttons, `0 18px 0` for raised panels) — never soft diffuse shadows. Primary buttons collapse their shadow on `:active` via `translate(4px, 4px)`. +- **Motion**: `steps(2, end)` easing for an arcade hard-cut feel. Durations `80ms` (fast) / `140ms` (base). All animations disabled under `prefers-reduced-motion: reduce`. +- **Icons**: [lucide-react](https://lucide.dev) throughout (tree-shakeable, consistent with shadcn/ui). + +### Token layer + +CSS custom properties live in `src/application/index.css` under `:root` (light) and `:root.dark` (dark, default). Tailwind v4 `@theme` maps them to utilities (`bg-bg`, `bg-surface`, `text-fg`, `border-border`, `bg-accent`, `font-display`, `shadow-raised`, `ease-standard`, `animate-*`, etc.). Custom utilities: `.block-shadow`, `.block-shadow-raised`, `.focus-ring`, `.cursor-blink`, `.scan-effect`, `.spin`, `.view-enter`. + +### Layout + +The app shell faithfully reproduces the maquette: a 220px left `Sidebar` (brand, nav, footer) + `MainHeader` (sticky, mobile menu button + route-derived title only — the header right side is now empty) + a scrollable `main` content area. The sidebar exposes a `System` section with a `Settings` nav link (`data-od-id="nav-settings"`). On mobile (`<768px`) the sidebar becomes off-canvas with a backdrop. The chat view adds a 260px `ThreadSidebar` inside a `chat-layout` grid (hidden below `lg:1024px`). + ## Prerequisites - [Bun](https://bun.sh/) >= 1.0 @@ -44,16 +67,27 @@ cp public/config.example.json public/config.json **`public/config.json`** - Application configuration: -| Field | Type | Description | -|---|---|---| -| `apiBaseUrl` | `string` | composable-agents API URL (e.g., `http://localhost:8010`) | +| Field | Type | Description | +| --------------- | ------------------- | --------------------------------------------------------------------------------------------------------- | +| `apiBaseUrl` | `string` | composable-agents API URL (e.g., `http://localhost:8010`) | | `ragApiBaseUrl` | `string` (optional) | RAG API URL for MinIO file browsing (e.g., `http://localhost:8020`). Defaults to `apiBaseUrl` if not set. | -| `wsBaseUrl` | `string` | WebSocket URL for streaming (e.g., `ws://localhost:8010`) | +| `wsBaseUrl` | `string` | WebSocket URL for streaming (e.g., `ws://localhost:8010`) | The config is validated with Zod on startup. Invalid configuration will show an error toast. **Note:** `config.json` is gitignored. Use `config.example.json` as a template. +## Settings + +The `/settings` page groups user preferences into four cards, all wired to the persisted `useSettingsStore` (Zustand, `localStorage["composable-ui-settings"]`). The page and its controls carry `data-od-id` attributes for QA parity with the Open Design `composable` maquette. + +- **Theme** -- Accent color picker (hex input that live-updates the `--accent` CSS variable), surface shade select, and the dark/light mode switch (the toggle previously in the header now lives here). +- **Chat** -- Message font size (range slider, 10-18px) and message font family (`Press Start 2P` / Monospace / Mono / Georgia). +- **LLM Provider** -- Provider select (`anthropic` / `openai` / `google` / `mistral` / `local` / `ollama`) and an API key field (password type, stored in `localStorage` only — never sent to the backend). +- **Reset** -- "Reset to Defaults" button that restores all settings to their initial values. + +The theme toggle is now owned by `useSettingsStore`; the legacy `useThemeStore` (`composable-ui-theme` key) is still present for boot-time anti-FOUC but is no longer driven by the UI and is slated for removal in a future refactor. + ## Running ```bash @@ -115,7 +149,7 @@ src/ domain/ # Business entities and port interfaces entities/ agent/ # AgentConfig, AgentConfigMetadata, McpServerConfig - chat/ # Message, Thread, ChatRequest + chat/ # Message, Thread, ChatRequest, TraceEvent (6 types: human_message, ai_message, thinking, content, tool_call, tool_result), ThreadHistory config/ # AppConfig (Zod-validated) rag/ # FileEntry, FolderEntry ports/ @@ -126,7 +160,7 @@ src/ infrastructure/ # External adapters (API clients, config) api/ agent/agentApi.ts # Agent API adapter (axios) - chat/chatApi.ts # Chat API adapter (axios + SSE) + chat/chatApi.ts # Chat API adapter (axios + SSE, emits TraceEvent) rag/ragApi.ts # RAG API adapter (axios) axiosInstance.ts # Shared axios instance ragAxiosInstance.ts # Separate axios client for RAG API @@ -136,22 +170,27 @@ src/ application/ # React UI layer components/ agent/ # AgentCard, AgentGrid, CreateAgentDialog, AgentConfigViewer - chat/ # ChatInput, ChatMessage, HITLReviewPanel, MessageList - layout/ # MainLayout, ThreadSidebar, TopNav - rag/ # BreadcrumbBar, FileList, FileRow, FolderRow - shared/ # StatusBadge, ToolTag - ui/ # shadcn/ui primitives + chat/ # ChatInput, ChatMessage, MessageList, HITLReviewPanel, ThinkingBlock, ToolCallBadge, ToolResultBlock, SubagentPanel + layout/ # AppShell, Sidebar (System section + Settings nav), MainHeader (mobile menu + title only), ThreadSidebar + rag/ # BreadcrumbBar, FileList, FileRow, FolderRow, CreateFolderDialog, ConfirmDeleteDialog, FileContentPanel, IndexActionMenu, QueryPanel, QueryResults, QueryOptions, RagTabBar, UploadButton, WorkspaceSelector + settings/ # SettingsPage cards (Theme, Chat, LLMProvider, Reset) + shared/ # SegmentedToggle, StatusBadge, ToolTag + ui/ # shadcn/ui primitives (Tetris-themed) hooks/ agent/ # useAgents, useCreateAgent, useDeleteAgent, useUpdateAgent, useAgentConfig - chat/ # useThreads, useCreateThread, useDeleteThread, useMessages, useSendMessage, useStreamChat + chat/ # useThreads, useCreateThread, useDeleteThread, useThreadHistory, useSendMessage, useStreamChat config/ # useConfig - rag/ # useFolders, useFiles + rag/ # useFolders, useFiles, useReadFile, useUploadFile, useCreateFolder, useDeleteFile, useDeleteFolder, useClassicalIndexFile, useClassicalIndexFolder, useClassicalQuery pages/ AgentsPage.tsx # /agents route ChatPage.tsx # /chat/:threadId? route RagPage.tsx # /rag route + SettingsPage.tsx # /settings route (Theme, Chat, LLM Provider, Reset cards) stores/ useChatStore.ts # Zustand store for chat state + useSettingsStore.ts # Zustand store for Settings page (theme, accent, chat, LLM, persisted to "composable-ui-settings") + useThemeStore.ts # Legacy Zustand store for theme (dark/light) + persistence (still used for boot-time anti-FOUC) + useSidebarStore.ts # Zustand store for mobile sidebar open/close public/ config.example.json # Example config (committed) config.json # Runtime config (gitignored) @@ -163,12 +202,85 @@ tests/ ## Routes -| Path | Page | Description | -|---|---|---| -| `/` | -- | Redirects to `/chat` | -| `/agents` | AgentsPage | List, create, view, and delete agents | -| `/chat/:threadId?` | ChatPage | Chat with agents, streaming responses, HITL validation | -| `/rag` | RagPage | Browse MinIO folders and files with breadcrumb navigation | +| Path | Page | Description | +| ------------------ | ------------ | ------------------------------------------------------------------------------ | +| `/` | -- | Redirects to `/chat` | +| `/agents` | AgentsPage | List, create, view, and delete agents | +| `/chat/:threadId?` | ChatPage | Chat with agents, streaming responses, HITL validation | +| `/rag` | RagPage | Browse MinIO folders and files with breadcrumb navigation | +| `/settings` | SettingsPage | Theme, chat, LLM provider, and reset preferences (persisted to `localStorage`) | + +## RAG File Browser + +The `/rag` page provides a MinIO-backed file browser with three tabs: Browse, Query, and Classical. The browse tab uses breadcrumb navigation driven by `useFolders` and `useFiles`, and supports the following file/folder management actions alongside read and upload: + +- **Create folder** -- The "New folder" button opens a `CreateFolderDialog` that collects a folder name, then calls `useCreateFolder` which posts `POST /api/v1/files/folders` to `mcp-raganything`. The folder is created as a 0-byte trailing-slash object. +- **Delete file** -- Each `FileRow` exposes a trash-icon action that opens a `ConfirmDeleteDialog`. On confirm, `useDeleteFile` calls `DELETE /api/v1/files?object_name=...&working_dir=...` with the currently selected `working_dir` from the `WorkspaceSelector`, and invalidates the file list query. Both MinIO object and pgvector vectors are deleted (MinIO first, then pgvector). +- **Delete folder** -- Each `FolderRow` exposes a trash-icon action that opens a `ConfirmDeleteDialog` warning that the deletion is recursive. On confirm, `useDeleteFolder` calls `DELETE /api/v1/files/folders?prefix=...` and invalidates both the folder and file queries. The `prefix` is used as the `working_dir` for pgvector cleanup — no separate `working_dir` parameter is sent. Both MinIO objects and pgvector vectors are deleted (MinIO first, then pgvector). + +The selected file's content is rendered by `FileContentPanel` via `useReadFile` (`POST /api/v1/files/read`). + +### File upload + +The `UploadButton` and surrounding browse section now support multi-file upload with drag-and-drop: + +- **Multi-file selection** -- The `UploadButton` input has `multiple` enabled, so the native file picker lets users select several files at once. All selected files are queued for upload to the current `working_dir`. +- **Drag-and-drop** -- The entire browse section (file list area) is a drop zone. Users can drag files from their file explorer directly onto the list area instead of using the Upload button. +- **Folder upload via drag-and-drop** -- Users can drag a whole folder from their file explorer onto the browse section. The folder structure is preserved in MinIO via `webkitRelativePath` (each file's relative path within the dropped folder is used as its object key), so nested sub-foldolders are recreated under the current `working_dir`. +- **Concurrency** -- Uploads are limited to **3 simultaneous requests** to avoid overloading the backend. Files are dequeued from the pending queue as slots free up. +- **Partial failure handling** -- If some files upload successfully and others fail, a toast reports the split: "X uploaded, Y failed". Individual per-file errors are logged to the console. +- **Accepted extensions** -- The accepted file extensions list is aligned with the backend allow-list: `.pdf`, `.doc`, `.docx`, `.txt`, `.md`, `.csv`, `.xlsx`, `.xls`, `.ppt`, `.pptx`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.bmp`, `.rtf`, `.odt`, `.ods`. + +### Indexing without a working_dir (Bug 1) + +Attempting to index a file or folder from the `IndexActionMenu` when no `working_dir` is selected in the `WorkspaceSelector` previously triggered an empty error or a silent failure. The action now short-circuits and surfaces a toast: "Set a working_dir in the workspace selector first". No request is sent to the backend. + +### FileContentPanel null-safety (Bug 2) + +`FileContentPanel` previously crashed when the `POST /files/read` response returned `metadata: null` or `tables: null`, or when the `content` field was returned as an array of page strings (Kreuzberg multi-page output). The component now: + +- Treats `metadata` and `tables` as optional, rendering an empty state instead of throwing. +- Renders `content` as a pages array when the API returns `string[]` (concatenated with a blank line), and as a plain string otherwise. +- Renders each entry in `tables` as a markdown string when it is a `string`, or via the existing `{ markdown }` shape when it is an object. + +## Trace Event Model + +The chat now consumes `TraceEvent`s from the backend (replacing the old `StreamEvent`). Each event has one of 6 types: `human_message`, `ai_message`, `thinking`, `content`, `tool_call`, `tool_result`. The `source` field distinguishes the parent agent (`null`) from a named sub-agent. + +### Streaming components + +- **`MessageList`** — uses `useThreadHistory` to fetch `GET /threads/{id}/history` and renders a timeline per turn. During streaming, the in-progress turn is rendered as a `StreamingTurn` block with sub-agent events grouped by `source`. +- **`ChatMessage`** — accepts an `events` prop to render the intermediate timeline (thinking, tool calls, tool results) beneath the AI message. +- **`ThinkingBlock`** — accepts a `source` prop and displays "Thinking — {source}" when the event comes from a sub-agent. +- **`ToolCallBadge`** — collapsable badge for a `tool_call` event, color-coded by `source`. +- **`ToolResultBlock`** — collapsable terminal-style block for a `tool_result` event. +- **`SubagentPanel`** — dedicated panel for a sub-agent (header, thinking, tool calls, content), grouped by `source`. + +### Store + +`useChatStore` now manages a `StreamingTurn` object instead of two simple strings: + +- `events` — accumulated `TraceEvent`s for the current turn. +- `parentContent` / `parentThinking` — aggregated text for the parent agent. +- `subagents` — map of sub-agent name -> `{ content, thinking, toolCalls, toolResults }`. + +### Structured response during streaming + +When an agent is configured with a `response_format`, the backend attaches the structured payload to the `AI_MESSAGE` event's `content` (as a JSON-serialized `Message`), not to `metadata`. The frontend extracts it via the `getStructuredResponseFromAIEvent(ev)` helper in `traceEvent.ts`, which returns a `StructuredResponseResult` discriminated union: + +| Variant | Condition | Rendered as | +| ----------- | -------------------------------------------------------- | -------------------------------------------------------------------- | +| `valid` | `AI_MESSAGE.content` parses and contains a structured response | `` (JSON display) | +| `malformed` | `AI_MESSAGE.content` is not valid JSON | Inline error block: "Malformed JSON: \" | +| `missing` | No structured response in the parsed `Message` | "No structured response" placeholder — **only** if the agent has a `response_format` configured (via the `agentHasResponseFormat` prop on `StructuredResponseLive`); otherwise nothing is rendered | + +The `StructuredResponseLive` component consumes this result and renders the appropriate case during streaming. The `agentHasResponseFormat` boolean is passed in from the agent configuration so the `missing` placeholder is suppressed for agents that never declare a `response_format`. + +## Breaking Changes + +- **`StreamEvent` removed** — replaced by `TraceEvent` (6 types). SSE parsing in `chatApi.ts` updated accordingly. +- **`useMessages` hook removed** — replaced by `useThreadHistory` (fetches `GET /threads/{id}/history`). +- **`useChatStore` refactored** — now exposes a `StreamingTurn` instead of separate `content` / `thinking` strings. ## CI/CD diff --git a/index.html b/index.html index f79f278..e949640 100644 --- a/index.html +++ b/index.html @@ -1,11 +1,27 @@ - + - - + + Composable UI diff --git a/src/application/App.tsx b/src/application/App.tsx index 8c59090..1dd0d88 100644 --- a/src/application/App.tsx +++ b/src/application/App.tsx @@ -1,6 +1,7 @@ import { Suspense, lazy } from "react"; import { Routes, Route, Navigate } from "react-router-dom"; import ChatPage from "@/application/pages/ChatPage"; +import SettingsPage from "@/application/pages/SettingsPage"; // Lazy-load secondary routes for code-splitting / smaller initial bundle. const AgentsPage = lazy(() => import("@/application/pages/AgentsPage")); @@ -23,6 +24,7 @@ function App() { } /> } /> } /> + } /> ); } diff --git a/src/application/components/agent/AgentCard.tsx b/src/application/components/agent/AgentCard.tsx index 7fe6363..ce47a6f 100644 --- a/src/application/components/agent/AgentCard.tsx +++ b/src/application/components/agent/AgentCard.tsx @@ -1,72 +1,59 @@ +import { Bot, Settings, Trash2 } from "lucide-react"; import type { AgentConfigMetadata } from "@/domain/entities/agent/agentConfigMetadata"; import StatusBadge from "@/application/components/shared/StatusBadge"; +import { Button } from "@/application/components/ui/button"; interface AgentCardProps { readonly agent: AgentConfigMetadata; readonly onConfigure: (name: string) => void; + readonly onDelete?: (name: string) => void; } -const AGENT_ICONS: Record = { - a: "smart_toy", - b: "psychology", - c: "code", - d: "data_object", - e: "engineering", - f: "functions", - g: "generating_tokens", - h: "hub", - i: "integration_instructions", - j: "join", - k: "key", - l: "lightbulb", - m: "model_training", - n: "neurology", - o: "offline_bolt", - p: "precision_manufacturing", - q: "query_stats", - r: "robot_2", - s: "schema", - t: "terminal", - u: "upgrade", - v: "verified", - w: "workspaces", - x: "extension", - y: "yield", - z: "zoom_in", -}; - -function getAgentIcon(name: string): string { - const firstLetter = name.charAt(0).toLowerCase(); - return AGENT_ICONS[firstLetter] ?? "smart_toy"; -} - -export default function AgentCard({ agent, onConfigure }: Readonly) { +export default function AgentCard({ agent, onConfigure, onDelete }: Readonly) { return ( -
- {/* Header: icon + status */} -
-
- - {getAgentIcon(agent.name)} - +
+
+
- {/* Name */} -

{agent.name}

+

+ {agent.name} +

+

{agent.model}

- {/* Model */} -

{agent.model}

- - {/* Configure link */} - +
+ + {onDelete && ( + + )} +
); } diff --git a/src/application/components/agent/AgentConfigForm.tsx b/src/application/components/agent/AgentConfigForm.tsx index 21e4de4..928ae72 100644 --- a/src/application/components/agent/AgentConfigForm.tsx +++ b/src/application/components/agent/AgentConfigForm.tsx @@ -1,5 +1,12 @@ -import { useForm, useFieldArray } from "react-hook-form"; +import { useCallback, memo } from "react"; +import { + useForm, + useFieldArray, + useWatch, + type Control, +} from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { Plus } from "lucide-react"; import type { AgentConfig, SubAgentConfig } from "@/domain/entities/agent/agentConfig"; import { BackendType, MiddlewareType } from "@/domain/entities/agent/agentConfig"; import { McpTransportType } from "@/domain/entities/agent/mcpServerConfig"; @@ -32,8 +39,7 @@ import SubAgentEditor from "./SubAgentEditor"; import HITLEditor from "./HITLEditor"; import ResponseFormatEditor from "./ResponseFormatEditor"; import { AGENT_CONFIG_FORM_ID as FORM_ID } from "./formConstants"; - -// ─── Constants ────────────────────────────────────────────────────────────── +import { cn } from "@/application/lib/utils"; type SectionValue = | "general" @@ -46,19 +52,18 @@ type SectionValue = | "subagents" | "response-format"; -// Only essential sections are open by default, others can be revealed on demand. const DEFAULT_OPEN_SECTIONS: SectionValue[] = ["general", "system-prompt"]; -const SECTION_META: Record = { - general: { label: "General", icon: "tune" }, - "system-prompt": { label: "System Prompt", icon: "description" }, - "tools-middleware": { label: "Tools & Middleware", icon: "build" }, - backend: { label: "Backend", icon: "storage" }, - hitl: { label: "HITL", icon: "verified_user" }, - "memory-skills": { label: "Memory & Skills", icon: "psychology" }, - "mcp-servers": { label: "MCP Servers", icon: "hub" }, - subagents: { label: "Subagents", icon: "group" }, - "response-format": { label: "Response Format", icon: "data_object" }, +const SECTION_META: Record = { + general: { label: "General" }, + "system-prompt": { label: "System Prompt" }, + "tools-middleware": { label: "Tools & Middleware" }, + backend: { label: "Backend" }, + hitl: { label: "HITL" }, + "memory-skills": { label: "Memory & Skills" }, + "mcp-servers": { label: "MCP Servers" }, + subagents: { label: "Subagents" }, + "response-format": { label: "Response Format" }, }; const MIDDLEWARE_OPTIONS: { label: string; value: MiddlewareType }[] = [ @@ -126,26 +131,10 @@ function getDefaultValues(mode: "create" | "edit", initialData?: AgentConfig): A return { ...DEFAULT_FORM_VALUES }; } -// ─── Helpers ──────────────────────────────────────────────────────────────── - -function cn(...classes: (string | false | undefined)[]): string { - return classes.filter(Boolean).join(" "); -} - -function buildAccordionTriggerClass(): string { - return cn( - "text-xs font-bold font-headline uppercase tracking-widest", - "text-on-surface hover:text-secondary-brand", - "data-[state=open]:text-secondary-brand", - ); -} - function showRootDir(type: BackendType): boolean { return type === BackendType.FILESYSTEM || type === BackendType.COMPOSITE; } -// ─── Props ────────────────────────────────────────────────────────────────── - interface AgentConfigFormProps { mode: "create" | "edit"; initialData?: AgentConfig; @@ -154,8 +143,6 @@ interface AgentConfigFormProps { isPending?: boolean; } -// ─── Component ──────────────────────────────────────────────────────────── - export default function AgentConfigForm({ mode, initialData, @@ -169,19 +156,17 @@ export default function AgentConfigForm({ control, setValue, getValues, - watch, formState: { errors }, } = useForm({ resolver: zodResolver(agentConfigSchema), defaultValues: getDefaultValues(mode, initialData), }); - const mcpServersArray = useFieldArray({ control, name: "mcp_servers" }); const subagentsArray = useFieldArray({ control, name: "subagents" }); // eslint-disable-next-line react-hooks/incompatible-library - const backendType = watch("backend.type"); - const middlewareValues = watch("middleware"); + const backendType = useWatch({ control, name: "backend.type" }); + const middlewareValues = useWatch({ control, name: "middleware" }); function handleFormSubmit(data: AgentConfigFormData) { const cleaned: AgentConfigFormData = { @@ -193,10 +178,6 @@ export default function AgentConfigForm({ onSubmit(cleaned); } - function addMcpServer() { - mcpServersArray.append({ ...EMPTY_MCP_SERVER }); - } - function addSubagent() { subagentsArray.append({ ...EMPTY_SUBAGENT }); } @@ -214,18 +195,12 @@ export default function AgentConfigForm({ } return ( -
+ - {/* General */} @@ -250,19 +225,18 @@ export default function AgentConfigForm({ />
-
- {/* System Prompt */} @@ -287,19 +261,18 @@ export default function AgentConfigForm({ - {/* Tools & Middleware */} setValue("tools", tools)} placeholder="Add tool…" />
- -
+ +
{MIDDLEWARE_OPTIONS.map((opt) => { const isActive = middlewareValues.includes(opt.value); return ( @@ -309,10 +282,10 @@ export default function AgentConfigForm({ onClick={() => toggleMiddleware(opt.value, !isActive)} aria-pressed={isActive} className={cn( - "px-3 py-1.5 rounded-full text-xs font-bold font-headline uppercase tracking-widest border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-secondary-brand focus-visible:ring-inset", + "inline-flex h-8 items-center gap-2 border px-3 font-mono text-xs uppercase tracking-[0.1em] transition-[background-color,border-color,color] duration-fast ease-standard focus-visible:outline-none focus-visible:shadow-focus", isActive - ? "bg-secondary-brand text-white border-secondary-brand" - : "bg-surface-container-low text-on-surface-variant border-outline-variant/30 hover:bg-surface-container-high", + ? "border-accent bg-accent/10 text-accent" + : "border-border bg-transparent text-muted hover:bg-surface-warm hover:text-fg", )} > {opt.label} @@ -324,12 +297,11 @@ export default function AgentConfigForm({ - {/* Backend */}
- + +
- - + +
+
)}
- {/* Footer — sticky at bottom */} -
- + +
{mode === "form" ? ( - // The form triggers its own submit; we use a form attribute to bind. - + ) : ( - + )}
diff --git a/src/application/components/agent/HITLEditor.tsx b/src/application/components/agent/HITLEditor.tsx index 7eef806..415abc0 100644 --- a/src/application/components/agent/HITLEditor.tsx +++ b/src/application/components/agent/HITLEditor.tsx @@ -1,4 +1,5 @@ import { useState, useId } from "react"; +import { Plus, X } from "lucide-react"; import { Button } from "@/application/components/ui/button"; import { Input } from "@/application/components/ui/input"; import { Label } from "@/application/components/ui/label"; @@ -34,7 +35,7 @@ export default function HITLEditor({ value, onChange }: Readonly +
{entries.map(([key, val]) => (
- {key} + {key}
{typeof val === "boolean" ? (
- + updateRule(key, v)} />
) : (
- + updateRule(key, { ...val, before: v })} />
- + updateRule(key, { ...val, after: v })} @@ -84,20 +86,20 @@ export default function HITLEditor({ value, onChange }: Readonly
)} - +
))} {entries.length === 0 && ( -

No HITL rules configured

+

No HITL rules configured

)}
); diff --git a/src/application/components/agent/KeyValueEditor.tsx b/src/application/components/agent/KeyValueEditor.tsx index 724c610..ec920bf 100644 --- a/src/application/components/agent/KeyValueEditor.tsx +++ b/src/application/components/agent/KeyValueEditor.tsx @@ -1,4 +1,5 @@ import { useState, useId } from "react"; +import { Plus, X } from "lucide-react"; import { Button } from "@/application/components/ui/button"; import { Input } from "@/application/components/ui/input"; import { Label } from "@/application/components/ui/label"; @@ -41,11 +42,8 @@ export default function KeyValueEditor({ const entries = Object.entries(value); return ( -
-