diff --git a/crates/goose-cli/src/agents/mock_agent.rs b/crates/goose-cli/src/agents/mock_agent.rs index 4ddcece28..bb7e1b091 100644 --- a/crates/goose-cli/src/agents/mock_agent.rs +++ b/crates/goose-cli/src/agents/mock_agent.rs @@ -10,7 +10,7 @@ pub struct MockAgent; #[async_trait] impl Agent for MockAgent { fn add_system(&mut self, _system: Box) { - () + } async fn reply(&self, _messages: &[Message]) -> Result>> { diff --git a/crates/goose-cli/src/commands/session.rs b/crates/goose-cli/src/commands/session.rs index e89fb5ac1..2f2ee2cb3 100644 --- a/crates/goose-cli/src/commands/session.rs +++ b/crates/goose-cli/src/commands/session.rs @@ -1,8 +1,8 @@ +use console::style; use rand::{distributions::Alphanumeric, Rng}; use std::path::{Path, PathBuf}; use goose::agent::Agent; -use goose::models::message::Message; use goose::providers::factory; use crate::commands::expected_config::get_recommended_models; @@ -11,7 +11,6 @@ use crate::profile::{ }; use crate::prompt::cliclack::CliclackPrompt; use crate::prompt::rustyline::RustylinePrompt; -use crate::prompt::thinking::get_random_goose_action; use crate::prompt::Prompt; use crate::session::{ensure_session_dir, Session}; @@ -48,7 +47,7 @@ pub fn build_session<'a>( // TODO: Odd to be prepping the provider rather than having that done in the agent? let provider = factory::get_provider(provider_config).unwrap(); let agent = Box::new(Agent::new(provider)); - let mut prompt = match std::env::var("GOOSE_INPUT") { + let prompt = match std::env::var("GOOSE_INPUT") { Ok(val) => match val.as_str() { "cliclack" => Box::new(CliclackPrompt::new()) as Box, "rustyline" => Box::new(RustylinePrompt::new()) as Box, @@ -57,17 +56,19 @@ pub fn build_session<'a>( Err(_) => Box::new(RustylinePrompt::new()), }; - prompt.render(Box::new(Message::assistant().with_text(format!( - r#"{}... - Provider: {} - Model: {} - Session file: {}"#, - get_random_goose_action(), - loaded_profile.provider, - loaded_profile.model, - session_file.display() - )))); - + println!( + "{} {} {} {} {}", + style("starting session |").dim(), + style("provider:").dim(), + style(loaded_profile.provider).cyan().dim(), + style("model:").dim(), + style(loaded_profile.model).cyan().dim(), + ); + println!( + " {} {}", + style("logging to").dim(), + style(session_file.display()).dim().cyan(), + ); Box::new(Session::new(agent, prompt, session_file)) } diff --git a/crates/goose-cli/src/prompt.rs b/crates/goose-cli/src/prompt.rs index 4ab52eb62..b564acc09 100644 --- a/crates/goose-cli/src/prompt.rs +++ b/crates/goose-cli/src/prompt.rs @@ -12,22 +12,9 @@ pub trait Prompt { fn hide_busy(&self); fn close(&self); fn goose_ready(&self) { - self.draw_goose(); - } - - fn draw_goose(&self) { - println!( - r#" __ - - - - - ( 0)> < honk! > - || - - - - - || - __||_ - <=/ \=> - \_____/ - | | - ^ ^ - "# - ); + println!("\n"); + println!("Goose is running! Enter your instructions, or try asking what goose can do."); + println!("\n"); } } diff --git a/crates/goose-cli/src/prompt/thinking.rs b/crates/goose-cli/src/prompt/thinking.rs index 51d415409..cc187522e 100644 --- a/crates/goose-cli/src/prompt/thinking.rs +++ b/crates/goose-cli/src/prompt/thinking.rs @@ -1,50 +1,5 @@ use rand::seq::SliceRandom; -/// List of goose-specific actions for noting goose readiness. -pub const GOOSE_ACTIONS: &[&str] = &[ - "Spreading wings", - "Honking thoughtfully", - "Waddling to conclusions", - "Flapping wings excitedly", - "Preening code feathers", - "Gathering digital breadcrumbs", - "Paddling through data", - "Migrating thoughts", - "Nesting ideas", - "Squawking calculations", - "Ruffling algorithmic feathers", - "Pecking at problems", - "Stretching webbed feet", - "Foraging for solutions", - "Grooming syntax", - "Building digital nest", - "Patrolling the codebase", - "Gosling about", - "Strutting with purpose", - "Diving for answers", - "Herding bytes", - "Molting old code", - "Swimming through streams", - "Goose-stepping through logic", - "Synchronizing flock algorithms", - "Navigating code marshes", - "Incubating brilliant ideas", - "Arranging feathers recursively", - "Gliding through branches", - "Migrating to better solutions", - "Nesting functions carefully", - "Hatching clever solutions", - "Preening parse trees", - "Flying through functions", - "Gathering syntax seeds", - "Webbing connections", - "Flocking to optimizations", - "Paddling through protocols", - "Honking success signals", - "Waddling through workflows", - "Nesting in neural networks", -]; - /// Extended list of playful thinking messages including both goose and general AI actions pub const THINKING_MESSAGES: &[&str] = &[ "Thinking", @@ -268,10 +223,3 @@ pub fn get_random_thinking_message() -> &'static str { .choose(&mut rand::thread_rng()) .unwrap_or(&THINKING_MESSAGES[0]) } - -/// Returns a random goose-specific action -pub fn get_random_goose_action() -> &'static str { - GOOSE_ACTIONS - .choose(&mut rand::thread_rng()) - .unwrap_or(&GOOSE_ACTIONS[0]) -} diff --git a/crates/goose-cli/src/session.rs b/crates/goose-cli/src/session.rs index 29ba6f8ff..6f0ec450e 100644 --- a/crates/goose-cli/src/session.rs +++ b/crates/goose-cli/src/session.rs @@ -99,6 +99,7 @@ impl<'a> Session<'a> { pub async fn start(&mut self) -> Result<(), Box> { self.setup_session(); + self.prompt.goose_ready(); loop { let input = self.prompt.get_input().unwrap(); @@ -202,14 +203,8 @@ impl<'a> Session<'a> { fn setup_session(&mut self) { let system = Box::new(DeveloperSystem::new()); self.agent.add_system(system); - self.prompt - .render(raw_message("Connected developer system.")); let goosehints_system = Box::new(GooseHintsSystem::new()); self.agent.add_system(goosehints_system); - self.prompt - .render(raw_message("Connected .goosehints system.")); - - self.prompt.goose_ready(); } fn close_session(&mut self) { diff --git a/crates/goose-server/src/routes/reply.rs b/crates/goose-server/src/routes/reply.rs index 36f8f55dd..d9b3aa3e6 100644 --- a/crates/goose-server/src/routes/reply.rs +++ b/crates/goose-server/src/routes/reply.rs @@ -139,7 +139,6 @@ fn convert_messages(incoming: Vec) -> Vec { struct ProtocolFormatter; impl ProtocolFormatter { - fn format_text(text: &str) -> String { let encoded_text = serde_json::to_string(text).unwrap_or_else(|_| String::new()); format!("0:{}\n", encoded_text) @@ -236,8 +235,8 @@ async fn stream_message( MessageContent::Text(text) => { for line in text.text.lines() { let modified_line = format!("{}\n", line); - tx.send(ProtocolFormatter::format_text(&modified_line)).await?; - + tx.send(ProtocolFormatter::format_text(&modified_line)) + .await?; } } MessageContent::Image(_) => { diff --git a/crates/goose/src/memory.rs b/crates/goose/src/memory.rs index 511c7c3ed..e522809f4 100644 --- a/crates/goose/src/memory.rs +++ b/crates/goose/src/memory.rs @@ -111,7 +111,6 @@ impl MemoryManager { if let Some(first_line) = lines.next() { if first_line.starts_with('#') { let tags = first_line[1..] - .trim() .split_whitespace() .map(String::from) .collect::>(); @@ -236,8 +235,7 @@ pub fn execute_tool_call(tool_call: ToolCall) -> Result { .map(|v| v.as_str().unwrap()) .collect(); let is_global = tool_call.arguments["is_global"].as_bool().unwrap(); - let _result = - MemoryManager::new()?.remember("context", category, data, &tags, is_global)?; + MemoryManager::new()?.remember("context", category, data, &tags, is_global)?; Ok(format!("Stored memory in category: {}", category)) } "retrieve_memories" => { @@ -274,6 +272,12 @@ pub struct MemorySystem { instructions: String, } +impl Default for MemorySystem { + fn default() -> Self { + Self::new() + } +} + impl MemorySystem { pub fn new() -> Self { let memory_manager = MemoryManager::new().expect("Failed to create MemoryManager"); diff --git a/crates/goose/src/providers/oauth.rs b/crates/goose/src/providers/oauth.rs index 6cedd6df2..0d6e5104c 100644 --- a/crates/goose/src/providers/oauth.rs +++ b/crates/goose/src/providers/oauth.rs @@ -40,7 +40,7 @@ impl TokenCache { let hash = format!("{:x}", hasher.finalize()); fs::create_dir_all(get_base_path()).unwrap(); - let cache_path = PathBuf::from(get_base_path()).join(format!("{}.json", hash)); + let cache_path = get_base_path().join(format!("{}.json", hash)); Self { cache_path } } diff --git a/ui/desktop/entitlements.plist b/ui/desktop/entitlements.plist new file mode 100644 index 000000000..856e308d7 --- /dev/null +++ b/ui/desktop/entitlements.plist @@ -0,0 +1,30 @@ + + + + + com.apple.security.files.user-selected.read-write + + com.apple.security.files.downloads.read-write + + com.apple.security.files.documents.read-write + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.screen-recording + + com.apple.security.automation.apple-events + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.device.camera + + com.apple.security.device.microphone + + + \ No newline at end of file diff --git a/ui/desktop/forge.config.ts b/ui/desktop/forge.config.ts index d95f89034..fe2a4991d 100644 --- a/ui/desktop/forge.config.ts +++ b/ui/desktop/forge.config.ts @@ -6,6 +6,11 @@ module.exports = { asar: true, extraResource: ['src/bin', 'src/images'], icon: 'src/images/icon', + osxSign: { + entitlements: 'entitlements.plist', + 'entitlements-inherit': 'entitlements.plist', + 'gatekeeper-assess': false, + }, }, rebuildConfig: {}, makers: [ diff --git a/ui/desktop/package.json b/ui/desktop/package.json index 322d23e7e..d6a239f8c 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -9,8 +9,10 @@ "package": "electron-forge package", "make": "electron-forge make", "publish": "electron-forge publish", + "debug": "echo 'run --remote-debugging-port=8315' && lldb out/Goose-darwin-arm64/Goose.app", "test-e2e": "electron-forge start > /tmp/out.txt & ELECTRON_PID=$! && sleep 12 && if grep -q 'renderer: ChatWindow loaded' /tmp/out.txt; then echo 'process is running'; pkill -f electron; else echo 'not starting correctly'; cat /tmp/out.txt; pkill -f electron; exit 1; fi", - "sign-macos": "cd ./out/Goose-darwin-arm64 && codesign --deep --force --verify --sign \"Developer ID Application: Michael Neale (W2L75AE9HQ)\" Goose.app && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose.zip" + "sign-macos": "cd ./out/Goose-darwin-arm64 && codesign --deep --force --verify --sign \"Developer ID Application: Michael Neale (W2L75AE9HQ)\" Goose.app && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose.zip", + "sign-verify": " cd ./out/Goose-darwin-arm64 && codesign --verify --deep --strict Goose.app && codesign -d --entitlements :- Goose.app" }, "devDependencies": { "@electron-forge/cli": "^7.5.0", diff --git a/ui/desktop/src/ChatWindow.tsx b/ui/desktop/src/ChatWindow.tsx index e0cf7d852..2fd81c22d 100644 --- a/ui/desktop/src/ChatWindow.tsx +++ b/ui/desktop/src/ChatWindow.tsx @@ -103,7 +103,7 @@ function ChatContent({ setWorking(Working.Idle); const promptTemplates = [ - "You are a simple classifier that takes content and decides if it is asking for input from a person before continuing, or not. If it is a question very clearly, return QUESTION, otherwise READY. ### Message Content:\n" + message.content + "\nYou must provide a response strictly limited to one of the following two words: QUESTION, READY. No other words, phrases, or explanations are allowed. Response:", + "You are a simple classifier that takes content and decides if it is asking for input from a person before continuing if there is more to do, or not. These are questions on if a course of action should proceeed or not, or approval is needed. If it is a question very clearly, return QUESTION, otherwise READY. If it of the form of 'anything else I can do?' sort of question, return READY as that is not the sort of question we are looking for. ### Message Content:\n" + message.content + "\nYou must provide a response strictly limited to one of the following two words: QUESTION, READY. No other words, phrases, or explanations are allowed. Response:", "You are a simple classifier that takes content and decides if it a list of options or plans to choose from, or not a list of options to choose from It is IMPORTANT that you really know this is a choice, just not numbered steps. If it is a list of options and you are 95% sure, return OPTIONS, otherwise return NO. ### Message Content:\n" + message.content + "\nYou must provide a response strictly limited to one of the following two words:OPTIONS, NO. No other words, phrases, or explanations are allowed. Response:", "If the content is list of distinct options or plans of action to choose from, and not just a list of things, but clearly a list of things to choose one from, taking into account the Message Content alone, try to format it in a json array, like this JSON array of objects of the form optionTitle:string, optionDescription:string (markdown).\n If is not a list of options or plans to choose from, then return empty list.\n ### Message Content:\n" + message.content + "\n\nYou must provide a response strictly as json in the format descriribed. No other words, phrases, or explanations are allowed. Response:", ]; diff --git a/ui/desktop/src/components/GooseMessage.tsx b/ui/desktop/src/components/GooseMessage.tsx index 8e0a6c46d..45a73f730 100644 --- a/ui/desktop/src/components/GooseMessage.tsx +++ b/ui/desktop/src/components/GooseMessage.tsx @@ -31,7 +31,7 @@ export default function GooseMessage({ message, metadata, messages, append }: Go {message.content && (
- {message.content} + {message.content}
)} @@ -43,8 +43,8 @@ export default function GooseMessage({ message, metadata, messages, append }: Go )} - {/* append false && to turn this off */} - {metadata && ( + {/* Currently disabled */} + {false && metadata && (
); -} \ No newline at end of file +} diff --git a/ui/desktop/src/components/Input.tsx b/ui/desktop/src/components/Input.tsx index 3efe57254..598767a92 100644 --- a/ui/desktop/src/components/Input.tsx +++ b/ui/desktop/src/components/Input.tsx @@ -1,27 +1,70 @@ -import React from 'react'; -import { Button } from './ui/button' -import Send from './ui/Send' +import React, { useRef, useState, useEffect } from 'react'; +import { Button } from './ui/button'; +import Send from './ui/Send'; interface InputProps { handleSubmit: (e: React.FormEvent) => void; - handleInputChange: (e: React.ChangeEvent) => void; + handleInputChange: (e: React.ChangeEvent) => void; input: string; disabled?: boolean; } export default function Input({ handleSubmit, handleInputChange, input, disabled = false }: InputProps) { + const [value, setValue] = useState(input); + const textAreaRef = useRef(null); + + const useAutosizeTextArea = (textAreaRef: HTMLTextAreaElement | null, value: string) => { + useEffect(() => { + if (textAreaRef) { + textAreaRef.style.height = "0px"; // Reset height + const scrollHeight = textAreaRef.scrollHeight; + textAreaRef.style.height = Math.min(scrollHeight, maxHeight) + "px"; + } + }, [textAreaRef, value]); + }; + + const minHeight = "1rem"; + const maxHeight = 10 * 24; + + useAutosizeTextArea(textAreaRef.current, value); + + const handleChange = (evt: React.ChangeEvent) => { + const val = evt.target?.value; + setValue(val); + handleInputChange(evt); + }; + + const handleKeyDown = (evt: React.KeyboardEvent) => { + if (evt.key === 'Enter' && !evt.shiftKey) { + evt.preventDefault(); + handleSubmit(new CustomEvent('submit', { detail: { value } })); // Trigger custom form submit + setValue(''); // Clear textarea + } + }; + return ( -
- { + handleSubmit(e); + setValue(''); + }} className="flex relative bg-white h-auto px-[16px] pr-[38px] py-[1rem] rounded-b-2xl"> +