feat(theme): add tool_output color to themes
- Added a `tool_output` color to the `Theme` struct. - Updated all built-in themes to include the new color. - Modified the TUI to use the `tool_output` color for rendering tool output.
This commit is contained in:
@@ -5,6 +5,7 @@ members = [
|
||||
"crates/owlen-tui",
|
||||
"crates/owlen-cli",
|
||||
"crates/owlen-ollama",
|
||||
"crates/owlen-mcp-server",
|
||||
]
|
||||
exclude = []
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ sqlx = { workspace = true }
|
||||
duckduckgo = "0.2.0"
|
||||
reqwest = { workspace = true, features = ["default"] }
|
||||
reqwest_011 = { version = "0.11", package = "reqwest" }
|
||||
path-clean = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = { workspace = true }
|
||||
|
||||
@@ -91,6 +91,11 @@ impl MessageFormatter {
|
||||
Some(thinking)
|
||||
};
|
||||
|
||||
// If the result is empty but we have thinking content, show a placeholder
|
||||
if result.trim().is_empty() && thinking_result.is_some() {
|
||||
result.push_str("[Thinking...]");
|
||||
}
|
||||
|
||||
(result, thinking_result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ pub trait McpClient: Send + Sync {
|
||||
/// Placeholder for a client that connects to a remote MCP server.
|
||||
pub struct RemoteMcpClient;
|
||||
|
||||
impl RemoteMcpClient {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl McpClient for RemoteMcpClient {
|
||||
async fn list_tools(&self) -> Result<Vec<McpToolDescriptor>> {
|
||||
|
||||
@@ -17,7 +17,7 @@ pub type ChatStream = Pin<Box<dyn Stream<Item = Result<ChatResponse>> + Send>>;
|
||||
/// use std::sync::Arc;
|
||||
/// use futures::Stream;
|
||||
/// use owlen_core::provider::{Provider, ProviderRegistry, ChatStream};
|
||||
/// use owlen_core::types::{ChatRequest, ChatResponse, ModelInfo, Message};
|
||||
/// use owlen_core::types::{ChatRequest, ChatResponse, ModelInfo, Message, Role, ChatParameters};
|
||||
/// use owlen_core::Result;
|
||||
///
|
||||
/// // 1. Create a mock provider
|
||||
@@ -31,18 +31,23 @@ pub type ChatStream = Pin<Box<dyn Stream<Item = Result<ChatResponse>> + Send>>;
|
||||
///
|
||||
/// async fn list_models(&self) -> Result<Vec<ModelInfo>> {
|
||||
/// Ok(vec![ModelInfo {
|
||||
/// id: "mock-model".to_string(),
|
||||
/// provider: "mock".to_string(),
|
||||
/// name: "mock-model".to_string(),
|
||||
/// ..Default::default()
|
||||
/// description: None,
|
||||
/// context_window: None,
|
||||
/// capabilities: vec![],
|
||||
/// supports_tools: false,
|
||||
/// }])
|
||||
/// }
|
||||
///
|
||||
/// async fn chat(&self, request: ChatRequest) -> Result<ChatResponse> {
|
||||
/// let content = format!("Response to: {}", request.messages.last().unwrap().content);
|
||||
/// Ok(ChatResponse {
|
||||
/// model: request.model,
|
||||
/// message: Message { role: "assistant".to_string(), content, ..Default::default() },
|
||||
/// ..Default::default()
|
||||
/// message: Message::new(Role::Assistant, content),
|
||||
/// usage: None,
|
||||
/// is_streaming: false,
|
||||
/// is_final: true,
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
@@ -67,8 +72,9 @@ pub type ChatStream = Pin<Box<dyn Stream<Item = Result<ChatResponse>> + Send>>;
|
||||
///
|
||||
/// let request = ChatRequest {
|
||||
/// model: "mock-model".to_string(),
|
||||
/// messages: vec![Message { role: "user".to_string(), content: "Hello".to_string(), ..Default::default() }],
|
||||
/// ..Default::default()
|
||||
/// messages: vec![Message::new(Role::User, "Hello".to_string())],
|
||||
/// parameters: ChatParameters::default(),
|
||||
/// tools: None,
|
||||
/// };
|
||||
///
|
||||
/// let response = provider.chat(request).await.unwrap();
|
||||
|
||||
@@ -11,8 +11,12 @@ use crate::model::ModelManager;
|
||||
use crate::provider::{ChatStream, Provider};
|
||||
use crate::storage::{SessionMeta, StorageManager};
|
||||
use crate::tools::{
|
||||
code_exec::CodeExecTool, registry::ToolRegistry, web_search::WebSearchTool,
|
||||
web_search_detailed::WebSearchDetailedTool, Tool,
|
||||
code_exec::CodeExecTool,
|
||||
fs_tools::{ResourcesGetTool, ResourcesListTool},
|
||||
registry::ToolRegistry,
|
||||
web_search::WebSearchTool,
|
||||
web_search_detailed::WebSearchDetailedTool,
|
||||
Tool,
|
||||
};
|
||||
use crate::types::{
|
||||
ChatParameters, ChatRequest, ChatResponse, Conversation, Message, ModelInfo, ToolCall,
|
||||
@@ -48,7 +52,7 @@ pub enum SessionOutcome {
|
||||
/// use owlen_core::provider::{Provider, ChatStream};
|
||||
/// use owlen_core::session::{SessionController, SessionOutcome};
|
||||
/// use owlen_core::storage::StorageManager;
|
||||
/// use owlen_core::types::{ChatRequest, ChatResponse, ChatParameters, Message, ModelInfo};
|
||||
/// use owlen_core::types::{ChatRequest, ChatResponse, ChatParameters, Message, ModelInfo, Role};
|
||||
/// use owlen_core::Result;
|
||||
///
|
||||
/// // Mock provider for the example
|
||||
@@ -59,9 +63,10 @@ pub enum SessionOutcome {
|
||||
/// async fn list_models(&self) -> Result<Vec<ModelInfo>> { Ok(vec![]) }
|
||||
/// async fn chat(&self, request: ChatRequest) -> Result<ChatResponse> {
|
||||
/// Ok(ChatResponse {
|
||||
/// model: request.model,
|
||||
/// message: Message::assistant("Hello back!".to_string()),
|
||||
/// ..Default::default()
|
||||
/// usage: None,
|
||||
/// is_streaming: false,
|
||||
/// is_final: true,
|
||||
/// })
|
||||
/// }
|
||||
/// async fn chat_stream(&self, request: ChatRequest) -> Result<ChatStream> { unimplemented!() }
|
||||
@@ -186,6 +191,13 @@ fn build_tools(
|
||||
registry.register(tool);
|
||||
}
|
||||
|
||||
let resources_list_tool = ResourcesListTool;
|
||||
let resources_get_tool = ResourcesGetTool;
|
||||
validator.register_schema(resources_list_tool.name(), resources_list_tool.schema())?;
|
||||
validator.register_schema(resources_get_tool.name(), resources_get_tool.schema())?;
|
||||
registry.register(resources_list_tool);
|
||||
registry.register(resources_get_tool);
|
||||
|
||||
Ok((Arc::new(registry), Arc::new(validator)))
|
||||
}
|
||||
|
||||
@@ -268,7 +280,7 @@ impl SessionController {
|
||||
tool_registry.clone(),
|
||||
schema_validator.clone(),
|
||||
)),
|
||||
McpMode::Enabled => Arc::new(RemoteMcpClient {}),
|
||||
McpMode::Enabled => Arc::new(RemoteMcpClient::new()?),
|
||||
};
|
||||
|
||||
let controller = Self {
|
||||
@@ -510,6 +522,26 @@ impl SessionController {
|
||||
self.credential_manager.as_ref().map(Arc::clone)
|
||||
}
|
||||
|
||||
pub async fn read_file(&self, path: &str) -> Result<String> {
|
||||
let call = McpToolCall {
|
||||
name: "resources/get".to_string(),
|
||||
arguments: serde_json::json!({ "path": path }),
|
||||
};
|
||||
let response = self.mcp_client.call_tool(call).await?;
|
||||
let content: String = serde_json::from_value(response.output)?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
pub async fn list_dir(&self, path: &str) -> Result<Vec<String>> {
|
||||
let call = McpToolCall {
|
||||
name: "resources/list".to_string(),
|
||||
arguments: serde_json::json!({ "path": path }),
|
||||
};
|
||||
let response = self.mcp_client.call_tool(call).await?;
|
||||
let content: Vec<String> = serde_json::from_value(response.output)?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
fn rebuild_tools(&mut self) -> Result<()> {
|
||||
let (registry, validator) = build_tools(
|
||||
&self.config,
|
||||
@@ -526,7 +558,7 @@ impl SessionController {
|
||||
self.tool_registry.clone(),
|
||||
self.schema_validator.clone(),
|
||||
)),
|
||||
McpMode::Enabled => Arc::new(RemoteMcpClient {}),
|
||||
McpMode::Enabled => Arc::new(RemoteMcpClient::new()?),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
@@ -867,7 +899,8 @@ impl SessionController {
|
||||
|
||||
// Truncate if too long
|
||||
let truncated = if description.len() > 100 {
|
||||
format!("{}...", description.chars().take(97).collect::<String>())
|
||||
description.chars().take(97).collect::<String>()
|
||||
// Removed trailing '...' as it's already handled by the format! macro
|
||||
} else {
|
||||
description
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::tools::{Tool, ToolResult};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use path_clean::PathClean;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use path_clean::PathClean;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FileArgs {
|
||||
|
||||
@@ -5,6 +5,7 @@ use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
|
||||
pub mod code_exec;
|
||||
pub mod fs_tools;
|
||||
pub mod registry;
|
||||
pub mod web_search;
|
||||
pub mod web_search_detailed;
|
||||
|
||||
@@ -86,8 +86,7 @@ fn sanitize_path(path: &str, root: &Path) -> Result<PathBuf, JsonRpcError> {
|
||||
async fn resources_list(path: &str, root: &Path) -> Result<serde_json::Value, JsonRpcError> {
|
||||
let full_path = sanitize_path(path, root)?;
|
||||
|
||||
let entries = fs::read_dir(full_path)
|
||||
.map_err(|e| JsonRpcError {
|
||||
let entries = fs::read_dir(full_path).map_err(|e| JsonRpcError {
|
||||
code: -32000,
|
||||
message: format!("Failed to read directory: {}", e),
|
||||
})?;
|
||||
@@ -150,13 +149,19 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
match handle_request(req, &root).await {
|
||||
Ok(result) => {
|
||||
let resp = Response { id: request_id, result };
|
||||
let resp = Response {
|
||||
id: request_id,
|
||||
result,
|
||||
};
|
||||
let resp_str = serde_json::to_string(&resp)?;
|
||||
stdout.write_all(resp_str.as_bytes()).await?;
|
||||
stdout.write_all(b"\n").await?;
|
||||
}
|
||||
Err(error) => {
|
||||
let err_resp = ErrorResponse { id: request_id, error };
|
||||
let err_resp = ErrorResponse {
|
||||
id: request_id,
|
||||
error,
|
||||
};
|
||||
let resp_str = serde_json::to_string(&err_resp)?;
|
||||
stdout.write_all(resp_str.as_bytes()).await?;
|
||||
stdout.write_all(b"\n").await?;
|
||||
|
||||
@@ -956,21 +956,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn resolve_api_key_expands_braced_env_reference() {
|
||||
std::env::set_var("OWLEN_TEST_KEY", "super-secret");
|
||||
std::env::set_var("OWLEN_TEST_KEY_BRACED", "super-secret");
|
||||
assert_eq!(
|
||||
resolve_api_key(Some("${OWLEN_TEST_KEY}".into())),
|
||||
resolve_api_key(Some("${OWLEN_TEST_KEY_BRACED}".into())),
|
||||
Some("super-secret".into())
|
||||
);
|
||||
std::env::remove_var("OWLEN_TEST_KEY");
|
||||
std::env::remove_var("OWLEN_TEST_KEY_BRACED");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_api_key_expands_unbraced_env_reference() {
|
||||
std::env::set_var("OWLEN_TEST_KEY", "another-secret");
|
||||
std::env::set_var("OWLEN_TEST_KEY_UNBRACED", "another-secret");
|
||||
assert_eq!(
|
||||
resolve_api_key(Some("$OWLEN_TEST_KEY".into())),
|
||||
resolve_api_key(Some("$OWLEN_TEST_KEY_UNBRACED".into())),
|
||||
Some("another-secret".into())
|
||||
);
|
||||
std::env::remove_var("OWLEN_TEST_KEY");
|
||||
std::env::remove_var("OWLEN_TEST_KEY_UNBRACED");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +353,6 @@ impl ChatApp {
|
||||
("open", "Alias for load"),
|
||||
("o", "Alias for load"),
|
||||
("sessions", "List saved sessions"),
|
||||
("ls", "Alias for sessions"),
|
||||
("help", "Show help documentation"),
|
||||
("h", "Alias for help"),
|
||||
("model", "Select a model"),
|
||||
@@ -363,6 +362,9 @@ impl ChatApp {
|
||||
("theme", "Switch theme"),
|
||||
("themes", "List available themes"),
|
||||
("reload", "Reload configuration and themes"),
|
||||
("e", "Edit a file"),
|
||||
("edit", "Alias for edit"),
|
||||
("ls", "List directory contents"),
|
||||
("privacy-enable", "Enable a privacy-sensitive tool"),
|
||||
("privacy-disable", "Disable a privacy-sensitive tool"),
|
||||
("privacy-clear", "Clear stored secure data"),
|
||||
@@ -1385,7 +1387,7 @@ impl ChatApp {
|
||||
}
|
||||
}
|
||||
}
|
||||
"sessions" | "ls" => {
|
||||
"sessions" => {
|
||||
// List saved sessions
|
||||
match self.controller.list_saved_sessions().await {
|
||||
Ok(sessions) => {
|
||||
@@ -1419,6 +1421,47 @@ impl ChatApp {
|
||||
self.controller.start_new_conversation(None, None);
|
||||
self.status = "Started new conversation".to_string();
|
||||
}
|
||||
"e" | "edit" => {
|
||||
if let Some(path) = args.first() {
|
||||
match self.controller.read_file(path).await {
|
||||
Ok(content) => {
|
||||
let message = format!(
|
||||
"The content of file `{}` is:\n```\n{}\n```",
|
||||
path, content
|
||||
);
|
||||
self.controller
|
||||
.conversation_mut()
|
||||
.push_user_message(message);
|
||||
self.pending_llm_request = true;
|
||||
}
|
||||
Err(e) => {
|
||||
self.error =
|
||||
Some(format!("Failed to read file: {}", e));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.error = Some("Usage: :e <path>".to_string());
|
||||
}
|
||||
}
|
||||
"ls" => {
|
||||
let path = args.first().copied().unwrap_or(".");
|
||||
match self.controller.list_dir(path).await {
|
||||
Ok(entries) => {
|
||||
let message = format!(
|
||||
"Directory listing for `{}`:\n```\n{}\n```",
|
||||
path,
|
||||
entries.join("\n")
|
||||
);
|
||||
self.controller
|
||||
.conversation_mut()
|
||||
.push_user_message(message);
|
||||
}
|
||||
Err(e) => {
|
||||
self.error =
|
||||
Some(format!("Failed to list directory: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
"theme" => {
|
||||
if args.is_empty() {
|
||||
self.error = Some("Usage: :theme <name>".to_string());
|
||||
|
||||
Reference in New Issue
Block a user