diff --git a/crates/executors/default_mcp.json b/crates/executors/default_mcp.json new file mode 100644 index 00000000..cedf445f --- /dev/null +++ b/crates/executors/default_mcp.json @@ -0,0 +1,43 @@ +{ + "vibe_kanban": { + "command": "npx", + "args": [ + "-y", + "vibe-kanban", + "--mcp" + ] + }, + "context7": { + "type": "http", + "url": "https://mcp.context7.com/mcp", + "headers": { + "CONTEXT7_API_KEY": "YOUR_API_KEY" + } + }, + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + }, + "meta": { + "vibe_kanban": { + "name": "Vibe Kanban", + "description": "Create, update and delete Vibe Kanban tasks", + "url": "https://www.vibekanban.com/docs/user-guide/vibe-kanban-mcp-server", + "icon": "viba-kanban-favicon.png" + }, + "context7": { + "name": "Context7", + "description": "Fetch up-to-date documentation and code examples", + "url": "https://github.com/upstash/context7", + "icon": "mcp/context7logo.png" + }, + "playwright": { + "name": "Playwright", + "description": "Browser automation with Playwright", + "url": "https://github.com/microsoft/playwright-mcp", + "icon": "mcp/playwright_logo_icon.svg" + } + } +} diff --git a/crates/executors/src/executors/mod.rs b/crates/executors/src/executors/mod.rs index b7d12f8c..318151bf 100644 --- a/crates/executors/src/executors/mod.rs +++ b/crates/executors/src/executors/mod.rs @@ -85,10 +85,7 @@ impl CodingAgent { serde_json::json!({ "mcp_servers": {} }), - serde_json::json!({ - "command": "npx", - "args": ["-y", "vibe-kanban", "--mcp"], - }), + self.preconfigured_mcp(), true, ), Self::Amp(_) => McpConfig::new( @@ -96,10 +93,7 @@ impl CodingAgent { serde_json::json!({ "amp.mcpServers": {} }), - serde_json::json!({ - "command": "npx", - "args": ["-y", "vibe-kanban", "--mcp"], - }), + self.preconfigured_mcp(), false, ), Self::Opencode(_) => McpConfig::new( @@ -108,11 +102,7 @@ impl CodingAgent { "mcp": {}, "$schema": "https://opencode.ai/config.json" }), - serde_json::json!({ - "type": "local", - "command": ["npx", "-y", "vibe-kanban", "--mcp"], - "enabled": true - }), + self.preconfigured_mcp(), false, ), _ => McpConfig::new( @@ -120,10 +110,7 @@ impl CodingAgent { serde_json::json!({ "mcpServers": {} }), - serde_json::json!({ - "command": "npx", - "args": ["-y", "vibe-kanban", "--mcp"], - }), + self.preconfigured_mcp(), false, ), } diff --git a/crates/executors/src/mcp_config.rs b/crates/executors/src/mcp_config.rs index 9ba93998..745f45a8 100644 --- a/crates/executors/src/mcp_config.rs +++ b/crates/executors/src/mcp_config.rs @@ -2,21 +2,26 @@ //! //! These helpers abstract over JSON vs TOML formats used by different agents. -use std::collections::HashMap; +use std::{collections::HashMap, sync::LazyLock}; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{Map, Value}; use tokio::fs; use ts_rs::TS; -use crate::executors::ExecutorError; +use crate::executors::{CodingAgent, ExecutorError}; + +static DEFAULT_MCP_JSON: &str = include_str!("../default_mcp.json"); +pub static PRECONFIGURED_MCP_SERVERS: LazyLock = LazyLock::new(|| { + serde_json::from_str::(DEFAULT_MCP_JSON).expect("Failed to parse default MCP JSON") +}); #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct McpConfig { servers: HashMap, pub servers_path: Vec, pub template: serde_json::Value, - pub vibe_kanban: serde_json::Value, + pub preconfigured: serde_json::Value, pub is_toml_config: bool, } @@ -24,14 +29,14 @@ impl McpConfig { pub fn new( servers_path: Vec, template: serde_json::Value, - vibe_kanban: serde_json::Value, + preconfigured: serde_json::Value, is_toml_config: bool, ) -> Self { Self { servers: HashMap::new(), servers_path, template, - vibe_kanban, + preconfigured, is_toml_config, } } @@ -79,3 +84,207 @@ pub async fn write_agent_config( } Ok(()) } + +type ServerMap = Map; + +fn is_http_server(s: &Map) -> bool { + matches!(s.get("type").and_then(Value::as_str), Some("http")) +} + +fn is_stdio(s: &Map) -> bool { + !is_http_server(s) && s.get("command").is_some() +} + +fn extract_meta(mut obj: ServerMap) -> (ServerMap, Option) { + let meta = obj.remove("meta"); + (obj, meta) +} + +fn attach_meta(mut obj: ServerMap, meta: Option) -> Value { + if let Some(m) = meta { + obj.insert("meta".to_string(), m); + } + Value::Object(obj) +} + +fn ensure_header(headers: &mut Map, key: &str, val: &str) { + match headers.get_mut(key) { + Some(Value::String(_)) => {} + _ => { + headers.insert(key.to_string(), Value::String(val.to_string())); + } + } +} + +fn transform_http_servers(mut servers: ServerMap, mut f: F) -> ServerMap +where + F: FnMut(Map) -> Map, +{ + for (_k, v) in servers.iter_mut() { + if let Value::Object(s) = v + && is_http_server(s) + { + let taken = std::mem::take(s); + *s = f(taken); + } + } + servers +} + +// --- Adapters --------------------------------------------------------------- + +fn adapt_passthrough(servers: ServerMap, meta: Option) -> Value { + attach_meta(servers, meta) +} + +fn adapt_gemini(servers: ServerMap, meta: Option) -> Value { + let servers = transform_http_servers(servers, |mut s| { + let url = s + .remove("url") + .unwrap_or_else(|| Value::String(String::new())); + let mut headers = s + .remove("headers") + .and_then(|v| v.as_object().cloned()) + .unwrap_or_default(); + + ensure_header( + &mut headers, + "Accept", + "application/json, text/event-stream", + ); + Map::from_iter([ + ("httpUrl".to_string(), url), + ("headers".to_string(), Value::Object(headers)), + ]) + }); + attach_meta(servers, meta) +} + +fn adapt_cursor(servers: ServerMap, meta: Option) -> Value { + let servers = transform_http_servers(servers, |mut s| { + let url = s + .remove("url") + .unwrap_or_else(|| Value::String(String::new())); + let headers = s + .remove("headers") + .unwrap_or_else(|| Value::Object(Default::default())); + Map::from_iter([("url".to_string(), url), ("headers".to_string(), headers)]) + }); + attach_meta(servers, meta) +} + +fn adapt_codex(mut servers: ServerMap, mut meta: Option) -> Value { + servers.retain(|_, v| v.as_object().map(is_stdio).unwrap_or(false)); + + if let Some(Value::Object(ref mut m)) = meta { + m.retain(|k, _| servers.contains_key(k)); + servers.insert("meta".to_string(), Value::Object(std::mem::take(m))); + meta = None; // already attached above + } + attach_meta(servers, meta) +} + +fn adapt_opencode(servers: ServerMap, meta: Option) -> Value { + let mut servers = transform_http_servers(servers, |mut s| { + let url = s + .remove("url") + .unwrap_or_else(|| Value::String(String::new())); + + let mut headers = s + .remove("headers") + .and_then(|v| v.as_object().cloned()) + .unwrap_or_default(); + + ensure_header( + &mut headers, + "Accept", + "application/json, text/event-stream", + ); + + Map::from_iter([ + ("type".to_string(), Value::String("remote".to_string())), + ("url".to_string(), url), + ("headers".to_string(), Value::Object(headers)), + ("enabled".to_string(), Value::Bool(true)), + ]) + }); + + for (_k, v) in servers.iter_mut() { + if let Value::Object(s) = v + && is_stdio(s) + { + let command_str = s + .remove("command") + .and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }) + .unwrap_or_default(); + + let mut cmd_vec: Vec = Vec::new(); + if !command_str.is_empty() { + cmd_vec.push(Value::String(command_str)); + } + + if let Some(arr) = s.remove("args").and_then(|v| match v { + Value::Array(arr) => Some(arr), + _ => None, + }) { + for a in arr { + match a { + Value::String(s) => cmd_vec.push(Value::String(s)), + other => cmd_vec.push(other), // fall back to raw value if not string + } + } + } + + let mut new_map = Map::new(); + new_map.insert("type".to_string(), Value::String("local".to_string())); + new_map.insert("command".to_string(), Value::Array(cmd_vec)); + new_map.insert("enabled".to_string(), Value::Bool(true)); + *s = new_map; + } + } + + attach_meta(servers, meta) +} + +enum Adapter { + Passthrough, + Gemini, + Cursor, + Codex, + Opencode, +} + +fn apply_adapter(adapter: Adapter, canonical: Value) -> Value { + let (servers_only, meta) = match canonical.as_object() { + Some(map) => extract_meta(map.clone()), + None => (ServerMap::new(), None), + }; + + match adapter { + Adapter::Passthrough => adapt_passthrough(servers_only, meta), + Adapter::Gemini => adapt_gemini(servers_only, meta), + Adapter::Cursor => adapt_cursor(servers_only, meta), + Adapter::Codex => adapt_codex(servers_only, meta), + Adapter::Opencode => adapt_opencode(servers_only, meta), + } +} + +impl CodingAgent { + pub fn preconfigured_mcp(&self) -> Value { + use Adapter::*; + + let adapter = match self { + CodingAgent::ClaudeCode(_) | CodingAgent::Amp(_) => Passthrough, + CodingAgent::QwenCode(_) | CodingAgent::Gemini(_) => Gemini, + CodingAgent::Cursor(_) => Cursor, + CodingAgent::Codex(_) => Codex, + CodingAgent::Opencode(_) => Opencode, + }; + + let canonical = PRECONFIGURED_MCP_SERVERS.clone(); + apply_adapter(adapter, canonical) + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 745bd235..001dfe44 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,6 +20,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-portal": "^1.1.9", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", @@ -1964,6 +1965,67 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5910b56a..cb0b68de 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -43,6 +43,7 @@ "click-to-react-component": "^1.1.2", "clsx": "^2.0.0", "diff": "^8.0.2", + "embla-carousel-react": "^8.6.0", "fancy-ansi": "^0.1.3", "lucide-react": "^0.539.0", "react": "^18.2.0", diff --git a/frontend/public/mcp/context7logo.png b/frontend/public/mcp/context7logo.png new file mode 100644 index 00000000..43ad8207 Binary files /dev/null and b/frontend/public/mcp/context7logo.png differ diff --git a/frontend/public/mcp/playwright_logo_icon.svg b/frontend/public/mcp/playwright_logo_icon.svg new file mode 100644 index 00000000..63802699 --- /dev/null +++ b/frontend/public/mcp/playwright_logo_icon.svg @@ -0,0 +1,12 @@ + + + Playwright Streamline Icon: https://streamlinehq.com + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/ui/carousel.tsx b/frontend/src/components/ui/carousel.tsx new file mode 100644 index 00000000..2abee3ee --- /dev/null +++ b/frontend/src/components/ui/carousel.tsx @@ -0,0 +1,260 @@ +import * as React from 'react'; +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from 'embla-carousel-react'; +import { ArrowLeft, ArrowRight } from 'lucide-react'; + +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; + +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; + +type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: 'horizontal' | 'vertical'; + setApi?: (api: CarouselApi) => void; +}; + +type CarouselContextProps = { + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; + +const CarouselContext = React.createContext(null); + +function useCarousel() { + const context = React.useContext(CarouselContext); + + if (!context) { + throw new Error('useCarousel must be used within a '); + } + + return context; +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = 'horizontal', + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === 'horizontal' ? 'x' : 'y', + }, + plugins + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return; + } + + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev(); + }, [api]); + + const scrollNext = React.useCallback(() => { + api?.scrollNext(); + }, [api]); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'ArrowLeft') { + event.preventDefault(); + scrollPrev(); + } else if (event.key === 'ArrowRight') { + event.preventDefault(); + scrollNext(); + } + }, + [scrollPrev, scrollNext] + ); + + React.useEffect(() => { + if (!api || !setApi) { + return; + } + + setApi(api); + }, [api, setApi]); + + React.useEffect(() => { + if (!api) { + return; + } + + onSelect(api); + api.on('reInit', onSelect); + api.on('select', onSelect); + + return () => { + api?.off('select', onSelect); + }; + }, [api, onSelect]); + + return ( + +
+ {children} +
+
+ ); + } +); +Carousel.displayName = 'Carousel'; + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel(); + + return ( +
+
+
+ ); +}); +CarouselContent.displayName = 'CarouselContent'; + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel(); + + return ( +
+ ); +}); +CarouselItem.displayName = 'CarouselItem'; + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); + + return ( + + ); +}); +CarouselPrevious.displayName = 'CarouselPrevious'; + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); +}); +CarouselNext.displayName = 'CarouselNext'; + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +}; diff --git a/frontend/src/lib/mcp-strategies.ts b/frontend/src/lib/mcp-strategies.ts index 9d7ad02f..21d3a022 100644 --- a/frontend/src/lib/mcp-strategies.ts +++ b/frontend/src/lib/mcp-strategies.ts @@ -53,32 +53,31 @@ export class McpConfigStrategyGeneral { return current; } - static addVibeKanbanToConfig( + static addPreconfiguredToConfig( mcp_config: McpConfig, - existingConfig: Record + existingConfig: Record, + serverKey: string ): Record { - // Clone the existing config to avoid mutations - const updatedConfig = JSON.parse(JSON.stringify(existingConfig)); - let current = updatedConfig; + const preconf = mcp_config.preconfigured as Record; + if (!preconf || typeof preconf !== 'object' || !(serverKey in preconf)) { + throw new Error(`Unknown preconfigured server '${serverKey}'`); + } + + const updated = JSON.parse(JSON.stringify(existingConfig || {})); + let current = updated; - // Navigate to the correct location for servers (all except the last element) for (let i = 0; i < mcp_config.servers_path.length - 1; i++) { const key = mcp_config.servers_path[i]; - if (!current[key]) { - current[key] = {}; - } + if (!current[key] || typeof current[key] !== 'object') current[key] = {}; current = current[key]; } - // Get or create the servers object at the final path element const lastKey = mcp_config.servers_path[mcp_config.servers_path.length - 1]; - if (!current[lastKey]) { + if (!current[lastKey] || typeof current[lastKey] !== 'object') current[lastKey] = {}; - } - // Add vibe_kanban server with the config from the schema - current[lastKey]['vibe_kanban'] = mcp_config.vibe_kanban; + current[lastKey][serverKey] = preconf[serverKey]; - return updatedConfig; + return updated; } } diff --git a/frontend/src/pages/settings/McpSettings.tsx b/frontend/src/pages/settings/McpSettings.tsx index 21120d01..1d318f4e 100644 --- a/frontend/src/pages/settings/McpSettings.tsx +++ b/frontend/src/pages/settings/McpSettings.tsx @@ -14,6 +14,13 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, +} from '@/components/ui/carousel'; import { Label } from '@/components/ui/label'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { JSONEditor } from '@/components/ui/json-editor'; @@ -119,29 +126,6 @@ export function McpSettings() { } }; - const handleConfigureVibeKanban = async () => { - if (!selectedProfile || !mcpConfig) return; - - try { - // Parse existing configuration - const existingConfig = mcpServers.trim() ? JSON.parse(mcpServers) : {}; - - // Add vibe_kanban to the existing configuration using the schema - const updatedConfig = McpConfigStrategyGeneral.addVibeKanbanToConfig( - mcpConfig, - existingConfig - ); - - // Update the textarea with the new configuration - const configJson = JSON.stringify(updatedConfig, null, 2); - setMcpServers(configJson); - setMcpError(null); - } catch (err) { - setMcpError('Failed to configure vibe-kanban MCP server'); - console.error('Error configuring vibe-kanban:', err); - } - }; - const handleApplyMcpServers = async () => { if (!selectedProfile || !mcpConfig) return; @@ -200,6 +184,36 @@ export function McpSettings() { } }; + const addServer = (key: string) => { + try { + const existing = mcpServers.trim() ? JSON.parse(mcpServers) : {}; + const updated = McpConfigStrategyGeneral.addPreconfiguredToConfig( + mcpConfig!, + existing, + key + ); + setMcpServers(JSON.stringify(updated, null, 2)); + setMcpError(null); + } catch (err) { + console.error(err); + setMcpError( + err instanceof Error + ? err.message + : 'Failed to add preconfigured server' + ); + } + }; + + const preconfigured = (mcpConfig?.preconfigured ?? {}) as Record; + const meta = (preconfigured.meta ?? {}) as Record< + string, + { name?: string; description?: string; url?: string; icon?: string } + >; + const servers = Object.fromEntries( + Object.entries(preconfigured).filter(([k]) => k !== 'meta') + ) as Record; + const getMetaFor = (key: string) => meta[key] || {}; + if (!config) { return (
@@ -324,18 +338,83 @@ export function McpSettings() { )}
-
- -

- Automatically adds the Vibe-Kanban MCP server configuration. -

-
+ {mcpConfig?.preconfigured && + typeof mcpConfig.preconfigured === 'object' && ( +
+ +

+ Click a card to insert that MCP Server into the JSON + above. +

+ +
+ + + {Object.entries(servers).map(([key]) => { + const metaObj = getMetaFor(key) as { + name?: string; + description?: string; + url?: string; + icon?: string; + }; + const name = metaObj.name || key; + const description = + metaObj.description || 'No description'; + const icon = metaObj.icon + ? `/${metaObj.icon}` + : null; + + return ( + + + + ); + })} + + + + + +
+
+ )}
)} diff --git a/package.json b/package.json index c71c5386..79a23c6d 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "npx-cli/dist/**" ], "scripts": { + "format": "cargo fmt --all && cd frontend && npm run format", "check": "npm run frontend:check && npm run backend:check", "dev": "export FRONTEND_PORT=$(node scripts/setup-dev-environment.js frontend) && export BACKEND_PORT=$(node scripts/setup-dev-environment.js backend) && concurrently \"npm run backend:dev:watch\" \"npm run frontend:dev\"", "test:npm": "./test-npm-package.sh", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2b78cd5..ec86c382 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,6 +111,9 @@ importers: diff: specifier: ^8.0.2 version: 8.0.2 + embla-carousel-react: + specifier: ^8.6.0 + version: 8.6.0(react@18.3.1) fancy-ansi: specifier: ^0.1.3 version: 0.1.3 @@ -2000,6 +2003,19 @@ packages: electron-to-chromium@1.5.170: resolution: {integrity: sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==} + embla-carousel-react@8.6.0: + resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.6.0: + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -5179,6 +5195,18 @@ snapshots: electron-to-chromium@1.5.170: {} + embla-carousel-react@8.6.0(react@18.3.1): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + react: 18.3.1 + + embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} diff --git a/shared/types.ts b/shared/types.ts index 32f22b19..968b3f1b 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -24,7 +24,7 @@ export type SearchMode = "taskform" | "settings"; export type ExecutorAction = { typ: ExecutorActionType, next_action: ExecutorAction | null, }; -export type McpConfig = { servers: { [key in string]?: JsonValue }, servers_path: Array, template: JsonValue, vibe_kanban: JsonValue, is_toml_config: boolean, }; +export type McpConfig = { servers: { [key in string]?: JsonValue }, servers_path: Array, template: JsonValue, preconfigured: JsonValue, is_toml_config: boolean, }; export type ExecutorActionType = { "type": "CodingAgentInitialRequest" } & CodingAgentInitialRequest | { "type": "CodingAgentFollowUpRequest" } & CodingAgentFollowUpRequest | { "type": "ScriptRequest" } & ScriptRequest;