UI form builder for editing executor config (#619)

* ## 🎉 Implementation Complete!

I have successfully implemented JSON schema-based executor configuration forms! Here's what was accomplished:

### ** Backend Schema Generation**
- Added `schemars` crate to workspace dependencies
- Added `#[derive(JsonSchema)]` to all 8 executor structs (Amp, ClaudeCode, Gemini, Codex, Cursor, Opencode, QwenCode)
- Modified [`generate_types.rs`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-4b73-use-the-sc/crates/server/src/bin/generate_types.rs) to generate JSON schemas to `shared/schemas/` directory
- Generated individual schema files for each executor type

### ** Frontend Form System**
- Added `@rjsf/core`, `@rjsf/utils`, and `@rjsf/validator-ajv8` packages
- Created [`ExecutorConfigForm`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-4b73-use-the-sc/frontend/src/components/ExecutorConfigForm.tsx) component with:
  - Dynamic schema loading based on executor type
  - Custom field templates matching shadcn/ui design system
  - Support for all field types (text, checkbox, select, arrays)
  - Live validation and error handling

### ** Enhanced Settings UI**
- Updated [`Settings.tsx`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-4b73-use-the-sc/frontend/src/pages/Settings.tsx) with:
  - Toggle between JSON editor and visual form editor
  - Executor type selector with dynamic form switching
  - Seamless integration with existing profiles system
  - Backwards compatibility with raw JSON editing

### ** Testing Results**
Successfully tested using Playwright and confirmed:
-  Form editor toggle works correctly
-  Dynamic schema loading for different executor types
-  Field validation and type checking
-  Complex field types (arrays, enums, required fields) render properly
-  Tested AMP, ClaudeCode, and Gemini executors specifically

### **Key Features Delivered**
1. **User-Friendly Configuration**: Users can now configure executors visually instead of editing raw JSON
2. **Type Safety**: Generated schemas ensure configuration validity
3. **Dynamic Forms**: Each executor type shows only relevant fields
4. **Backwards Compatibility**: Users can still switch back to JSON mode
5. **Professional UI**: Custom templates match the existing design system
6. **Extensible**: Adding new executor types automatically generates forms

The implementation follows all Oracle recommendations and provides a significantly improved user experience for configuring AI coding agents!

* Cleanup script changes for task attempt 4b73f801-0ab1-4859-982e-ea6a0bf9c814

* ## 🚀 **Implementation Complete with Profile Selection!**

Your question was perfectly answered! The new implementation now provides:

### ** Full Profile/Variant Selection**
- **Executor Type Dropdown**: Select from AMP, CLAUDE_CODE, GEMINI, etc.
- **Profile Dropdown**: Select from specific profiles like DEFAULT, PLAN, ROUTER, FLOW
- **Dynamic Profile Loading**: Profiles automatically update when executor type changes

### ** Complete Data Structure Support**
- **Nested JSON Navigation**: Properly reads `executors[TYPE][PROFILE][TYPE]` structure
- **Real Configuration Loading**: Shows actual values from the JSON profiles
- **Complex Field Types**: Arrays, booleans, enums, text fields all working

### ** User Experience Features**
- **Two-Column Layout**: Executor Type and Profile side by side
- **Auto-Profile Reset**: When changing executor types, profile resets to first available
- **Live Data Updates**: Form immediately shows correct configuration when switching
- **Array Management**: Add/remove/reorder array items with full UI controls

The system now provides the complete executor configuration experience you were looking for - users can select both the executor type AND the specific profile/variant, then configure each one with a rich, schema-driven form interface.

* Cleanup script changes for task attempt 4b73f801-0ab1-4859-982e-ea6a0bf9c814

* improvements

* append_prompt

* generate forms

* order

* settings

* amp MCP config update

* form styles

* textarea

* style additional params

* validate

* menu styles

* prevent reload

* fmt

* add and delete configurations

* lint

* fmnt

* clippy

* prettier

* copy

* remove old MCP

* Auto detect schemas on FE

* wipe shared before generation

* fmt

* clippy fmt

* fixes

* fmt

* update shared types check

* disable clippy for large enum

* copy

* tweaks

* fmt

* fmt
This commit is contained in:
Louis Knight-Webb
2025-09-04 20:46:26 +01:00
committed by GitHub
parent 71fda5eb90
commit 3c05db3c49
54 changed files with 3535 additions and 1129 deletions

View File

@@ -14,6 +14,7 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
openssl-sys = { version = "0.9", features = ["vendored"] } openssl-sys = { version = "0.9", features = ["vendored"] }
ts-rs = { git = "https://github.com/xazukx/ts-rs.git", branch = "use-ts-enum", features = ["uuid-impl", "chrono-impl", "no-serde-warnings"] } ts-rs = { git = "https://github.com/xazukx/ts-rs.git", branch = "use-ts-enum", features = ["uuid-impl", "chrono-impl", "no-serde-warnings"] }
schemars = { version = "1.0.4", features = ["derive", "chrono04", "uuid1", "preserve_order"] }
[profile.release] [profile.release]
debug = true debug = true

View File

@@ -16,6 +16,7 @@ tracing-subscriber = { workspace = true }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4", "serde"] } uuid = { version = "1.0", features = ["v4", "serde"] }
ts-rs = { workspace = true, features = ["serde-json-impl"]} ts-rs = { workspace = true, features = ["serde-json-impl"]}
schemars = { workspace = true }
dirs = "5.0" dirs = "5.0"
xdg = "3.0" xdg = "3.0"
async-trait = "0.1" async-trait = "0.1"

View File

@@ -1,15 +1,24 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, Default)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema, Default)]
pub struct CmdOverrides { pub struct CmdOverrides {
#[schemars(
title = "Base Command Override",
description = "Override the base command with a custom command"
)]
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub base_command_override: Option<String>, pub base_command_override: Option<String>,
#[schemars(
title = "Additional Parameters",
description = "Additional parameters to append to the base command"
)]
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub additional_params: Option<Vec<String>>, pub additional_params: Option<Vec<String>>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
pub struct CommandBuilder { pub struct CommandBuilder {
/// Base executable command (e.g., "npx -y @anthropic-ai/claude-code@latest") /// Base executable command (e.g., "npx -y @anthropic-ai/claude-code@latest")
pub base: String, pub base: String,

View File

@@ -2,6 +2,7 @@ use std::{path::Path, process::Stdio, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use command_group::{AsyncCommandGroup, AsyncGroupChild}; use command_group::{AsyncCommandGroup, AsyncGroupChild};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::{io::AsyncWriteExt, process::Command}; use tokio::{io::AsyncWriteExt, process::Command};
use ts_rs::TS; use ts_rs::TS;
@@ -10,18 +11,21 @@ use utils::{msg_store::MsgStore, shell::get_shell_command};
use crate::{ use crate::{
command::{CmdOverrides, CommandBuilder, apply_overrides}, command::{CmdOverrides, CommandBuilder, apply_overrides},
executors::{ executors::{
ExecutorError, StandardCodingAgentExecutor, AppendPrompt, ExecutorError, StandardCodingAgentExecutor,
claude::{ClaudeLogProcessor, HistoryStrategy}, claude::{ClaudeLogProcessor, HistoryStrategy},
}, },
logs::{stderr_processor::normalize_stderr_logs, utils::EntryIndexProvider}, logs::{stderr_processor::normalize_stderr_logs, utils::EntryIndexProvider},
}; };
/// An executor that uses Amp to process tasks #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
pub struct Amp { pub struct Amp {
#[serde(default)]
pub append_prompt: AppendPrompt,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub append_prompt: Option<String>, #[schemars(
#[serde(default, skip_serializing_if = "Option::is_none")] title = "Dangerously Allow All",
description = "Allow all commands to be executed, even if they are not safe."
)]
pub dangerously_allow_all: Option<bool>, pub dangerously_allow_all: Option<bool>,
#[serde(flatten)] #[serde(flatten)]
pub cmd: CmdOverrides, pub cmd: CmdOverrides,
@@ -48,7 +52,7 @@ impl StandardCodingAgentExecutor for Amp {
let (shell_cmd, shell_arg) = get_shell_command(); let (shell_cmd, shell_arg) = get_shell_command();
let amp_command = self.build_command_builder().build_initial(); let amp_command = self.build_command_builder().build_initial();
let combined_prompt = utils::text::combine_prompt(&self.append_prompt, prompt); let combined_prompt = self.append_prompt.combine_prompt(prompt);
let mut command = Command::new(shell_cmd); let mut command = Command::new(shell_cmd);
command command
@@ -118,7 +122,7 @@ impl StandardCodingAgentExecutor for Amp {
new_thread_id.clone(), new_thread_id.clone(),
]); ]);
let combined_prompt = utils::text::combine_prompt(&self.append_prompt, prompt); let combined_prompt = self.append_prompt.combine_prompt(prompt);
let mut command = Command::new(shell_cmd); let mut command = Command::new(shell_cmd);
command command
@@ -158,6 +162,6 @@ impl StandardCodingAgentExecutor for Amp {
// MCP configuration methods // MCP configuration methods
fn default_mcp_config_path(&self) -> Option<std::path::PathBuf> { fn default_mcp_config_path(&self) -> Option<std::path::PathBuf> {
dirs::config_dir().map(|config| config.join("amp").join("settings.json")) dirs::home_dir().map(|home| home.join(".config").join("amp").join("settings.json"))
} }
} }

View File

