Merge branch 'main' into feat/feishu-doc-tool
This commit is contained in:
commit
888bd4101d
@ -78,6 +78,7 @@ Notes:
|
||||
- You can restrict who can use approval-management commands via `[autonomy].non_cli_approval_approvers`.
|
||||
- Configure natural-language approval mode via `[autonomy].non_cli_natural_language_approval_mode`.
|
||||
- `autonomy.non_cli_excluded_tools` is reloaded from `config.toml` at runtime; `/approvals` shows the currently effective list.
|
||||
- Default non-CLI exclusions include both `shell` and `process`; remove `process` from `[autonomy].non_cli_excluded_tools` only when you explicitly want background command execution in chat channels.
|
||||
- Each incoming message injects a runtime tool-availability snapshot into the system prompt, derived from the same exclusion policy used by execution.
|
||||
|
||||
## Inbound Image Marker Protocol
|
||||
|
||||
@ -868,7 +868,7 @@ Environment overrides:
|
||||
| `allow_sensitive_file_writes` | `false` | allow `file_write`/`file_edit` on sensitive files/dirs (for example `.env`, `.aws/credentials`, private keys) |
|
||||
| `auto_approve` | `[]` | tool operations always auto-approved |
|
||||
| `always_ask` | `[]` | tool operations that always require approval |
|
||||
| `non_cli_excluded_tools` | `[]` | tools hidden from non-CLI channel tool specs |
|
||||
| `non_cli_excluded_tools` | built-in denylist (includes `shell`, `process`, `file_write`, ...) | tools hidden from non-CLI channel tool specs |
|
||||
| `non_cli_approval_approvers` | `[]` | optional allowlist for who can run non-CLI approval-management commands |
|
||||
| `non_cli_natural_language_approval_mode` | `direct` | natural-language behavior for approval-management commands (`direct`, `request_confirm`, `disabled`) |
|
||||
| `non_cli_natural_language_approval_mode_by_channel` | `{}` | per-channel override map for natural-language approval mode |
|
||||
@ -908,6 +908,7 @@ Notes:
|
||||
- `telegram:alice` allows only that channel+sender pair.
|
||||
- `telegram:*` allows any sender on Telegram.
|
||||
- `*:alice` allows `alice` on any channel.
|
||||
- By default, `process` is excluded on non-CLI channels alongside `shell`. To opt in intentionally, remove `"process"` from `[autonomy].non_cli_excluded_tools` in `config.toml`.
|
||||
- Use `/unapprove <tool>` to remove persisted approval from `autonomy.auto_approve`.
|
||||
- `/approve-pending` lists pending requests for the current sender+chat/channel scope.
|
||||
- If a tool remains unavailable after approval, check `autonomy.non_cli_excluded_tools` (runtime `/approvals` shows this list). Channel runtime reloads this list from `config.toml` automatically.
|
||||
|
||||
@ -6747,6 +6747,36 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
}
|
||||
}
|
||||
|
||||
struct MockProcessTool;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Tool for MockProcessTool {
|
||||
fn name(&self) -> &str {
|
||||
"process"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Mock process tool for runtime visibility tests"
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string" }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, _args: serde_json::Value) -> anyhow::Result<ToolResult> {
|
||||
Ok(ToolResult {
|
||||
success: true,
|
||||
output: String::new(),
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_runtime_tool_visibility_prompt_respects_excluded_snapshot() {
|
||||
let tools: Vec<Box<dyn Tool>> = vec![Box::new(MockPriceTool), Box::new(MockEchoTool)];
|
||||
@ -6765,6 +6795,23 @@ BTC is currently around $65,000 based on latest tool output."#
|
||||
assert!(!native.contains("## Tool Use Protocol"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_runtime_tool_visibility_prompt_excludes_process_with_default_policy() {
|
||||
let tools: Vec<Box<dyn Tool>> = vec![Box::new(MockProcessTool), Box::new(MockEchoTool)];
|
||||
let excluded = crate::config::AutonomyConfig::default().non_cli_excluded_tools;
|
||||
|
||||
assert!(
|
||||
excluded.contains(&"process".to_string()),
|
||||
"default non-CLI exclusion list must include process"
|
||||
);
|
||||
|
||||
let prompt = build_runtime_tool_visibility_prompt(&tools, &excluded, false);
|
||||
assert!(prompt.contains("Excluded by runtime policy:"));
|
||||
assert!(prompt.contains("process"));
|
||||
assert!(!prompt.contains("**process**:"));
|
||||
assert!(prompt.contains("`mock_echo`"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn process_channel_message_injects_runtime_tool_visibility_prompt() {
|
||||
let channel_impl = Arc::new(RecordingChannel::default());
|
||||
|
||||
@ -3320,6 +3320,7 @@ fn default_always_ask() -> Vec<String> {
|
||||
fn default_non_cli_excluded_tools() -> Vec<String> {
|
||||
[
|
||||
"shell",
|
||||
"process",
|
||||
"file_write",
|
||||
"file_edit",
|
||||
"git_operations",
|
||||
@ -9427,6 +9428,7 @@ mod tests {
|
||||
assert!(!a.allow_sensitive_file_reads);
|
||||
assert!(!a.allow_sensitive_file_writes);
|
||||
assert!(a.non_cli_excluded_tools.contains(&"shell".to_string()));
|
||||
assert!(a.non_cli_excluded_tools.contains(&"process".to_string()));
|
||||
assert!(a.non_cli_excluded_tools.contains(&"delegate".to_string()));
|
||||
}
|
||||
|
||||
@ -9460,6 +9462,9 @@ allowed_roots = []
|
||||
"Missing command_context_rules must default to empty"
|
||||
);
|
||||
assert!(parsed.non_cli_excluded_tools.contains(&"shell".to_string()));
|
||||
assert!(parsed
|
||||
.non_cli_excluded_tools
|
||||
.contains(&"process".to_string()));
|
||||
assert!(parsed
|
||||
.non_cli_excluded_tools
|
||||
.contains(&"browser".to_string()));
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
//! - Google Cloud ADC (`GOOGLE_APPLICATION_CREDENTIALS`)
|
||||
|
||||
use crate::auth::AuthService;
|
||||
use crate::multimodal;
|
||||
use crate::providers::traits::{ChatMessage, ChatResponse, Provider, TokenUsage};
|
||||
use async_trait::async_trait;
|
||||
use base64::Engine;
|
||||
@ -135,8 +136,22 @@ struct Content {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
struct Part {
|
||||
text: String,
|
||||
#[serde(untagged)]
|
||||
enum Part {
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
InlineData {
|
||||
#[serde(rename = "inlineData")]
|
||||
inline_data: InlineDataPart,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
struct InlineDataPart {
|
||||
#[serde(rename = "mimeType")]
|
||||
mime_type: String,
|
||||
data: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
@ -930,6 +945,57 @@ impl GeminiProvider {
|
||||
|| status.is_server_error()
|
||||
|| error_text.contains("RESOURCE_EXHAUSTED")
|
||||
}
|
||||
|
||||
fn parse_inline_image_marker(image_ref: &str) -> Option<InlineDataPart> {
|
||||
let rest = image_ref.strip_prefix("data:")?;
|
||||
let semi_index = rest.find(';')?;
|
||||
let mime_type = rest[..semi_index].trim();
|
||||
if mime_type.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let payload = rest[semi_index + 1..].strip_prefix("base64,")?.trim();
|
||||
if payload.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(InlineDataPart {
|
||||
mime_type: mime_type.to_string(),
|
||||
data: payload.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_user_parts(content: &str) -> Vec<Part> {
|
||||
let (cleaned_text, image_refs) = multimodal::parse_image_markers(content);
|
||||
if image_refs.is_empty() {
|
||||
return vec![Part::Text {
|
||||
text: content.to_string(),
|
||||
}];
|
||||
}
|
||||
|
||||
let mut parts: Vec<Part> = Vec::with_capacity(image_refs.len() + 1);
|
||||
if !cleaned_text.is_empty() {
|
||||
parts.push(Part::Text { text: cleaned_text });
|
||||
}
|
||||
|
||||
for image_ref in image_refs {
|
||||
if let Some(inline_data) = Self::parse_inline_image_marker(&image_ref) {
|
||||
parts.push(Part::InlineData { inline_data });
|
||||
} else {
|
||||
parts.push(Part::Text {
|
||||
text: format!("[IMAGE:{image_ref}]"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
vec![Part::Text {
|
||||
text: String::new(),
|
||||
}]
|
||||
} else {
|
||||
parts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GeminiProvider {
|
||||
@ -1154,16 +1220,14 @@ impl Provider for GeminiProvider {
|
||||
) -> anyhow::Result<String> {
|
||||
let system_instruction = system_prompt.map(|sys| Content {
|
||||
role: None,
|
||||
parts: vec![Part {
|
||||
parts: vec![Part::Text {
|
||||
text: sys.to_string(),
|
||||
}],
|
||||
});
|
||||
|
||||
let contents = vec![Content {
|
||||
role: Some("user".to_string()),
|
||||
parts: vec![Part {
|
||||
text: message.to_string(),
|
||||
}],
|
||||
parts: Self::build_user_parts(message),
|
||||
}];
|
||||
|
||||
let (text, _usage) = self
|
||||
@ -1189,16 +1253,14 @@ impl Provider for GeminiProvider {
|
||||
"user" => {
|
||||
contents.push(Content {
|
||||
role: Some("user".to_string()),
|
||||
parts: vec![Part {
|
||||
text: msg.content.clone(),
|
||||
}],
|
||||
parts: Self::build_user_parts(&msg.content),
|
||||
});
|
||||
}
|
||||
"assistant" => {
|
||||
// Gemini API uses "model" role instead of "assistant"
|
||||
contents.push(Content {
|
||||
role: Some("model".to_string()),
|
||||
parts: vec![Part {
|
||||
parts: vec![Part::Text {
|
||||
text: msg.content.clone(),
|
||||
}],
|
||||
});
|
||||
@ -1212,7 +1274,7 @@ impl Provider for GeminiProvider {
|
||||
} else {
|
||||
Some(Content {
|
||||
role: None,
|
||||
parts: vec![Part {
|
||||
parts: vec![Part::Text {
|
||||
text: system_parts.join("\n\n"),
|
||||
}],
|
||||
})
|
||||
@ -1238,13 +1300,11 @@ impl Provider for GeminiProvider {
|
||||
"system" => system_parts.push(&msg.content),
|
||||
"user" => contents.push(Content {
|
||||
role: Some("user".to_string()),
|
||||
parts: vec![Part {
|
||||
text: msg.content.clone(),
|
||||
}],
|
||||
parts: Self::build_user_parts(&msg.content),
|
||||
}),
|
||||
"assistant" => contents.push(Content {
|
||||
role: Some("model".to_string()),
|
||||
parts: vec![Part {
|
||||
parts: vec![Part::Text {
|
||||
text: msg.content.clone(),
|
||||
}],
|
||||
}),
|
||||
@ -1257,7 +1317,7 @@ impl Provider for GeminiProvider {
|
||||
} else {
|
||||
Some(Content {
|
||||
role: None,
|
||||
parts: vec![Part {
|
||||
parts: vec![Part::Text {
|
||||
text: system_parts.join("\n\n"),
|
||||
}],
|
||||
})
|
||||
@ -1545,7 +1605,7 @@ mod tests {
|
||||
let body = GenerateContentRequest {
|
||||
contents: vec![Content {
|
||||
role: Some("user".into()),
|
||||
parts: vec![Part {
|
||||
parts: vec![Part::Text {
|
||||
text: "hello".into(),
|
||||
}],
|
||||
}],
|
||||
@ -1586,7 +1646,7 @@ mod tests {
|
||||
let body = GenerateContentRequest {
|
||||
contents: vec![Content {
|
||||
role: Some("user".into()),
|
||||
parts: vec![Part {
|
||||
parts: vec![Part::Text {
|
||||
text: "hello".into(),
|
||||
}],
|
||||
}],
|
||||
@ -1630,7 +1690,7 @@ mod tests {
|
||||
let body = GenerateContentRequest {
|
||||
contents: vec![Content {
|
||||
role: Some("user".into()),
|
||||
parts: vec![Part {
|
||||
parts: vec![Part::Text {
|
||||
text: "hello".into(),
|
||||
}],
|
||||
}],
|
||||
@ -1662,13 +1722,13 @@ mod tests {
|
||||
let request = GenerateContentRequest {
|
||||
contents: vec![Content {
|
||||
role: Some("user".to_string()),
|
||||
parts: vec![Part {
|
||||
parts: vec![Part::Text {
|
||||
text: "Hello".to_string(),
|
||||
}],
|
||||
}],
|
||||
system_instruction: Some(Content {
|
||||
role: None,
|
||||
parts: vec![Part {
|
||||
parts: vec![Part::Text {
|
||||
text: "You are helpful".to_string(),
|
||||
}],
|
||||
}),
|
||||
@ -1687,6 +1747,74 @@ mod tests {
|
||||
assert!(json.contains("\"maxOutputTokens\":8192"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_user_parts_text_only_is_backward_compatible() {
|
||||
let content = "Plain text message without image markers.";
|
||||
let parts = GeminiProvider::build_user_parts(content);
|
||||
assert_eq!(parts.len(), 1);
|
||||
match &parts[0] {
|
||||
Part::Text { text } => assert_eq!(text, content),
|
||||
Part::InlineData { .. } => panic!("text-only message must stay text-only"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_user_parts_single_image() {
|
||||
let parts = GeminiProvider::build_user_parts(
|
||||
"Describe this image [IMAGE:data:image/png;base64,aGVsbG8=]",
|
||||
);
|
||||
assert_eq!(parts.len(), 2);
|
||||
match &parts[0] {
|
||||
Part::Text { text } => assert_eq!(text, "Describe this image"),
|
||||
Part::InlineData { .. } => panic!("first part should be text"),
|
||||
}
|
||||
match &parts[1] {
|
||||
Part::InlineData { inline_data } => {
|
||||
assert_eq!(inline_data.mime_type, "image/png");
|
||||
assert_eq!(inline_data.data, "aGVsbG8=");
|
||||
}
|
||||
Part::Text { .. } => panic!("second part should be inline image data"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_user_parts_multiple_images() {
|
||||
let parts = GeminiProvider::build_user_parts(
|
||||
"Compare [IMAGE:data:image/png;base64,aQ==] and [IMAGE:data:image/jpeg;base64,ag==]",
|
||||
);
|
||||
assert_eq!(parts.len(), 3);
|
||||
assert!(matches!(parts[0], Part::Text { .. }));
|
||||
assert!(matches!(parts[1], Part::InlineData { .. }));
|
||||
assert!(matches!(parts[2], Part::InlineData { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_user_parts_image_only() {
|
||||
let parts = GeminiProvider::build_user_parts("[IMAGE:data:image/webp;base64,YWJjZA==]");
|
||||
assert_eq!(parts.len(), 1);
|
||||
match &parts[0] {
|
||||
Part::InlineData { inline_data } => {
|
||||
assert_eq!(inline_data.mime_type, "image/webp");
|
||||
assert_eq!(inline_data.data, "YWJjZA==");
|
||||
}
|
||||
Part::Text { .. } => panic!("image-only message should create inline image part"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_user_parts_fallback_for_non_data_uri_markers() {
|
||||
let parts = GeminiProvider::build_user_parts("Inspect [IMAGE:https://example.com/img.png]");
|
||||
assert_eq!(parts.len(), 2);
|
||||
match &parts[0] {
|
||||
Part::Text { text } => assert_eq!(text, "Inspect"),
|
||||
Part::InlineData { .. } => panic!("first part should be text"),
|
||||
}
|
||||
match &parts[1] {
|
||||
Part::Text { text } => assert_eq!(text, "[IMAGE:https://example.com/img.png]"),
|
||||
Part::InlineData { .. } => panic!("invalid markers should fall back to text"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_request_includes_model() {
|
||||
let request = InternalGenerateContentEnvelope {
|
||||
@ -1696,7 +1824,7 @@ mod tests {
|
||||
request: InternalGenerateContentRequest {
|
||||
contents: vec![Content {
|
||||
role: Some("user".to_string()),
|
||||
parts: vec![Part {
|
||||
parts: vec![Part::Text {
|
||||
text: "Hello".to_string(),
|
||||
}],
|
||||
}],
|
||||
@ -1728,7 +1856,7 @@ mod tests {
|
||||
request: InternalGenerateContentRequest {
|
||||
contents: vec![Content {
|
||||
role: Some("user".to_string()),
|
||||
parts: vec![Part {
|
||||
parts: vec![Part::Text {
|
||||
text: "Hello".to_string(),
|
||||
}],
|
||||
}],
|
||||
@ -1751,7 +1879,7 @@ mod tests {
|
||||
request: InternalGenerateContentRequest {
|
||||
contents: vec![Content {
|
||||
role: Some("user".to_string()),
|
||||
parts: vec![Part {
|
||||
parts: vec![Part::Text {
|
||||
text: "Hello".to_string(),
|
||||
}],
|
||||
}],
|
||||
|
||||
1
web/dist/assets/index-BCWngppm.css
vendored
Normal file
1
web/dist/assets/index-BCWngppm.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/index-BW-KH83H.css
vendored
1
web/dist/assets/index-BW-KH83H.css
vendored
File diff suppressed because one or more lines are too long
1
web/dist/assets/index-BarGrDiR.css
vendored
1
web/dist/assets/index-BarGrDiR.css
vendored
File diff suppressed because one or more lines are too long
305
web/dist/assets/index-Bv7iLnHl.js
vendored
305
web/dist/assets/index-Bv7iLnHl.js
vendored
File diff suppressed because one or more lines are too long
693
web/dist/assets/index-D0O_BdVX.js
vendored
693
web/dist/assets/index-D0O_BdVX.js
vendored
File diff suppressed because one or more lines are too long
706
web/dist/assets/index-DOPK6_Za.js
vendored
Normal file
706
web/dist/assets/index-DOPK6_Za.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
web/dist/index.html
vendored
4
web/dist/index.html
vendored
@ -9,8 +9,8 @@
|
||||
/>
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>ZeroClaw</title>
|
||||
<script type="module" crossorigin src="/_app/assets/index-D0O_BdVX.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/_app/assets/index-BarGrDiR.css">
|
||||
<script type="module" crossorigin src="/_app/assets/index-DOPK6_Za.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/_app/assets/index-BCWngppm.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
232
web/package-lock.json
generated
232
web/package-lock.json
generated
@ -9,6 +9,11 @@
|
||||
"version": "0.1.0",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.12.2",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.39.15",
|
||||
"@uiw/react-codemirror": "^4.25.5",
|
||||
"lucide-react": "^0.468.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@ -260,6 +265,15 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||
@ -308,6 +322,108 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
|
||||
"integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz",
|
||||
"integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz",
|
||||
"integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/legacy-modes": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz",
|
||||
"integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.4.tgz",
|
||||
"integrity": "sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.35.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz",
|
||||
"integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.37.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
|
||||
"integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/theme-one-dark": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
|
||||
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.39.15",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.15.tgz",
|
||||
"integrity": "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
@ -800,6 +916,36 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz",
|
||||
"integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz",
|
||||
"integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.27",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||
@ -1511,6 +1657,59 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/codemirror-extensions-basic-setup": {
|
||||
"version": "4.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.5.tgz",
|
||||
"integrity": "sha512-2KWS4NqrS9SQzlPs/3sxFhuArvjB3JF6WpsrZqBtGHM5/smCNTULX3lUGeRH+f3mkfMt0k6DR+q0xCW9k+Up5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/autocomplete": ">=6.0.0",
|
||||
"@codemirror/commands": ">=6.0.0",
|
||||
"@codemirror/language": ">=6.0.0",
|
||||
"@codemirror/lint": ">=6.0.0",
|
||||
"@codemirror/search": ">=6.0.0",
|
||||
"@codemirror/state": ">=6.0.0",
|
||||
"@codemirror/view": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/react-codemirror": {
|
||||
"version": "4.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.5.tgz",
|
||||
"integrity": "sha512-WUMBGwfstufdbnaiMzQzmOf+6Mzf0IbiOoleexC9ItWcDTJybidLtEi20aP2N58Wn/AQxsd5Otebydaimh7Opw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.6",
|
||||
"@codemirror/commands": "^6.1.0",
|
||||
"@codemirror/state": "^6.1.1",
|
||||
"@codemirror/theme-one-dark": "^6.0.0",
|
||||
"@uiw/codemirror-extensions-basic-setup": "4.25.5",
|
||||
"codemirror": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/runtime": ">=7.11.0",
|
||||
"@codemirror/state": ">=6.0.0",
|
||||
"@codemirror/theme-one-dark": ">=6.0.0",
|
||||
"@codemirror/view": ">=6.0.0",
|
||||
"codemirror": ">=6.0.0",
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||
@ -1600,6 +1799,21 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@ -1620,6 +1834,12 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@ -2351,6 +2571,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz",
|
||||
@ -2516,6 +2742,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@ -10,6 +10,11 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.12.2",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.39.15",
|
||||
"@uiw/react-codemirror": "^4.25.5",
|
||||
"lucide-react": "^0.468.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
import { StreamLanguage } from '@codemirror/language';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
|
||||
interface Props {
|
||||
rawToml: string;
|
||||
onChange: (raw: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const tomlLanguage = StreamLanguage.define(toml);
|
||||
|
||||
export default function ConfigRawEditor({ rawToml, onChange, disabled }: Props) {
|
||||
return (
|
||||
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
|
||||
@ -15,14 +23,22 @@ export default function ConfigRawEditor({ rawToml, onChange, disabled }: Props)
|
||||
{rawToml.split('\n').length} lines
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
<CodeMirror
|
||||
value={rawToml}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
spellCheck={false}
|
||||
aria-label="Raw TOML configuration editor"
|
||||
className="w-full min-h-[500px] bg-gray-950 text-gray-200 font-mono text-sm p-4 resize-y focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset disabled:opacity-50"
|
||||
style={{ tabSize: 4 }}
|
||||
onChange={onChange}
|
||||
theme={oneDark}
|
||||
readOnly={Boolean(disabled)}
|
||||
editable={!disabled}
|
||||
height="500px"
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
foldGutter: false,
|
||||
highlightActiveLineGutter: false,
|
||||
highlightActiveLine: false,
|
||||
}}
|
||||
extensions={[tomlLanguage, EditorView.lineWrapping]}
|
||||
className="text-sm [&_.cm-scroller]:font-mono [&_.cm-scroller]:leading-6 [&_.cm-content]:py-4 [&_.cm-content]:px-0 [&_.cm-gutters]:border-r [&_.cm-gutters]:border-gray-800 [&_.cm-gutters]:bg-gray-950 [&_.cm-editor]:bg-gray-950 [&_.cm-editor]:focus:outline-none [&_.cm-focused]:ring-2 [&_.cm-focused]:ring-blue-500/70 [&_.cm-focused]:ring-inset"
|
||||
aria-label="Raw TOML configuration editor with syntax highlighting"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user