mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
run tool manually via goose-server, automatic cwd analysis injected into tui context
This commit is contained in:
@@ -14,11 +14,11 @@ use serde::Serialize;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use types::{
|
||||
AddExtensionRequest, ChatRequest, ConfigResponse, ExtensionQuery, ExtensionResponse,
|
||||
RemoveExtensionRequest, ResumeAgentRequest, SessionListResponse, StartAgentRequest,
|
||||
UpdateProviderRequest, UpsertConfigQuery,
|
||||
AddExtensionRequest, CallToolRequest, ChatRequest, ConfigResponse, ExtensionQuery,
|
||||
ExtensionResponse, RemoveExtensionRequest, ResumeAgentRequest, SessionListResponse,
|
||||
StartAgentRequest, UpdateProviderRequest, UpsertConfigQuery,
|
||||
};
|
||||
pub use types::{ProviderDetails, ToolInfo};
|
||||
pub use types::{CallToolResponse, ProviderDetails, ToolInfo};
|
||||
|
||||
pub struct ClientBuilder {
|
||||
host: String,
|
||||
@@ -391,4 +391,21 @@ impl Client {
|
||||
Err(e) => Err(anyhow::anyhow!("SSE stream error: {}", e)),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn call_tool(
|
||||
&self,
|
||||
session_id: &str,
|
||||
tool_name: &str,
|
||||
arguments: Option<serde_json::Value>,
|
||||
) -> Result<CallToolResponse> {
|
||||
self.post("/agent/call_tool")
|
||||
.json(&CallToolRequest {
|
||||
session_id: session_id.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
arguments,
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to call tool")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub use goose::agents::extension::ToolInfo;
|
||||
pub use goose_server::routes::agent::{
|
||||
AddExtensionRequest, RemoveExtensionRequest, ResumeAgentRequest, StartAgentRequest,
|
||||
UpdateProviderRequest,
|
||||
AddExtensionRequest, CallToolRequest, CallToolResponse, RemoveExtensionRequest,
|
||||
ResumeAgentRequest, StartAgentRequest, UpdateProviderRequest,
|
||||
};
|
||||
pub use goose_server::routes::config_management::{
|
||||
ConfigResponse, ExtensionQuery, ExtensionResponse, ProviderDetails, UpsertConfigQuery,
|
||||
|
||||
@@ -11,6 +11,7 @@ use axum::{
|
||||
Json, Router,
|
||||
};
|
||||
use goose::config::PermissionManager;
|
||||
use uuid::Uuid;
|
||||
|
||||
use goose::agents::ExtensionConfig;
|
||||
use goose::config::{Config, GooseMode};
|
||||
@@ -632,6 +633,89 @@ async fn stop_agent(
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct CallToolRequest {
|
||||
pub session_id: String,
|
||||
pub tool_name: String,
|
||||
#[schema(value_type = Option<Object>)]
|
||||
pub arguments: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct CallToolResponse {
|
||||
pub output: String,
|
||||
pub is_error: bool,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/call_tool",
|
||||
request_body = CallToolRequest,
|
||||
responses(
|
||||
(status = 200, description = "Tool executed successfully", body = CallToolResponse),
|
||||
(status = 404, description = "Session not found"),
|
||||
(status = 424, description = "Agent not initialized for session"),
|
||||
(status = 500, description = "Tool execution failed")
|
||||
)
|
||||
)]
|
||||
async fn call_tool(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<CallToolRequest>,
|
||||
) -> Result<Json<CallToolResponse>, ErrorResponse> {
|
||||
let agent = state
|
||||
.get_agent_for_route(request.session_id.clone())
|
||||
.await
|
||||
.map_err(|status| ErrorResponse {
|
||||
message: "Agent not initialized for this session".into(),
|
||||
status,
|
||||
})?;
|
||||
|
||||
let session = SessionManager::get_session(&request.session_id, false)
|
||||
.await
|
||||
.map_err(|e| ErrorResponse {
|
||||
message: format!("Session not found: {}", e),
|
||||
status: StatusCode::NOT_FOUND,
|
||||
})?;
|
||||
|
||||
let tool_call = rmcp::model::CallToolRequestParam {
|
||||
name: request.tool_name.clone().into(),
|
||||
arguments: request.arguments.and_then(|v| v.as_object().cloned()),
|
||||
};
|
||||
|
||||
let request_id = Uuid::new_v4().to_string();
|
||||
|
||||
let (_req_id, result) = agent
|
||||
.dispatch_tool_call(tool_call, request_id, None, &session)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(tool_result) => {
|
||||
let content_result = tool_result.result.await;
|
||||
match content_result {
|
||||
Ok(content) => {
|
||||
let output = content
|
||||
.iter()
|
||||
.filter_map(|c| c.as_text().map(|t| t.text.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
Ok(Json(CallToolResponse {
|
||||
output,
|
||||
is_error: false,
|
||||
}))
|
||||
}
|
||||
Err(error_data) => Ok(Json(CallToolResponse {
|
||||
output: error_data.message.to_string(),
|
||||
is_error: true,
|
||||
})),
|
||||
}
|
||||
}
|
||||
Err(error_data) => Ok(Json(CallToolResponse {
|
||||
output: error_data.message.to_string(),
|
||||
is_error: true,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/agent/start", post(start_agent))
|
||||
@@ -646,5 +730,6 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
.route("/agent/add_extension", post(agent_add_extension))
|
||||
.route("/agent/remove_extension", post(agent_remove_extension))
|
||||
.route("/agent/stop", post(stop_agent))
|
||||
.route("/agent/call_tool", post(call_tool))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,58 @@
|
||||
use crate::services::events::Event;
|
||||
use crate::state::action::Action;
|
||||
use crate::state::AppState;
|
||||
use crate::state::{AppState, CwdAnalysisState};
|
||||
use goose::conversation::message::Message;
|
||||
use goose_client::Client;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
const CWD_ANALYSIS_TAG: &str = "cwd_analysis";
|
||||
const CWD_ANALYSIS_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const CWD_ANALYSIS_GRACE_PERIOD: Duration = Duration::from_secs(2);
|
||||
const CWD_ANALYSIS_DEPTH: u32 = 3;
|
||||
|
||||
fn detect_analysis_target(cwd: &std::path::Path) -> (std::path::PathBuf, u32) {
|
||||
let monorepo_markers = [
|
||||
"pnpm-workspace.yaml",
|
||||
"lerna.json",
|
||||
"nx.json",
|
||||
"turbo.json",
|
||||
"rush.json",
|
||||
"go.work",
|
||||
];
|
||||
|
||||
let is_rust_workspace = std::fs::read_to_string(cwd.join("Cargo.toml"))
|
||||
.map(|content| content.contains("[workspace]"))
|
||||
.unwrap_or(false);
|
||||
|
||||
let is_monorepo = is_rust_workspace || monorepo_markers.iter().any(|m| cwd.join(m).exists());
|
||||
|
||||
if is_monorepo {
|
||||
let monorepo_dirs = ["crates", "packages", "apps", "libs", "cmd", "internal"];
|
||||
for dir in monorepo_dirs {
|
||||
let path = cwd.join(dir);
|
||||
if path.is_dir() {
|
||||
return (path, CWD_ANALYSIS_DEPTH);
|
||||
}
|
||||
}
|
||||
return (cwd.to_path_buf(), 1);
|
||||
}
|
||||
|
||||
let source_dirs = ["src", "lib", "app", "cmd", "Sources", "internal"];
|
||||
for dir in source_dirs {
|
||||
let path = cwd.join(dir);
|
||||
if path.is_dir() {
|
||||
return (path, CWD_ANALYSIS_DEPTH);
|
||||
}
|
||||
}
|
||||
|
||||
(cwd.to_path_buf(), 1)
|
||||
}
|
||||
|
||||
pub fn handle_action(
|
||||
action: &Action,
|
||||
state: &AppState,
|
||||
state: &mut AppState,
|
||||
client: &Client,
|
||||
tx: &mpsc::UnboundedSender<Event>,
|
||||
reply_task: &mut Option<tokio::task::JoinHandle<()>>,
|
||||
@@ -20,7 +65,7 @@ pub fn handle_action(
|
||||
handle_resume_session(id, client, tx);
|
||||
}
|
||||
Action::CreateNewSession => {
|
||||
handle_create_new_session(client, tx);
|
||||
handle_create_new_session(state, client, tx);
|
||||
}
|
||||
Action::OpenSessionPicker => {
|
||||
handle_open_session_picker(client, tx);
|
||||
@@ -59,9 +104,120 @@ pub fn handle_action(
|
||||
false
|
||||
}
|
||||
|
||||
pub fn spawn_cwd_analysis(session_id: &str, client: &Client, tx: &mpsc::UnboundedSender<Event>) {
|
||||
let client = client.clone();
|
||||
let tx = tx.clone();
|
||||
let session_id = session_id.to_string();
|
||||
tokio::spawn(async move {
|
||||
let result = fetch_cwd_analysis(&client, &session_id).await;
|
||||
let _ = tx.send(Event::CwdAnalysisComplete(result));
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn fetch_cwd_analysis_sync(client: &Client, session_id: &str) -> Option<String> {
|
||||
fetch_cwd_analysis(client, session_id).await
|
||||
}
|
||||
|
||||
async fn fetch_cwd_analysis(client: &Client, session_id: &str) -> Option<String> {
|
||||
let cwd = std::env::current_dir().ok()?;
|
||||
let (target_path, depth) = detect_analysis_target(&cwd);
|
||||
let start = std::time::Instant::now();
|
||||
tracing::info!(
|
||||
"Smart context: analyzing {} (depth={})",
|
||||
target_path.display(),
|
||||
depth
|
||||
);
|
||||
|
||||
let analyze_future = client.call_tool(
|
||||
session_id,
|
||||
"developer__analyze",
|
||||
Some(serde_json::json!({
|
||||
"path": target_path.to_string_lossy(),
|
||||
"max_depth": depth
|
||||
})),
|
||||
);
|
||||
|
||||
let result = tokio::time::timeout(CWD_ANALYSIS_TIMEOUT, analyze_future).await;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
match result {
|
||||
Ok(Ok(response)) if !response.is_error && !response.output.is_empty() => {
|
||||
tracing::info!(
|
||||
"Smart context: completed in {:.2}s ({} chars)",
|
||||
elapsed.as_secs_f64(),
|
||||
response.output.len()
|
||||
);
|
||||
Some(response.output)
|
||||
}
|
||||
Ok(Ok(response)) if response.is_error => {
|
||||
tracing::warn!(
|
||||
"Smart context: failed in {:.2}s: {}",
|
||||
elapsed.as_secs_f64(),
|
||||
response.output
|
||||
);
|
||||
None
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
tracing::warn!(
|
||||
"Smart context: error in {:.2}s: {}",
|
||||
elapsed.as_secs_f64(),
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!(
|
||||
"Smart context: timed out after {:.2}s",
|
||||
elapsed.as_secs_f64()
|
||||
);
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
tracing::debug!(
|
||||
"Smart context: empty result in {:.2}s",
|
||||
elapsed.as_secs_f64()
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prepend_cwd_analysis(message: &Message, analysis: &str) -> Message {
|
||||
let original_text = message.as_concat_text();
|
||||
let augmented =
|
||||
format!("<{CWD_ANALYSIS_TAG}>\n{analysis}\n</{CWD_ANALYSIS_TAG}>\n\n{original_text}");
|
||||
Message::user().with_text(augmented)
|
||||
}
|
||||
|
||||
async fn wait_for_pending_analysis(
|
||||
client: &Client,
|
||||
session_id: &str,
|
||||
tx: &mpsc::UnboundedSender<Event>,
|
||||
) -> Option<String> {
|
||||
let (result_tx, mut result_rx) = mpsc::unbounded_channel();
|
||||
let client = client.clone();
|
||||
let session_id = session_id.to_string();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let result = fetch_cwd_analysis(&client, &session_id).await;
|
||||
let _ = result_tx.send(result);
|
||||
});
|
||||
|
||||
match tokio::time::timeout(CWD_ANALYSIS_GRACE_PERIOD, result_rx.recv()).await {
|
||||
Ok(Some(result)) => {
|
||||
let _ = tx.send(Event::CwdAnalysisComplete(result.clone()));
|
||||
result
|
||||
}
|
||||
_ => {
|
||||
tracing::info!("Smart context: grace period expired, proceeding without analysis");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_send_message(
|
||||
message_to_send: &goose::conversation::message::Message,
|
||||
state: &AppState,
|
||||
message_to_send: &Message,
|
||||
state: &mut AppState,
|
||||
client: &Client,
|
||||
tx: &mpsc::UnboundedSender<Event>,
|
||||
reply_task: &mut Option<tokio::task::JoinHandle<()>>,
|
||||
@@ -69,10 +225,34 @@ fn handle_send_message(
|
||||
let client = client.clone();
|
||||
let tx = tx.clone();
|
||||
let mut messages_snapshot = state.messages.clone();
|
||||
messages_snapshot.push(message_to_send.clone());
|
||||
let session_id = state.session_id.clone();
|
||||
|
||||
let is_first_message = messages_snapshot.is_empty();
|
||||
let analysis_result = if is_first_message {
|
||||
state.cwd_analysis.take_result()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let analysis_pending = is_first_message && state.cwd_analysis.is_pending();
|
||||
|
||||
let user_message = message_to_send.clone();
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let analysis = if analysis_result.is_some() {
|
||||
analysis_result
|
||||
} else if analysis_pending {
|
||||
wait_for_pending_analysis(&client, &session_id, &tx).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let final_user_message = match analysis {
|
||||
Some(ref a) => prepend_cwd_analysis(&user_message, a),
|
||||
None => user_message,
|
||||
};
|
||||
|
||||
messages_snapshot.push(final_user_message);
|
||||
|
||||
match client.reply(messages_snapshot, session_id).await {
|
||||
Ok(mut stream) => {
|
||||
while let Some(result) = stream.next().await {
|
||||
@@ -110,7 +290,15 @@ fn handle_resume_session(id: &str, client: &Client, tx: &mpsc::UnboundedSender<E
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_create_new_session(client: &Client, tx: &mpsc::UnboundedSender<Event>) {
|
||||
fn handle_create_new_session(
|
||||
state: &mut AppState,
|
||||
client: &Client,
|
||||
tx: &mpsc::UnboundedSender<Event>,
|
||||
) {
|
||||
let smart_context = state.config.smart_context;
|
||||
if smart_context {
|
||||
state.cwd_analysis = CwdAnalysisState::Pending;
|
||||
}
|
||||
let client = client.clone();
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
@@ -118,6 +306,9 @@ fn handle_create_new_session(client: &Client, tx: &mpsc::UnboundedSender<Event>)
|
||||
match client.start_agent(cwd.to_string_lossy().to_string()).await {
|
||||
Ok(s) => {
|
||||
crate::configure_session_from_global(&client, &s.id).await;
|
||||
if smart_context {
|
||||
spawn_cwd_analysis(&s.id, &client, &tx);
|
||||
}
|
||||
let _ = tx.send(Event::SessionResumed(Box::new(s)));
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -146,6 +146,9 @@ impl<'a> Component for App<'a> {
|
||||
Event::Error(e) => return Ok(Some(Action::Error(e.clone()))),
|
||||
Event::Flash(msg) => return Ok(Some(Action::ShowFlash(msg.clone()))),
|
||||
Event::Resize => return Ok(Some(Action::Resize)),
|
||||
Event::CwdAnalysisComplete(result) => {
|
||||
return Ok(Some(Action::CwdAnalysisComplete(result.clone())))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -239,12 +239,24 @@ fn print_help() {
|
||||
println!();
|
||||
}
|
||||
|
||||
const CWD_ANALYSIS_TAG: &str = "cwd_analysis";
|
||||
|
||||
fn print_context_status(success: bool) {
|
||||
use colors::*;
|
||||
if success {
|
||||
println!("{GREEN}✓{RESET} {DIM}Project context loaded{RESET}");
|
||||
} else {
|
||||
println!("{YELLOW}⚠{RESET} {DIM}Project context unavailable{RESET}");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_cli(
|
||||
client: Client,
|
||||
session_id: String,
|
||||
provider: String,
|
||||
model: Option<String>,
|
||||
context_limit: usize,
|
||||
cwd_analysis: Option<String>,
|
||||
) -> Result<()> {
|
||||
use colors::*;
|
||||
|
||||
@@ -255,8 +267,11 @@ pub async fn run_cli(
|
||||
);
|
||||
let mut messages: Vec<Message> = Vec::new();
|
||||
let mut last_token_count: i32 = 0;
|
||||
let mut cwd_analysis_used = false;
|
||||
|
||||
print_session_header(&provider, model.as_deref(), &session_id);
|
||||
print_context_status(cwd_analysis.is_some());
|
||||
println!();
|
||||
|
||||
let mut pending_tools: std::collections::HashMap<String, String> =
|
||||
std::collections::HashMap::new();
|
||||
@@ -266,7 +281,6 @@ pub async fn run_cli(
|
||||
print_context_usage(last_token_count, context_limit);
|
||||
}
|
||||
|
||||
// Read input with readline support
|
||||
let prompt = format!("{CYAN}{BOLD}( O)>{RESET} ");
|
||||
let input = match rl.readline(&prompt) {
|
||||
Ok(line) => {
|
||||
@@ -288,7 +302,6 @@ pub async fn run_cli(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle commands
|
||||
match trimmed {
|
||||
"/exit" | "/quit" | "/q" => break,
|
||||
"/help" | "/?" => {
|
||||
@@ -309,8 +322,19 @@ pub async fn run_cli(
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Send message and stream response
|
||||
let user_message = Message::user().with_text(trimmed);
|
||||
let message_text = if !cwd_analysis_used {
|
||||
if let Some(ref analysis) = cwd_analysis {
|
||||
cwd_analysis_used = true;
|
||||
format!("<{CWD_ANALYSIS_TAG}>\n{analysis}\n</{CWD_ANALYSIS_TAG}>\n\n{trimmed}")
|
||||
} else {
|
||||
cwd_analysis_used = true;
|
||||
trimmed.to_string()
|
||||
}
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
};
|
||||
|
||||
let user_message = Message::user().with_text(&message_text);
|
||||
messages.push(user_message.clone());
|
||||
|
||||
// Start spinner while waiting for response
|
||||
|
||||
@@ -109,6 +109,20 @@ impl ChatComponent {
|
||||
(items, map)
|
||||
}
|
||||
|
||||
fn strip_cwd_analysis(text: &str) -> String {
|
||||
const START_TAG: &str = "<cwd_analysis>";
|
||||
const END_TAG: &str = "</cwd_analysis>";
|
||||
|
||||
if let Some(start) = text.find(START_TAG) {
|
||||
if let Some(end) = text[start..].find(END_TAG) {
|
||||
let before = &text[..start];
|
||||
let after = &text[start + end + END_TAG.len()..];
|
||||
return format!("{}{}", before, after.trim_start());
|
||||
}
|
||||
}
|
||||
text.to_string()
|
||||
}
|
||||
|
||||
fn render_user_text(
|
||||
t: &rmcp::model::TextContent,
|
||||
msg_idx: usize,
|
||||
@@ -117,12 +131,17 @@ impl ChatComponent {
|
||||
items: &mut Vec<ListItem<'static>>,
|
||||
map: &mut Vec<usize>,
|
||||
) {
|
||||
let display_text = if msg_idx == 0 {
|
||||
Self::strip_cwd_analysis(&t.text)
|
||||
} else {
|
||||
t.text.clone()
|
||||
};
|
||||
|
||||
let user_text_style = Style::default().fg(theme.base.user_message_foreground);
|
||||
let renderer = MarkdownRenderer::new(theme, Some(user_text_style));
|
||||
let mut rendered_lines = renderer.render_lines(&t.text, width.saturating_sub(4));
|
||||
let mut rendered_lines = renderer.render_lines(&display_text, width.saturating_sub(4));
|
||||
|
||||
// Remove trailing empty line if text doesn't end with double newline
|
||||
if !t.text.ends_with("\n\n")
|
||||
if !display_text.ends_with("\n\n")
|
||||
&& rendered_lines
|
||||
.last()
|
||||
.is_some_and(|line| line.spans.is_empty() || line.width() == 0)
|
||||
|
||||
@@ -6,10 +6,22 @@ use std::io::Write;
|
||||
use tokio_stream::StreamExt;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
const CWD_ANALYSIS_TAG: &str = "cwd_analysis";
|
||||
|
||||
fn prepend_context(prompt: &str, context: Option<&str>) -> String {
|
||||
match context {
|
||||
Some(analysis) => {
|
||||
format!("<{CWD_ANALYSIS_TAG}>\n{analysis}\n</{CWD_ANALYSIS_TAG}>\n\n{prompt}")
|
||||
}
|
||||
None => prompt.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_headless(
|
||||
client: Client,
|
||||
session_id: String,
|
||||
initial_prompt: String,
|
||||
cwd_analysis: Option<String>,
|
||||
) -> Result<()> {
|
||||
let cancel_token = CancellationToken::new();
|
||||
|
||||
@@ -20,7 +32,8 @@ pub async fn run_headless(
|
||||
cancel_clone.cancel();
|
||||
});
|
||||
|
||||
let user_message = Message::user().with_text(&initial_prompt);
|
||||
let full_prompt = prepend_context(&initial_prompt, cwd_analysis.as_deref());
|
||||
let user_message = Message::user().with_text(&full_prompt);
|
||||
println!("[user] {initial_prompt}");
|
||||
|
||||
let messages = vec![user_message];
|
||||
|
||||
@@ -288,6 +288,7 @@ async fn run_recipe_mode(
|
||||
recipe_path: PathBuf,
|
||||
headless: bool,
|
||||
) -> Result<()> {
|
||||
let config = TuiConfig::load()?;
|
||||
let recipe = load_recipe(&recipe_path)?;
|
||||
let prompt = recipe
|
||||
.prompt
|
||||
@@ -302,12 +303,18 @@ async fn run_recipe_mode(
|
||||
|
||||
configure_session_from_global(&client, &initial_session.id).await;
|
||||
|
||||
let cwd_analysis = if config.smart_context {
|
||||
action_handler::fetch_cwd_analysis_sync(&client, &initial_session.id).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let use_headless = headless || !std::io::stdout().is_terminal();
|
||||
|
||||
if use_headless {
|
||||
headless::run_headless(client, initial_session.id, prompt).await
|
||||
headless::run_headless(client, initial_session.id, prompt, cwd_analysis).await
|
||||
} else {
|
||||
run_recipe_tui_mode(client, initial_session, prompt).await
|
||||
run_recipe_tui_mode(client, initial_session, prompt, cwd_analysis).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,9 +327,11 @@ async fn run_text_mode(
|
||||
) -> Result<()> {
|
||||
info!("Running with text input");
|
||||
|
||||
let initial_session = if let Some(id) = session {
|
||||
let config = TuiConfig::load()?;
|
||||
|
||||
let (initial_session, is_new_session) = if let Some(id) = session {
|
||||
info!("Resuming session: {}", id);
|
||||
client.resume_agent(&id).await?
|
||||
(client.resume_agent(&id).await?, false)
|
||||
} else {
|
||||
let new_session = client
|
||||
.start_agent(cwd.to_string_lossy().to_string())
|
||||
@@ -337,18 +346,30 @@ async fn run_text_mode(
|
||||
}
|
||||
}
|
||||
|
||||
new_session
|
||||
(new_session, true)
|
||||
};
|
||||
|
||||
configure_session_from_global(&client, &initial_session.id).await;
|
||||
|
||||
headless::run_headless(client, initial_session.id, prompt).await
|
||||
let has_messages = initial_session
|
||||
.conversation
|
||||
.as_ref()
|
||||
.is_some_and(|c| !c.messages().is_empty());
|
||||
|
||||
let cwd_analysis = if config.smart_context && is_new_session && !has_messages {
|
||||
action_handler::fetch_cwd_analysis_sync(&client, &initial_session.id).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
headless::run_headless(client, initial_session.id, prompt, cwd_analysis).await
|
||||
}
|
||||
|
||||
async fn run_recipe_tui_mode(
|
||||
client: Client,
|
||||
initial_session: goose::session::Session,
|
||||
prompt: String,
|
||||
cwd_analysis: Option<String>,
|
||||
) -> Result<()> {
|
||||
let config = TuiConfig::load()?;
|
||||
let mut state = AppState::new(initial_session.id.clone(), config, None, None);
|
||||
@@ -358,6 +379,10 @@ async fn run_recipe_tui_mode(
|
||||
.map(|c| c.messages().clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(analysis) = cwd_analysis {
|
||||
state.cwd_analysis = state::CwdAnalysisState::Complete(analysis);
|
||||
}
|
||||
|
||||
let terminal = tui::init()?;
|
||||
let app = App::new();
|
||||
let event_handler = EventHandler::new();
|
||||
@@ -374,9 +399,13 @@ async fn run_interactive_mode(
|
||||
session: Option<String>,
|
||||
name: Option<String>,
|
||||
) -> Result<()> {
|
||||
let initial_session = if let Some(id) = session {
|
||||
let config = TuiConfig::load()?;
|
||||
let event_handler = EventHandler::new();
|
||||
let tx = event_handler.sender();
|
||||
|
||||
let (initial_session, is_new_session) = if let Some(id) = session {
|
||||
info!("Resuming agent session: {}", id);
|
||||
client.resume_agent(&id).await?
|
||||
(client.resume_agent(&id).await?, false)
|
||||
} else {
|
||||
let new_session = client
|
||||
.start_agent(cwd.to_string_lossy().to_string())
|
||||
@@ -391,12 +420,11 @@ async fn run_interactive_mode(
|
||||
}
|
||||
}
|
||||
|
||||
new_session
|
||||
(new_session, true)
|
||||
};
|
||||
|
||||
let (provider, model) = configure_session_from_global(&client, &initial_session.id).await;
|
||||
|
||||
let config = TuiConfig::load()?;
|
||||
let mut state = AppState::new(
|
||||
initial_session.id.clone(),
|
||||
config,
|
||||
@@ -422,9 +450,13 @@ async fn run_interactive_mode(
|
||||
state.token_state.input_tokens = initial_session.input_tokens.unwrap_or(0);
|
||||
state.token_state.output_tokens = initial_session.output_tokens.unwrap_or(0);
|
||||
|
||||
if state.config.smart_context && is_new_session && state.messages.is_empty() {
|
||||
state.cwd_analysis = state::CwdAnalysisState::Pending;
|
||||
action_handler::spawn_cwd_analysis(&initial_session.id, &client, &tx);
|
||||
}
|
||||
|
||||
let terminal = tui::init()?;
|
||||
let app = App::new();
|
||||
let event_handler = EventHandler::new();
|
||||
|
||||
let result = run_event_loop(terminal, app, event_handler, state, client).await;
|
||||
|
||||
@@ -440,9 +472,11 @@ async fn run_cli_mode(
|
||||
) -> Result<()> {
|
||||
info!("Starting CLI mode");
|
||||
|
||||
let initial_session = if let Some(id) = session {
|
||||
let config = TuiConfig::load()?;
|
||||
|
||||
let (initial_session, is_new_session) = if let Some(id) = session {
|
||||
info!("Resuming session: {}", id);
|
||||
client.resume_agent(&id).await?
|
||||
(client.resume_agent(&id).await?, false)
|
||||
} else {
|
||||
let new_session = client
|
||||
.start_agent(cwd.to_string_lossy().to_string())
|
||||
@@ -457,7 +491,7 @@ async fn run_cli_mode(
|
||||
}
|
||||
}
|
||||
|
||||
new_session
|
||||
(new_session, true)
|
||||
};
|
||||
|
||||
let (provider, model) = configure_session_from_global(&client, &initial_session.id).await;
|
||||
@@ -471,5 +505,24 @@ async fn run_cli_mode(
|
||||
})
|
||||
.unwrap_or(goose_tui::DEFAULT_CONTEXT_LIMIT);
|
||||
|
||||
cli::run_cli(client, initial_session.id, provider, model, context_limit).await
|
||||
let has_messages = initial_session
|
||||
.conversation
|
||||
.as_ref()
|
||||
.is_some_and(|c| !c.messages().is_empty());
|
||||
|
||||
let cwd_analysis = if config.smart_context && is_new_session && !has_messages {
|
||||
action_handler::fetch_cwd_analysis_sync(&client, &initial_session.id).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
cli::run_cli(
|
||||
client,
|
||||
initial_session.id,
|
||||
provider,
|
||||
model,
|
||||
context_limit,
|
||||
cwd_analysis,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ pub struct CustomCommand {
|
||||
pub struct TuiConfig {
|
||||
pub theme: Theme,
|
||||
pub custom_commands: Vec<CustomCommand>,
|
||||
pub smart_context: bool,
|
||||
}
|
||||
|
||||
impl TuiConfig {
|
||||
@@ -29,9 +30,14 @@ impl TuiConfig {
|
||||
.get_param::<Vec<CustomCommand>>("tui_custom_commands")
|
||||
.unwrap_or_else(|_| Self::default_commands());
|
||||
|
||||
let smart_context = global
|
||||
.get_param::<bool>("tui_smart_context")
|
||||
.unwrap_or(true);
|
||||
|
||||
Ok(Self {
|
||||
theme: Theme::from_name(&theme_name),
|
||||
custom_commands,
|
||||
smart_context,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ pub enum Event {
|
||||
ConfigLoaded(serde_json::Value),
|
||||
Error(String),
|
||||
Flash(String),
|
||||
CwdAnalysisComplete(Option<String>),
|
||||
}
|
||||
pub struct EventHandler {
|
||||
rx: mpsc::UnboundedReceiver<Event>,
|
||||
|
||||
@@ -64,4 +64,5 @@ pub enum Action {
|
||||
id: String,
|
||||
approved: bool,
|
||||
},
|
||||
CwdAnalysisComplete(Option<String>),
|
||||
}
|
||||
|
||||
@@ -43,6 +43,31 @@ pub struct TodoItem {
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum CwdAnalysisState {
|
||||
#[default]
|
||||
NotStarted,
|
||||
Pending,
|
||||
Complete(String),
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl CwdAnalysisState {
|
||||
pub fn take_result(&mut self) -> Option<String> {
|
||||
match std::mem::take(self) {
|
||||
CwdAnalysisState::Complete(s) => Some(s),
|
||||
other => {
|
||||
*self = other;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_pending(&self) -> bool {
|
||||
matches!(self, CwdAnalysisState::Pending)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub session_id: String,
|
||||
pub messages: Vec<Message>,
|
||||
@@ -65,6 +90,7 @@ pub struct AppState {
|
||||
pub needs_refresh: bool,
|
||||
pub copy_mode: bool,
|
||||
pub pending_confirmation: Option<PendingToolConfirmation>,
|
||||
pub cwd_analysis: CwdAnalysisState,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -96,6 +122,7 @@ impl AppState {
|
||||
needs_refresh: false,
|
||||
copy_mode: false,
|
||||
pending_confirmation: None,
|
||||
cwd_analysis: CwdAnalysisState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::action::Action;
|
||||
use crate::state::{ActivePopup, AppState, InputMode, PendingToolConfirmation, TodoItem};
|
||||
use crate::state::{
|
||||
ActivePopup, AppState, CwdAnalysisState, InputMode, PendingToolConfirmation, TodoItem,
|
||||
};
|
||||
use goose::conversation::message::{Message, MessageContent, ToolConfirmationRequest};
|
||||
use goose::model::ModelConfig;
|
||||
use goose::providers::base::ModelInfo;
|
||||
@@ -28,6 +30,24 @@ pub fn update(state: &mut AppState, action: Action) {
|
||||
Action::ServerMessage(msg) => handle_server_message(state, msg),
|
||||
Action::ModelsLoaded { provider, models } => handle_models_loaded(state, provider, models),
|
||||
Action::ConfigLoaded(config) => handle_config_loaded(state, config),
|
||||
Action::CwdAnalysisComplete(result) => {
|
||||
state.cwd_analysis = match result {
|
||||
Some(s) => {
|
||||
state.flash_message = Some((
|
||||
"✓ Project context loaded".to_string(),
|
||||
Instant::now() + FLASH_DURATION_SHORT,
|
||||
));
|
||||
CwdAnalysisState::Complete(s)
|
||||
}
|
||||
None => {
|
||||
state.flash_message = Some((
|
||||
"⚠ Project context unavailable".to_string(),
|
||||
Instant::now() + FLASH_DURATION_SHORT,
|
||||
));
|
||||
CwdAnalysisState::Failed
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
if !handle_data_loaded(state, &action)
|
||||
&& !handle_chat(state, &action)
|
||||
@@ -41,6 +61,7 @@ pub fn update(state: &mut AppState, action: Action) {
|
||||
fn handle_data_loaded(state: &mut AppState, action: &Action) -> bool {
|
||||
match action {
|
||||
Action::SessionResumed(session) => {
|
||||
let is_new_session = state.session_id != session.id;
|
||||
state.session_id = session.id.clone();
|
||||
state.messages = session
|
||||
.conversation
|
||||
@@ -60,6 +81,9 @@ fn handle_data_loaded(state: &mut AppState, action: &Action) -> bool {
|
||||
state.active_popup = ActivePopup::None;
|
||||
state.is_working = false;
|
||||
state.pending_confirmation = None;
|
||||
if is_new_session && !state.messages.is_empty() {
|
||||
state.cwd_analysis = CwdAnalysisState::Failed;
|
||||
}
|
||||
|
||||
let pending = state.messages.last().and_then(|last_msg| {
|
||||
extract_tool_confirmation(last_msg).map(|req| PendingToolConfirmation {
|
||||
|
||||
Reference in New Issue
Block a user