@@ -3,6 +3,7 @@ use std::{path::Path, process::Stdio, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use command_group::{AsyncCommandGroup, AsyncGroupChild}; use command_group::{AsyncCommandGroup, AsyncGroupChild};
use futures::StreamExt; use futures::StreamExt;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::{io::AsyncWriteExt, process::Command}; use tokio::{io::AsyncWriteExt, process::Command};
use ts_rs::TS; use ts_rs::TS;
@@ -16,7 +17,7 @@ use utils::{
use crate::{ use crate::{
command::{CmdOverrides, CommandBuilder, apply_overrides}, command::{CmdOverrides, CommandBuilder, apply_overrides},
executors::{ExecutorError, StandardCodingAgentExecutor}, executors::{AppendPrompt, ExecutorError, StandardCodingAgentExecutor},
logs::{ logs::{
ActionType, FileChange, NormalizedEntry, NormalizedEntryType, TodoItem, ActionType, FileChange, NormalizedEntry, NormalizedEntryType, TodoItem,
stderr_processor::normalize_stderr_logs, stderr_processor::normalize_stderr_logs,
@@ -32,14 +33,13 @@ fn base_command(claude_code_router: bool) -> &'static str {
} }
} }
/// An executor that uses Claude CLI to process tasks #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
pub struct ClaudeCode { pub struct ClaudeCode {
#[serde(default)]
pub append_prompt: AppendPrompt,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub claude_code_router: Option<bool>, pub claude_code_router: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub append_prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plan: Option<bool>, pub plan: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub dangerously_skip_permissions: Option<bool>, pub dangerously_skip_permissions: Option<bool>,
@@ -88,7 +88,7 @@ impl StandardCodingAgentExecutor for ClaudeCode {
base_command base_command
}; };
let combined_prompt = utils::text::combine_prompt(&self.append_prompt, prompt); let combined_prompt = self.append_prompt.combine_prompt(prompt);
let mut command = Command::new(shell_cmd); let mut command = Command::new(shell_cmd);
command command
@@ -128,7 +128,7 @@ impl StandardCodingAgentExecutor for ClaudeCode {
base_command base_command
}; };
let combined_prompt = utils::text::combine_prompt(&self.append_prompt, prompt); let combined_prompt = self.append_prompt.combine_prompt(prompt);
let mut command = Command::new(shell_cmd); let mut command = Command::new(shell_cmd);
command command
@@ -1502,7 +1502,7 @@ mod tests {
let executor = ClaudeCode { let executor = ClaudeCode {
claude_code_router: Some(false), claude_code_router: Some(false),
plan: None, plan: None,
append_prompt: None, append_prompt: AppendPrompt::default(),
dangerously_skip_permissions: None, dangerously_skip_permissions: None,
cmd: crate::command::CmdOverrides { cmd: crate::command::CmdOverrides {
base_command_override: None, base_command_override: None,

View File

@@ -8,6 +8,7 @@ use async_trait::async_trait;
use command_group::{AsyncCommandGroup, AsyncGroupChild}; use command_group::{AsyncCommandGroup, AsyncGroupChild};
use futures::StreamExt; use futures::StreamExt;
use regex::Regex; use regex::Regex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum_macros::AsRefStr; use strum_macros::AsRefStr;
use tokio::{io::AsyncWriteExt, process::Command}; use tokio::{io::AsyncWriteExt, process::Command};
@@ -21,7 +22,7 @@ use utils::{
use crate::{ use crate::{
command::{CmdOverrides, CommandBuilder, apply_overrides}, command::{CmdOverrides, CommandBuilder, apply_overrides},
executors::{ExecutorError, StandardCodingAgentExecutor}, executors::{AppendPrompt, ExecutorError, StandardCodingAgentExecutor},
logs::{ logs::{
ActionType, FileChange, NormalizedEntry, NormalizedEntryType, ActionType, FileChange, NormalizedEntry, NormalizedEntryType,
utils::{EntryIndexProvider, patch::ConversationPatch}, utils::{EntryIndexProvider, patch::ConversationPatch},
@@ -29,7 +30,7 @@ use crate::{
}; };
/// Sandbox policy modes for Codex /// Sandbox policy modes for Codex
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, AsRefStr)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema, AsRefStr)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")] #[strum(serialize_all = "kebab-case")]
pub enum SandboxMode { pub enum SandboxMode {
@@ -39,7 +40,7 @@ pub enum SandboxMode {
} }
/// Approval policy for Codex /// Approval policy for Codex
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, AsRefStr)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, AsRefStr, JsonSchema)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")] #[strum(serialize_all = "kebab-case")]
pub enum ApprovalPolicy { pub enum ApprovalPolicy {
@@ -201,11 +202,10 @@ impl SessionHandler {
} }
} }
/// An executor that uses Codex CLI to process tasks #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
pub struct Codex { pub struct Codex {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default)]
pub append_prompt: Option<String>, pub append_prompt: AppendPrompt,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub sandbox: Option<SandboxMode>, pub sandbox: Option<SandboxMode>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -256,7 +256,7 @@ impl StandardCodingAgentExecutor for Codex {
let (shell_cmd, shell_arg) = get_shell_command(); let (shell_cmd, shell_arg) = get_shell_command();
let codex_command = self.build_command_builder().build_initial(); let codex_command = self.build_command_builder().build_initial();
let combined_prompt = utils::text::combine_prompt(&self.append_prompt, prompt); let combined_prompt = self.append_prompt.combine_prompt(prompt);
let mut command = Command::new(shell_cmd); let mut command = Command::new(shell_cmd);
command command
@@ -297,7 +297,7 @@ impl StandardCodingAgentExecutor for Codex {
format!("experimental_resume={}", rollout_file_path.display()), format!("experimental_resume={}", rollout_file_path.display()),
]); ]);
let combined_prompt = utils::text::combine_prompt(&self.append_prompt, prompt); let combined_prompt = self.append_prompt.combine_prompt(prompt);
let mut command = Command::new(shell_cmd); let mut command = Command::new(shell_cmd);
command command

View File

@@ -4,6 +4,7 @@ use std::{path::Path, process::Stdio, sync::Arc, time::Duration};
use async_trait::async_trait; use async_trait::async_trait;
use command_group::{AsyncCommandGroup, AsyncGroupChild}; use command_group::{AsyncCommandGroup, AsyncGroupChild};
use futures::StreamExt; use futures::StreamExt;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::{io::AsyncWriteExt, process::Command}; use tokio::{io::AsyncWriteExt, process::Command};
use ts_rs::TS; use ts_rs::TS;
@@ -19,7 +20,7 @@ use utils::{
use crate::{ use crate::{
command::{CmdOverrides, CommandBuilder, apply_overrides}, command::{CmdOverrides, CommandBuilder, apply_overrides},
executors::{ExecutorError, StandardCodingAgentExecutor}, executors::{AppendPrompt, ExecutorError, StandardCodingAgentExecutor},
logs::{ logs::{
ActionType, FileChange, NormalizedEntry, NormalizedEntryType, TodoItem, ActionType, FileChange, NormalizedEntry, NormalizedEntryType, TodoItem,
plain_text_processor::PlainTextLogProcessor, plain_text_processor::PlainTextLogProcessor,
@@ -27,11 +28,10 @@ use crate::{
}, },
}; };
/// Executor for running Cursor CLI and normalizing its JSONL stream #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
pub struct Cursor { pub struct Cursor {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default)]
pub append_prompt: Option<String>, pub append_prompt: AppendPrompt,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub force: Option<bool>, pub force: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -67,7 +67,7 @@ impl StandardCodingAgentExecutor for Cursor {
let (shell_cmd, shell_arg) = get_shell_command(); let (shell_cmd, shell_arg) = get_shell_command();
let agent_cmd = self.build_command_builder().build_initial(); let agent_cmd = self.build_command_builder().build_initial();
let combined_prompt = utils::text::combine_prompt(&self.append_prompt, prompt); let combined_prompt = self.append_prompt.combine_prompt(prompt);
let mut command = Command::new(shell_cmd); let mut command = Command::new(shell_cmd);
command command
@@ -100,7 +100,7 @@ impl StandardCodingAgentExecutor for Cursor {
.build_command_builder() .build_command_builder()
.build_follow_up(&["--resume".to_string(), session_id.to_string()]); .build_follow_up(&["--resume".to_string(), session_id.to_string()]);
let combined_prompt = utils::text::combine_prompt(&self.append_prompt, prompt); let combined_prompt = self.append_prompt.combine_prompt(prompt);
let mut command = Command::new(shell_cmd); let mut command = Command::new(shell_cmd);
command command
@@ -1072,7 +1072,7 @@ mod tests {
// Avoid relying on feature flag in tests; construct with a dummy command // Avoid relying on feature flag in tests; construct with a dummy command
let executor = Cursor { let executor = Cursor {
// No command field needed anymore // No command field needed anymore
append_prompt: None, append_prompt: AppendPrompt::default(),
force: None, force: None,
model: None, model: None,
cmd: Default::default(), cmd: Default::default(),

View File

@@ -7,6 +7,7 @@ use std::{
use async_trait::async_trait; use async_trait::async_trait;
use command_group::{AsyncCommandGroup, AsyncGroupChild}; use command_group::{AsyncCommandGroup, AsyncGroupChild};
use futures::{StreamExt, stream::BoxStream}; use futures::{StreamExt, stream::BoxStream};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::{ use tokio::{
fs::{self, OpenOptions}, fs::{self, OpenOptions},
@@ -18,7 +19,7 @@ use utils::{msg_store::MsgStore, shell::get_shell_command};
use crate::{ use crate::{
command::{CmdOverrides, CommandBuilder, apply_overrides}, command::{CmdOverrides, CommandBuilder, apply_overrides},
executors::{ExecutorError, StandardCodingAgentExecutor}, executors::{AppendPrompt, ExecutorError, StandardCodingAgentExecutor},
logs::{ logs::{
NormalizedEntry, NormalizedEntryType, plain_text_processor::PlainTextLogProcessor, NormalizedEntry, NormalizedEntryType, plain_text_processor::PlainTextLogProcessor,
stderr_processor::normalize_stderr_logs, utils::EntryIndexProvider, stderr_processor::normalize_stderr_logs, utils::EntryIndexProvider,
@@ -26,8 +27,7 @@ use crate::{
stdout_dup, stdout_dup,
}; };
/// Model variant of Gemini to use #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum GeminiModel { pub enum GeminiModel {
Default, // no --model flag Default, // no --model flag
@@ -50,13 +50,12 @@ impl GeminiModel {
} }
} }
/// An executor that uses Gemini to process tasks #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
pub struct Gemini { pub struct Gemini {
#[serde(default)]
pub append_prompt: AppendPrompt,
pub model: GeminiModel, pub model: GeminiModel,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub append_prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub yolo: Option<bool>, pub yolo: Option<bool>,
#[serde(flatten)] #[serde(flatten)]
pub cmd: CmdOverrides, pub cmd: CmdOverrides,
@@ -84,7 +83,7 @@ impl StandardCodingAgentExecutor for Gemini {
let (shell_cmd, shell_arg) = get_shell_command(); let (shell_cmd, shell_arg) = get_shell_command();
let gemini_command = self.build_command_builder().build_initial(); let gemini_command = self.build_command_builder().build_initial();
let combined_prompt = utils::text::combine_prompt(&self.append_prompt, prompt); let combined_prompt = self.append_prompt.combine_prompt(prompt);
let mut command = Command::new(shell_cmd); let mut command = Command::new(shell_cmd);
command command
@@ -364,7 +363,7 @@ The following is the conversation history from this session:
=== INSTRUCTIONS === === INSTRUCTIONS ===
You are continuing work on the above task. The execution history shows the previous conversation in this session. Please continue from where the previous execution left off, taking into account all the context provided above.{} You are continuing work on the above task. The execution history shows the previous conversation in this session. Please continue from where the previous execution left off, taking into account all the context provided above.{}
"#, "#,
self.append_prompt.clone().unwrap_or_default(), self.append_prompt.get().unwrap_or_default(),
)) ))
} }

View File

@@ -4,6 +4,7 @@ use async_trait::async_trait;
use command_group::AsyncGroupChild; use command_group::AsyncGroupChild;
use enum_dispatch::enum_dispatch; use enum_dispatch::enum_dispatch;
use futures_io::Error as FuturesIoError; use futures_io::Error as FuturesIoError;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::Type; use sqlx::Type;
use strum_macros::{Display, EnumDiscriminants, EnumString, VariantNames}; use strum_macros::{Display, EnumDiscriminants, EnumString, VariantNames};
@@ -167,3 +168,26 @@ pub trait StandardCodingAgentExecutor {
.unwrap_or(false) .unwrap_or(false)
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
#[serde(transparent)]
#[schemars(
title = "Append Prompt",
description = "Extra text appended to the prompt",
extend("format" = "textarea")
)]
#[derive(Default)]
pub struct AppendPrompt(pub Option<String>);
impl AppendPrompt {
pub fn get(&self) -> Option<String> {
self.0.clone()
}
pub fn combine_prompt(&self, prompt: &str) -> String {
match self {
AppendPrompt(Some(value)) => format!("{prompt}{value}"),
AppendPrompt(None) => prompt.to_string(),
}
}
}

View File

@@ -11,6 +11,7 @@ use fork_stream::StreamExt as _;
use futures::{StreamExt, future::ready, stream::BoxStream}; use futures::{StreamExt, future::ready, stream::BoxStream};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::{io::AsyncWriteExt, process::Command}; use tokio::{io::AsyncWriteExt, process::Command};
use ts_rs::TS; use ts_rs::TS;
@@ -21,7 +22,7 @@ use utils::{
use crate::{ use crate::{
command::{CmdOverrides, CommandBuilder, apply_overrides}, command::{CmdOverrides, CommandBuilder, apply_overrides},
executors::{ExecutorError, StandardCodingAgentExecutor}, executors::{AppendPrompt, ExecutorError, StandardCodingAgentExecutor},
logs::{ logs::{
ActionType, FileChange, NormalizedEntry, NormalizedEntryType, TodoItem, ActionType, FileChange, NormalizedEntry, NormalizedEntryType, TodoItem,
plain_text_processor::{MessageBoundary, PlainTextLogProcessor}, plain_text_processor::{MessageBoundary, PlainTextLogProcessor},
@@ -29,11 +30,10 @@ use crate::{
}, },
}; };
/// An executor that uses OpenCode to process tasks #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
pub struct Opencode { pub struct Opencode {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default)]
pub append_prompt: Option<String>, pub append_prompt: AppendPrompt,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>, pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -69,7 +69,7 @@ impl StandardCodingAgentExecutor for Opencode {
let (shell_cmd, shell_arg) = get_shell_command(); let (shell_cmd, shell_arg) = get_shell_command();
let opencode_command = self.build_command_builder().build_initial(); let opencode_command = self.build_command_builder().build_initial();
let combined_prompt = utils::text::combine_prompt(&self.append_prompt, prompt); let combined_prompt = self.append_prompt.combine_prompt(prompt);
let mut command = Command::new(shell_cmd); let mut command = Command::new(shell_cmd);
command command
@@ -104,7 +104,7 @@ impl StandardCodingAgentExecutor for Opencode {
.build_command_builder() .build_command_builder()
.build_follow_up(&["--session".to_string(), session_id.to_string()]); .build_follow_up(&["--session".to_string(), session_id.to_string()]);
let combined_prompt = utils::text::combine_prompt(&self.append_prompt, prompt); let combined_prompt = self.append_prompt.combine_prompt(prompt);
let mut command = Command::new(shell_cmd); let mut command = Command::new(shell_cmd);
command command
@@ -383,7 +383,7 @@ pub enum Tool {
} }
/// TODO information structure /// TODO information structure
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
pub struct TodoInfo { pub struct TodoInfo {
pub content: String, pub content: String,
pub status: String, pub status: String,
@@ -392,7 +392,7 @@ pub struct TodoInfo {
} }
/// Web fetch format options /// Web fetch format options
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum WebFetchFormat { pub enum WebFetchFormat {
Text, Text,

View File

@@ -2,6 +2,7 @@ use std::{path::Path, process::Stdio, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use command_group::{AsyncCommandGroup, AsyncGroupChild}; use command_group::{AsyncCommandGroup, AsyncGroupChild};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::{io::AsyncWriteExt, process::Command}; use tokio::{io::AsyncWriteExt, process::Command};
use ts_rs::TS; use ts_rs::TS;
@@ -9,15 +10,14 @@ use utils::{msg_store::MsgStore, shell::get_shell_command};
use crate::{ use crate::{
command::{CmdOverrides, CommandBuilder, apply_overrides}, command::{CmdOverrides, CommandBuilder, apply_overrides},
executors::{ExecutorError, StandardCodingAgentExecutor, gemini::Gemini}, executors::{AppendPrompt, ExecutorError, StandardCodingAgentExecutor, gemini::Gemini},
logs::{stderr_processor::normalize_stderr_logs, utils::EntryIndexProvider}, logs::{stderr_processor::normalize_stderr_logs, utils::EntryIndexProvider},
}; };
/// An executor that uses QwenCode CLI to process tasks #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
pub struct QwenCode { pub struct QwenCode {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default)]
pub append_prompt: Option<String>, pub append_prompt: AppendPrompt,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub yolo: Option<bool>, pub yolo: Option<bool>,
#[serde(flatten)] #[serde(flatten)]
@@ -46,7 +46,7 @@ impl StandardCodingAgentExecutor for QwenCode {
let (shell_cmd, shell_arg) = get_shell_command(); let (shell_cmd, shell_arg) = get_shell_command();
let qwen_command = self.build_command_builder().build_initial(); let qwen_command = self.build_command_builder().build_initial();
let combined_prompt = utils::text::combine_prompt(&self.append_prompt, prompt); let combined_prompt = self.append_prompt.combine_prompt(prompt);
let mut command = Command::new(shell_cmd); let mut command = Command::new(shell_cmd);
command command
@@ -80,7 +80,7 @@ impl StandardCodingAgentExecutor for QwenCode {
.build_command_builder() .build_command_builder()
.build_follow_up(&["--resume".to_string(), session_id.to_string()]); .build_follow_up(&["--resume".to_string(), session_id.to_string()]);
let combined_prompt = utils::text::combine_prompt(&self.append_prompt, prompt); let combined_prompt = self.append_prompt.combine_prompt(prompt);
let mut command = Command::new(shell_cmd); let mut command = Command::new(shell_cmd);
command command

View File

@@ -14,6 +14,7 @@ enum PatchOperation {
Remove, Remove,
} }
#[allow(clippy::large_enum_variant)]
#[derive(Serialize, TS)] #[derive(Serialize, TS)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type", content = "content")] #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type", content = "content")]
pub enum PatchType { pub enum PatchType {

View File

@@ -31,7 +31,7 @@ command-group = { version = "5.0", features = ["with-tokio"] }
nix = { version = "0.29", features = ["signal", "process"] } nix = { version = "0.29", features = ["signal", "process"] }
openssl-sys = { workspace = true } openssl-sys = { workspace = true }
rmcp = { version = "0.5.0", features = ["server", "transport-io"] } rmcp = { version = "0.5.0", features = ["server", "transport-io"] }
schemars = "0.8" schemars = { workspace = true }
regex = "1.11.1" regex = "1.11.1"
toml = "0.8" toml = "0.8"
sentry = { version = "0.41.0", features = ["anyhow", "backtrace", "panic", "debug-images"] } sentry = { version = "0.41.0", features = ["anyhow", "backtrace", "panic", "debug-images"] }

View File

@@ -1,5 +1,6 @@
use std::{env, fs, path::Path}; use std::{env, fs, path::Path};
use schemars::{JsonSchema, Schema, SchemaGenerator, generate::SchemaSettings};
use ts_rs::TS; use ts_rs::TS;
fn generate_types_content() -> String { fn generate_types_content() -> String {
@@ -74,6 +75,7 @@ fn generate_types_content() -> String {
executors::executors::cursor::Cursor::decl(), executors::executors::cursor::Cursor::decl(),
executors::executors::opencode::Opencode::decl(), executors::executors::opencode::Opencode::decl(),
executors::executors::qwen::QwenCode::decl(), executors::executors::qwen::QwenCode::decl(),
executors::executors::AppendPrompt::decl(),
executors::actions::coding_agent_initial::CodingAgentInitialRequest::decl(), executors::actions::coding_agent_initial::CodingAgentInitialRequest::decl(),
executors::actions::coding_agent_follow_up::CodingAgentFollowUpRequest::decl(), executors::actions::coding_agent_follow_up::CodingAgentFollowUpRequest::decl(),
server::routes::task_attempts::CreateTaskAttemptBody::decl(), server::routes::task_attempts::CreateTaskAttemptBody::decl(),
@@ -125,13 +127,54 @@ fn generate_types_content() -> String {
format!("{HEADER}\n\n{body}") format!("{HEADER}\n\n{body}")
} }
fn write_schema<T: JsonSchema>(
name: &str,
schemas_dir: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
// Draft-07, inline everything (no $defs)
let mut settings = SchemaSettings::draft07();
settings.inline_subschemas = true;
let generator: SchemaGenerator = settings.into_generator();
let schema: Schema = generator.into_root_schema_for::<T>();
// Convert to JSON value to manipulate it
let mut schema_value: serde_json::Value = serde_json::to_value(&schema)?;
// Remove the title from root schema to prevent RJSF from creating an outer field container
if let Some(obj) = schema_value.as_object_mut() {
obj.remove("title");
}
let schema_json = serde_json::to_string_pretty(&schema_value)?;
std::fs::write(schemas_dir.join(format!("{name}.json")), schema_json)?;
Ok(())
}
fn generate_schemas() -> Result<(), Box<dyn std::error::Error>> {
// Create schemas directory
let schemas_dir = Path::new("shared/schemas");
fs::create_dir_all(schemas_dir)?;
println!("Generating JSON schemas…");
// Generate schemas for all executor types
write_schema::<executors::executors::amp::Amp>("amp", schemas_dir)?;
write_schema::<executors::executors::claude::ClaudeCode>("claude_code", schemas_dir)?;
write_schema::<executors::executors::gemini::Gemini>("gemini", schemas_dir)?;
write_schema::<executors::executors::codex::Codex>("codex", schemas_dir)?;
write_schema::<executors::executors::cursor::Cursor>("cursor", schemas_dir)?;
write_schema::<executors::executors::opencode::Opencode>("opencode", schemas_dir)?;
write_schema::<executors::executors::qwen::QwenCode>("qwen_code", schemas_dir)?;
Ok(())
}
fn main() { fn main() {
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().collect();
let check_mode = args.iter().any(|arg| arg == "--check"); let check_mode = args.iter().any(|arg| arg == "--check");
// 1. Make sure ../shared exists
let shared_path = Path::new("shared"); let shared_path = Path::new("shared");
fs::create_dir_all(shared_path).expect("cannot create shared");
println!("Generating TypeScript types…"); println!("Generating TypeScript types…");
@@ -151,8 +194,22 @@ fn main() {
std::process::exit(1); std::process::exit(1);
} }
} else { } else {
// Wipe existing shared
fs::remove_dir_all(shared_path).ok();
// Recreate folder
fs::create_dir_all(shared_path).expect("cannot create shared");
// Write the file as before // Write the file as before
fs::write(&types_path, generated).expect("unable to write types.ts"); fs::write(&types_path, generated).expect("unable to write types.ts");
println!("✅ TypeScript types generated in shared/"); println!("✅ TypeScript types generated in shared/");
// Generate JSON schemas
if let Err(e) = generate_schemas() {
eprintln!("❌ Failed to generate schemas: {}", e);
std::process::exit(1);
}
println!("✅ JSON schemas generated in shared/schemas/");
} }
} }

View File

@@ -22,10 +22,3 @@ pub fn short_uuid(u: &Uuid) -> String {
let full = u.simple().to_string(); let full = u.simple().to_string();
full.chars().take(4).collect() // grab the first 4 chars full.chars().take(4).collect() // grab the first 4 chars
} }
pub fn combine_prompt(append: &Option<String>, prompt: &str) -> String {
match append {
Some(append) => format!("{prompt}{append}"),
None => prompt.to_string(),
}
}

View File

@@ -25,6 +25,9 @@
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@rjsf/core": "^5.24.13",
"@rjsf/utils": "^5.24.13",
"@rjsf/validator-ajv8": "^5.24.13",
"@sentry/react": "^9.34.0", "@sentry/react": "^9.34.0",
"@sentry/vite-plugin": "^3.5.0", "@sentry/vite-plugin": "^3.5.0",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
@@ -2273,6 +2276,90 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@rjsf/core": {
"version": "5.24.13",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.24.13.tgz",
"integrity": "sha512-ONTr14s7LFIjx2VRFLuOpagL76sM/HPy6/OhdBfq6UukINmTIs6+aFN0GgcR0aXQHFDXQ7f/fel0o/SO05Htdg==",
"license": "Apache-2.0",
"dependencies": {
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"markdown-to-jsx": "^7.4.1",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@rjsf/utils": "^5.24.x",
"react": "^16.14.0 || >=17"
}
},
"node_modules/@rjsf/utils": {
"version": "5.24.13",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.13.tgz",
"integrity": "sha512-rNF8tDxIwTtXzz5O/U23QU73nlhgQNYJ+Sv5BAwQOIyhIE2Z3S5tUiSVMwZHt0julkv/Ryfwi+qsD4FiE5rOuw==",
"license": "Apache-2.0",
"dependencies": {
"json-schema-merge-allof": "^0.8.1",
"jsonpointer": "^5.0.1",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"react-is": "^18.2.0"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": "^16.14.0 || >=17"
}
},
"node_modules/@rjsf/utils/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/@rjsf/validator-ajv8": {
"version": "5.24.13",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.24.13.tgz",
"integrity": "sha512-oWHP7YK581M8I5cF1t+UXFavnv+bhcqjtL1a7MG/Kaffi0EwhgcYjODrD8SsnrhncsEYMqSECr4ZOEoirnEUWw==",
"license": "Apache-2.0",
"dependencies": {
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@rjsf/utils": "^5.24.x"
}
},
"node_modules/@rjsf/validator-ajv8/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@rjsf/validator-ajv8/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.11", "version": "1.0.0-beta.11",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz",
@@ -3513,6 +3600,45 @@
"url": "https://github.com/sponsors/epoberezkin" "url": "https://github.com/sponsors/epoberezkin"
} }
}, },
"node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -3959,6 +4085,27 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/compute-gcd": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/compute-gcd/-/compute-gcd-1.2.1.tgz",
"integrity": "sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==",
"dependencies": {
"validate.io-array": "^1.0.3",
"validate.io-function": "^1.0.2",
"validate.io-integer-array": "^1.0.0"
}
},
"node_modules/compute-lcm": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/compute-lcm/-/compute-lcm-1.1.2.tgz",
"integrity": "sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==",
"dependencies": {
"compute-gcd": "^1.2.1",
"validate.io-array": "^1.0.3",
"validate.io-function": "^1.0.2",
"validate.io-integer-array": "^1.0.0"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -4558,7 +4705,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-diff": { "node_modules/fast-diff": {
@@ -4609,6 +4755,22 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.19.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -5241,6 +5403,29 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-schema-compare": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz",
"integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.4"
}
},
"node_modules/json-schema-merge-allof": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz",
"integrity": "sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==",
"license": "MIT",
"dependencies": {
"compute-lcm": "^1.1.2",
"json-schema-compare": "^0.2.2",
"lodash": "^4.17.20"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/json-schema-traverse": { "node_modules/json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -5267,6 +5452,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsonpointer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
"integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -5324,6 +5518,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash.castarray": { "node_modules/lodash.castarray": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
@@ -5409,6 +5615,18 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/markdown-to-jsx": {
"version": "7.7.13",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.13.tgz",
"integrity": "sha512-DiueEq2bttFcSxUs85GJcQVrOr0+VVsPfj9AEUPqmExJ3f8P/iQNvZHltV4tm1XVhu1kl0vWBZWT3l99izRMaA==",
"license": "MIT",
"engines": {
"node": ">= 10"
},
"peerDependencies": {
"react": ">= 0.14.0"
}
},
"node_modules/mdast-util-from-markdown": { "node_modules/mdast-util-from-markdown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
@@ -6960,6 +7178,15 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.10", "version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -7821,6 +8048,39 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/validate.io-array": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz",
"integrity": "sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==",
"license": "MIT"
},
"node_modules/validate.io-function": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz",
"integrity": "sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ=="
},
"node_modules/validate.io-integer": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz",
"integrity": "sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==",
"dependencies": {
"validate.io-number": "^1.0.3"
}
},
"node_modules/validate.io-integer-array": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz",
"integrity": "sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==",
"dependencies": {
"validate.io-array": "^1.0.3",
"validate.io-integer": "^1.0.4"
}
},
"node_modules/validate.io-number": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz",
"integrity": "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg=="
},
"node_modules/vfile": { "node_modules/vfile": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",

View File

@@ -31,6 +31,7 @@
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@rjsf/shadcn": "6.0.0-beta.10",
"@sentry/react": "^9.34.0", "@sentry/react": "^9.34.0",
"@sentry/vite-plugin": "^3.5.0", "@sentry/vite-plugin": "^3.5.0",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
@@ -58,6 +59,9 @@
"zustand": "^4.5.4" "zustand": "^4.5.4"
}, },
"devDependencies": { "devDependencies": {
"@rjsf/core": "6.0.0-beta.11",
"@rjsf/utils": "6.0.0-beta.11",
"@rjsf/validator-ajv8": "6.0.0-beta.11",
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@types/react": "^18.2.43", "@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",

View File

@@ -1,11 +1,21 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { BrowserRouter, Route, Routes, useLocation } from 'react-router-dom'; import {
BrowserRouter,
Route,
Routes,
useLocation,
Navigate,
} from 'react-router-dom';
import { Navbar } from '@/components/layout/navbar'; import { Navbar } from '@/components/layout/navbar';
import { Projects } from '@/pages/projects'; import { Projects } from '@/pages/projects';
import { ProjectTasks } from '@/pages/project-tasks'; import { ProjectTasks } from '@/pages/project-tasks';
import { Settings } from '@/pages/Settings'; import {
import { McpServers } from '@/pages/McpServers'; SettingsLayout,
GeneralSettings,
AgentSettings,
McpSettings,
} from '@/pages/settings/';
import { DisclaimerDialog } from '@/components/DisclaimerDialog'; import { DisclaimerDialog } from '@/components/DisclaimerDialog';
import { OnboardingDialog } from '@/components/OnboardingDialog'; import { OnboardingDialog } from '@/components/OnboardingDialog';
import { PrivacyOptInDialog } from '@/components/PrivacyOptInDialog'; import { PrivacyOptInDialog } from '@/components/PrivacyOptInDialog';
@@ -237,8 +247,17 @@ function AppContent() {
path="/projects/:projectId/tasks/:taskId" path="/projects/:projectId/tasks/:taskId"
element={<ProjectTasks />} element={<ProjectTasks />}
/> />
<Route path="/settings" element={<Settings />} /> <Route path="/settings/*" element={<SettingsLayout />}>
<Route path="/mcp-servers" element={<McpServers />} /> <Route index element={<Navigate to="general" replace />} />
<Route path="general" element={<GeneralSettings />} />
<Route path="agents" element={<AgentSettings />} />
<Route path="mcp" element={<McpSettings />} />
</Route>
{/* Redirect old MCP route */}
<Route
path="/mcp-servers"
element={<Navigate to="/settings/mcp" replace />}
/>
</SentryRoutes> </SentryRoutes>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,138 @@
import { useMemo, useEffect, useState } from 'react';
import Form from '@rjsf/core';
import { RJSFValidationError } from '@rjsf/utils';
import validator from '@rjsf/validator-ajv8';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';
import { shadcnTheme } from './rjsf';
// Using custom shadcn/ui widgets instead of @rjsf/shadcn theme
type ExecutorType =
| 'AMP'
| 'CLAUDE_CODE'
| 'GEMINI'
| 'CODEX'
| 'CURSOR'
| 'OPENCODE'
| 'QWEN_CODE';
interface ExecutorConfigFormProps {
executor: ExecutorType;
value: any;
onSubmit?: (formData: any) => void;
onChange?: (formData: any) => void;
onSave?: (formData: any) => Promise<void>;
disabled?: boolean;
isSaving?: boolean;
isDirty?: boolean;
}
import schemas from 'virtual:executor-schemas';
export function ExecutorConfigForm({
executor,
value,
onSubmit,
onChange,
onSave,
disabled = false,
isSaving = false,
isDirty = false,
}: ExecutorConfigFormProps) {
const [formData, setFormData] = useState(value || {});
const [validationErrors, setValidationErrors] = useState<
RJSFValidationError[]
>([]);
const schema = useMemo(() => {
return schemas[executor];
}, [executor]);
useEffect(() => {
setFormData(value || {});
setValidationErrors([]);
}, [value, executor]);
const handleChange = ({ formData: newFormData }: any) => {
setFormData(newFormData);
if (onChange) {
onChange(newFormData);
}
};
const handleSubmit = async ({ formData: submitData }: any) => {
setValidationErrors([]);
if (onSave) {
await onSave(submitData);
} else if (onSubmit) {
onSubmit(submitData);
}
};
const handleError = (errors: RJSFValidationError[]) => {
setValidationErrors(errors);
};
if (!schema) {
return (
<Alert variant="destructive">
<AlertDescription>
Schema not found for executor type: {executor}
</AlertDescription>
</Alert>
);
}
return (
<div className="space-y-8">
<Card>
<CardContent className="p-0">
<Form
schema={schema}
formData={formData}
onChange={handleChange}
onSubmit={handleSubmit}
onError={handleError}
validator={validator}
disabled={disabled}
liveValidate
showErrorList={false}
widgets={shadcnTheme.widgets}
templates={shadcnTheme.templates}
>
{onSave && (
<div className="flex justify-end pt-4">
<Button
type="submit"
disabled={!isDirty || validationErrors.length > 0 || isSaving}
>
{isSaving && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Save Configuration
</Button>
</div>
)}
</Form>
</CardContent>
</Card>
{validationErrors.length > 0 && (
<Alert variant="destructive">
<AlertDescription>
<ul className="list-disc list-inside space-y-1">
{validationErrors.map((error, index) => (
<li key={index}>
{error.property}: {error.message}
</li>
))}
</ul>
</AlertDescription>
</Alert>
)}
</div>
);
}

View File

@@ -11,7 +11,6 @@ import {
FolderOpen, FolderOpen,
Settings, Settings,
BookOpen, BookOpen,
Server,
MessageCircleQuestion, MessageCircleQuestion,
Menu, Menu,
Plus, Plus,
@@ -27,7 +26,6 @@ import { useState } from 'react';
const INTERNAL_NAV = [ const INTERNAL_NAV = [
{ label: 'Projects', icon: FolderOpen, to: '/projects' }, { label: 'Projects', icon: FolderOpen, to: '/projects' },
{ label: 'MCP Servers', icon: Server, to: '/mcp-servers' },
{ label: 'Settings', icon: Settings, to: '/settings' }, { label: 'Settings', icon: Settings, to: '/settings' },
]; ];
@@ -125,7 +123,7 @@ export function Navbar() {
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
{INTERNAL_NAV.map((item) => { {INTERNAL_NAV.map((item) => {
const active = location.pathname === item.to; const active = location.pathname.startsWith(item.to);
const Icon = item.icon; const Icon = item.icon;
return ( return (
<DropdownMenuItem <DropdownMenuItem

View File

@@ -0,0 +1,3 @@
export { shadcnTheme, customWidgets, customTemplates } from './theme';
export * from './widgets';
export * from './templates';

View File

@@ -0,0 +1,78 @@
import {
ArrayFieldTemplateProps,
ArrayFieldTemplateItemType,
} from '@rjsf/utils';
import { Button } from '@/components/ui/button';
import { Plus, X } from 'lucide-react';
export const ArrayFieldTemplate = (props: ArrayFieldTemplateProps) => {
const { canAdd, items, onAddClick, disabled, readonly } = props;
if (!items || (items.length === 0 && !canAdd)) {
return null;
}
return (
<div className="space-y-4">
<div>
{items.length > 0 &&
items.map((element: ArrayFieldTemplateItemType) => (
<ArrayItem
key={element.key}
element={element}
disabled={disabled}
readonly={readonly}
/>
))}
</div>
{canAdd && (
<Button
type="button"
variant="outline"
size="sm"
onClick={onAddClick}
disabled={disabled || readonly}
className="w-full"
>
<Plus className="w-4 h-4 mr-2" />
Add Item
</Button>
)}
</div>
);
};
interface ArrayItemProps {
element: ArrayFieldTemplateItemType;
disabled?: boolean;
readonly?: boolean;
}
const ArrayItem = ({ element, disabled, readonly }: ArrayItemProps) => {
const { children } = element;
const elementAny = element as any; // Type assertion needed for RJSF v6 beta properties
return (
<div className="flex items-center gap-2">
<div className="flex-1">{children}</div>
{/* Remove button */}
{elementAny.buttonsProps?.hasRemove && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={elementAny.buttonsProps.onDropIndexClick(
elementAny.buttonsProps.index
)}
disabled={disabled || readonly || elementAny.buttonsProps.disabled}
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-all duration-200 shrink-0"
title="Remove item"
>
<X className="w-4 h-4" />
</Button>
)}
</div>
);
};

View File

@@ -0,0 +1,59 @@
import { FieldTemplateProps } from '@rjsf/utils';
export const FieldTemplate = (props: FieldTemplateProps) => {
const {
children,
rawErrors = [],
rawHelp,
rawDescription,
label,
required,
schema,
} = props;
if (schema.type === 'object') {
return children;
}
// Two-column layout for other field types
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 py-6">
{/* Left column: Label and description */}
<div className="space-y-2">
{label && (
<div className="text-sm font-bold leading-relaxed">
{label}
{required && <span className="text-destructive ml-1">*</span>}
</div>
)}
{rawDescription && (
<p className="text-sm text-muted-foreground leading-relaxed">
{rawDescription}
</p>
)}
{rawHelp && (
<p className="text-sm text-muted-foreground leading-relaxed">
{rawHelp}
</p>
)}
</div>
{/* Right column: Field content */}
<div className="space-y-2">
{children}
{rawErrors.length > 0 && (
<div className="space-y-1">
{rawErrors.map((error, index) => (
<p key={index} className="text-sm text-destructive">
{error}
</p>
))}
</div>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,5 @@
export const FormTemplate = (props: any) => {
const { children } = props;
return <div className="w-full">{children}</div>;
};

View File

@@ -0,0 +1,13 @@
import { ObjectFieldTemplateProps } from '@rjsf/utils';
export const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => {
const { properties } = props;
return (
<div className="divide-y">
{properties.map((element) => (
<div key={element.name}>{element.content}</div>
))}
</div>
);
};

View File

@@ -0,0 +1,4 @@
export { ArrayFieldTemplate } from './ArrayFieldTemplate';
export { FieldTemplate } from './FieldTemplate';
export { ObjectFieldTemplate } from './ObjectFieldTemplate';
export { FormTemplate } from './FormTemplate';

View File

@@ -0,0 +1,33 @@
import { RegistryWidgetsType } from '@rjsf/utils';
import {
TextWidget,
SelectWidget,
CheckboxWidget,
TextareaWidget,
} from './widgets';
import {
ArrayFieldTemplate,
FieldTemplate,
ObjectFieldTemplate,
FormTemplate,
} from './templates';
export const customWidgets: RegistryWidgetsType = {
TextWidget,
SelectWidget,
CheckboxWidget,
TextareaWidget,
textarea: TextareaWidget,
};
export const customTemplates = {
ArrayFieldTemplate,
FieldTemplate,
ObjectFieldTemplate,
FormTemplate,
};
export const shadcnTheme = {
widgets: customWidgets,
templates: customTemplates,
};

View File

@@ -0,0 +1,23 @@
import { WidgetProps } from '@rjsf/utils';
import { Checkbox } from '@/components/ui/checkbox';
export const CheckboxWidget = (props: WidgetProps) => {
const { id, value, disabled, readonly, onChange } = props;
const handleChange = (checked: boolean) => {
onChange(checked);
};
const checked = Boolean(value);
return (
<div className="flex items-center space-x-2">
<Checkbox
id={id}
checked={checked}
onCheckedChange={handleChange}
disabled={disabled || readonly}
/>
</div>
);
};

View File

@@ -0,0 +1,69 @@
import { WidgetProps } from '@rjsf/utils';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
export const SelectWidget = (props: WidgetProps) => {
const {
id,
value,
disabled,
readonly,
onChange,
onBlur,
onFocus,
options,
schema,
placeholder,
} = props;
const { enumOptions } = options;
const handleChange = (newValue: string) => {
// Handle nullable enum values - '__null__' means null for nullable types
const finalValue = newValue === '__null__' ? options.emptyValue : newValue;
onChange(finalValue);
};
const handleOpenChange = (open: boolean) => {
if (!open && onBlur) {
onBlur(id, value);
}
if (open && onFocus) {
onFocus(id, value);
}
};
// Convert enumOptions to the format expected by our Select component
const selectOptions = enumOptions || [];
// Handle nullable types by adding a null option
const isNullable = Array.isArray(schema.type) && schema.type.includes('null');
const allOptions = isNullable
? [{ value: '__null__', label: 'None' }, ...selectOptions]
: selectOptions;
return (
<Select
value={value === null ? '__null__' : (value ?? '')}
onValueChange={handleChange}
onOpenChange={handleOpenChange}
disabled={disabled || readonly}
>
<SelectTrigger id={id}>
<SelectValue placeholder={placeholder || 'Select an option...'} />
</SelectTrigger>
<SelectContent>
{allOptions.map((option) => (
<SelectItem key={option.value} value={String(option.value)}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
};

View File

@@ -0,0 +1,45 @@
import { WidgetProps } from '@rjsf/utils';
import { Input } from '@/components/ui/input';
export const TextWidget = (props: WidgetProps) => {
const {
id,
value,
disabled,
readonly,
onChange,
onBlur,
onFocus,
placeholder,
options,
} = props;
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
onChange(newValue === '' ? options.emptyValue : newValue);
};
const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
if (onBlur) {
onBlur(id, event.target.value);
}
};
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
if (onFocus) {
onFocus(id, event.target.value);
}
};
return (
<Input
id={id}
value={value ?? ''}
placeholder={placeholder || ''}
disabled={disabled || readonly}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
/>
);
};

View File

@@ -0,0 +1,53 @@
import { WidgetProps } from '@rjsf/utils';
import { Textarea } from '@/components/ui/textarea';
export const TextareaWidget = (props: WidgetProps) => {
const {
id,
value,
disabled,
readonly,
onChange,
onBlur,
onFocus,
placeholder,
options,
schema,
} = props;
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = event.target.value;
onChange(newValue === '' ? options.emptyValue : newValue);
};
const handleBlur = (event: React.FocusEvent<HTMLTextAreaElement>) => {
if (onBlur) {
onBlur(id, event.target.value);
}
};
const handleFocus = (event: React.FocusEvent<HTMLTextAreaElement>) => {
if (onFocus) {
onFocus(id, event.target.value);
}
};
// Get rows from ui:options or default based on field name
const rows =
options.rows ||
((schema.title || '').toLowerCase().includes('prompt') ? 4 : 3);
return (
<Textarea
id={id}
value={value ?? ''}
placeholder={placeholder || ''}
disabled={disabled || readonly}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
rows={rows}
className="resize-vertical"
/>
);
};

View File

@@ -0,0 +1,4 @@
export { TextWidget } from './TextWidget';
export { SelectWidget } from './SelectWidget';
export { CheckboxWidget } from './CheckboxWidget';
export { TextareaWidget } from './TextareaWidget';

View File

@@ -12,7 +12,7 @@ const buttonVariants = cva(
default: default:
'text-primary-foreground hover:bg-primary/90 border border-foreground', 'text-primary-foreground hover:bg-primary/90 border border-foreground',
destructive: destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90', 'border border-destructive text-destructive hover:bg-destructive/10',
outline: outline:
'border border-input hover:bg-accent hover:text-accent-foreground', 'border border-input hover:bg-accent hover:text-accent-foreground',
secondary: 'text-secondary-foreground hover:bg-secondary/80 border', secondary: 'text-secondary-foreground hover:bg-secondary/80 border',

View File

@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
return ( return (
<textarea <textarea
className={cn( className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', 'flex min-h-[80px] w-full bg-transparent border px-3 py-2 text-sm ring-offset-background focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
className className
)} )}
ref={ref} ref={ref}

View File

@@ -0,0 +1,71 @@
import { useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { profilesApi } from '@/lib/api';
export type UseProfilesReturn = {
// data
profilesContent: string;
parsedProfiles: any | null;
profilesPath: string;
// status
isLoading: boolean;
isError: boolean;
error: unknown;
isSaving: boolean;
// actions
refetch: () => void;
save: (content: string) => Promise<void>;
saveParsed: (obj: unknown) => Promise<void>;
};
export function useProfiles(): UseProfilesReturn {
const queryClient = useQueryClient();
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['profiles'],
queryFn: () => profilesApi.load(),
staleTime: 1000 * 60, // 1 minute cache
});
const { mutateAsync: saveMutation, isPending: isSaving } = useMutation({
mutationFn: (content: string) => profilesApi.save(content),
onSuccess: (_, content) => {
// Optimistically update cache with new content
queryClient.setQueryData(['profiles'], (old: any) =>
old ? { ...old, content } : old
);
},
});
const save = async (content: string): Promise<void> => {
await saveMutation(content);
};
const parsedProfiles = useMemo(() => {
if (!data?.content) return null;
try {
return JSON.parse(data.content);
} catch {
return null;
}
}, [data?.content]);
const saveParsed = async (obj: unknown) => {
await save(JSON.stringify(obj, null, 2));
};
return {
profilesContent: data?.content ?? '',
parsedProfiles,
profilesPath: data?.path ?? '',
isLoading,
isError,
error,
isSaving,
refetch,
save,
saveParsed,
};
}

View File

@@ -1,862 +0,0 @@
import { useCallback, useState, useEffect } from 'react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { JSONEditor } from '@/components/ui/json-editor';
import { ChevronDown, Key, Loader2, Volume2 } from 'lucide-react';
import { ThemeMode, EditorType, SoundFile } from 'shared/types';
import type { BaseCodingAgent, ExecutorProfileId } from 'shared/types';
import { toPrettyCase } from '@/utils/string';
import { useTheme } from '@/components/theme-provider';
import { useUserSystem } from '@/components/config-provider';
import { GitHubLoginDialog } from '@/components/GitHubLoginDialog';
import { TaskTemplateManager } from '@/components/TaskTemplateManager';
import { profilesApi } from '@/lib/api';
export function Settings() {
const {
config,
updateConfig,
saveConfig,
loading,
updateAndSaveConfig,
profiles,
reloadSystem,
} = useUserSystem();
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const { setTheme } = useTheme();
const [showGitHubLogin, setShowGitHubLogin] = useState(false);
// Profiles editor state
const [profilesContent, setProfilesContent] = useState('');
const [profilesPath, setProfilesPath] = useState('');
const [profilesError, setProfilesError] = useState<string | null>(null);
const [profilesLoading, setProfilesLoading] = useState(false);
const [profilesSaving, setProfilesSaving] = useState(false);
const [profilesSuccess, setProfilesSuccess] = useState(false);
// Load profiles content on mount
useEffect(() => {
const loadProfiles = async () => {
setProfilesLoading(true);
try {
const result = await profilesApi.load();
setProfilesContent(result.content);
setProfilesPath(result.path);
} catch (err) {
console.error('Failed to load profiles:', err);
setProfilesError('Failed to load profiles');
} finally {
setProfilesLoading(false);
}
};
loadProfiles();
}, []);
const playSound = async (soundFile: SoundFile) => {
const audio = new Audio(`/api/sounds/${soundFile}`);
try {
await audio.play();
} catch (err) {
console.error('Failed to play sound:', err);
}
};
const handleProfilesChange = (value: string) => {
setProfilesContent(value);
setProfilesError(null);
// Validate JSON on change
if (value.trim()) {
try {
const parsed = JSON.parse(value);
// Basic structure validation
if (!parsed.executors) {
setProfilesError('Invalid structure: must have a "executors" object');
}
} catch (err) {
if (err instanceof SyntaxError) {
setProfilesError('Invalid JSON format');
} else {
setProfilesError('Validation error');
}
}
}
};
const handleSaveProfiles = async () => {
setProfilesSaving(true);
setProfilesError(null);
setProfilesSuccess(false);
try {
await profilesApi.save(profilesContent);
// Reload the system to get the updated profiles
await reloadSystem();
setProfilesSuccess(true);
setTimeout(() => setProfilesSuccess(false), 3000);
} catch (err: any) {
setProfilesError(err.message || 'Failed to save profiles');
} finally {
setProfilesSaving(false);
}
};
const handleSave = async () => {
if (!config) return;
setSaving(true);
setError(null);
setSuccess(false);
try {
// Save the main configuration
const success = await saveConfig();
if (success) {
setSuccess(true);
// Update theme provider to reflect the saved theme
setTheme(config.theme);
setTimeout(() => setSuccess(false), 3000);
} else {
setError('Failed to save configuration');
}
} catch (err) {
setError('Failed to save configuration');
console.error('Error saving config:', err);
} finally {
setSaving(false);
}
};
const resetDisclaimer = async () => {
if (!config) return;
updateConfig({ disclaimer_acknowledged: false });
};
const resetOnboarding = async () => {
if (!config) return;
updateConfig({ onboarding_acknowledged: false });
};
const isAuthenticated = !!(
config?.github?.username && config?.github?.oauth_token
);
const handleLogout = useCallback(async () => {
if (!config) return;
updateAndSaveConfig({
github: {
...config.github,
oauth_token: null,
username: null,
primary_email: null,
},
});
}, [config, updateAndSaveConfig]);
if (loading) {
return (
<div className="container mx-auto px-4 py-8">
<div className="flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">Loading settings...</span>
</div>
</div>
);
}
if (!config) {
return (
<div className="container mx-auto px-4 py-8">
<Alert variant="destructive">
<AlertDescription>Failed to load settings. {error}</AlertDescription>
</Alert>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Settings</h1>
<p className="text-muted-foreground">
Configure your preferences and application settings.
</p>
</div>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200">
<AlertDescription className="font-medium">
Settings saved successfully!
</AlertDescription>
</Alert>
)}
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>Appearance</CardTitle>
<CardDescription>
Customize how the application looks and feels.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="theme">Theme</Label>
<Select
value={config.theme}
onValueChange={(value: ThemeMode) => {
updateConfig({ theme: value });
setTheme(value);
}}
>
<SelectTrigger id="theme">
<SelectValue placeholder="Select theme" />
</SelectTrigger>
<SelectContent>
{Object.values(ThemeMode).map((theme) => (
<SelectItem key={theme} value={theme}>
{toPrettyCase(theme)}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Choose your preferred color scheme.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Task Execution</CardTitle>
<CardDescription>
Configure how tasks are executed and processed.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="executor">Default Executor Profile</Label>
<div className="grid grid-cols-2 gap-2">
<Select
value={config.executor_profile?.executor ?? ''}
onValueChange={(value: string) => {
const newProfile: ExecutorProfileId = {
executor: value as BaseCodingAgent,
variant: null,
};
updateConfig({
executor_profile: newProfile,
});
}}
>
<SelectTrigger id="executor">
<SelectValue placeholder="Select profile" />
</SelectTrigger>
<SelectContent>
{profiles &&
Object.entries(profiles)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([profileKey]) => (
<SelectItem key={profileKey} value={profileKey}>
{profileKey}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Show variant selector if selected profile has variants */}
{(() => {
const currentProfileVariant = config.executor_profile;
const selectedProfile =
profiles?.[currentProfileVariant?.executor || ''];
const hasVariants =
selectedProfile &&
Object.keys(selectedProfile).length > 0;
if (hasVariants) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="w-full h-10 px-2 flex items-center justify-between"
>
<span className="text-sm truncate flex-1 text-left">
{currentProfileVariant?.variant || 'DEFAULT'}
</span>
<ChevronDown className="h-4 w-4 ml-1 flex-shrink-0" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{Object.entries(selectedProfile).map(
([variantLabel]) => (
<DropdownMenuItem
key={variantLabel}
onClick={() => {
const newProfile: ExecutorProfileId = {
executor:
currentProfileVariant?.executor || '',
variant: variantLabel,
};
updateConfig({
executor_profile: newProfile,
});
}}
className={
currentProfileVariant?.variant ===
variantLabel
? 'bg-accent'
: ''
}
>
{variantLabel}
</DropdownMenuItem>
)
)}
</DropdownMenuContent>
</DropdownMenu>
);
} else if (selectedProfile) {
// Show disabled button when profile exists but has no variants
return (
<Button
variant="outline"
className="w-full h-10 px-2 flex items-center justify-between"
disabled
>
<span className="text-sm truncate flex-1 text-left">
Default
</span>
</Button>
);
}
return null;
})()}
</div>
<p className="text-sm text-muted-foreground">
Choose the default executor profile to use when creating a
task attempt.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Editor</CardTitle>
<CardDescription>
Configure which editor to open when viewing task attempts.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="editor">Preferred Editor</Label>
<Select
value={config.editor.editor_type}
onValueChange={(value: EditorType) =>
updateConfig({
editor: {
...config.editor,
editor_type: value,
custom_command:
value === EditorType.CUSTOM
? config.editor.custom_command
: null,
},
})
}
>
<SelectTrigger id="editor">
<SelectValue placeholder="Select editor" />
</SelectTrigger>
<SelectContent>
{Object.values(EditorType).map((type) => (
<SelectItem key={type} value={type}>
{toPrettyCase(type)}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Choose your preferred code editor for opening task attempts.
</p>
</div>
{config.editor.editor_type === EditorType.CUSTOM && (
<div className="space-y-2">
<Label htmlFor="custom-command">Custom Command</Label>
<Input
id="custom-command"
placeholder="e.g., code, subl, vim"
value={config.editor.custom_command || ''}
onChange={(e) =>
updateConfig({
editor: {
...config.editor,
custom_command: e.target.value || null,
},
})
}
/>
<p className="text-sm text-muted-foreground">
Enter the command to run your custom editor. Use spaces for
arguments (e.g., "code --wait").
</p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Key className="h-5 w-5" />
GitHub Integration
</CardTitle>
<CardDescription>
Configure GitHub settings for creating pull requests from task
attempts.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="github-token">Personal Access Token</Label>
<Input
id="github-token"
type="password"
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx"
value={config.github.pat || ''}
onChange={(e) =>
updateConfig({
github: {
...config.github,
pat: e.target.value || null,
},
})
}
/>
<p className="text-sm text-muted-foreground">
GitHub Personal Access Token with 'repo' permissions. Required
for creating pull requests.{' '}
<a
href="https://github.com/settings/tokens"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
Create token here
</a>
</p>
</div>
{config && isAuthenticated ? (
<div className="flex items-center justify-between gap-4">
<div>
<Label>Signed in as</Label>
<div className="text-lg font-mono">
{config.github.username}
</div>
</div>
<Button variant="outline" onClick={handleLogout}>
Log out
</Button>
</div>
) : (
<Button onClick={() => setShowGitHubLogin(true)}>
Sign in with GitHub
</Button>
)}
<GitHubLoginDialog
open={showGitHubLogin}
onOpenChange={setShowGitHubLogin}
/>
<div className="space-y-2 pt-4">
<Label htmlFor="default-pr-base">Default PR Base Branch</Label>
<Input
id="default-pr-base"
placeholder="main"
value={config.github.default_pr_base || ''}
onChange={(e) =>
updateConfig({
github: {
...config.github,
default_pr_base: e.target.value || null,
},
})
}
/>
<p className="text-sm text-muted-foreground">
Default base branch for pull requests. Defaults to 'main' if
not specified.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>
Configure how you receive notifications about task completion.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="sound-alerts"
checked={config.notifications.sound_enabled}
onCheckedChange={(checked: boolean) =>
updateConfig({
notifications: {
...config.notifications,
sound_enabled: checked,
},
})
}
/>
<div className="space-y-0.5">
<Label htmlFor="sound-alerts" className="cursor-pointer">
Sound Alerts
</Label>
<p className="text-sm text-muted-foreground">
Play a sound when task attempts finish running.
</p>
</div>
</div>
{config.notifications.sound_enabled && (
<div className="space-y-2 ml-6">
<Label htmlFor="sound-file">Sound</Label>
<div className="flex items-center gap-2">
<Select
value={config.notifications.sound_file}
onValueChange={(value: SoundFile) =>
updateConfig({
notifications: {
...config.notifications,
sound_file: value,
},
})
}
>
<SelectTrigger id="sound-file" className="flex-1">
<SelectValue placeholder="Select sound" />
</SelectTrigger>
<SelectContent>
{Object.values(SoundFile).map((soundFile) => (
<SelectItem key={soundFile} value={soundFile}>
{toPrettyCase(soundFile)}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() => playSound(config.notifications.sound_file)}
className="px-3"
>
<Volume2 className="h-4 w-4" />
</Button>
</div>
<p className="text-sm text-muted-foreground">
Choose the sound to play when tasks complete. Click the
volume button to preview.
</p>
</div>
)}
<div className="flex items-center space-x-2">
<Checkbox
id="push-notifications"
checked={config.notifications.push_enabled}
onCheckedChange={(checked: boolean) =>
updateConfig({
notifications: {
...config.notifications,
push_enabled: checked,
},
})
}
/>
<div className="space-y-0.5">
<Label
htmlFor="push-notifications"
className="cursor-pointer"
>
Push Notifications
</Label>
<p className="text-sm text-muted-foreground">
Show system notifications when task attempts finish running.
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Privacy</CardTitle>
<CardDescription>
Help improve Vibe-Kanban by sharing anonymous usage data.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="analytics-enabled"
checked={config.analytics_enabled ?? false}
onCheckedChange={(checked: boolean) =>
updateConfig({ analytics_enabled: checked })
}
/>
<div className="space-y-0.5">
<Label htmlFor="analytics-enabled" className="cursor-pointer">
Enable Telemetry
</Label>
<p className="text-sm text-muted-foreground">
Enables anonymous usage events tracking to help improve the
application. No prompts or project information are
collected.
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Task Templates</CardTitle>
<CardDescription>
Manage global task templates that can be used across all
projects.
</CardDescription>
</CardHeader>
<CardContent>
<TaskTemplateManager isGlobal={true} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
Agent Profiles
</CardTitle>
<CardDescription>
Configure coding agent profiles with specific command-line
parameters.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{profilesError && (
<Alert variant="destructive">
<AlertDescription>{profilesError}</AlertDescription>
</Alert>
)}
{profilesSuccess && (
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200">
<AlertDescription className="font-medium">
Profiles saved successfully!
</AlertDescription>
</Alert>
)}
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="profiles-editor">
Profiles Configuration
</Label>
<JSONEditor
id="profiles-editor"
placeholder={
profilesLoading
? 'Loading profiles...'
: '{\n "profiles": [\n {\n "label": "my-custom-profile",\n "agent": "ClaudeCode",\n "command": {...}\n }\n ]\n}'
}
value={profilesLoading ? 'Loading...' : profilesContent}
onChange={handleProfilesChange}
disabled={profilesLoading}
minHeight={300}
/>
</div>
<div className="space-y-2">
{!profilesError && profilesPath && (
<p className="text-sm text-muted-foreground">
<span className="font-medium">Configuration file:</span>{' '}
<span className="font-mono text-xs">{profilesPath}</span>
</p>
)}
<p className="text-sm text-muted-foreground">
Edit coding agent profiles. Each profile needs a unique
label, agent type, and command configuration.
</p>
</div>
<div className="flex justify-end pt-2">
<Button
onClick={handleSaveProfiles}
disabled={
profilesSaving ||
profilesLoading ||
!!profilesError ||
profilesSuccess
}
className={
profilesSuccess ? 'bg-green-600 hover:bg-green-700' : ''
}
>
{profilesSaving && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
{profilesSuccess && <span className="mr-2"></span>}
{profilesSuccess ? 'Profiles Saved!' : 'Save Profiles'}
</Button>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Safety & Disclaimers</CardTitle>
<CardDescription>
Manage safety warnings and acknowledgments.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<Label>Disclaimer Status</Label>
<p className="text-sm text-muted-foreground">
{config.disclaimer_acknowledged
? 'You have acknowledged the safety disclaimer.'
: 'The safety disclaimer has not been acknowledged.'}
</p>
</div>
<Button
onClick={resetDisclaimer}
variant="outline"
size="sm"
disabled={!config.disclaimer_acknowledged}
>
Reset Disclaimer
</Button>
</div>
<p className="text-xs text-muted-foreground">
Resetting the disclaimer will require you to acknowledge the
safety warning again.
</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<Label>Onboarding Status</Label>
<p className="text-sm text-muted-foreground">
{config.onboarding_acknowledged
? 'You have completed the onboarding process.'
: 'The onboarding process has not been completed.'}
</p>
</div>
<Button
onClick={resetOnboarding}
variant="outline"
size="sm"
disabled={!config.onboarding_acknowledged}
>
Reset Onboarding
</Button>
</div>
<p className="text-xs text-muted-foreground">
Resetting the onboarding will show the setup screen again.
</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<Label>Telemetry Acknowledgment</Label>
<p className="text-sm text-muted-foreground">
{config.telemetry_acknowledged
? 'You have acknowledged the telemetry notice.'
: 'The telemetry notice has not been acknowledged.'}
</p>
</div>
<Button
onClick={() =>
updateConfig({ telemetry_acknowledged: false })
}
variant="outline"
size="sm"
disabled={!config.telemetry_acknowledged}
>
Reset Acknowledgment
</Button>
</div>
<p className="text-xs text-muted-foreground">
Resetting the acknowledgment will require you to acknowledge
the telemetry notice again.
</p>
</div>
</CardContent>
</Card>
</div>
{/* Sticky save button */}
<div className="fixed bottom-0 left-0 right-0 bg-background/80 backdrop-blur-sm border-t p-4 z-10">
<div className="container mx-auto max-w-4xl flex justify-end">
<Button
onClick={handleSave}
disabled={saving || success}
className={success ? 'bg-green-600 hover:bg-green-700' : ''}
>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{success && <span className="mr-2"></span>}
{success ? 'Settings Saved!' : 'Save Settings'}
</Button>
</div>
</div>
{/* Spacer to prevent content from being hidden behind sticky button */}
<div className="h-20"></div>
</div>
</div>
);
}

View File

@@ -0,0 +1,672 @@
import { useState, useEffect } from 'react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Checkbox } from '@/components/ui/checkbox';
import { JSONEditor } from '@/components/ui/json-editor';
import { Input } from '@/components/ui/input';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Loader2 } from 'lucide-react';
import { ExecutorConfigForm } from '@/components/ExecutorConfigForm';
import { useProfiles } from '@/hooks/useProfiles';
import { useUserSystem } from '@/components/config-provider';
export function AgentSettings() {
// Use profiles hook for server state
const {
profilesContent: serverProfilesContent,
parsedProfiles: serverParsedProfiles,
profilesPath,
isLoading: profilesLoading,
isSaving: profilesSaving,
error: profilesError,
save: saveProfiles,
} = useProfiles();
const { reloadSystem } = useUserSystem();
useEffect(() => {
return () => {
reloadSystem();
};
}, []);
// Local editor state (draft that may differ from server)
const [localProfilesContent, setLocalProfilesContent] = useState('');
const [profilesSuccess, setProfilesSuccess] = useState(false);
// Form-based editor state
const [useFormEditor, setUseFormEditor] = useState(true);
const [selectedExecutorType, setSelectedExecutorType] =
useState<string>('CLAUDE_CODE');
const [selectedConfiguration, setSelectedConfiguration] =
useState<string>('DEFAULT');
const [localParsedProfiles, setLocalParsedProfiles] = useState<any>(null);
const [isDirty, setIsDirty] = useState(false);
// Create configuration dialog state
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [newConfigName, setNewConfigName] = useState('');
const [cloneFrom, setCloneFrom] = useState<string | null>(null);
const [dialogError, setDialogError] = useState<string | null>(null);
// Delete configuration dialog state
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [configToDelete, setConfigToDelete] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
// Sync server state to local state when not dirty
useEffect(() => {
if (!isDirty && serverProfilesContent) {
setLocalProfilesContent(serverProfilesContent);
setLocalParsedProfiles(serverParsedProfiles);
}
}, [serverProfilesContent, serverParsedProfiles, isDirty]);
// Sync raw profiles with parsed profiles
const syncRawProfiles = (profiles: any) => {
setLocalProfilesContent(JSON.stringify(profiles, null, 2));
};
// Mark profiles as dirty
const markDirty = (nextProfiles: any) => {
setLocalParsedProfiles(nextProfiles);
syncRawProfiles(nextProfiles);
setIsDirty(true);
};
// Validate configuration name
const validateConfigName = (name: string): string | null => {
const trimmedName = name.trim();
if (!trimmedName) return 'Configuration name cannot be empty';
if (trimmedName.length > 40)
return 'Configuration name must be 40 characters or less';
if (!/^[a-zA-Z0-9_-]+$/.test(trimmedName)) {
return 'Configuration name can only contain letters, numbers, underscores, and hyphens';
}
if (localParsedProfiles?.executors?.[selectedExecutorType]?.[trimmedName]) {
return 'A configuration with this name already exists';
}
return null;
};
// Open create dialog
const openCreateDialog = () => {
setNewConfigName('');
setCloneFrom(null);
setDialogError(null);
setShowCreateDialog(true);
};
// Create new configuration
const createConfiguration = (
executorType: string,
configName: string,
baseConfig?: string | null
) => {
if (!localParsedProfiles || !localParsedProfiles.executors) return;
const base =
baseConfig &&
localParsedProfiles.executors[executorType]?.[baseConfig]?.[executorType]
? localParsedProfiles.executors[executorType][baseConfig][executorType]
: {};
const updatedProfiles = {
...localParsedProfiles,
executors: {
...localParsedProfiles.executors,
[executorType]: {
...localParsedProfiles.executors[executorType],
[configName]: {
[executorType]: base,
},
},
},
};
markDirty(updatedProfiles);
setSelectedConfiguration(configName);
};
// Handle create dialog submission
const handleCreateConfiguration = () => {
const validationError = validateConfigName(newConfigName);
if (validationError) {
setDialogError(validationError);
return;
}
createConfiguration(selectedExecutorType, newConfigName.trim(), cloneFrom);
setShowCreateDialog(false);
};
// Open delete dialog
const openDeleteDialog = (configName: string) => {
setConfigToDelete(configName);
setDeleteError(null);
setShowDeleteDialog(true);
};
// Handle delete configuration
const handleDeleteConfiguration = async () => {
if (!localParsedProfiles || !configToDelete) {
setDeleteError('Invalid configuration data');
return;
}
try {
// Validate that the configuration exists
if (
!localParsedProfiles.executors[selectedExecutorType]?.[configToDelete]
) {
setDeleteError(`Configuration "${configToDelete}" not found`);
return;
}
// Check if this is the last configuration
const currentConfigs = Object.keys(
localParsedProfiles.executors[selectedExecutorType] || {}
);
if (currentConfigs.length <= 1) {
setDeleteError('Cannot delete the last configuration');
return;
}
// Remove the configuration from the executor
const remainingConfigs = {
...localParsedProfiles.executors[selectedExecutorType],
};
delete remainingConfigs[configToDelete];
const updatedProfiles = {
...localParsedProfiles,
executors: {
...localParsedProfiles.executors,
[selectedExecutorType]: remainingConfigs,
},
};
// If no configurations left, create a blank DEFAULT (should not happen due to check above)
if (Object.keys(remainingConfigs).length === 0) {
updatedProfiles.executors[selectedExecutorType] = {
DEFAULT: { [selectedExecutorType]: {} },
};
}
try {
// Save using hook
await saveProfiles(JSON.stringify(updatedProfiles, null, 2));
// Update local state and reset dirty flag
setLocalParsedProfiles(updatedProfiles);
setLocalProfilesContent(JSON.stringify(updatedProfiles, null, 2));
setIsDirty(false);
// Select the next available configuration
const nextConfigs = Object.keys(
updatedProfiles.executors[selectedExecutorType]
);
const nextSelected = nextConfigs[0] || 'DEFAULT';
setSelectedConfiguration(nextSelected);
// Show success and close dialog
setProfilesSuccess(true);
setTimeout(() => setProfilesSuccess(false), 3000);
setShowDeleteDialog(false);
} catch (saveError: any) {
console.error('Failed to save deletion to backend:', saveError);
setDeleteError(
saveError.message || 'Failed to save deletion. Please try again.'
);
}
} catch (error) {
console.error('Error deleting configuration:', error);
setDeleteError('Failed to delete configuration. Please try again.');
}
};
const handleProfilesChange = (value: string) => {
setLocalProfilesContent(value);
setIsDirty(true);
// Validate JSON on change
if (value.trim()) {
try {
const parsed = JSON.parse(value);
setLocalParsedProfiles(parsed);
} catch (err) {
// Invalid JSON, keep local content but clear parsed
setLocalParsedProfiles(null);
}
}
};
const handleSaveProfiles = async () => {
try {
const contentToSave =
useFormEditor && localParsedProfiles
? JSON.stringify(localParsedProfiles, null, 2)
: localProfilesContent;
await saveProfiles(contentToSave);
setProfilesSuccess(true);
setIsDirty(false);
setTimeout(() => setProfilesSuccess(false), 3000);
// Update the local content if using form editor
if (useFormEditor && localParsedProfiles) {
setLocalProfilesContent(contentToSave);
}
} catch (err: any) {
console.error('Failed to save profiles:', err);
}
};
const handleExecutorConfigChange = (
executorType: string,
configuration: string,
formData: any
) => {
if (!localParsedProfiles || !localParsedProfiles.executors) return;
// Update the parsed profiles with the new config
const updatedProfiles = {
...localParsedProfiles,
executors: {
...localParsedProfiles.executors,
[executorType]: {
...localParsedProfiles.executors[executorType],
[configuration]: {
[executorType]: formData,
},
},
},
};
markDirty(updatedProfiles);
};
const handleExecutorConfigSave = async (formData: any) => {
if (!localParsedProfiles || !localParsedProfiles.executors) return;
// Update the parsed profiles with the saved config
const updatedProfiles = {
...localParsedProfiles,
executors: {
...localParsedProfiles.executors,
[selectedExecutorType]: {
...localParsedProfiles.executors[selectedExecutorType],
[selectedConfiguration]: {
[selectedExecutorType]: formData,
},
},
},
};
// Update state
setLocalParsedProfiles(updatedProfiles);
// Save the updated profiles directly
try {
const contentToSave = JSON.stringify(updatedProfiles, null, 2);
await saveProfiles(contentToSave);
setProfilesSuccess(true);
setIsDirty(false);
setTimeout(() => setProfilesSuccess(false), 3000);
// Update the local content as well
setLocalProfilesContent(contentToSave);
} catch (err: any) {
console.error('Failed to save profiles:', err);
}
};
if (profilesLoading) {
return (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">Loading agent configurations...</span>
</div>
);
}
return (
<div className="space-y-6">
{!!profilesError && (
<Alert variant="destructive">
<AlertDescription>
{profilesError instanceof Error
? profilesError.message
: String(profilesError)}
</AlertDescription>
</Alert>
)}
{profilesSuccess && (
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200">
<AlertDescription className="font-medium">
Executor configurations saved successfully!
</AlertDescription>
</Alert>
)}
<Card>
<CardHeader>
<CardTitle>Coding Agent Configurations</CardTitle>
<CardDescription>
Customize the behavior of coding agents with different
configurations.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Editor type toggle */}
<div className="flex items-center space-x-2">
<Checkbox
id="use-form-editor"
checked={!useFormEditor}
onCheckedChange={(checked) => setUseFormEditor(!checked)}
disabled={profilesLoading || !localParsedProfiles}
/>
<Label htmlFor="use-form-editor">Edit JSON</Label>
</div>
{useFormEditor &&
localParsedProfiles &&
localParsedProfiles.executors ? (
// Form-based editor
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="executor-type">Agent</Label>
<Select
value={selectedExecutorType}
onValueChange={(value) => {
setSelectedExecutorType(value);
// Reset configuration selection when executor type changes
setSelectedConfiguration('DEFAULT');
}}
>
<SelectTrigger id="executor-type">
<SelectValue placeholder="Select executor type" />
</SelectTrigger>
<SelectContent>
{Object.keys(localParsedProfiles.executors).map(
(type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="configuration">Configuration</Label>
<div className="flex gap-2">
<Select
value={selectedConfiguration}
onValueChange={(value) => {
if (value === '__create__') {
openCreateDialog();
} else {
setSelectedConfiguration(value);
}
}}
disabled={
!localParsedProfiles.executors[selectedExecutorType]
}
>
<SelectTrigger id="configuration">
<SelectValue placeholder="Select configuration" />
</SelectTrigger>
<SelectContent>
{Object.keys(
localParsedProfiles.executors[selectedExecutorType] ||
{}
).map((configuration) => (
<SelectItem key={configuration} value={configuration}>
{configuration}
</SelectItem>
))}
<SelectItem value="__create__">
Create new...
</SelectItem>
</SelectContent>
</Select>
<Button
variant="destructive"
size="sm"
className="h-10"
onClick={() => openDeleteDialog(selectedConfiguration)}
disabled={
profilesSaving ||
!localParsedProfiles.executors[selectedExecutorType] ||
Object.keys(
localParsedProfiles.executors[selectedExecutorType] ||
{}
).length <= 1
}
title={
Object.keys(
localParsedProfiles.executors[selectedExecutorType] ||
{}
).length <= 1
? 'Cannot delete the last configuration'
: `Delete ${selectedConfiguration}`
}
>
Delete
</Button>
</div>
</div>
</div>
{localParsedProfiles.executors[selectedExecutorType]?.[
selectedConfiguration
]?.[selectedExecutorType] && (
<ExecutorConfigForm
executor={selectedExecutorType as any}
value={
localParsedProfiles.executors[selectedExecutorType][
selectedConfiguration
][selectedExecutorType] || {}
}
onChange={(formData) =>
handleExecutorConfigChange(
selectedExecutorType,
selectedConfiguration,
formData
)
}
onSave={handleExecutorConfigSave}
disabled={profilesSaving}
isSaving={profilesSaving}
isDirty={isDirty}
/>
)}
</div>
) : (
// Raw JSON editor
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="profiles-editor">
Profiles Configuration (JSON)
</Label>
<JSONEditor
id="profiles-editor"
placeholder="Loading profiles..."
value={profilesLoading ? 'Loading...' : localProfilesContent}
onChange={handleProfilesChange}
disabled={profilesLoading}
minHeight={300}
/>
</div>
{!profilesError && profilesPath && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">
<span className="font-medium">
Configuration file location:
</span>{' '}
<span className="font-mono text-xs">{profilesPath}</span>
</p>
</div>
)}
{/* Save button for JSON editor mode */}
<div className="flex justify-end pt-4">
<Button
onClick={handleSaveProfiles}
disabled={!isDirty || profilesSaving || !!profilesError}
>
{profilesSaving && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Save Executor Configurations
</Button>
</div>
</div>
)}
</CardContent>
</Card>
{/* Create Configuration Dialog */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Create New Configuration</DialogTitle>
<DialogDescription>
Add a new configuration for the {selectedExecutorType} executor.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="config-name">Configuration Name</Label>
<Input
id="config-name"
value={newConfigName}
onChange={(e) => {
setNewConfigName(e.target.value);
setDialogError(null);
}}
placeholder="e.g., PRODUCTION, DEVELOPMENT"
maxLength={40}
/>
</div>
<div className="space-y-2">
<Label htmlFor="clone-from">Clone from (optional)</Label>
<Select
value={cloneFrom || '__blank__'}
onValueChange={(value) =>
setCloneFrom(value === '__blank__' ? null : value)
}
>
<SelectTrigger id="clone-from">
<SelectValue placeholder="Start blank or clone existing" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__blank__">Start blank</SelectItem>
{Object.keys(
localParsedProfiles?.executors?.[selectedExecutorType] || {}
).map((configuration) => (
<SelectItem key={configuration} value={configuration}>
Clone from {configuration}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{dialogError && (
<Alert variant="destructive">
<AlertDescription>{dialogError}</AlertDescription>
</Alert>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowCreateDialog(false)}
disabled={profilesSaving}
>
Cancel
</Button>
<Button
onClick={handleCreateConfiguration}
disabled={!newConfigName.trim() || profilesSaving}
>
Create Configuration
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Configuration Dialog */}
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Delete Configuration?</DialogTitle>
<DialogDescription>
This will permanently remove "{configToDelete}" from the{' '}
{selectedExecutorType} executor. You can't undo this action.
</DialogDescription>
</DialogHeader>
{deleteError && (
<Alert variant="destructive">
<AlertDescription>{deleteError}</AlertDescription>
</Alert>
)}
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowDeleteDialog(false)}
disabled={profilesSaving}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteConfiguration}
disabled={profilesSaving}
>
{profilesSaving && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -0,0 +1,580 @@
import { useCallback, useState } from 'react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Checkbox } from '@/components/ui/checkbox';
import { ChevronDown, Key, Loader2, Volume2 } from 'lucide-react';
import {
ThemeMode,
EditorType,
SoundFile,
ExecutorProfileId,
BaseCodingAgent,
} from 'shared/types';
import { toPrettyCase } from '@/utils/string';
import { useTheme } from '@/components/theme-provider';
import { useUserSystem } from '@/components/config-provider';
import { GitHubLoginDialog } from '@/components/GitHubLoginDialog';
import { TaskTemplateManager } from '@/components/TaskTemplateManager';
export function GeneralSettings() {
const {
config,
updateConfig,
saveConfig,
loading,
updateAndSaveConfig,
profiles,
} = useUserSystem();
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const { setTheme } = useTheme();
const [showGitHubLogin, setShowGitHubLogin] = useState(false);
const playSound = async (soundFile: SoundFile) => {
const audio = new Audio(`/api/sounds/${soundFile}`);
try {
await audio.play();
} catch (err) {
console.error('Failed to play sound:', err);
}
};
const handleSave = async () => {
if (!config) return;
setSaving(true);
setError(null);
setSuccess(false);
try {
const success = await saveConfig();
if (success) {
setSuccess(true);
setTheme(config.theme);
setTimeout(() => setSuccess(false), 3000);
} else {
setError('Failed to save configuration');
}
} catch (err) {
setError('Failed to save configuration');
console.error('Error saving config:', err);
} finally {
setSaving(false);
}
};
const resetDisclaimer = async () => {
if (!config) return;
updateConfig({ disclaimer_acknowledged: false });
};
const resetOnboarding = async () => {
if (!config) return;
updateConfig({ onboarding_acknowledged: false });
};
const isAuthenticated = !!(
config?.github?.username && config?.github?.oauth_token
);
const handleLogout = useCallback(async () => {
if (!config) return;
updateAndSaveConfig({
github: {
...config.github,
oauth_token: null,
username: null,
primary_email: null,
},
});
}, [config, updateAndSaveConfig]);
if (loading) {
return (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">Loading settings...</span>
</div>
);
}
if (!config) {
return (
<div className="py-8">
<Alert variant="destructive">
<AlertDescription>Failed to load configuration.</AlertDescription>
</Alert>
</div>
);
}
return (
<div className="space-y-6">
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200">
<AlertDescription className="font-medium">
Settings saved successfully!
</AlertDescription>
</Alert>
)}
<Card>
<CardHeader>
<CardTitle>Appearance</CardTitle>
<CardDescription>
Customize how the application looks and feels.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="theme">Theme</Label>
<Select
value={config.theme}
onValueChange={(value: ThemeMode) =>
updateConfig({ theme: value })
}
>
<SelectTrigger id="theme">
<SelectValue placeholder="Select theme" />
</SelectTrigger>
<SelectContent>
{Object.values(ThemeMode).map((theme) => (
<SelectItem key={theme} value={theme}>
{toPrettyCase(theme)}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Choose your preferred color scheme.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Task Execution</CardTitle>
<CardDescription>
Configure how tasks are executed and processed.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="executor">Default Executor Profile</Label>
<div className="grid grid-cols-2 gap-2">
<Select
value={config.executor_profile?.executor ?? ''}
onValueChange={(value: string) => {
const variants = profiles?.[value];
const keepCurrentVariant =
variants &&
config.executor_profile?.variant &&
variants[config.executor_profile.variant];
const newProfile: ExecutorProfileId = {
executor: value as BaseCodingAgent,
variant: keepCurrentVariant
? config.executor_profile!.variant
: null,
};
updateConfig({
executor_profile: newProfile,
});
}}
disabled={!profiles}
>
<SelectTrigger id="executor">
<SelectValue placeholder="Select profile" />
</SelectTrigger>
<SelectContent>
{profiles &&
Object.entries(profiles)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([profileKey]) => (
<SelectItem key={profileKey} value={profileKey}>
{profileKey}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Show variant selector if selected profile has variants */}
{(() => {
const currentProfileVariant = config.executor_profile;
const selectedProfile =
profiles?.[currentProfileVariant?.executor || ''];
const hasVariants =
selectedProfile && Object.keys(selectedProfile).length > 0;
if (hasVariants) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="w-full h-10 px-2 flex items-center justify-between"
>
<span className="text-sm truncate flex-1 text-left">
{currentProfileVariant?.variant || 'DEFAULT'}
</span>
<ChevronDown className="h-4 w-4 ml-1 flex-shrink-0" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{Object.entries(selectedProfile).map(
([variantLabel]) => (
<DropdownMenuItem
key={variantLabel}
onClick={() => {
const newProfile: ExecutorProfileId = {
executor: currentProfileVariant!.executor,
variant: variantLabel,
};
updateConfig({
executor_profile: newProfile,
});
}}
className={
currentProfileVariant?.variant === variantLabel
? 'bg-accent'
: ''
}
>
{variantLabel}
</DropdownMenuItem>
)
)}
</DropdownMenuContent>
</DropdownMenu>
);
} else if (selectedProfile) {
// Show disabled button when profile exists but has no variants
return (
<Button
variant="outline"
className="w-full h-10 px-2 flex items-center justify-between"
disabled
>
<span className="text-sm truncate flex-1 text-left">
Default
</span>
</Button>
);
}
return null;
})()}
</div>
<p className="text-sm text-muted-foreground">
Choose the default executor profile to use when creating a task
attempt.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Editor</CardTitle>
<CardDescription>
Configure your code editing experience.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="editor-type">Editor Type</Label>
<Select
value={config.editor.editor_type}
onValueChange={(value: EditorType) =>
updateConfig({
editor: { ...config.editor, editor_type: value },
})
}
>
<SelectTrigger id="editor-type">
<SelectValue placeholder="Select editor" />
</SelectTrigger>
<SelectContent>
{Object.values(EditorType).map((editor) => (
<SelectItem key={editor} value={editor}>
{toPrettyCase(editor)}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Choose your preferred code editor interface.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Key className="h-5 w-5" />
GitHub Integration
</CardTitle>
<CardDescription>
Connect your GitHub account to enable advanced features.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{isAuthenticated ? (
<div className="space-y-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<p className="font-medium">
Connected as {config.github.username}
</p>
{config.github.primary_email && (
<p className="text-sm text-muted-foreground">
{config.github.primary_email}
</p>
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
Manage <ChevronDown className="ml-1 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleLogout}>
Disconnect
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
) : (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Connect your GitHub account to access private repositories and
enable advanced Git operations.
</p>
<Button onClick={() => setShowGitHubLogin(true)}>
Connect GitHub Account
</Button>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>
Control when and how you receive notifications.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="sound-enabled"
checked={config.notifications.sound_enabled}
onCheckedChange={(checked: boolean) =>
updateConfig({
notifications: {
...config.notifications,
sound_enabled: checked,
},
})
}
/>
<div className="space-y-0.5">
<Label htmlFor="sound-enabled" className="cursor-pointer">
Sound Notifications
</Label>
<p className="text-sm text-muted-foreground">
Play a sound when task attempts finish running.
</p>
</div>
</div>
{config.notifications.sound_enabled && (
<div className="ml-6 space-y-2">
<Label htmlFor="sound-file">Sound</Label>
<div className="flex gap-2">
<Select
value={config.notifications.sound_file}
onValueChange={(value: SoundFile) =>
updateConfig({
notifications: {
...config.notifications,
sound_file: value,
},
})
}
>
<SelectTrigger id="sound-file" className="flex-1">
<SelectValue placeholder="Select sound" />
</SelectTrigger>
<SelectContent>
{Object.values(SoundFile).map((soundFile) => (
<SelectItem key={soundFile} value={soundFile}>
{toPrettyCase(soundFile)}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() => playSound(config.notifications.sound_file)}
className="px-3"
>
<Volume2 className="h-4 w-4" />
</Button>
</div>
<p className="text-sm text-muted-foreground">
Choose the sound to play when tasks complete. Click the volume
button to preview.
</p>
</div>
)}
<div className="flex items-center space-x-2">
<Checkbox
id="push-notifications"
checked={config.notifications.push_enabled}
onCheckedChange={(checked: boolean) =>
updateConfig({
notifications: {
...config.notifications,
push_enabled: checked,
},
})
}
/>
<div className="space-y-0.5">
<Label htmlFor="push-notifications" className="cursor-pointer">
Push Notifications
</Label>
<p className="text-sm text-muted-foreground">
Show system notifications when task attempts finish running.
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Privacy</CardTitle>
<CardDescription>
Help improve Vibe-Kanban by sharing anonymous usage data.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="analytics-enabled"
checked={config.analytics_enabled ?? false}
onCheckedChange={(checked: boolean) =>
updateConfig({ analytics_enabled: checked })
}
/>
<div className="space-y-0.5">
<Label htmlFor="analytics-enabled" className="cursor-pointer">
Enable Telemetry
</Label>
<p className="text-sm text-muted-foreground">
Enables anonymous usage events tracking to help improve the
application. No prompts or project information are collected.
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Task Templates</CardTitle>
<CardDescription>
Manage global task templates that can be used across all projects.
</CardDescription>
</CardHeader>
<CardContent>
<TaskTemplateManager isGlobal={true} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Safety & Disclaimers</CardTitle>
<CardDescription>
Reset acknowledgments for safety warnings and onboarding.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Disclaimer Acknowledgment</p>
<p className="text-sm text-muted-foreground">
Reset the safety disclaimer to show it again on next startup.
</p>
</div>
<Button variant="outline" onClick={resetDisclaimer}>
Reset
</Button>
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Onboarding</p>
<p className="text-sm text-muted-foreground">
Reset the onboarding flow to show it again on next startup.
</p>
</div>
<Button variant="outline" onClick={resetOnboarding}>
Reset
</Button>
</div>
</CardContent>
</Card>
{/* Sticky Save Button */}
<div className="sticky bottom-0 z-10 bg-background/80 backdrop-blur-sm border-t pt-4">
<div className="flex justify-end">
<Button onClick={handleSave} disabled={saving}>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Settings
</Button>
</div>
</div>
<GitHubLoginDialog
open={showGitHubLogin}
onOpenChange={(open) => setShowGitHubLogin(open)}
/>
</div>
);
}

View File

@@ -21,10 +21,10 @@ import { Loader2 } from 'lucide-react';
import { McpConfig } from 'shared/types'; import { McpConfig } from 'shared/types';
import type { BaseCodingAgent, ExecutorConfig } from 'shared/types'; import type { BaseCodingAgent, ExecutorConfig } from 'shared/types';
import { useUserSystem } from '@/components/config-provider'; import { useUserSystem } from '@/components/config-provider';
import { mcpServersApi } from '../lib/api'; import { mcpServersApi } from '@/lib/api';
import { McpConfigStrategyGeneral } from '../lib/mcp-strategies'; import { McpConfigStrategyGeneral } from '@/lib/mcp-strategies';
export function McpServers() { export function McpSettings() {
const { config, profiles } = useUserSystem(); const { config, profiles } = useUserSystem();
const [mcpServers, setMcpServers] = useState('{}'); const [mcpServers, setMcpServers] = useState('{}');
const [mcpConfig, setMcpConfig] = useState<McpConfig | null>(null); const [mcpConfig, setMcpConfig] = useState<McpConfig | null>(null);
@@ -202,7 +202,7 @@ export function McpServers() {
if (!config) { if (!config) {
return ( return (
<div className="container mx-auto px-4 py-8"> <div className="py-8">
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription>Failed to load configuration.</AlertDescription> <AlertDescription>Failed to load configuration.</AlertDescription>
</Alert> </Alert>
@@ -211,161 +211,149 @@ export function McpServers() {
} }
return ( return (
<div className="container mx-auto px-4 py-8 max-w-4xl"> <div className="space-y-6">
<div className="space-y-6"> {mcpError && (
<div> <Alert variant="destructive">
<h1 className="text-3xl font-bold">MCP Servers</h1> <AlertDescription>
<p className="text-muted-foreground"> MCP Configuration Error: {mcpError}
Configure MCP servers to extend coding agent capabilities. </AlertDescription>
</p> </Alert>
</div> )}
{mcpError && ( {success && (
<Alert variant="destructive"> <Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200">
<AlertDescription> <AlertDescription className="font-medium">
MCP Configuration Error: {mcpError} MCP configuration saved successfully!
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
{success && ( <Card>
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200"> <CardHeader>
<AlertDescription className="font-medium"> <CardTitle>MCP Server Configuration</CardTitle>
MCP configuration saved successfully! <CardDescription>
</AlertDescription> Configure Model Context Protocol servers to extend coding agent
</Alert> capabilities with custom tools and resources.
)} </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="mcp-executor">Agent</Label>
<Select
value={
selectedProfile
? Object.keys(profiles || {}).find(
(key) => profiles![key] === selectedProfile
) || ''
: ''
}
onValueChange={(value: string) => {
const profile = profiles?.[value];
if (profile) setSelectedProfile(profile);
}}
>
<SelectTrigger id="mcp-executor">
<SelectValue placeholder="Select executor" />
</SelectTrigger>
<SelectContent>
{profiles &&
Object.entries(profiles)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([profileKey]) => (
<SelectItem key={profileKey} value={profileKey}>
{profileKey}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Choose which agent to configure MCP servers for.
</p>
</div>
<Card> {mcpError && mcpError.includes('does not support MCP') ? (
<CardHeader> <div className="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950">
<CardTitle>Configuration</CardTitle> <div className="flex">
<CardDescription> <div className="ml-3">
Configure MCP servers for different coding agents to extend their <h3 className="text-sm font-medium text-amber-800 dark:text-amber-200">
capabilities with custom tools and resources. MCP Not Supported
</CardDescription> </h3>
</CardHeader> <div className="mt-2 text-sm text-amber-700 dark:text-amber-300">
<CardContent className="space-y-4"> <p>{mcpError}</p>
<div className="space-y-2"> <p className="mt-1">
<Label htmlFor="mcp-executor">Profile</Label> To use MCP servers, please select a different executor
<Select that supports MCP (Claude, Amp, Gemini, Codex, or
value={ Opencode) above.
selectedProfile </p>
? Object.keys(profiles || {}).find(
(key) => profiles![key] === selectedProfile
) || ''
: ''
}
onValueChange={(value: string) => {
const profile = profiles?.[value];
if (profile) setSelectedProfile(profile);
}}
>
<SelectTrigger id="mcp-executor">
<SelectValue placeholder="Select executor" />
</SelectTrigger>
<SelectContent>
{profiles &&
Object.entries(profiles)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([profileKey]) => (
<SelectItem key={profileKey} value={profileKey}>
{profileKey}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Choose which profile to configure MCP servers for.
</p>
</div>
{mcpError && mcpError.includes('does not support MCP') ? (
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950">
<div className="flex">
<div className="ml-3">
<h3 className="text-sm font-medium text-amber-800 dark:text-amber-200">
MCP Not Supported
</h3>
<div className="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>{mcpError}</p>
<p className="mt-1">
To use MCP servers, please select a different profile
that supports MCP (Claude, Amp, Gemini, Codex, or
Opencode) above.
</p>
</div>
</div> </div>
</div> </div>
</div> </div>
) : ( </div>
<div className="space-y-2"> ) : (
<Label htmlFor="mcp-servers">MCP Server Configuration</Label> <div className="space-y-2">
<JSONEditor <Label htmlFor="mcp-servers">Server Configuration (JSON)</Label>
id="mcp-servers" <JSONEditor
placeholder={ id="mcp-servers"
mcpLoading placeholder={
? 'Loading current configuration...' mcpLoading
: '{\n "server-name": {\n "type": "stdio",\n "command": "your-command",\n "args": ["arg1", "arg2"]\n }\n}' ? 'Loading current configuration...'
} : '{\n "server-name": {\n "type": "stdio",\n "command": "your-command",\n "args": ["arg1", "arg2"]\n }\n}'
value={mcpLoading ? 'Loading...' : mcpServers} }
onChange={handleMcpServersChange} value={mcpLoading ? 'Loading...' : mcpServers}
disabled={mcpLoading} onChange={handleMcpServersChange}
minHeight={300} disabled={mcpLoading}
/> minHeight={300}
{mcpError && !mcpError.includes('does not support MCP') && ( />
<p className="text-sm text-destructive dark:text-red-400"> {mcpError && !mcpError.includes('does not support MCP') && (
{mcpError} <p className="text-sm text-destructive dark:text-red-400">
</p> {mcpError}
</p>
)}
<div className="text-sm text-muted-foreground">
{mcpLoading ? (
'Loading current MCP server configuration...'
) : (
<span>
Changes will be saved to:
{mcpConfigPath && (
<span className="ml-2 font-mono text-xs">
{mcpConfigPath}
</span>
)}
</span>
)} )}
<div className="text-sm text-muted-foreground">
{mcpLoading ? (
'Loading current MCP server configuration...'
) : (
<span>
Changes will be saved to:
{mcpConfigPath && (
<span className="ml-2 font-mono text-xs">
{mcpConfigPath}
</span>
)}
</span>
)}
</div>
<div className="pt-4">
<Button
onClick={handleConfigureVibeKanban}
disabled={mcpApplying || mcpLoading || !selectedProfile}
className="w-64"
>
Add Vibe-Kanban MCP
</Button>
<p className="text-sm text-muted-foreground mt-2">
Automatically adds the Vibe-Kanban MCP server.
</p>
</div>
</div> </div>
)}
</CardContent>
</Card>
{/* Sticky save button */} <div className="pt-4">
<div className="fixed bottom-0 left-0 right-0 bg-background/80 backdrop-blur-sm border-t p-4 z-10"> <Button
<div className="container mx-auto max-w-4xl flex justify-end"> onClick={handleConfigureVibeKanban}
<Button disabled={mcpApplying || mcpLoading || !selectedProfile}
onClick={handleApplyMcpServers} className="w-64"
disabled={mcpApplying || mcpLoading || !!mcpError || success} >
className={success ? 'bg-green-600 hover:bg-green-700' : ''} Add Vibe-Kanban MCP
> </Button>
{mcpApplying && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} <p className="text-sm text-muted-foreground mt-2">
{success && <span className="mr-2"></span>} Automatically adds the Vibe-Kanban MCP server configuration.
{success ? 'Settings Saved!' : 'Save Settings'} </p>
</Button> </div>
</div> </div>
)}
</CardContent>
</Card>
{/* Sticky Save Button */}
<div className="sticky bottom-0 z-10 bg-background/80 backdrop-blur-sm border-t pt-4">
<div className="flex justify-end">
<Button
onClick={handleApplyMcpServers}
disabled={mcpApplying || mcpLoading || !!mcpError || success}
className={success ? 'bg-green-600 hover:bg-green-700' : ''}
>
{mcpApplying && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{success && <span className="mr-2"></span>}
{success ? 'Settings Saved!' : 'Save MCP Configuration'}
</Button>
</div> </div>
{/* Spacer to prevent content from being hidden behind sticky button */}
<div className="h-20"></div>
</div> </div>
</div> </div>
); );

View File

@@ -0,0 +1,71 @@
import { NavLink, Outlet } from 'react-router-dom';
import { Settings, Cpu, Server } from 'lucide-react';
import { cn } from '@/lib/utils';
const settingsNavigation = [
{
path: 'general',
label: 'General',
icon: Settings,
description: 'Theme, notifications, and preferences',
},
{
path: 'agents',
label: 'Agents',
icon: Cpu,
description: 'Coding agent configurations',
},
{
path: 'mcp',
label: 'MCP Servers',
icon: Server,
description: 'Model Context Protocol servers',
},
];
export function SettingsLayout() {
return (
<div className="container mx-auto px-4 py-8">
<div className="flex flex-col lg:flex-row gap-8">
{/* Sidebar Navigation */}
<aside className="w-full lg:w-64 lg:shrink-0 lg:sticky lg:top-8 lg:h-fit lg:max-h-[calc(100vh-4rem)] lg:overflow-y-auto">
<div className="space-y-1">
<h2 className="px-3 py-2 text-lg font-semibold">Settings</h2>
<nav className="space-y-1">
{settingsNavigation.map((item) => {
const Icon = item.icon;
return (
<NavLink
key={item.path}
to={item.path}
end
className={({ isActive }) =>
cn(
'flex items-start gap-3 px-3 py-2 text-sm transition-colors',
'hover:text-accent-foreground',
isActive
? 'text-primary-foreground'
: 'text-secondary-foreground'
)
}
>
<Icon className="h-4 w-4 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<div className="font-medium">{item.label}</div>
<div>{item.description}</div>
</div>
</NavLink>
);
})}
</nav>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 min-w-0">
<Outlet />
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,4 @@
export { SettingsLayout } from './SettingsLayout';
export { GeneralSettings } from './GeneralSettings';
export { AgentSettings } from './AgentSettings';
export { McpSettings } from './McpSettings';

View File

@@ -0,0 +1,8 @@
declare module 'virtual:executor-schemas' {
import type { RJSFSchema } from '@rjsf/utils';
import type { BaseCodingAgent } from '@/shared/types';
const schemas: Record<BaseCodingAgent, RJSFSchema>;
export { schemas };
export default schemas;
}

View File

@@ -6,6 +6,7 @@ module.exports = {
'./components/**/*.{ts,tsx}', './components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}', './app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}', './src/**/*.{ts,tsx}',
"node_modules/@rjsf/shadcn/src/**/*.{js,ts,jsx,tsx,mdx}"
], ],
safelist: [ safelist: [
'xl:hidden', 'xl:hidden',

View File

@@ -1,32 +1,77 @@
// vite.config.ts
import { sentryVitePlugin } from "@sentry/vite-plugin"; import { sentryVitePlugin } from "@sentry/vite-plugin";
import { defineConfig } from 'vite' import { defineConfig, Plugin } from "vite";
import react from '@vitejs/plugin-react' import react from "@vitejs/plugin-react";
import path from 'path' import path from "path";
import fs from "fs";
function executorSchemasPlugin(): Plugin {
const VIRTUAL_ID = "virtual:executor-schemas";
const RESOLVED_VIRTUAL_ID = "\0" + VIRTUAL_ID;
return {
name: "executor-schemas-plugin",
resolveId(id) {
if (id === VIRTUAL_ID) return RESOLVED_VIRTUAL_ID; // keep it virtual
return null;
},
load(id) {
if (id !== RESOLVED_VIRTUAL_ID) return null;
const schemasDir = path.resolve(__dirname, "../shared/schemas");
const files = fs.existsSync(schemasDir)
? fs.readdirSync(schemasDir).filter((f) => f.endsWith(".json"))
: [];
const imports: string[] = [];
const entries: string[] = [];
files.forEach((file, i) => {
const varName = `__schema_${i}`;
const importPath = `shared/schemas/${file}`; // uses your alias
const key = file.replace(/\.json$/, "").toUpperCase(); // claude_code -> CLAUDE_CODE
imports.push(`import ${varName} from "${importPath}";`);
entries.push(` "${key}": ${varName}`);
});
// IMPORTANT: pure JS (no TS types), and quote keys.
const code = `
${imports.join("\n")}
export const schemas = {
${entries.join(",\n")}
};
export default schemas;
`;
return code;
},
};
}
export default defineConfig({ export default defineConfig({
plugins: [react(), sentryVitePlugin({ plugins: [
org: "bloop-ai", react(),
project: "vibe-kanban" sentryVitePlugin({ org: "bloop-ai", project: "vibe-kanban" }),
})], executorSchemasPlugin(),
],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),
"shared": path.resolve(__dirname, "../shared"), shared: path.resolve(__dirname, "../shared"),
}, },
}, },
server: { server: {
port: parseInt(process.env.FRONTEND_PORT || '3000'), port: parseInt(process.env.FRONTEND_PORT || "3000"),
proxy: { proxy: {
'/api': { "/api": {
target: `http://localhost:${process.env.BACKEND_PORT || '3001'}`, target: `http://localhost:${process.env.BACKEND_PORT || "3001"}`,
changeOrigin: true, changeOrigin: true,
}, },
}, },
fs: {
allow: [path.resolve(__dirname, "."), path.resolve(__dirname, "..")],
},
}, },
build: { sourcemap: true },
build: { });
sourcemap: true
}
})

585
pnpm-lock.yaml generated
View File

@@ -71,6 +71,9 @@ importers:
'@radix-ui/react-tooltip': '@radix-ui/react-tooltip':
specifier: ^1.2.7 specifier: ^1.2.7
version: 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@rjsf/shadcn':
specifier: 6.0.0-beta.10
version: 6.0.0-beta.10(@rjsf/core@6.0.0-beta.11(@rjsf/utils@6.0.0-beta.11(react@18.3.1))(react@18.3.1))(@rjsf/utils@6.0.0-beta.11(react@18.3.1))(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.17)
'@sentry/react': '@sentry/react':
specifier: ^9.34.0 specifier: ^9.34.0
version: 9.34.0(react@18.3.1) version: 9.34.0(react@18.3.1)
@@ -147,6 +150,15 @@ importers:
specifier: ^4.5.4 specifier: ^4.5.4
version: 4.5.7(@types/react@18.3.23)(react@18.3.1) version: 4.5.7(@types/react@18.3.23)(react@18.3.1)
devDependencies: devDependencies:
'@rjsf/core':
specifier: 6.0.0-beta.11
version: 6.0.0-beta.11(@rjsf/utils@6.0.0-beta.11(react@18.3.1))(react@18.3.1)
'@rjsf/utils':
specifier: 6.0.0-beta.11
version: 6.0.0-beta.11(react@18.3.1)
'@rjsf/validator-ajv8':
specifier: 6.0.0-beta.11
version: 6.0.0-beta.11(@rjsf/utils@6.0.0-beta.11(react@18.3.1))
'@tailwindcss/container-queries': '@tailwindcss/container-queries':
specifier: ^0.1.1 specifier: ^0.1.1
version: 0.1.1(tailwindcss@3.4.17) version: 0.1.1(tailwindcss@3.4.17)
@@ -805,6 +817,9 @@ packages:
'@radix-ui/primitive@1.1.2': '@radix-ui/primitive@1.1.2':
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
'@radix-ui/react-arrow@1.1.7': '@radix-ui/react-arrow@1.1.7':
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
peerDependencies: peerDependencies:
@@ -818,6 +833,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-checkbox@1.3.3':
resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
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
'@radix-ui/react-collection@1.1.7': '@radix-ui/react-collection@1.1.7':
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies: peerDependencies:
@@ -849,6 +877,19 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-dialog@1.1.15':
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
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
'@radix-ui/react-direction@1.1.1': '@radix-ui/react-direction@1.1.1':
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
peerDependencies: peerDependencies:
@@ -871,6 +912,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-dismissable-layer@1.1.11':
resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
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
'@radix-ui/react-dropdown-menu@2.1.15': '@radix-ui/react-dropdown-menu@2.1.15':
resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==} resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==}
peerDependencies: peerDependencies:
@@ -893,6 +947,15 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-focus-guards@1.1.3':
resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-focus-scope@1.1.7': '@radix-ui/react-focus-scope@1.1.7':
resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
peerDependencies: peerDependencies:
@@ -906,6 +969,11 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-icons@1.3.2':
resolution: {integrity: sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==}
peerDependencies:
react: ^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc
'@radix-ui/react-id@1.1.1': '@radix-ui/react-id@1.1.1':
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies: peerDependencies:
@@ -941,6 +1009,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-popover@1.1.15':
resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
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
'@radix-ui/react-popper@1.2.7': '@radix-ui/react-popper@1.2.7':
resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==}
peerDependencies: peerDependencies:
@@ -954,6 +1035,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-popper@1.2.8':
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
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
'@radix-ui/react-portal@1.1.9': '@radix-ui/react-portal@1.1.9':
resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
peerDependencies: peerDependencies:
@@ -980,6 +1074,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-presence@1.1.5':
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
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
'@radix-ui/react-primitive@2.1.3': '@radix-ui/react-primitive@2.1.3':
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
peerDependencies: peerDependencies:
@@ -993,6 +1100,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-radio-group@1.3.8':
resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==}
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
'@radix-ui/react-roving-focus@1.1.10': '@radix-ui/react-roving-focus@1.1.10':
resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==}
peerDependencies: peerDependencies:
@@ -1006,6 +1126,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-roving-focus@1.1.11':
resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
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
'@radix-ui/react-select@2.2.5': '@radix-ui/react-select@2.2.5':
resolution: {integrity: sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==} resolution: {integrity: sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==}
peerDependencies: peerDependencies:
@@ -1032,6 +1165,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-slider@1.3.6':
resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==}
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
'@radix-ui/react-slot@1.2.3': '@radix-ui/react-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies: peerDependencies:
@@ -1155,10 +1301,42 @@ packages:
'@radix-ui/rect@1.1.1': '@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
'@react-icons/all-files@4.1.0':
resolution: {integrity: sha512-hxBI2UOuVaI3O/BhQfhtb4kcGn9ft12RWAFVMUeNjqqhLsHvFtzIkFaptBJpFDANTKoDfdVoHTKZDlwKCACbMQ==}
peerDependencies:
react: '*'
'@remix-run/router@1.23.0': '@remix-run/router@1.23.0':
resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
'@rjsf/core@6.0.0-beta.11':
resolution: {integrity: sha512-tadVN0B7CjAzl2p3HDsZKR3V8iCIiQMncblfCYSK0PAh9RqIjigFp8ovrZ3UqkVIRRcD+tQ6Y+psY5yEB4GWqA==}
engines: {node: '>=20'}
peerDependencies:
'@rjsf/utils': ^6.0.0-beta
react: '>=18'
'@rjsf/shadcn@6.0.0-beta.10':
resolution: {integrity: sha512-BXgbCZF+Xb7lqLYBWYRcq4xamB2I41yOx0fT7dNs09CbnyWge4yziE6Ebm7R4N87qf2M320PLdJymcpUe5WtLA==}
engines: {node: '>=20'}
peerDependencies:
'@rjsf/core': ^6.0.0-beta
'@rjsf/utils': ^6.0.0-beta
react: '>=18'
'@rjsf/utils@6.0.0-beta.11':
resolution: {integrity: sha512-tQeyacweXmquRjPcXCVS1pMFfhamFSoqpczNi0zEvMbx+mfu2b6CgoRKi2Hm8KgFEafdVBF+6HLqs+0FnYhRlQ==}
engines: {node: '>=20'}
peerDependencies:
react: '>=18'
'@rjsf/validator-ajv8@6.0.0-beta.11':
resolution: {integrity: sha512-gR8pbCsR91LiayDKdIz7d7m7A2ozUv9J+7Tpl5/qUQ69Q/NmnHo+cAiVW04/yvqzf1cbRwMgkc5pZyu01Uu1/w==}
engines: {node: '>=20'}
peerDependencies:
'@rjsf/utils': ^6.0.0-beta
'@rolldown/pluginutils@1.0.0-beta.11': '@rolldown/pluginutils@1.0.0-beta.11':
resolution: {integrity: sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==} resolution: {integrity: sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==}
@@ -1545,9 +1723,20 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'} engines: {node: '>= 6.0.0'}
ajv-formats@2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
ajv@6.12.6: ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
ansi-regex@5.0.1: ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1674,6 +1863,12 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
cmdk@1.1.1:
resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc
codemirror@6.0.2: codemirror@6.0.2:
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
@@ -1691,6 +1886,12 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
compute-gcd@1.2.1:
resolution: {integrity: sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==}
compute-lcm@1.1.2:
resolution: {integrity: sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==}
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -1918,6 +2119,9 @@ packages:
fast-levenshtein@2.0.6: fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
fastq@1.19.1: fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
@@ -2144,9 +2348,19 @@ packages:
json-parse-even-better-errors@2.3.1: json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
json-schema-compare@0.2.2:
resolution: {integrity: sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==}
json-schema-merge-allof@0.8.1:
resolution: {integrity: sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==}
engines: {node: '>=12.0.0'}
json-schema-traverse@0.4.1: json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-stable-stringify-without-jsonify@1.0.1: json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@@ -2155,6 +2369,10 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
jsonpointer@5.0.1:
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
engines: {node: '>=0.10.0'}
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -2173,6 +2391,9 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash.castarray@4.4.0: lodash.castarray@4.4.0:
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
@@ -2201,6 +2422,11 @@ packages:
lru-cache@5.1.1: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-react@0.503.0:
resolution: {integrity: sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
lucide-react@0.539.0: lucide-react@0.539.0:
resolution: {integrity: sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==} resolution: {integrity: sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==}
peerDependencies: peerDependencies:
@@ -2210,6 +2436,12 @@ packages:
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
markdown-to-jsx@7.7.13:
resolution: {integrity: sha512-DiueEq2bttFcSxUs85GJcQVrOr0+VVsPfj9AEUPqmExJ3f8P/iQNvZHltV4tm1XVhu1kl0vWBZWT3l99izRMaA==}
engines: {node: '>= 10'}
peerDependencies:
react: '>= 0.14.0'
mdast-util-from-markdown@2.0.2: mdast-util-from-markdown@2.0.2:
resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==}
@@ -2345,6 +2577,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
nanoid@5.1.5:
resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
engines: {node: ^18 || >=20}
hasBin: true
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -2543,6 +2780,9 @@ packages:
react-is@16.13.1: react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
react-markdown@10.1.0: react-markdown@10.1.0:
resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
peerDependencies: peerDependencies:
@@ -2647,6 +2887,10 @@ packages:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
resolve-from@4.0.0: resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -2784,6 +3028,9 @@ packages:
tailwind-merge@2.6.0: tailwind-merge@2.6.0:
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
tailwind-merge@3.3.1:
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
tailwindcss-animate@1.0.7: tailwindcss-animate@1.0.7:
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
peerDependencies: peerDependencies:
@@ -2917,6 +3164,25 @@ packages:
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@11.1.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
validate.io-array@1.0.6:
resolution: {integrity: sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==}
validate.io-function@1.0.2:
resolution: {integrity: sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==}
validate.io-integer-array@1.0.0:
resolution: {integrity: sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==}
validate.io-integer@1.0.5:
resolution: {integrity: sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==}
validate.io-number@1.0.3:
resolution: {integrity: sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==}
vfile-message@4.0.2: vfile-message@4.0.2:
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
@@ -3656,6 +3922,8 @@ snapshots:
'@radix-ui/primitive@1.1.2': {} '@radix-ui/primitive@1.1.2': {}
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -3665,6 +3933,22 @@ snapshots:
'@types/react': 18.3.23 '@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23) '@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
@@ -3689,6 +3973,28 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.23 '@types/react': 18.3.23
'@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
aria-hidden: 1.2.6
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-direction@1.1.1(@types/react@18.3.23)(react@18.3.1)': '@radix-ui/react-direction@1.1.1(@types/react@18.3.23)(react@18.3.1)':
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
@@ -3708,6 +4014,19 @@ snapshots:
'@types/react': 18.3.23 '@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23) '@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.23)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/primitive': 1.1.2 '@radix-ui/primitive': 1.1.2
@@ -3729,6 +4048,12 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.23 '@types/react': 18.3.23
'@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.23)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.23
'@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
@@ -3740,6 +4065,10 @@ snapshots:
'@types/react': 18.3.23 '@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23) '@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-icons@1.3.2(react@18.3.1)':
dependencies:
react: 18.3.1
'@radix-ui/react-id@1.1.1(@types/react@18.3.23)(react@18.3.1)': '@radix-ui/react-id@1.1.1(@types/react@18.3.23)(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1)
@@ -3782,6 +4111,29 @@ snapshots:
'@types/react': 18.3.23 '@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23) '@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
aria-hidden: 1.2.6
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@floating-ui/react-dom': 2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@floating-ui/react-dom': 2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -3800,6 +4152,24 @@ snapshots:
'@types/react': 18.3.23 '@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23) '@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/react-dom': 2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/rect': 1.1.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -3820,6 +4190,16 @@ snapshots:
'@types/react': 18.3.23 '@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23) '@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1)
@@ -3829,6 +4209,24 @@ snapshots:
'@types/react': 18.3.23 '@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23) '@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/primitive': 1.1.2 '@radix-ui/primitive': 1.1.2
@@ -3846,6 +4244,23 @@ snapshots:
'@types/react': 18.3.23 '@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23) '@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-select@2.2.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-select@2.2.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/number': 1.1.1 '@radix-ui/number': 1.1.1
@@ -3884,6 +4299,25 @@ snapshots:
'@types/react': 18.3.23 '@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23) '@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-slider@1.3.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1)': '@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
@@ -3992,8 +4426,72 @@ snapshots:
'@radix-ui/rect@1.1.1': {} '@radix-ui/rect@1.1.1': {}
'@react-icons/all-files@4.1.0(react@18.3.1)':
dependencies:
react: 18.3.1
'@remix-run/router@1.23.0': {} '@remix-run/router@1.23.0': {}
'@rjsf/core@6.0.0-beta.11(@rjsf/utils@6.0.0-beta.11(react@18.3.1))(react@18.3.1)':
dependencies:
'@rjsf/utils': 6.0.0-beta.11(react@18.3.1)
lodash: 4.17.21
lodash-es: 4.17.21
markdown-to-jsx: 7.7.13(react@18.3.1)
nanoid: 5.1.5
prop-types: 15.8.1
react: 18.3.1
'@rjsf/shadcn@6.0.0-beta.10(@rjsf/core@6.0.0-beta.11(@rjsf/utils@6.0.0-beta.11(react@18.3.1))(react@18.3.1))(@rjsf/utils@6.0.0-beta.11(react@18.3.1))(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.17)':
dependencies:
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-icons': 1.3.2(react@18.3.1)
'@radix-ui/react-label': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-popover': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-radio-group': 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-select': 2.2.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-separator': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slider': 1.3.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1)
'@react-icons/all-files': 4.1.0(react@18.3.1)
'@rjsf/core': 6.0.0-beta.11(@rjsf/utils@6.0.0-beta.11(react@18.3.1))(react@18.3.1)
'@rjsf/utils': 6.0.0-beta.11(react@18.3.1)
class-variance-authority: 0.7.1
clsx: 2.1.1
cmdk: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
lodash: 4.17.21
lodash-es: 4.17.21
lucide-react: 0.503.0(react@18.3.1)
react: 18.3.1
tailwind-merge: 3.3.1
tailwindcss-animate: 1.0.7(tailwindcss@3.4.17)
uuid: 11.1.0
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
- react-dom
- tailwindcss
'@rjsf/utils@6.0.0-beta.11(react@18.3.1)':
dependencies:
fast-uri: 3.1.0
json-schema-merge-allof: 0.8.1
jsonpointer: 5.0.1
lodash: 4.17.21
lodash-es: 4.17.21
nanoid: 5.1.5
react: 18.3.1
react-is: 18.3.1
'@rjsf/validator-ajv8@6.0.0-beta.11(@rjsf/utils@6.0.0-beta.11(react@18.3.1))':
dependencies:
'@rjsf/utils': 6.0.0-beta.11(react@18.3.1)
ajv: 8.17.1
ajv-formats: 2.1.1(ajv@8.17.1)
lodash: 4.17.21
lodash-es: 4.17.21
'@rolldown/pluginutils@1.0.0-beta.11': {} '@rolldown/pluginutils@1.0.0-beta.11': {}
'@rollup/rollup-android-arm-eabi@4.44.0': '@rollup/rollup-android-arm-eabi@4.44.0':
@@ -4393,6 +4891,10 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
ajv-formats@2.1.1(ajv@8.17.1):
optionalDependencies:
ajv: 8.17.1
ajv@6.12.6: ajv@6.12.6:
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@@ -4400,6 +4902,13 @@ snapshots:
json-schema-traverse: 0.4.1 json-schema-traverse: 0.4.1
uri-js: 4.4.1 uri-js: 4.4.1
ajv@8.17.1:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.0
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-regex@5.0.1: {} ansi-regex@5.0.1: {}
ansi-regex@6.1.0: {} ansi-regex@6.1.0: {}
@@ -4526,6 +5035,18 @@ snapshots:
clsx@2.1.1: {} clsx@2.1.1: {}
cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
codemirror@6.0.2: codemirror@6.0.2:
dependencies: dependencies:
'@codemirror/autocomplete': 6.18.6 '@codemirror/autocomplete': 6.18.6
@@ -4546,6 +5067,19 @@ snapshots:
commander@4.1.1: {} commander@4.1.1: {}
compute-gcd@1.2.1:
dependencies:
validate.io-array: 1.0.6
validate.io-function: 1.0.2
validate.io-integer-array: 1.0.0
compute-lcm@1.1.2:
dependencies:
compute-gcd: 1.2.1
validate.io-array: 1.0.6
validate.io-function: 1.0.2
validate.io-integer-array: 1.0.0
concat-map@0.0.1: {} concat-map@0.0.1: {}
concurrently@8.2.2: concurrently@8.2.2:
@@ -4817,6 +5351,8 @@ snapshots:
fast-levenshtein@2.0.6: {} fast-levenshtein@2.0.6: {}
fast-uri@3.1.0: {}
fastq@1.19.1: fastq@1.19.1:
dependencies: dependencies:
reusify: 1.1.0 reusify: 1.1.0
@@ -5040,12 +5576,26 @@ snapshots:
json-parse-even-better-errors@2.3.1: {} json-parse-even-better-errors@2.3.1: {}
json-schema-compare@0.2.2:
dependencies:
lodash: 4.17.21
json-schema-merge-allof@0.8.1:
dependencies:
compute-lcm: 1.1.2
json-schema-compare: 0.2.2
lodash: 4.17.21
json-schema-traverse@0.4.1: {} json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
json-stable-stringify-without-jsonify@1.0.1: {} json-stable-stringify-without-jsonify@1.0.1: {}
json5@2.2.3: {} json5@2.2.3: {}
jsonpointer@5.0.1: {}
keyv@4.5.4: keyv@4.5.4:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
@@ -5063,6 +5613,8 @@ snapshots:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
lodash-es@4.17.21: {}
lodash.castarray@4.4.0: {} lodash.castarray@4.4.0: {}
lodash.isplainobject@4.0.6: {} lodash.isplainobject@4.0.6: {}
@@ -5089,6 +5641,10 @@ snapshots:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
lucide-react@0.503.0(react@18.3.1):
dependencies:
react: 18.3.1
lucide-react@0.539.0(react@18.3.1): lucide-react@0.539.0(react@18.3.1):
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
@@ -5097,6 +5653,10 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
markdown-to-jsx@7.7.13(react@18.3.1):
dependencies:
react: 18.3.1
mdast-util-from-markdown@2.0.2: mdast-util-from-markdown@2.0.2:
dependencies: dependencies:
'@types/mdast': 4.0.4 '@types/mdast': 4.0.4
@@ -5360,6 +5920,8 @@ snapshots:
nanoid@3.3.11: {} nanoid@3.3.11: {}
nanoid@5.1.5: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
node-fetch@2.7.0: node-fetch@2.7.0:
@@ -5533,6 +6095,8 @@ snapshots:
react-is@16.13.1: {} react-is@16.13.1: {}
react-is@18.3.1: {}
react-markdown@10.1.0(@types/react@18.3.23)(react@18.3.1): react-markdown@10.1.0(@types/react@18.3.23)(react@18.3.1):
dependencies: dependencies:
'@types/hast': 3.0.4 '@types/hast': 3.0.4
@@ -5650,6 +6214,8 @@ snapshots:
require-directory@2.1.1: {} require-directory@2.1.1: {}
require-from-string@2.0.2: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
resolve@1.22.10: resolve@1.22.10:
@@ -5793,6 +6359,8 @@ snapshots:
tailwind-merge@2.6.0: {} tailwind-merge@2.6.0: {}
tailwind-merge@3.3.1: {}
tailwindcss-animate@1.0.7(tailwindcss@3.4.17): tailwindcss-animate@1.0.7(tailwindcss@3.4.17):
dependencies: dependencies:
tailwindcss: 3.4.17 tailwindcss: 3.4.17
@@ -5944,6 +6512,23 @@ snapshots:
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
uuid@11.1.0: {}
validate.io-array@1.0.6: {}
validate.io-function@1.0.2: {}
validate.io-integer-array@1.0.0:
dependencies:
validate.io-array: 1.0.6
validate.io-integer: 1.0.5
validate.io-integer@1.0.5:
dependencies:
validate.io-number: 1.0.3
validate.io-number@1.0.3: {}
vfile-message@4.0.2: vfile-message@4.0.2:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3

View File

@@ -1,12 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs).
// Do not edit this file manually.
// Auto-generated from Rust backend types using ts-rs
export const MCP_SUPPORTED_EXECUTORS: string[] = [
"claude",
"amp",
"gemini",
"sst-opencode",
"charm-opencode",
"claude-code-router"
];

43
shared/schemas/amp.json Normal file
View File

@@ -0,0 +1,43 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"append_prompt": {
"title": "Append Prompt",
"description": "Extra text appended to the prompt",
"type": [
"string",
"null"
],
"format": "textarea",
"default": null
},
"dangerously_allow_all": {
"title": "Dangerously Allow All",
"description": "Allow all commands to be executed, even if they are not safe.",
"type": [
"boolean",
"null"
]
},
"base_command_override": {
"title": "Base Command Override",
"description": "Override the base command with a custom command",
"type": [
"string",
"null"
]
},
"additional_params": {
"title": "Additional Parameters",
"description": "Additional parameters to append to the base command",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
},
"type": "object"
}

View File

@@ -0,0 +1,53 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"append_prompt": {
"title": "Append Prompt",
"description": "Extra text appended to the prompt",
"type": [
"string",
"null"
],
"format": "textarea",
"default": null
},
"claude_code_router": {
"type": [
"boolean",
"null"
]
},
"plan": {
"type": [
"boolean",
"null"
]
},
"dangerously_skip_permissions": {
"type": [
"boolean",
"null"
]
},
"base_command_override": {
"title": "Base Command Override",
"description": "Override the base command with a custom command",
"type": [
"string",
"null"
]
},
"additional_params": {
"title": "Additional Parameters",
"description": "Additional parameters to append to the base command",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
},
"type": "object"
}

74
shared/schemas/codex.json Normal file
View File

@@ -0,0 +1,74 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"append_prompt": {
"title": "Append Prompt",
"description": "Extra text appended to the prompt",
"type": [
"string",
"null"
],
"format": "textarea",
"default": null
},
"sandbox": {
"description": "Sandbox policy modes for Codex",
"type": [
"string",
"null"
],
"enum": [
"read-only",
"workspace-write",
"danger-full-access",
null
]
},
"approval": {
"description": "Approval policy for Codex",
"type": [
"string",
"null"
],
"enum": [
"untrusted",
"on-failure",
"on-request",
"never",
null
]
},
"oss": {
"type": [
"boolean",
"null"
]
},
"model": {
"type": [
"string",
"null"
]
},
"base_command_override": {
"title": "Base Command Override",
"description": "Override the base command with a custom command",
"type": [
"string",
"null"
]
},
"additional_params": {
"title": "Additional Parameters",
"description": "Additional parameters to append to the base command",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
},
"type": "object"
}

View File

@@ -0,0 +1,47 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"append_prompt": {
"title": "Append Prompt",
"description": "Extra text appended to the prompt",
"type": [
"string",
"null"
],
"format": "textarea",
"default": null
},
"force": {
"type": [
"boolean",
"null"
]
},
"model": {
"type": [
"string",
"null"
]
},
"base_command_override": {
"title": "Base Command Override",
"description": "Override the base command with a custom command",
"type": [
"string",
"null"
]
},
"additional_params": {
"title": "Additional Parameters",
"description": "Additional parameters to append to the base command",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
},
"type": "object"
}

View File

@@ -0,0 +1,51 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"model"
],
"type": "object",
"properties": {
"append_prompt": {
"title": "Append Prompt",
"description": "Extra text appended to the prompt",
"type": [
"string",
"null"
],
"format": "textarea",
"default": null
},
"model": {
"type": "string",
"enum": [
"default",
"flash"
]
},
"yolo": {
"type": [
"boolean",
"null"
]
},
"base_command_override": {
"title": "Base Command Override",
"description": "Override the base command with a custom command",
"type": [
"string",
"null"
]
},
"additional_params": {
"title": "Additional Parameters",
"description": "Additional parameters to append to the base command",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
}
}

View File

@@ -0,0 +1,47 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"append_prompt": {
"title": "Append Prompt",
"description": "Extra text appended to the prompt",
"type": [
"string",
"null"
],
"format": "textarea",
"default": null
},
"model": {
"type": [
"string",
"null"
]
},
"agent": {
"type": [
"string",
"null"
]
},
"base_command_override": {
"title": "Base Command Override",
"description": "Override the base command with a custom command",
"type": [
"string",
"null"
]
},
"additional_params": {
"title": "Additional Parameters",
"description": "Additional parameters to append to the base command",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
},
"type": "object"
}

View File

@@ -0,0 +1,41 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"append_prompt": {
"title": "Append Prompt",
"description": "Extra text appended to the prompt",
"type": [
"string",
"null"
],
"format": "textarea",
"default": null
},
"yolo": {
"type": [
"boolean",
"null"
]
},
"base_command_override": {
"title": "Base Command Override",
"description": "Override the base command with a custom command",
"type": [
"string",
"null"
]
},
"additional_params": {
"title": "Additional Parameters",
"description": "Additional parameters to append to the base command",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
},
"type": "object"
}

View File

@@ -134,25 +134,27 @@ export type ExecutorConfig = { [key in string]?: { "CLAUDE_CODE": ClaudeCode } |
export type BaseAgentCapability = "RESTORE_CHECKPOINT"; export type BaseAgentCapability = "RESTORE_CHECKPOINT";
export type ClaudeCode = { claude_code_router?: boolean | null, append_prompt?: string | null, plan?: boolean | null, dangerously_skip_permissions?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, }; export type ClaudeCode = { append_prompt: AppendPrompt, claude_code_router?: boolean | null, plan?: boolean | null, dangerously_skip_permissions?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, };
export type Gemini = { model: GeminiModel, append_prompt?: string | null, yolo?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, }; export type Gemini = { append_prompt: AppendPrompt, model: GeminiModel, yolo?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, };
export type GeminiModel = "default" | "flash"; export type GeminiModel = "default" | "flash";
export type Amp = { append_prompt?: string | null, dangerously_allow_all?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, }; export type Amp = { append_prompt: AppendPrompt, dangerously_allow_all?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, };
export type Codex = { append_prompt?: string | null, sandbox?: SandboxMode | null, approval?: ApprovalPolicy | null, oss?: boolean | null, model?: string | null, base_command_override?: string | null, additional_params?: Array<string> | null, }; export type Codex = { append_prompt: AppendPrompt, sandbox?: SandboxMode | null, approval?: ApprovalPolicy | null, oss?: boolean | null, model?: string | null, base_command_override?: string | null, additional_params?: Array<string> | null, };
export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access"; export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access";
export type ApprovalPolicy = "untrusted" | "on-failure" | "on-request" | "never"; export type ApprovalPolicy = "untrusted" | "on-failure" | "on-request" | "never";
export type Cursor = { append_prompt?: string | null, force?: boolean | null, model?: string | null, base_command_override?: string | null, additional_params?: Array<string> | null, }; export type Cursor = { append_prompt: AppendPrompt, force?: boolean | null, model?: string | null, base_command_override?: string | null, additional_params?: Array<string> | null, };
export type Opencode = { append_prompt?: string | null, model?: string | null, agent?: string | null, base_command_override?: string | null, additional_params?: Array<string> | null, }; export type Opencode = { append_prompt: AppendPrompt, model?: string | null, agent?: string | null, base_command_override?: string | null, additional_params?: Array<string> | null, };
export type QwenCode = { append_prompt?: string | null, yolo?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, }; export type QwenCode = { append_prompt: AppendPrompt, yolo?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, };
export type AppendPrompt = string | null;
export type CodingAgentInitialRequest = { prompt: string, export type CodingAgentInitialRequest = { prompt: string,
/** /**