mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
feat: @goose in terminal (native terminal support) (#5887)
Co-authored-by: Bradley Axen <baxen@squareup.com> Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Generated
+1
-1
@@ -2025,7 +2025,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -13,6 +13,9 @@ use crate::commands::configure::handle_configure;
|
||||
use crate::commands::info::handle_info;
|
||||
use crate::commands::project::{handle_project_default, handle_projects_interactive};
|
||||
use crate::commands::recipe::{handle_deeplink, handle_list, handle_open, handle_validate};
|
||||
use crate::commands::term::{
|
||||
handle_term_info, handle_term_init, handle_term_log, handle_term_run, Shell,
|
||||
};
|
||||
|
||||
use crate::commands::schedule::{
|
||||
handle_schedule_add, handle_schedule_cron_help, handle_schedule_list, handle_schedule_remove,
|
||||
@@ -829,6 +832,84 @@ enum Command {
|
||||
#[arg(long, help = "Authentication token to secure the web interface")]
|
||||
auth_token: Option<String>,
|
||||
},
|
||||
|
||||
/// Terminal-integrated session (one session per terminal)
|
||||
#[command(
|
||||
about = "Terminal-integrated goose session",
|
||||
long_about = "Runs a goose session tied to your terminal window.\n\
|
||||
Each terminal maintains its own persistent session that resumes automatically.\n\n\
|
||||
Setup:\n \
|
||||
eval \"$(goose term init zsh)\" # Add to ~/.zshrc\n\n\
|
||||
Usage:\n \
|
||||
goose term run \"list files in this directory\"\n \
|
||||
@goose \"create a python script\" # using alias\n \
|
||||
@g \"quick question\" # short alias"
|
||||
)]
|
||||
Term {
|
||||
#[command(subcommand)]
|
||||
command: TermCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum TermCommand {
|
||||
/// Print shell initialization script
|
||||
#[command(
|
||||
about = "Print shell initialization script",
|
||||
long_about = "Prints shell configuration to set up terminal-integrated sessions.\n\
|
||||
Each terminal gets a persistent goose session that automatically resumes.\n\n\
|
||||
Setup:\n \
|
||||
echo 'eval \"$(goose term init zsh)\"' >> ~/.zshrc\n \
|
||||
source ~/.zshrc\n\n\
|
||||
With --default (anything typed that isn't a command goes to goose):\n \
|
||||
echo 'eval \"$(goose term init zsh --default)\"' >> ~/.zshrc"
|
||||
)]
|
||||
Init {
|
||||
/// Shell type (bash, zsh, fish, powershell)
|
||||
#[arg(value_enum)]
|
||||
shell: Shell,
|
||||
|
||||
#[arg(short, long, help = "Name for the terminal session")]
|
||||
name: Option<String>,
|
||||
|
||||
/// Make goose the default handler for unknown commands
|
||||
#[arg(
|
||||
long = "default",
|
||||
help = "Make goose the default handler for unknown commands",
|
||||
long_help = "When enabled, anything you type that isn't a valid command will be sent to goose. Only supported for zsh and bash."
|
||||
)]
|
||||
default: bool,
|
||||
},
|
||||
|
||||
/// Log a shell command (called by shell hook)
|
||||
#[command(about = "Log a shell command to the session", hide = true)]
|
||||
Log {
|
||||
/// The command that was executed
|
||||
command: String,
|
||||
},
|
||||
|
||||
/// Run a prompt in the terminal session
|
||||
#[command(
|
||||
about = "Run a prompt in the terminal session",
|
||||
long_about = "Run a prompt in the terminal-integrated session.\n\n\
|
||||
Examples:\n \
|
||||
goose term run list files in this directory\n \
|
||||
@goose list files # using alias\n \
|
||||
@g why did that fail # short alias"
|
||||
)]
|
||||
Run {
|
||||
/// The prompt to send to goose (multiple words allowed without quotes)
|
||||
#[arg(required = true, num_args = 1..)]
|
||||
prompt: Vec<String>,
|
||||
},
|
||||
|
||||
/// Print session info for prompt integration
|
||||
#[command(
|
||||
about = "Print session info for prompt integration",
|
||||
long_about = "Prints compact session info (token usage, model) for shell prompt integration.\n\
|
||||
Example output: ●○○○○ sonnet"
|
||||
)]
|
||||
Info,
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Debug)]
|
||||
@@ -874,6 +955,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
||||
Some(Command::Bench { .. }) => "bench",
|
||||
Some(Command::Recipe { .. }) => "recipe",
|
||||
Some(Command::Web { .. }) => "web",
|
||||
Some(Command::Term { .. }) => "term",
|
||||
None => "default_session",
|
||||
};
|
||||
|
||||
@@ -1387,6 +1469,27 @@ pub async fn cli() -> anyhow::Result<()> {
|
||||
crate::commands::web::handle_web(port, host, open, auth_token).await?;
|
||||
return Ok(());
|
||||
}
|
||||
Some(Command::Term { command }) => {
|
||||
match command {
|
||||
TermCommand::Init {
|
||||
shell,
|
||||
name,
|
||||
default,
|
||||
} => {
|
||||
handle_term_init(shell, name, default).await?;
|
||||
}
|
||||
TermCommand::Log { command } => {
|
||||
handle_term_log(command).await?;
|
||||
}
|
||||
TermCommand::Run { prompt } => {
|
||||
handle_term_run(prompt).await?;
|
||||
}
|
||||
TermCommand::Info => {
|
||||
handle_term_info().await?;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
None => {
|
||||
return if !Config::global().exists() {
|
||||
handle_configure().await?;
|
||||
|
||||
@@ -6,5 +6,6 @@ pub mod project;
|
||||
pub mod recipe;
|
||||
pub mod schedule;
|
||||
pub mod session;
|
||||
pub mod term;
|
||||
pub mod update;
|
||||
pub mod web;
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono;
|
||||
use goose::conversation::message::{Message, MessageContent, MessageMetadata};
|
||||
use goose::session::SessionManager;
|
||||
use goose::session::SessionType;
|
||||
use rmcp::model::Role;
|
||||
|
||||
use crate::session::{build_session, SessionBuilderConfig};
|
||||
|
||||
use clap::ValueEnum;
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug)]
|
||||
pub enum Shell {
|
||||
Bash,
|
||||
Zsh,
|
||||
Fish,
|
||||
#[value(alias = "pwsh")]
|
||||
Powershell,
|
||||
}
|
||||
|
||||
struct ShellConfig {
|
||||
script_template: &'static str,
|
||||
command_not_found: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
fn config(&self) -> &'static ShellConfig {
|
||||
match self {
|
||||
Shell::Bash => &BASH_CONFIG,
|
||||
Shell::Zsh => &ZSH_CONFIG,
|
||||
Shell::Fish => &FISH_CONFIG,
|
||||
Shell::Powershell => &POWERSHELL_CONFIG,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static BASH_CONFIG: ShellConfig = ShellConfig {
|
||||
script_template: r#"export GOOSE_SESSION_ID="{session_id}"
|
||||
alias @goose='{goose_bin} term run'
|
||||
alias @g='{goose_bin} term run'
|
||||
|
||||
goose_preexec() {{
|
||||
[[ "$1" =~ ^goose\ term ]] && return
|
||||
[[ "$1" =~ ^(@goose|@g)($|[[:space:]]) ]] && return
|
||||
('{goose_bin}' term log "$1" &) 2>/dev/null
|
||||
}}
|
||||
|
||||
if [[ -z "$goose_preexec_installed" ]]; then
|
||||
goose_preexec_installed=1
|
||||
trap 'goose_preexec "$BASH_COMMAND"' DEBUG
|
||||
fi{command_not_found_handler}"#,
|
||||
command_not_found: Some(
|
||||
r#"
|
||||
|
||||
command_not_found_handle() {{
|
||||
echo "🪿 Command '$1' not found. Asking goose..."
|
||||
'{goose_bin}' term run "$@"
|
||||
return 0
|
||||
}}"#,
|
||||
),
|
||||
};
|
||||
|
||||
static ZSH_CONFIG: ShellConfig = ShellConfig {
|
||||
script_template: r#"export GOOSE_SESSION_ID="{session_id}"
|
||||
alias @goose='{goose_bin} term run'
|
||||
alias @g='{goose_bin} term run'
|
||||
|
||||
goose_preexec() {{
|
||||
[[ "$1" =~ ^goose\ term ]] && return
|
||||
[[ "$1" =~ ^(@goose|@g)($|[[:space:]]) ]] && return
|
||||
('{goose_bin}' term log "$1" &) 2>/dev/null
|
||||
}}
|
||||
|
||||
autoload -Uz add-zsh-hook
|
||||
add-zsh-hook preexec goose_preexec
|
||||
|
||||
if [[ -z "$GOOSE_PROMPT_INSTALLED" ]]; then
|
||||
export GOOSE_PROMPT_INSTALLED=1
|
||||
PROMPT='%F{{cyan}}🪿%f '$PROMPT
|
||||
fi{command_not_found_handler}"#,
|
||||
command_not_found: Some(
|
||||
r#"
|
||||
|
||||
command_not_found_handler() {{
|
||||
echo "🪿 Command '$1' not found. Asking goose..."
|
||||
'{goose_bin}' term run "$@"
|
||||
return 0
|
||||
}}"#,
|
||||
),
|
||||
};
|
||||
|
||||
static FISH_CONFIG: ShellConfig = ShellConfig {
|
||||
script_template: r#"set -gx GOOSE_SESSION_ID "{session_id}"
|
||||
function @goose; {goose_bin} term run $argv; end
|
||||
function @g; {goose_bin} term run $argv; end
|
||||
|
||||
function goose_preexec --on-event fish_preexec
|
||||
string match -q -r '^goose term' -- $argv[1]; and return
|
||||
string match -q -r '^(@goose|@g)($|\s)' -- $argv[1]; and return
|
||||
{goose_bin} term log "$argv[1]" 2>/dev/null &
|
||||
end"#,
|
||||
command_not_found: None,
|
||||
};
|
||||
|
||||
static POWERSHELL_CONFIG: ShellConfig = ShellConfig {
|
||||
script_template: r#"$env:GOOSE_SESSION_ID = "{session_id}"
|
||||
function @goose {{ & '{goose_bin}' term run @args }}
|
||||
function @g {{ & '{goose_bin}' term run @args }}
|
||||
|
||||
Set-PSReadLineKeyHandler -Chord Enter -ScriptBlock {{
|
||||
$line = $null
|
||||
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$null)
|
||||
if ($line -notmatch '^goose term' -and $line -notmatch '^(@goose|@g)($|\s)') {{
|
||||
Start-Job -ScriptBlock {{ & '{goose_bin}' term log $using:line }} | Out-Null
|
||||
}}
|
||||
[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
|
||||
}}"#,
|
||||
command_not_found: None,
|
||||
};
|
||||
|
||||
pub async fn handle_term_init(
|
||||
shell: Shell,
|
||||
name: Option<String>,
|
||||
with_command_not_found: bool,
|
||||
) -> Result<()> {
|
||||
let config = shell.config();
|
||||
|
||||
let working_dir = std::env::current_dir()?;
|
||||
let named_session = if let Some(ref name) = name {
|
||||
let sessions = SessionManager::list_sessions_by_types(&[SessionType::Terminal]).await?;
|
||||
sessions.into_iter().find(|s| s.name == *name)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let session = match named_session {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
let session = SessionManager::create_session(
|
||||
working_dir,
|
||||
"Goose Term Session".to_string(),
|
||||
SessionType::Terminal,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(name) = name {
|
||||
SessionManager::update_session(&session.id)
|
||||
.user_provided_name(name)
|
||||
.apply()
|
||||
.await?;
|
||||
}
|
||||
|
||||
session
|
||||
}
|
||||
};
|
||||
|
||||
let goose_bin = std::env::current_exe()
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|_| "goose".to_string());
|
||||
|
||||
let command_not_found_handler = if with_command_not_found {
|
||||
config
|
||||
.command_not_found
|
||||
.map(|s| s.replace("{goose_bin}", &goose_bin))
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let script = config
|
||||
.script_template
|
||||
.replace("{session_id}", &session.id)
|
||||
.replace("{goose_bin}", &goose_bin)
|
||||
.replace("{command_not_found_handler}", &command_not_found_handler);
|
||||
|
||||
println!("{}", script);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_term_log(command: String) -> Result<()> {
|
||||
let session_id = std::env::var("GOOSE_SESSION_ID").map_err(|_| {
|
||||
anyhow!("GOOSE_SESSION_ID not set. Run 'eval \"$(goose term init <shell>)\"' first.")
|
||||
})?;
|
||||
|
||||
let message = Message::new(
|
||||
Role::User,
|
||||
chrono::Utc::now().timestamp_millis(),
|
||||
vec![MessageContent::text(command)],
|
||||
)
|
||||
.with_metadata(MessageMetadata::user_only());
|
||||
|
||||
SessionManager::add_message(&session_id, &message).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_term_run(prompt: Vec<String>) -> Result<()> {
|
||||
let prompt = prompt.join(" ");
|
||||
let session_id = std::env::var("GOOSE_SESSION_ID").map_err(|_| {
|
||||
anyhow!(
|
||||
"GOOSE_SESSION_ID not set.\n\n\
|
||||
Add to your shell config (~/.zshrc or ~/.bashrc):\n \
|
||||
eval \"$(goose term init zsh)\"\n\n\
|
||||
Then restart your terminal or run: source ~/.zshrc"
|
||||
)
|
||||
})?;
|
||||
|
||||
let working_dir = std::env::current_dir()?;
|
||||
|
||||
SessionManager::update_session(&session_id)
|
||||
.working_dir(working_dir)
|
||||
.apply()
|
||||
.await?;
|
||||
|
||||
let session = SessionManager::get_session(&session_id, true).await?;
|
||||
let user_messages_after_last_assistant: Vec<&Message> =
|
||||
if let Some(conv) = &session.conversation {
|
||||
conv.messages()
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(|m| m.role != Role::Assistant)
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
if let Some(oldest_user) = user_messages_after_last_assistant.last() {
|
||||
SessionManager::truncate_conversation(&session_id, oldest_user.created).await?;
|
||||
}
|
||||
|
||||
let prompt_with_context = if user_messages_after_last_assistant.is_empty() {
|
||||
prompt
|
||||
} else {
|
||||
let history = user_messages_after_last_assistant
|
||||
.iter()
|
||||
.rev() // back to chronological order
|
||||
.map(|m| m.as_concat_text())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
format!(
|
||||
"<shell_history>\n{}\n</shell_history>\n\n{}",
|
||||
history, prompt
|
||||
)
|
||||
};
|
||||
|
||||
let config = SessionBuilderConfig {
|
||||
session_id: Some(session_id),
|
||||
resume: true,
|
||||
interactive: false,
|
||||
quiet: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut session = build_session(config).await;
|
||||
session.headless(prompt_with_context).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle `goose term info` - print compact session info for prompt integration
|
||||
pub async fn handle_term_info() -> Result<()> {
|
||||
let session_id = match std::env::var("GOOSE_SESSION_ID") {
|
||||
Ok(id) => id,
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
|
||||
let session = SessionManager::get_session(&session_id, false).await.ok();
|
||||
let total_tokens = session.as_ref().and_then(|s| s.total_tokens).unwrap_or(0) as usize;
|
||||
|
||||
let model_name = session
|
||||
.as_ref()
|
||||
.and_then(|s| s.model_config.as_ref().map(|mc| mc.model_name.clone()))
|
||||
.map(|name| {
|
||||
let short = name.rsplit('/').next().unwrap_or(&name);
|
||||
if let Some(stripped) = short.strip_prefix("goose-") {
|
||||
stripped.to_string()
|
||||
} else {
|
||||
short.to_string()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "?".to_string());
|
||||
|
||||
let context_limit = session
|
||||
.as_ref()
|
||||
.and_then(|s| s.model_config.as_ref().map(|mc| mc.context_limit()))
|
||||
.unwrap_or(128_000);
|
||||
|
||||
let percentage = if context_limit > 0 {
|
||||
((total_tokens as f64 / context_limit as f64) * 100.0).round() as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let filled = (percentage / 20).min(5);
|
||||
let empty = 5 - filled;
|
||||
let dots = format!("{}{}", "●".repeat(filled), "○".repeat(empty));
|
||||
|
||||
println!("{} {}", dots, model_name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -30,6 +30,7 @@ pub enum SessionType {
|
||||
Scheduled,
|
||||
SubAgent,
|
||||
Hidden,
|
||||
Terminal,
|
||||
}
|
||||
|
||||
impl Default for SessionType {
|
||||
@@ -45,6 +46,7 @@ impl std::fmt::Display for SessionType {
|
||||
SessionType::SubAgent => write!(f, "sub_agent"),
|
||||
SessionType::Hidden => write!(f, "hidden"),
|
||||
SessionType::Scheduled => write!(f, "scheduled"),
|
||||
SessionType::Terminal => write!(f, "terminal"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +60,7 @@ impl std::str::FromStr for SessionType {
|
||||
"sub_agent" => Ok(SessionType::SubAgent),
|
||||
"hidden" => Ok(SessionType::Hidden),
|
||||
"scheduled" => Ok(SessionType::Scheduled),
|
||||
"terminal" => Ok(SessionType::Terminal),
|
||||
_ => Err(anyhow::anyhow!("Invalid session type: {}", s)),
|
||||
}
|
||||
}
|
||||
@@ -291,6 +294,10 @@ impl SessionManager {
|
||||
Self::instance().await?.list_sessions().await
|
||||
}
|
||||
|
||||
pub async fn list_sessions_by_types(types: &[SessionType]) -> Result<Vec<Session>> {
|
||||
Self::instance().await?.list_sessions_by_types(types).await
|
||||
}
|
||||
|
||||
pub async fn delete_session(id: &str) -> Result<()> {
|
||||
Self::instance().await?.delete_session(id).await
|
||||
}
|
||||
@@ -1121,25 +1128,40 @@ impl SessionStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_sessions(&self) -> Result<Vec<Session>> {
|
||||
sqlx::query_as::<_, Session>(
|
||||
async fn list_sessions_by_types(&self, types: &[SessionType]) -> Result<Vec<Session>> {
|
||||
if types.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let placeholders: String = types.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
|
||||
let query = format!(
|
||||
r#"
|
||||
SELECT s.id, s.working_dir, s.name, s.description, s.user_set_name, s.session_type, s.created_at, s.updated_at, s.extension_data,
|
||||
s.total_tokens, s.input_tokens, s.output_tokens,
|
||||
s.accumulated_total_tokens, s.accumulated_input_tokens, s.accumulated_output_tokens,
|
||||
s.schedule_id, s.recipe_json, s.user_recipe_values_json,
|
||||
s.provider_name, s.model_config_json,
|
||||
COUNT(m.id) as message_count
|
||||
FROM sessions s
|
||||
INNER JOIN messages m ON s.id = m.session_id
|
||||
WHERE s.session_type = 'user' OR s.session_type = 'scheduled'
|
||||
GROUP BY s.id
|
||||
ORDER BY s.updated_at DESC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
SELECT s.id, s.working_dir, s.name, s.description, s.user_set_name, s.session_type, s.created_at, s.updated_at, s.extension_data,
|
||||
s.total_tokens, s.input_tokens, s.output_tokens,
|
||||
s.accumulated_total_tokens, s.accumulated_input_tokens, s.accumulated_output_tokens,
|
||||
s.schedule_id, s.recipe_json, s.user_recipe_values_json,
|
||||
s.provider_name, s.model_config_json,
|
||||
COUNT(m.id) as message_count
|
||||
FROM sessions s
|
||||
INNER JOIN messages m ON s.id = m.session_id
|
||||
WHERE s.session_type IN ({})
|
||||
GROUP BY s.id
|
||||
ORDER BY s.updated_at DESC
|
||||
"#,
|
||||
placeholders
|
||||
);
|
||||
|
||||
let mut q = sqlx::query_as::<_, Session>(&query);
|
||||
for t in types {
|
||||
q = q.bind(t.to_string());
|
||||
}
|
||||
|
||||
q.fetch_all(&self.pool).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn list_sessions(&self) -> Result<Vec<Session>> {
|
||||
self.list_sessions_by_types(&[SessionType::User, SessionType::Scheduled])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn delete_session(&self, session_id: &str) -> Result<()> {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
unlisted: true
|
||||
---
|
||||
# Terminal Integration
|
||||
|
||||
The `goose term` commands let you talk to goose directly from your shell prompt. Instead of switching to a separate REPL session, you stay in your terminal and call goose when you need it.
|
||||
|
||||
```bash
|
||||
@goose "what does this error mean?"
|
||||
```
|
||||
|
||||
Goose responds, you read the answer, and you're back at your prompt. The conversation lives alongside your work, not in a separate window you have to manage.
|
||||
|
||||
## Command History Awareness
|
||||
|
||||
The real power comes from shell integration. Once set up, goose tracks the commands you run, so when you ask a question, it already knows what you've been doing.
|
||||
|
||||
No more copy-pasting error messages or explaining "I ran these commands...". Just work normally, then ask goose for help.
|
||||
|
||||
## Setup
|
||||
|
||||
Add one line to your shell config:
|
||||
|
||||
**zsh** (`~/.zshrc`)
|
||||
```bash
|
||||
eval "$(goose term init zsh)"
|
||||
```
|
||||
|
||||
**bash** (`~/.bashrc`)
|
||||
```bash
|
||||
eval "$(goose term init bash)"
|
||||
```
|
||||
|
||||
**fish** (`~/.config/fish/config.fish`)
|
||||
```fish
|
||||
goose term init fish | source
|
||||
```
|
||||
|
||||
**PowerShell** (`$PROFILE`)
|
||||
```powershell
|
||||
Invoke-Expression (goose term init powershell)
|
||||
```
|
||||
|
||||
Then restart your terminal or source the config.
|
||||
|
||||
### Default Mode
|
||||
|
||||
For **bash** and **zsh**, you can make goose the default handler for anything that isn't a valid command:
|
||||
|
||||
```bash
|
||||
# zsh
|
||||
eval "$(goose term init zsh --default)"
|
||||
|
||||
# bash
|
||||
eval "$(goose term init bash --default)"
|
||||
```
|
||||
|
||||
With this enabled, anything you type that isn't a command will be sent to goose:
|
||||
|
||||
```bash
|
||||
$ what files are in this directory?
|
||||
🪿 Command 'what' not found. Asking goose...
|
||||
```
|
||||
|
||||
Goose will interpret what you typed and help you accomplish the task.
|
||||
|
||||
## Usage
|
||||
|
||||
Once set up, your terminal session is linked to a goose session. All commands you run are logged to that session.
|
||||
|
||||
To talk to goose about what you've been doing:
|
||||
|
||||
```bash
|
||||
@goose "why did that fail?"
|
||||
```
|
||||
|
||||
You can also use `@g` as a shorter alias:
|
||||
|
||||
```bash
|
||||
@g "explain this error"
|
||||
```
|
||||
|
||||
Both `@goose` and `@g` are aliases for `goose term run`. They open goose with your command history already loaded.
|
||||
|
||||
## What Gets Logged
|
||||
|
||||
Every command you type gets stored. Goose sees commands you ran since your last message to it.
|
||||
|
||||
Commands starting with `goose term`, `@goose`, or `@g` are not logged (to avoid noise).
|
||||
|
||||
## Performance
|
||||
|
||||
- **Shell startup**: adds ~10ms
|
||||
- **Per command**: ~10ms, runs in background (non-blocking)
|
||||
|
||||
You won't notice any delay. The logging happens asynchronously after your command starts executing.
|
||||
|
||||
## How It Works
|
||||
|
||||
`goose term init` outputs shell code that:
|
||||
1. Sets a `GOOSE_SESSION_ID` environment variable linking your terminal to a goose session
|
||||
2. Creates `@goose` and `@g` aliases for quick access
|
||||
3. Installs a preexec hook that calls `goose term log` for each command
|
||||
4. Optionally installs a command-not-found handler (with `--default`)
|
||||
|
||||
The hook runs `goose term log <command> &` in the background, which appends to the goose session.
|
||||
When you run `@goose`, goose reads from the goose session any commands that happened since it
|
||||
was last called and incorporates them in the next call.
|
||||
|
||||
## Session Management
|
||||
|
||||
By default a new goose session is created each time you run init and
|
||||
that session lasts as long as you keep that terminal open.
|
||||
|
||||
You can create a named session by passing --name:
|
||||
|
||||
```bash
|
||||
eval "$(goose term init zsh --name my_project)"
|
||||
```
|
||||
|
||||
which will create a session with the name `my_project` if it doesn't exist yet or continues
|
||||
that session if it does.
|
||||
+21
-6
@@ -12,13 +12,28 @@ source "$SCRIPT_DIR/clippy-baseline.sh"
|
||||
|
||||
echo "🔍 Running all clippy checks..."
|
||||
|
||||
# Run standard clippy with strict warnings
|
||||
echo " → Standard clippy rules (strict)"
|
||||
cargo clippy --all-targets --jobs 2 -- -D warnings
|
||||
FIX_MODE=0
|
||||
[[ "$1" == "--fix" ]] && FIX_MODE=1
|
||||
|
||||
# Run baseline rules check
|
||||
run_clippy() {
|
||||
if [[ "$FIX_MODE" -eq 1 ]]; then
|
||||
cargo fmt
|
||||
cargo clippy --all-targets --jobs 2 \
|
||||
--fix --allow-dirty --allow-staged \
|
||||
-- -D warnings
|
||||
else
|
||||
cargo clippy --all-targets --jobs 2 -- -D warnings
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$FIX_MODE" -eq 1 ]]; then
|
||||
echo "🛠 Applying fixes..."
|
||||
else
|
||||
echo "🔍 Running clippy..."
|
||||
fi
|
||||
|
||||
run_clippy
|
||||
echo ""
|
||||
check_all_baseline_rules
|
||||
|
||||
echo ""
|
||||
echo "✅ All lint checks passed!"
|
||||
echo "✅ Done"
|
||||
|
||||
@@ -4756,7 +4756,8 @@
|
||||
"user",
|
||||
"scheduled",
|
||||
"sub_agent",
|
||||
"hidden"
|
||||
"hidden",
|
||||
"terminal"
|
||||
]
|
||||
},
|
||||
"SessionsQuery": {
|
||||
|
||||
@@ -761,7 +761,7 @@ export type SessionListResponse = {
|
||||
sessions: Array<Session>;
|
||||
};
|
||||
|
||||
export type SessionType = 'user' | 'scheduled' | 'sub_agent' | 'hidden';
|
||||
export type SessionType = 'user' | 'scheduled' | 'sub_agent' | 'hidden' | 'terminal';
|
||||
|
||||
export type SessionsQuery = {
|
||||
limit: number;
|
||||
|
||||
Reference in New Issue
Block a user