From a7fb7e199cbe175e0988874c1df280cbdc3c8c6c Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Sat, 7 Mar 2026 12:09:05 -0500 Subject: [PATCH] delete goose web (#7696) Co-authored-by: Douwe Osinga --- Cargo.lock | 16 - crates/goose-cli/Cargo.toml | 4 - crates/goose-cli/WEB_INTERFACE.md | 78 -- crates/goose-cli/src/cli.rs | 44 -- crates/goose-cli/src/commands/mod.rs | 1 - crates/goose-cli/src/commands/web.rs | 721 ------------------ crates/goose-cli/static/index.html | 46 -- crates/goose-cli/static/script.js | 548 ------------- crates/goose-cli/static/style.css | 490 ------------ .../docs/guides/goose-cli-commands.md | 51 -- .../guides/sessions/session-management.md | 4 - documentation/docs/quickstart.md | 7 - scripts/test_web.sh | 26 - 13 files changed, 2036 deletions(-) delete mode 100644 crates/goose-cli/WEB_INTERFACE.md delete mode 100644 crates/goose-cli/src/commands/web.rs delete mode 100644 crates/goose-cli/static/index.html delete mode 100644 crates/goose-cli/static/script.js delete mode 100644 crates/goose-cli/static/style.css delete mode 100644 scripts/test_web.sh diff --git a/Cargo.lock b/Cargo.lock index 61e62f6f2b..9b8d446050 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4428,7 +4428,6 @@ dependencies = [ "anstream", "anyhow", "async-trait", - "axum 0.8.8", "base64 0.22.1", "bat", "bzip2 0.5.2", @@ -4445,7 +4444,6 @@ dependencies = [ "goose", "goose-acp", "goose-mcp", - "http 1.4.0", "indicatif 0.18.3", "open", "rand 0.8.5", @@ -4462,7 +4460,6 @@ dependencies = [ "test-case", "tokio", "tokio-util", - "tower-http", "tracing", "tracing-appender", "tracing-subscriber", @@ -4880,12 +4877,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" - [[package]] name = "httparse" version = "1.10.1" @@ -10847,7 +10838,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "base64 0.22.1", "bitflags 2.10.0", "bytes", "futures-core", @@ -10855,19 +10845,13 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "http-range-header", - "httpdate", "iri-string", - "mime", - "mime_guess", - "percent-encoding", "pin-project-lite", "tokio", "tokio-util", "tower", "tower-layer", "tower-service", - "tracing", ] [[package]] diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index c919c662b6..41e5d64c58 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -52,10 +52,6 @@ tar = "0.4" reqwest = { workspace = true, features = ["blocking", "rustls"], default-features = false } zip = { version = "^8.0", default-features = false, features = ["deflate"] } bzip2 = "0.5" -# Web server dependencies -axum = { workspace = true, features = ["ws", "macros"] } -tower-http = { workspace = true, features = ["cors", "fs", "auth"] } -http = { workspace = true } webbrowser = { workspace = true } indicatif = "0.18.1" tokio-util = { workspace = true, features = ["compat", "rt"] } diff --git a/crates/goose-cli/WEB_INTERFACE.md b/crates/goose-cli/WEB_INTERFACE.md deleted file mode 100644 index a4a51309f4..0000000000 --- a/crates/goose-cli/WEB_INTERFACE.md +++ /dev/null @@ -1,78 +0,0 @@ -# goose Web Interface - -The `goose web` command provides a (preview) web-based chat interface for interacting with goose. -Do not expose this publicly - this is in a preview state as an option. - -## Usage - -```bash -# Start the web server on default port (3000) -goose web - -# Start on a specific port -goose web --port 8080 - -# Start and automatically open in browser -goose web --open - -# Bind to a specific host -goose web --host 0.0.0.0 --port 8080 -``` - -## Features - -- **Real-time chat interface**: Communicate with goose through a clean web UI -- **WebSocket support**: Real-time message streaming -- **Session management**: Each browser tab maintains its own session -- **Responsive design**: Works on desktop and mobile devices - -## Architecture - -The web interface is built with: -- **Backend**: Rust with Axum web framework -- **Frontend**: Vanilla JavaScript with WebSocket communication -- **Styling**: CSS with dark/light mode support - -## Development Notes - -### Current Implementation - -The web interface provides: -1. A simple chat UI similar to the desktop Electron app -2. WebSocket-based real-time communication -3. Basic session management (messages are stored in memory) - -### Future Enhancements - -- [ ] Persistent session storage -- [ ] Tool call visualization -- [ ] File upload support -- [ ] Multiple session tabs -- [ ] Authentication/authorization -- [ ] Streaming responses with proper formatting -- [ ] Code syntax highlighting -- [ ] Export chat history - -### Integration with goose Agent - -The web server creates an instance of the goose Agent and processes messages through the same pipeline as the CLI. However, some features like: -- Extension management -- Tool confirmations -- File system interactions - -...may require additional UI components to be fully functional. - -## Security Considerations - -Currently, the web interface: -- Binds to localhost by default for security -- Does not include authentication (planned for future) -- Should not be exposed to the internet without proper security measures - -## Troubleshooting - -If you encounter issues: - -1. **Port already in use**: Try a different port with `--port` -2. **Cannot connect**: Ensure no firewall is blocking the port -3. **Agent not configured**: Run `goose configure` first to set up a provider \ No newline at end of file diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 2f56562ac9..b6a5072acb 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -841,42 +841,6 @@ enum Command { reconfigure: bool, }, - /// Start a web server with a chat interface - #[command(about = "Experimental: Start a web server with a chat interface")] - Web { - /// Port to run the web server on - #[arg( - short, - long, - default_value = "3000", - help = "Port to run the web server on" - )] - port: u16, - - /// Host to bind the web server to - #[arg( - long, - default_value = "127.0.0.1", - help = "Host to bind the web server to" - )] - host: String, - - /// Open browser automatically - #[arg(long, help = "Open browser automatically when server starts")] - open: bool, - - /// Authentication token for both Basic Auth (password) and Bearer token - #[arg(long, help = "Authentication token to secure the web interface")] - auth_token: Option, - - /// Allow running without authentication when exposed on the network (unsafe) - #[arg( - long, - help = "Skip auth requirement when exposed on the network (unsafe)" - )] - no_auth: bool, - }, - /// Terminal-integrated session (one session per terminal) #[command( about = "Terminal-integrated goose session", @@ -1041,7 +1005,6 @@ fn get_command_name(command: &Option) -> &'static str { Some(Command::Schedule { .. }) => "schedule", Some(Command::Update { .. }) => "update", Some(Command::Recipe { .. }) => "recipe", - Some(Command::Web { .. }) => "web", Some(Command::Term { .. }) => "term", Some(Command::LocalModels { .. }) => "local-models", Some(Command::Completion { .. }) => "completion", @@ -1780,13 +1743,6 @@ pub async fn cli() -> anyhow::Result<()> { Ok(()) } Some(Command::Recipe { command }) => handle_recipe_subcommand(command), - Some(Command::Web { - port, - host, - open, - auth_token, - no_auth, - }) => crate::commands::web::handle_web(port, host, open, auth_token, no_auth).await, Some(Command::Term { command }) => handle_term_subcommand(command).await, Some(Command::LocalModels { command }) => handle_local_models_command(command).await, Some(Command::ValidateExtensions { file }) => { diff --git a/crates/goose-cli/src/commands/mod.rs b/crates/goose-cli/src/commands/mod.rs index 74cb50fc40..6bc3314247 100644 --- a/crates/goose-cli/src/commands/mod.rs +++ b/crates/goose-cli/src/commands/mod.rs @@ -7,4 +7,3 @@ pub mod schedule; pub mod session; pub mod term; pub mod update; -pub mod web; diff --git a/crates/goose-cli/src/commands/web.rs b/crates/goose-cli/src/commands/web.rs deleted file mode 100644 index 09f91befb1..0000000000 --- a/crates/goose-cli/src/commands/web.rs +++ /dev/null @@ -1,721 +0,0 @@ -use anyhow::Result; -use axum::response::Redirect; -use axum::{ - extract::{ - ws::{Message, WebSocket, WebSocketUpgrade}, - Query, Request, State, - }, - http::{StatusCode, Uri}, - middleware::{self, Next}, - response::{Html, IntoResponse, Response}, - routing::get, - Json, Router, -}; -use base64::Engine; -use futures::{sink::SinkExt, stream::StreamExt}; -use goose::agents::{Agent, AgentEvent}; -use goose::conversation::message::Message as GooseMessage; -use goose::session::session_manager::SessionType; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::{net::ToSocketAddrs, sync::Arc}; -use tokio::sync::{Mutex, RwLock}; -use tower_http::cors::{AllowOrigin, Any, CorsLayer}; -use tracing::error; -use webbrowser; - -type CancellationStore = Arc>>; - -#[derive(Clone)] -struct AppState { - agent: Arc, - cancellations: CancellationStore, - auth_token: Option, - ws_token: String, -} - -#[derive(Serialize, Deserialize)] -#[serde(tag = "type")] -enum WebSocketMessage { - #[serde(rename = "message")] - Message { - content: String, - session_id: String, - timestamp: i64, - }, - #[serde(rename = "cancel")] - Cancel { session_id: String }, - #[serde(rename = "response")] - Response { - content: String, - role: String, - timestamp: i64, - }, - #[serde(rename = "tool_request")] - ToolRequest { - id: String, - tool_name: String, - arguments: serde_json::Value, - }, - #[serde(rename = "tool_response")] - ToolResponse { - id: String, - result: serde_json::Value, - is_error: bool, - }, - #[serde(rename = "tool_confirmation")] - ToolConfirmation { - id: String, - tool_name: String, - arguments: serde_json::Value, - needs_confirmation: bool, - }, - #[serde(rename = "error")] - Error { message: String }, - #[serde(rename = "thinking")] - Thinking { message: String }, - #[serde(rename = "context_exceeded")] - ContextExceeded { message: String }, - #[serde(rename = "cancelled")] - Cancelled { message: String }, - #[serde(rename = "complete")] - Complete { message: String }, -} - -async fn auth_middleware( - State(state): State, - req: Request, - next: Next, -) -> Result { - if req.uri().path() == "/api/health" { - return Ok(next.run(req).await); - } - - let Some(ref expected_token) = state.auth_token else { - return Ok(next.run(req).await); - }; - - if let Some(auth_header) = req.headers().get("authorization") { - if let Ok(auth_str) = auth_header.to_str() { - if let Some(token) = auth_str.strip_prefix("Bearer ") { - if token == expected_token { - return Ok(next.run(req).await); - } - } - - if let Some(basic_token) = auth_str.strip_prefix("Basic ") { - if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(basic_token) { - if let Ok(credentials) = String::from_utf8(decoded) { - if credentials.ends_with(expected_token) { - return Ok(next.run(req).await); - } - } - } - } - } - } - - let mut response = Response::new("Authentication required".into()); - *response.status_mut() = StatusCode::UNAUTHORIZED; - response.headers_mut().insert( - "WWW-Authenticate", - "Basic realm=\"Goose Web Interface\"".parse().unwrap(), - ); - Ok(response) -} - -fn is_loopback_address(host: &str) -> bool { - (host, 0) - .to_socket_addrs() - .map(|mut addrs| addrs.any(|addr| addr.ip().is_loopback())) - .unwrap_or(false) -} - -fn validate_network_auth(host: &str, auth_token: &Option, no_auth: bool) { - if !is_loopback_address(host) && auth_token.is_none() && !no_auth { - eprintln!( - "Error: --auth-token is required when the server is exposed on the network ({}).", - host - ); - eprintln!( - "For security, use --auth-token or bind to a local address (e.g., localhost)." - ); - eprintln!("To skip this check, use --no-auth (unsafe)."); - std::process::exit(1); - } -} - -fn get_provider_and_model() -> (String, String) { - let config = goose::config::Config::global(); - - let provider_name: String = match config.get_goose_provider() { - Ok(p) => p, - Err(_) => { - eprintln!("No provider configured. Run 'goose configure' first"); - std::process::exit(1); - } - }; - - let model: String = match config.get_goose_model() { - Ok(m) => m, - Err(_) => { - eprintln!("No model configured. Run 'goose configure' first"); - std::process::exit(1); - } - }; - - (provider_name, model) -} - -async fn create_agent(provider_name: &str, model: &str) -> Result { - let model_config = goose::model::ModelConfig::new(model)?.with_canonical_limits(provider_name); - - let agent = Agent::new(); - - let session_manager = agent.config.session_manager.clone(); - let init_session = session_manager - .create_session( - std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), - "Web Agent Initialization".to_string(), - SessionType::Hidden, - ) - .await?; - - let enabled_configs = goose::config::get_enabled_extensions(); - for config in &enabled_configs { - if let Err(e) = agent.add_extension(config.clone(), &init_session.id).await { - eprintln!("Warning: Failed to load extension {}: {}", config.name(), e); - } - } - - let provider = goose::providers::create(provider_name, model_config, enabled_configs).await?; - agent.update_provider(provider, &init_session.id).await?; - - Ok(agent) -} - -fn build_cors_layer(auth_token: &Option, host: &str, port: u16) -> CorsLayer { - if auth_token.is_none() { - let allowed_origins = [ - "http://localhost:3000".parse().unwrap(), - "http://127.0.0.1:3000".parse().unwrap(), - format!("http://{}:{}", host, port).parse().unwrap(), - ]; - CorsLayer::new() - .allow_origin(AllowOrigin::list(allowed_origins)) - .allow_methods(Any) - .allow_headers(Any) - } else { - CorsLayer::new() - .allow_origin(Any) - .allow_methods(Any) - .allow_headers(Any) - } -} - -fn build_router(state: AppState, cors_layer: CorsLayer) -> Router { - Router::new() - .route("/", get(serve_index)) - .route("/session/{session_name}", get(serve_session)) - .route("/ws", get(websocket_handler)) - .route("/api/health", get(health_check)) - .route("/api/sessions", get(list_sessions)) - .route("/api/sessions/{session_id}", get(get_session)) - .route("/static/{*path}", get(serve_static)) - .layer(middleware::from_fn_with_state( - state.clone(), - auth_middleware, - )) - .layer(cors_layer) - .with_state(state) -} - -pub async fn handle_web( - port: u16, - host: String, - open: bool, - auth_token: Option, - no_auth: bool, -) -> Result<()> { - validate_network_auth(&host, &auth_token, no_auth); - crate::logging::setup_logging(Some("goose-web"))?; - - let (provider_name, model) = get_provider_and_model(); - let agent = create_agent(&provider_name, &model).await?; - - let ws_token = if auth_token.is_none() { - uuid::Uuid::new_v4().to_string() - } else { - String::new() - }; - - let state = AppState { - agent: Arc::new(agent), - cancellations: Arc::new(RwLock::new(std::collections::HashMap::new())), - auth_token: auth_token.clone(), - ws_token, - }; - - let cors_layer = build_cors_layer(&auth_token, &host, port); - let app = build_router(state, cors_layer); - - let addr = (host.as_str(), port) - .to_socket_addrs()? - .next() - .ok_or_else(|| anyhow::anyhow!("Could not resolve address: {}", host))?; - - println!("\n🪿 Starting goose web server"); - println!(" Provider: {} | Model: {}", provider_name, model); - println!( - " Working directory: {}", - std::env::current_dir()?.display() - ); - println!(" Server: http://{}", addr); - println!(" Press Ctrl+C to stop\n"); - - if open { - let url = format!("http://{}", addr); - if let Err(e) = webbrowser::open(&url) { - eprintln!("Failed to open browser: {}", e); - } - } - - let listener = tokio::net::TcpListener::bind(addr).await?; - axum::serve(listener, app).await?; - - Ok(()) -} - -async fn serve_index( - State(state): State, - uri: Uri, -) -> Result { - let session = state - .agent - .config - .session_manager - .create_session( - std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), - "Web session".to_string(), - SessionType::User, - ) - .await - .map_err(|err| (http::StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?; - - let redirect_url = if let Some(query) = uri.query() { - format!("/session/{}?{}", session.id, query) - } else { - format!("/session/{}", session.id) - }; - - Ok(Redirect::to(&redirect_url)) -} - -async fn serve_session( - axum::extract::Path(session_name): axum::extract::Path, - State(state): State, -) -> Html { - let html = include_str!("../../static/index.html"); - let html_with_session = html.replace( - "", - &format!( - "\n ", - session_name, - state.ws_token - ) - ); - Html(html_with_session) -} - -async fn serve_static(axum::extract::Path(path): axum::extract::Path) -> Response { - match path.as_str() { - "style.css" => ( - [("content-type", "text/css")], - include_str!("../../static/style.css"), - ) - .into_response(), - "script.js" => ( - [("content-type", "application/javascript")], - include_str!("../../static/script.js"), - ) - .into_response(), - "img/logo_dark.png" => ( - [("content-type", "image/png")], - include_bytes!("../../../../documentation/static/img/logo_dark.png").to_vec(), - ) - .into_response(), - "img/logo_light.png" => ( - [("content-type", "image/png")], - include_bytes!("../../../../documentation/static/img/logo_light.png").to_vec(), - ) - .into_response(), - _ => (http::StatusCode::NOT_FOUND, "Not found").into_response(), - } -} - -async fn health_check() -> Json { - Json(serde_json::json!({ - "status": "ok", - "service": "goose-web" - })) -} - -async fn list_sessions(State(state): State) -> Json { - match state.agent.config.session_manager.list_sessions().await { - Ok(sessions) => { - let mut session_info = Vec::new(); - - for session in sessions { - session_info.push(serde_json::json!({ - "name": session.id, - "path": session.id, - "description": session.name, - "message_count": session.message_count, - "working_dir": session.working_dir - })); - } - Json(serde_json::json!({ - "sessions": session_info - })) - } - Err(e) => Json(serde_json::json!({ - "error": e.to_string() - })), - } -} -async fn get_session( - State(state): State, - axum::extract::Path(session_id): axum::extract::Path, -) -> Json { - match state - .agent - .config - .session_manager - .get_session(&session_id, true) - .await - { - Ok(session) => Json(serde_json::json!({ - "metadata": session, - "messages": session.conversation.unwrap_or_default().messages() - })), - Err(e) => Json(serde_json::json!({ - "error": e.to_string() - })), - } -} - -#[derive(Deserialize)] -struct WsQuery { - token: Option, -} - -async fn websocket_handler( - ws: WebSocketUpgrade, - State(state): State, - Query(query): Query, -) -> Result { - if state.auth_token.is_none() { - let provided_token = query.token.as_deref().unwrap_or(""); - if provided_token != state.ws_token { - tracing::warn!("WebSocket connection rejected: invalid token"); - return Err(StatusCode::FORBIDDEN); - } - } - - Ok(ws.on_upgrade(|socket| handle_socket(socket, state))) -} - -async fn handle_socket(socket: WebSocket, state: AppState) { - let (sender, mut receiver) = socket.split(); - let sender = Arc::new(Mutex::new(sender)); - - while let Some(msg) = receiver.next().await { - if let Ok(msg) = msg { - match msg { - Message::Text(text) => { - handle_text_message(&text.to_string(), &sender, &state).await; - } - Message::Close(_) => break, - _ => {} - } - } else { - break; - } - } -} - -async fn handle_text_message( - text: &str, - sender: &Arc>>, - state: &AppState, -) { - match serde_json::from_str::(text) { - Ok(WebSocketMessage::Message { - content, - session_id, - .. - }) => { - handle_user_message(content, session_id, sender.clone(), state).await; - } - Ok(WebSocketMessage::Cancel { session_id }) => { - handle_cancel_message(session_id, sender, state).await; - } - Ok(_) => {} - Err(e) => { - error!("Failed to parse WebSocket message: {}", e); - } - } -} - -async fn handle_user_message( - content: String, - session_id: String, - sender: Arc>>, - state: &AppState, -) { - let agent = state.agent.clone(); - let session_id_clone = session_id.clone(); - - let task_handle = tokio::spawn(async move { - let result = process_message_streaming(&agent, session_id_clone, content, sender).await; - - if let Err(e) = result { - error!("Error processing message: {}", e); - } - }); - - { - let mut cancellations = state.cancellations.write().await; - cancellations.insert(session_id.clone(), task_handle.abort_handle()); - } - - let cancellations_for_cleanup = state.cancellations.clone(); - let session_id_for_cleanup = session_id; - - tokio::spawn(async move { - if let Err(e) = task_handle.await { - if e.is_cancelled() { - tracing::debug!("Task was cancelled"); - } else { - error!("Task error: {}", e); - } - } - - let mut cancellations = cancellations_for_cleanup.write().await; - cancellations.remove(&session_id_for_cleanup); - }); -} - -async fn handle_cancel_message( - session_id: String, - sender: &Arc>>, - state: &AppState, -) { - let abort_handle = { - let mut cancellations = state.cancellations.write().await; - cancellations.remove(&session_id) - }; - - if let Some(handle) = abort_handle { - handle.abort(); - - let mut sender = sender.lock().await; - let _ = sender - .send(Message::Text( - serde_json::to_string(&WebSocketMessage::Cancelled { - message: "Operation cancelled".to_string(), - }) - .unwrap() - .into(), - )) - .await; - } -} - -async fn process_message_streaming( - agent: &Agent, - session_id: String, - content: String, - sender: Arc>>, -) -> Result<()> { - use goose::agents::SessionConfig; - - let user_message = GooseMessage::user().with_text(content.clone()); - - let provider = agent.provider().await; - if provider.is_err() { - let error_msg = "I'm not properly configured yet. Please configure a provider through the CLI first using `goose configure`.".to_string(); - let mut sender = sender.lock().await; - let _ = sender - .send(Message::Text( - serde_json::to_string(&WebSocketMessage::Response { - content: error_msg, - role: "assistant".to_string(), - timestamp: chrono::Utc::now().timestamp_millis(), - }) - .unwrap() - .into(), - )) - .await; - return Ok(()); - } - - let session = agent - .config - .session_manager - .get_session(&session_id, true) - .await?; - let mut messages = session.conversation.unwrap_or_default(); - messages.push(user_message.clone()); - - let session_config = SessionConfig { - id: session.id.clone(), - schedule_id: None, - max_turns: None, - retry_config: None, - }; - - match agent.reply(user_message, session_config, None).await { - Ok(mut stream) => { - while let Some(result) = stream.next().await { - match result { - Ok(AgentEvent::Message(message)) => { - process_agent_message(&message, &sender, agent).await; - } - Ok(AgentEvent::HistoryReplaced(_)) => { - tracing::info!("History replaced, compacting happened in reply"); - } - Ok(AgentEvent::McpNotification(_)) => { - tracing::info!("Received MCP notification in web interface"); - } - Ok(AgentEvent::ModelChange { model, mode }) => { - tracing::info!("Model changed to {} in {} mode", model, mode); - } - Err(e) => { - error!("Error in message stream: {}", e); - send_error(&sender, &format!("Error: {}", e)).await; - break; - } - } - } - } - Err(e) => { - error!("Error calling agent: {}", e); - send_error(&sender, &format!("Error: {}", e)).await; - } - } - - let mut sender = sender.lock().await; - let _ = sender - .send(Message::Text( - serde_json::to_string(&WebSocketMessage::Complete { - message: "Response complete".to_string(), - }) - .unwrap() - .into(), - )) - .await; - - Ok(()) -} - -async fn process_agent_message( - message: &GooseMessage, - sender: &Arc>>, - agent: &Agent, -) { - use goose::conversation::message::MessageContent; - - for content in &message.content { - match content { - MessageContent::Text(text) => { - let mut sender = sender.lock().await; - let _ = sender - .send(Message::Text( - serde_json::to_string(&WebSocketMessage::Response { - content: text.text.clone(), - role: "assistant".to_string(), - timestamp: chrono::Utc::now().timestamp_millis(), - }) - .unwrap() - .into(), - )) - .await; - } - MessageContent::ToolRequest(req) => { - let mut sender = sender.lock().await; - if let Ok(tool_call) = &req.tool_call { - let _ = sender - .send(Message::Text( - serde_json::to_string(&WebSocketMessage::ToolRequest { - id: req.id.clone(), - tool_name: tool_call.name.to_string(), - arguments: Value::from(tool_call.arguments.clone()), - }) - .unwrap() - .into(), - )) - .await; - } - } - MessageContent::ToolResponse(_) => {} - MessageContent::ToolConfirmationRequest(confirmation) => { - { - let mut sender = sender.lock().await; - let _ = sender - .send(Message::Text( - serde_json::to_string(&WebSocketMessage::ToolConfirmation { - id: confirmation.id.clone(), - tool_name: confirmation.tool_name.to_string(), - arguments: Value::from(confirmation.arguments.clone()), - needs_confirmation: true, - }) - .unwrap() - .into(), - )) - .await; - } - - agent - .handle_confirmation( - confirmation.id.clone(), - goose::permission::PermissionConfirmation { - principal_type: - goose::permission::permission_confirmation::PrincipalType::Tool, - permission: goose::permission::Permission::AllowOnce, - }, - ) - .await; - } - MessageContent::Thinking(thinking) => { - let mut sender = sender.lock().await; - let _ = sender - .send(Message::Text( - serde_json::to_string(&WebSocketMessage::Thinking { - message: thinking.thinking.clone(), - }) - .unwrap() - .into(), - )) - .await; - } - _ => {} - } - } -} - -async fn send_error( - sender: &Arc>>, - message: &str, -) { - let mut sender = sender.lock().await; - let _ = sender - .send(Message::Text( - serde_json::to_string(&WebSocketMessage::Error { - message: message.to_string(), - }) - .unwrap() - .into(), - )) - .await; -} diff --git a/crates/goose-cli/static/index.html b/crates/goose-cli/static/index.html deleted file mode 100644 index 5afda5baa5..0000000000 --- a/crates/goose-cli/static/index.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - goose chat - - - -
-
-

goose chat

-
Connecting...
-
- -
-
-
-

Welcome to goose!

-

I'm your AI assistant. How can I help you today?

- -
-
What can you do?
-
Demo writing and reading files
-
Make a snake game in a new folder
-
List files in my current directory
-
Take a screenshot and summarize
-
-
-
- -
- - -
-
-
- - - - \ No newline at end of file diff --git a/crates/goose-cli/static/script.js b/crates/goose-cli/static/script.js deleted file mode 100644 index ba0504ebdd..0000000000 --- a/crates/goose-cli/static/script.js +++ /dev/null @@ -1,548 +0,0 @@ -// WebSocket connection and chat functionality -let socket = null; -let sessionId = getSessionId(); -let isConnected = false; - -// DOM elements -const messagesContainer = document.getElementById('messages'); -const messageInput = document.getElementById('message-input'); -const sendButton = document.getElementById('send-button'); -const connectionStatus = document.getElementById('connection-status'); - -// Track if we're currently processing -let isProcessing = false; - -// Get session ID - either from URL parameter, injected session name, or generate new one -function getSessionId() { - // Check if session name was injected by server (for /session/:name routes) - if (window.GOOSE_SESSION_NAME) { - return window.GOOSE_SESSION_NAME; - } - - // Check URL parameters - const urlParams = new URLSearchParams(window.location.search); - const sessionParam = urlParams.get('session') || urlParams.get('name'); - if (sessionParam) { - return sessionParam; - } - - // Generate new session ID using CLI format - return generateSessionId(); -} - -// Generate a session ID using timestamp format (yyyymmdd_hhmmss) like CLI -function generateSessionId() { - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - const day = String(now.getDate()).padStart(2, '0'); - const hour = String(now.getHours()).padStart(2, '0'); - const minute = String(now.getMinutes()).padStart(2, '0'); - const second = String(now.getSeconds()).padStart(2, '0'); - - return `${year}${month}${day}_${hour}${minute}${second}`; -} - -// Format timestamp -function formatTimestamp(date) { - return date.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit' - }); -} - -// Create message element -function createMessageElement(content, role, timestamp) { - const messageDiv = document.createElement('div'); - messageDiv.className = `message ${role}`; - - // Create content div - const contentDiv = document.createElement('div'); - contentDiv.className = 'message-content'; - contentDiv.innerHTML = formatMessageContent(content); - messageDiv.appendChild(contentDiv); - - // Add timestamp - const timestampDiv = document.createElement('div'); - timestampDiv.className = 'timestamp'; - timestampDiv.textContent = formatTimestamp(new Date(timestamp || Date.now())); - messageDiv.appendChild(timestampDiv); - - return messageDiv; -} - -// Format message content (handle markdown-like formatting) -function formatMessageContent(content) { - // Escape HTML - let formatted = content - .replace(/&/g, '&') - .replace(//g, '>'); - - // Handle code blocks - formatted = formatted.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { - return `
${code.trim()}
`; - }); - - // Handle inline code - formatted = formatted.replace(/`([^`]+)`/g, '$1'); - - // Handle line breaks - formatted = formatted.replace(/\n/g, '
'); - - return formatted; -} - -// Add message to chat -function addMessage(content, role, timestamp) { - // Remove welcome message if it exists - const welcomeMessage = messagesContainer.querySelector('.welcome-message'); - if (welcomeMessage) { - welcomeMessage.remove(); - } - - const messageElement = createMessageElement(content, role, timestamp); - messagesContainer.appendChild(messageElement); - - // Scroll to bottom - messagesContainer.scrollTop = messagesContainer.scrollHeight; -} - -// Add thinking indicator -function addThinkingIndicator() { - removeThinkingIndicator(); // Remove any existing one first - - const thinkingDiv = document.createElement('div'); - thinkingDiv.id = 'thinking-indicator'; - thinkingDiv.className = 'message thinking-message'; - thinkingDiv.innerHTML = ` -
- - - -
- goose is thinking... - `; - messagesContainer.appendChild(thinkingDiv); - messagesContainer.scrollTop = messagesContainer.scrollHeight; -} - -// Remove thinking indicator -function removeThinkingIndicator() { - const thinking = document.getElementById('thinking-indicator'); - if (thinking) { - thinking.remove(); - } -} - -// Connect to WebSocket -function connectWebSocket() { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const token = window.GOOSE_WS_TOKEN || ''; - const wsUrl = `${protocol}//${window.location.host}/ws?token=${encodeURIComponent(token)}`; - - socket = new WebSocket(wsUrl); - - socket.onopen = () => { - console.log('WebSocket connected'); - isConnected = true; - connectionStatus.textContent = 'Connected'; - connectionStatus.className = 'status connected'; - sendButton.disabled = false; - - // Check if this session exists and load history if it does - loadSessionIfExists(); - }; - - socket.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - handleServerMessage(data); - } catch (e) { - console.error('Failed to parse message:', e); - } - }; - - socket.onclose = () => { - console.log('WebSocket disconnected'); - isConnected = false; - connectionStatus.textContent = 'Disconnected'; - connectionStatus.className = 'status disconnected'; - sendButton.disabled = true; - - // Attempt to reconnect after 3 seconds - setTimeout(connectWebSocket, 3000); - }; - - socket.onerror = (error) => { - console.error('WebSocket error:', error); - }; -} - -// Handle messages from server -function handleServerMessage(data) { - switch (data.type) { - case 'response': - // For streaming responses, we need to handle partial messages - handleStreamingResponse(data); - break; - case 'tool_request': - handleToolRequest(data); - break; - case 'tool_response': - handleToolResponse(data); - break; - case 'tool_confirmation': - handleToolConfirmation(data); - break; - case 'thinking': - handleThinking(data); - break; - case 'context_exceeded': - handleContextExceeded(data); - break; - case 'cancelled': - handleCancelled(data); - break; - case 'complete': - handleComplete(data); - break; - case 'error': - removeThinkingIndicator(); - resetSendButton(); - addMessage(`Error: ${data.message}`, 'assistant', Date.now()); - break; - default: - console.log('Unknown message type:', data.type); - } -} - -// Track current streaming message -let currentStreamingMessage = null; - -// Handle streaming responses -function handleStreamingResponse(data) { - removeThinkingIndicator(); - - // If this is the first chunk of a new message, or we don't have a current streaming message - if (!currentStreamingMessage) { - // Create a new message element - const messageElement = createMessageElement(data.content, data.role || 'assistant', data.timestamp); - messageElement.setAttribute('data-streaming', 'true'); - messagesContainer.appendChild(messageElement); - - currentStreamingMessage = { - element: messageElement, - content: data.content, - role: data.role || 'assistant', - timestamp: data.timestamp - }; - } else { - // Append to existing streaming message - currentStreamingMessage.content += data.content; - - // Update the message content using the proper content div - const contentDiv = currentStreamingMessage.element.querySelector('.message-content'); - if (contentDiv) { - contentDiv.innerHTML = formatMessageContent(currentStreamingMessage.content); - } - } - - // Scroll to bottom - messagesContainer.scrollTop = messagesContainer.scrollHeight; -} - -// Handle tool requests -function handleToolRequest(data) { - removeThinkingIndicator(); // Remove thinking when tool starts - - // Reset streaming message so tool doesn't interfere with message flow - currentStreamingMessage = null; - - const toolDiv = document.createElement('div'); - toolDiv.className = 'message assistant tool-message'; - - const headerDiv = document.createElement('div'); - headerDiv.className = 'tool-header'; - headerDiv.innerHTML = `🔧 ${data.tool_name}`; - - const contentDiv = document.createElement('div'); - contentDiv.className = 'tool-content'; - - const isShellTool = data.tool_name === 'shell'; - const isDeveloperFileTool = [ - 'read', - 'write', - 'edit' - ].includes(data.tool_name); - - // Format the arguments - if (isShellTool && data.arguments.command) { - contentDiv.innerHTML = `
${escapeHtml(data.arguments.command)}
`; - } else if (isDeveloperFileTool) { - const action = data.arguments.command || data.tool_name; - const path = data.arguments.path || 'unknown'; - contentDiv.innerHTML = `
action: ${action}
`; - contentDiv.innerHTML += `
path: ${escapeHtml(path)}
`; - if (data.arguments.file_text) { - contentDiv.innerHTML += `
content:
${escapeHtml(data.arguments.file_text.substring(0, 200))}${data.arguments.file_text.length > 200 ? '...' : ''}
`; - } - } else { - contentDiv.innerHTML = `
${JSON.stringify(data.arguments, null, 2)}
`; - } - - toolDiv.appendChild(headerDiv); - toolDiv.appendChild(contentDiv); - - // Add a "running" indicator - const runningDiv = document.createElement('div'); - runningDiv.className = 'tool-running'; - runningDiv.innerHTML = '⏳ Running...'; - toolDiv.appendChild(runningDiv); - - messagesContainer.appendChild(toolDiv); - messagesContainer.scrollTop = messagesContainer.scrollHeight; -} - -// Handle tool responses -function handleToolResponse(data) { - // Remove the "running" indicator from the last tool message - const toolMessages = messagesContainer.querySelectorAll('.tool-message'); - if (toolMessages.length > 0) { - const lastToolMessage = toolMessages[toolMessages.length - 1]; - const runningIndicator = lastToolMessage.querySelector('.tool-running'); - if (runningIndicator) { - runningIndicator.remove(); - } - } - - if (data.is_error) { - const errorDiv = document.createElement('div'); - errorDiv.className = 'message tool-error'; - errorDiv.innerHTML = `Tool Error: ${escapeHtml(data.result.error || 'Unknown error')}`; - messagesContainer.appendChild(errorDiv); - } else { - // Handle successful tool response - if (Array.isArray(data.result)) { - data.result.forEach(content => { - if (content.type === 'text' && content.text) { - const responseDiv = document.createElement('div'); - responseDiv.className = 'message tool-result'; - responseDiv.innerHTML = `
${escapeHtml(content.text)}
`; - messagesContainer.appendChild(responseDiv); - } - }); - } - } - messagesContainer.scrollTop = messagesContainer.scrollHeight; - - // Reset streaming message so next assistant response creates a new message - currentStreamingMessage = null; - - // Show thinking indicator because assistant will likely follow up with explanation - // Only show if we're still processing (cancel button is active) - if (isProcessing) { - addThinkingIndicator(); - } -} - -// Handle tool confirmations -function handleToolConfirmation(data) { - const confirmDiv = document.createElement('div'); - confirmDiv.className = 'message tool-confirmation'; - confirmDiv.innerHTML = ` -
⚠️ Tool Confirmation Required
-
- ${data.tool_name} wants to execute with: -
${JSON.stringify(data.arguments, null, 2)}
-
-
Auto-approved in web mode (UI coming soon)
- `; - messagesContainer.appendChild(confirmDiv); - messagesContainer.scrollTop = messagesContainer.scrollHeight; -} - -// Handle thinking messages -function handleThinking(data) { - // For now, just log thinking messages - console.log('Thinking:', data.message); -} - -// Handle context exceeded -function handleContextExceeded(data) { - const contextDiv = document.createElement('div'); - contextDiv.className = 'message context-warning'; - contextDiv.innerHTML = ` -
⚠️ Context Length Exceeded
-
${escapeHtml(data.message)}
-
Auto-summarizing conversation...
- `; - messagesContainer.appendChild(contextDiv); - messagesContainer.scrollTop = messagesContainer.scrollHeight; -} - -// Handle cancelled operation -function handleCancelled(data) { - removeThinkingIndicator(); - resetSendButton(); - - const cancelDiv = document.createElement('div'); - cancelDiv.className = 'message system-message cancelled'; - cancelDiv.innerHTML = `${escapeHtml(data.message)}`; - messagesContainer.appendChild(cancelDiv); - messagesContainer.scrollTop = messagesContainer.scrollHeight; -} - -// Handle completion of response -function handleComplete(data) { - removeThinkingIndicator(); - resetSendButton(); - // Finalize any streaming message - if (currentStreamingMessage) { - currentStreamingMessage = null; - } -} - -// Reset send button to normal state -function resetSendButton() { - isProcessing = false; - sendButton.textContent = 'Send'; - sendButton.classList.remove('cancel-mode'); -} - -// Escape HTML to prevent XSS -function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - -// Send message or cancel -function sendMessage() { - if (isProcessing) { - // Cancel the current operation - socket.send(JSON.stringify({ - type: 'cancel', - session_id: sessionId - })); - return; - } - - const message = messageInput.value.trim(); - if (!message || !isConnected) return; - - // Add user message to chat - addMessage(message, 'user', Date.now()); - - // Clear input - messageInput.value = ''; - messageInput.style.height = 'auto'; - - // Add thinking indicator - addThinkingIndicator(); - - // Update button to show cancel - isProcessing = true; - sendButton.textContent = 'Cancel'; - sendButton.classList.add('cancel-mode'); - - // Send message through WebSocket - socket.send(JSON.stringify({ - type: 'message', - content: message, - session_id: sessionId, - timestamp: Date.now() - })); -} - -// Handle suggestion pill clicks -function sendSuggestion(text) { - if (!isConnected || isProcessing) return; - - messageInput.value = text; - sendMessage(); -} - -// Load session history if the session exists (like --resume in CLI) -async function loadSessionIfExists() { - try { - const response = await fetch(`/api/sessions/${sessionId}`); - if (response.ok) { - const sessionData = await response.json(); - if (sessionData.messages && sessionData.messages.length > 0) { - // Remove welcome message since we're resuming - const welcomeMessage = messagesContainer.querySelector('.welcome-message'); - if (welcomeMessage) { - welcomeMessage.remove(); - } - - // Display session resumed message - const resumeDiv = document.createElement('div'); - resumeDiv.className = 'message system-message'; - resumeDiv.innerHTML = `Session resumed: ${sessionData.messages.length} messages loaded`; - messagesContainer.appendChild(resumeDiv); - - // Update page title with session description if available - if (sessionData.metadata && sessionData.metadata.description) { - document.title = `goose chat - ${sessionData.metadata.description}`; - } - - messagesContainer.scrollTop = messagesContainer.scrollHeight; - } - } - } catch (error) { - console.log('No existing session found or error loading:', error); - // This is fine - just means it's a new session - } -} - - -// Event listeners -sendButton.addEventListener('click', sendMessage); - -messageInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - sendMessage(); - } -}); - -// Auto-resize textarea -messageInput.addEventListener('input', () => { - messageInput.style.height = 'auto'; - messageInput.style.height = messageInput.scrollHeight + 'px'; -}); - -// Initialize WebSocket connection -connectWebSocket(); - -// Read 'q' parameter from URL and set it to the message input -function getQueryParam() { - const urlParams = new URLSearchParams(window.location.search); - const queryParam = urlParams.get('q'); - if (queryParam) { - messageInput.value = queryParam; - urlParams.delete('q'); - - let newUrl = window.location.pathname; - if (urlParams.toString()) { - newUrl = `${window.location.pathname}?${urlParams.toString()}`; - } - window.history.replaceState({}, '', newUrl); - } -} - -getQueryParam(); - -// Focus on input -messageInput.focus(); - -// Update session title -function updateSessionTitle() { - const titleElement = document.getElementById('session-title'); - // Just show "goose chat" - no need to show session ID - titleElement.textContent = 'goose chat'; -} - -// Update title on load -updateSessionTitle(); diff --git a/crates/goose-cli/static/style.css b/crates/goose-cli/static/style.css deleted file mode 100644 index 06624e1d6b..0000000000 --- a/crates/goose-cli/static/style.css +++ /dev/null @@ -1,490 +0,0 @@ -:root { - /* Dark theme colors (matching the dark.png) */ - --bg-primary: #000000; - --bg-secondary: #0a0a0a; - --bg-tertiary: #1a1a1a; - --text-primary: #ffffff; - --text-secondary: #a0a0a0; - --text-muted: #666666; - --border-color: #333333; - --border-subtle: #1a1a1a; - --accent-color: #ffffff; - --accent-hover: #f0f0f0; - --user-bg: #1a1a1a; - --assistant-bg: #0a0a0a; - --input-bg: #0a0a0a; - --input-border: #333333; - --button-bg: #ffffff; - --button-text: #000000; - --button-hover: #e0e0e0; - --pill-bg: transparent; - --pill-border: #333333; - --pill-hover: #1a1a1a; - --tool-bg: #0f0f0f; - --code-bg: #0f0f0f; -} - -/* Light theme */ -@media (prefers-color-scheme: light) { - :root { - --bg-primary: #ffffff; - --bg-secondary: #fafafa; - --bg-tertiary: #f5f5f5; - --text-primary: #000000; - --text-secondary: #666666; - --text-muted: #999999; - --border-color: #e1e5e9; - --border-subtle: #f0f0f0; - --accent-color: #000000; - --accent-hover: #333333; - --user-bg: #f0f0f0; - --assistant-bg: #fafafa; - --input-bg: #ffffff; - --input-border: #e1e5e9; - --button-bg: #000000; - --button-text: #ffffff; - --button-hover: #333333; - --pill-bg: #f5f5f5; - --pill-border: #e1e5e9; - --pill-hover: #e8eaed; - --tool-bg: #f8f9fa; - --code-bg: #f5f5f5; - } - - header h1::before { - background-image: url('/static/img/logo_light.png'); - } -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - background-color: var(--bg-primary); - color: var(--text-primary); - line-height: 1.5; - height: 100vh; - overflow: hidden; - font-size: 14px; -} - -.container { - display: flex; - flex-direction: column; - height: 100vh; - max-width: 100%; - margin: 0 auto; -} - -header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 1.5rem; - background-color: var(--bg-primary); - border-bottom: 1px solid var(--border-subtle); -} - -header h1 { - font-size: 1.25rem; - font-weight: 600; - display: flex; - align-items: center; - gap: 0.75rem; -} - -header h1::before { - content: ""; - width: 32px; - height: 32px; - background-image: url('/static/img/logo_dark.png'); - background-size: contain; - background-repeat: no-repeat; - background-position: center; - display: inline-block; -} - -.status { - font-size: 0.75rem; - color: var(--text-secondary); - padding: 0.25rem 0.75rem; - border-radius: 1rem; - background-color: var(--bg-secondary); - border: 1px solid var(--border-color); -} - -.status.connected { - color: #10b981; - border-color: #10b981; - background-color: rgba(16, 185, 129, 0.1); -} - -.status.disconnected { - color: #ef4444; - border-color: #ef4444; - background-color: rgba(239, 68, 68, 0.1); -} - -.chat-container { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.messages { - flex: 1; - overflow-y: auto; - padding: 2rem; - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.welcome-message { - text-align: center; - padding: 4rem 2rem; - color: var(--text-secondary); -} - -.welcome-message h2 { - font-size: 1.5rem; - margin-bottom: 1rem; - color: var(--text-primary); - font-weight: 600; -} - -.welcome-message p { - font-size: 1rem; - margin-bottom: 2rem; -} - -/* Suggestion pills like in the design */ -.suggestion-pills { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - justify-content: center; - margin-top: 2rem; -} - -.suggestion-pill { - padding: 0.75rem 1.25rem; - background-color: var(--pill-bg); - border: 1px solid var(--pill-border); - border-radius: 2rem; - color: var(--text-primary); - font-size: 0.875rem; - cursor: pointer; - transition: all 0.2s ease; - text-decoration: none; - display: inline-block; -} - -.suggestion-pill:hover { - background-color: var(--pill-hover); - border-color: var(--border-color); -} - -.message { - max-width: 80%; - padding: 1rem 1.25rem; - border-radius: 1rem; - word-wrap: break-word; - position: relative; -} - -.message.user { - align-self: flex-end; - background-color: var(--user-bg); - margin-left: auto; - border: 1px solid var(--border-subtle); -} - -.message.assistant { - align-self: flex-start; - background-color: var(--assistant-bg); - border: 1px solid var(--border-subtle); -} - -.message-content { - flex: 1; - margin-bottom: 0.5rem; -} - -.message .timestamp { - font-size: 0.6875rem; - color: var(--text-muted); - margin-top: 0.5rem; - opacity: 0.7; -} - -.message pre { - background-color: var(--code-bg); - padding: 0.75rem; - border-radius: 0.5rem; - overflow-x: auto; - margin: 0.75rem 0; - border: 1px solid var(--border-color); - font-size: 0.8125rem; -} - -.message code { - background-color: var(--code-bg); - padding: 0.125rem 0.375rem; - border-radius: 0.25rem; - font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace; - font-size: 0.8125rem; - border: 1px solid var(--border-color); -} - -.input-container { - display: flex; - gap: 0.75rem; - padding: 1.5rem; - background-color: var(--bg-primary); - border-top: 1px solid var(--border-subtle); -} - -#message-input { - flex: 1; - padding: 0.875rem 1rem; - border: 1px solid var(--input-border); - border-radius: 0.75rem; - background-color: var(--input-bg); - color: var(--text-primary); - font-family: inherit; - font-size: 0.875rem; - resize: none; - min-height: 2.75rem; - max-height: 8rem; - outline: none; - transition: border-color 0.2s ease; -} - -#message-input:focus { - border-color: var(--accent-color); -} - -#message-input::placeholder { - color: var(--text-muted); -} - -#send-button { - padding: 0.875rem 1.5rem; - background-color: var(--button-bg); - color: var(--button-text); - border: none; - border-radius: 0.75rem; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - min-width: 4rem; -} - -#send-button:hover { - background-color: var(--button-hover); - transform: translateY(-1px); -} - -#send-button:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none; -} - -#send-button.cancel-mode { - background-color: #ef4444; - color: #ffffff; -} - -#send-button.cancel-mode:hover { - background-color: #dc2626; -} - -/* Scrollbar styling */ -.messages::-webkit-scrollbar { - width: 6px; -} - -.messages::-webkit-scrollbar-track { - background: transparent; -} - -.messages::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 3px; -} - -.messages::-webkit-scrollbar-thumb:hover { - background: var(--text-secondary); -} - -/* Tool call styling */ -.tool-message, .tool-result, .tool-error, .tool-confirmation, .context-warning { - background-color: var(--tool-bg); - border: 1px solid var(--border-color); - border-radius: 0.75rem; - padding: 1rem; - margin: 0.75rem 0; - max-width: 90%; -} - -.tool-header, .tool-confirm-header, .context-header { - font-weight: 600; - color: var(--accent-color); - margin-bottom: 0.75rem; - font-size: 0.875rem; -} - -.tool-content { - font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace; - font-size: 0.8125rem; - color: var(--text-secondary); -} - -.tool-param { - margin: 0.5rem 0; -} - -.tool-param strong { - color: var(--text-primary); -} - -.tool-running { - font-size: 0.8125rem; - color: var(--accent-color); - margin-top: 0.75rem; - font-style: italic; -} - -.tool-error { - border-color: #ef4444; - background-color: rgba(239, 68, 68, 0.05); -} - -.tool-error strong { - color: #ef4444; -} - -.tool-result { - background-color: var(--tool-bg); - border-left: 3px solid var(--accent-color); - margin-left: 1.5rem; - border-radius: 0.5rem; -} - -.tool-confirmation { - border-color: #f59e0b; - background-color: rgba(245, 158, 11, 0.05); -} - -.tool-confirm-note, .context-note { - font-size: 0.75rem; - color: var(--text-muted); - margin-top: 0.75rem; - font-style: italic; -} - -.context-warning { - border-color: #f59e0b; - background-color: rgba(245, 158, 11, 0.05); -} - -.context-header { - color: #f59e0b; -} - -.system-message { - text-align: center; - color: var(--text-secondary); - font-style: italic; - margin: 1rem 0; - font-size: 0.875rem; -} - -.cancelled { - color: #ef4444; -} - -/* Thinking indicator */ -.thinking-message { - display: flex; - align-items: center; - gap: 0.75rem; - color: var(--text-secondary); - font-style: italic; - padding: 1rem 1.25rem; - background-color: var(--bg-secondary); - border-radius: 1rem; - border: 1px solid var(--border-subtle); - max-width: 80%; - font-size: 0.875rem; -} - -.thinking-dots { - display: flex; - gap: 0.25rem; -} - -.thinking-dots span { - width: 6px; - height: 6px; - border-radius: 50%; - background-color: var(--text-secondary); - animation: thinking-bounce 1.4s infinite ease-in-out both; -} - -.thinking-dots span:nth-child(1) { - animation-delay: -0.32s; -} - -.thinking-dots span:nth-child(2) { - animation-delay: -0.16s; -} - -@keyframes thinking-bounce { - 0%, 80%, 100% { - transform: scale(0.6); - opacity: 0.5; - } - 40% { - transform: scale(1); - opacity: 1; - } -} - -/* Keep the old loading indicator for backwards compatibility */ -.loading-message { - display: none; -} - -/* Responsive design */ -@media (max-width: 768px) { - .messages { - padding: 1rem; - gap: 1rem; - } - - .message { - max-width: 90%; - padding: 0.875rem 1rem; - } - - .input-container { - padding: 1rem; - } - - header { - padding: 0.75rem 1rem; - } - - .welcome-message { - padding: 2rem 1rem; - } -} \ No newline at end of file diff --git a/documentation/docs/guides/goose-cli-commands.md b/documentation/docs/guides/goose-cli-commands.md index 50d026632c..1f3144af57 100644 --- a/documentation/docs/guides/goose-cli-commands.md +++ b/documentation/docs/guides/goose-cli-commands.md @@ -591,57 +591,6 @@ Choose one of your projects to start working on. goose projects ``` ---- - -### Interface - -#### web -Start a new session in goose Web, a lightweight web-based interface launched via the CLI that mirrors the desktop app's chat experience. - -goose Web is particularly useful when: -- You want to access goose with a graphical interface without installing the desktop app -- You need to use goose from different devices, including mobile -- You're working in an environment where installing desktop apps isn't practical - -:::warning -Don't expose the web interface to the internet without proper security measures. -::: - -**Options:** -- **`-p, --port `**: Port number to run the web server on. Default is `3000` -- **`--host `**: Host to bind the web server to. Default is `127.0.0.1` -- **`--open`**: Automatically open the browser when the server starts -- **`--auth-token `**: Require a password to access the web interface - -**Usage:** -```bash -# Start web interface at `http://127.0.0.1:3000` and open the browser -goose web --open - -# Start web interface at `http://127.0.0.1:8080` -goose web --port 8080 - -# Start web interface accessible from local network at `http://192.168.1.7:8080` -goose web --host 192.168.1.7 --port 8080 - -# Start web interface with authentication required -goose web --auth-token -``` - -:::info -Use `Ctrl+C` to stop the server. -::: - -**Limitations:** - -While the web interface provides most core features, be aware of these limitations: -- Some file system operations may require additional confirmation -- Extension management must be done through the CLI -- Certain tool interactions might need extra setup -- Configuration changes require a server restart - - - --- ### Terminal Integration diff --git a/documentation/docs/guides/sessions/session-management.md b/documentation/docs/guides/sessions/session-management.md index d5d112615a..93d81ea813 100644 --- a/documentation/docs/guides/sessions/session-management.md +++ b/documentation/docs/guides/sessions/session-management.md @@ -76,10 +76,6 @@ In your first session, goose prompts you to [set up an LLM (Large Language Model goose session ``` - If you want to interact with goose in a web-based chat interface, start a session with the [`web`](/docs/guides/goose-cli-commands#web) command: - ```sh - goose web --open - ``` diff --git a/documentation/docs/quickstart.md b/documentation/docs/quickstart.md index 8eb3a027d0..565633f66f 100644 --- a/documentation/docs/quickstart.md +++ b/documentation/docs/quickstart.md @@ -230,13 +230,6 @@ Sessions are single, continuous conversations between you and goose. Let's start goose session ``` - :::tip goose Web - CLI users can also start a session in [goose Web](/docs/guides/goose-cli-commands#web), a web-based chat interface: - ```sh - goose web --open - ``` - ::: - diff --git a/scripts/test_web.sh b/scripts/test_web.sh deleted file mode 100644 index adfd4d952f..0000000000 --- a/scripts/test_web.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Test script for Goose Web Interface - -echo "Testing Goose Web Interface..." -echo "================================" - -# Start the web server in the background -echo "Starting web server on port 8080..." -./target/debug/goose web --port 8080 & -SERVER_PID=$! - -# Wait for server to start -sleep 2 - -# Test the health endpoint -echo -e "\nTesting health endpoint:" -curl -s http://localhost:8080/api/health | jq . - -# Open browser (optional) -# open http://localhost:8080 - -echo -e "\nWeb server is running at http://localhost:8080" -echo "Press Ctrl+C to stop the server" - -# Wait for user to stop -wait $SERVER_PID \ No newline at end of file