aster_cli/commands/
term.rs1use anyhow::{anyhow, Result};
2use aster::conversation::message::{Message, MessageContent, MessageMetadata};
3use aster::session::SessionManager;
4use aster::session::SessionType;
5use chrono;
6use rmcp::model::Role;
7
8use crate::session::{build_session, SessionBuilderConfig};
9
10use clap::ValueEnum;
11
12#[derive(ValueEnum, Clone, Debug)]
13pub enum Shell {
14 Bash,
15 Zsh,
16 Fish,
17 #[value(alias = "pwsh")]
18 Powershell,
19}
20
21struct ShellConfig {
22 script_template: &'static str,
23 command_not_found: Option<&'static str>,
24}
25
26impl Shell {
27 fn config(&self) -> &'static ShellConfig {
28 match self {
29 Shell::Bash => &BASH_CONFIG,
30 Shell::Zsh => &ZSH_CONFIG,
31 Shell::Fish => &FISH_CONFIG,
32 Shell::Powershell => &POWERSHELL_CONFIG,
33 }
34 }
35}
36
37static BASH_CONFIG: ShellConfig = ShellConfig {
38 script_template: r#"export ASTER_SESSION_ID="{session_id}"
39alias @aster='{aster_bin} term run'
40alias @g='{aster_bin} term run'
41
42aster_preexec() {
43 [[ "$1" =~ ^aster\ term ]] && return
44 [[ "$1" =~ ^(@aster|@g)($|[[:space:]]) ]] && return
45 ('{aster_bin}' term log "$1" &) 2>/dev/null
46}
47
48if [[ -z "$aster_preexec_installed" ]]; then
49 aster_preexec_installed=1
50 trap 'aster_preexec "$BASH_COMMAND"' DEBUG
51fi{command_not_found_handler}"#,
52 command_not_found: Some(
53 r#"
54
55command_not_found_handle() {
56 echo "🪿 Command '$1' not found. Asking aster..."
57 '{aster_bin}' term run "$@"
58 return 0
59}"#,
60 ),
61};
62
63static ZSH_CONFIG: ShellConfig = ShellConfig {
64 script_template: r#"export ASTER_SESSION_ID="{session_id}"
65alias @aster='{aster_bin} term run'
66alias @g='{aster_bin} term run'
67
68aster_preexec() {
69 [[ "$1" =~ ^aster\ term ]] && return
70 [[ "$1" =~ ^(@aster|@g)($|[[:space:]]) ]] && return
71 ('{aster_bin}' term log "$1" &) 2>/dev/null
72}
73
74autoload -Uz add-zsh-hook
75add-zsh-hook preexec aster_preexec{command_not_found_handler}"#,
76 command_not_found: Some(
77 r#"
78
79command_not_found_handler() {
80 echo "🪿 Command '$1' not found. Asking aster..."
81 '{aster_bin}' term run "$@"
82 return 0
83}"#,
84 ),
85};
86
87static FISH_CONFIG: ShellConfig = ShellConfig {
88 script_template: r#"set -gx ASTER_SESSION_ID "{session_id}"
89function @aster; {aster_bin} term run $argv; end
90function @g; {aster_bin} term run $argv; end
91
92function aster_preexec --on-event fish_preexec
93 string match -q -r '^aster term' -- $argv[1]; and return
94 string match -q -r '^(@aster|@g)($|\s)' -- $argv[1]; and return
95 {aster_bin} term log "$argv[1]" 2>/dev/null &
96end"#,
97 command_not_found: None,
98};
99
100static POWERSHELL_CONFIG: ShellConfig = ShellConfig {
101 script_template: r#"$env:ASTER_SESSION_ID = "{session_id}"
102function @aster {{ & '{aster_bin}' term run @args }}
103function @g {{ & '{aster_bin}' term run @args }}
104
105Set-PSReadLineKeyHandler -Chord Enter -ScriptBlock {{
106 $line = $null
107 [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$null)
108 if ($line -notmatch '^aster term' -and $line -notmatch '^(@aster|@g)($|\s)') {{
109 Start-Job -ScriptBlock {{ & '{aster_bin}' term log $using:line }} | Out-Null
110 }}
111 [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
112}}"#,
113 command_not_found: None,
114};
115
116pub async fn handle_term_init(
117 shell: Shell,
118 name: Option<String>,
119 with_command_not_found: bool,
120) -> Result<()> {
121 let config = shell.config();
122
123 let working_dir = std::env::current_dir()?;
124 let named_session = if let Some(ref name) = name {
125 let sessions = SessionManager::list_sessions_by_types(&[SessionType::Terminal]).await?;
126 sessions.into_iter().find(|s| s.name == *name)
127 } else {
128 None
129 };
130
131 let session = match named_session {
132 Some(s) => s,
133 None => {
134 let session = SessionManager::create_session(
135 working_dir,
136 "Aster Term Session".to_string(),
137 SessionType::Terminal,
138 )
139 .await?;
140
141 if let Some(name) = name {
142 SessionManager::update_session(&session.id)
143 .user_provided_name(name)
144 .apply()
145 .await?;
146 }
147
148 session
149 }
150 };
151
152 let aster_bin = std::env::current_exe()
153 .map(|p| p.to_string_lossy().into_owned())
154 .unwrap_or_else(|_| "aster".to_string());
155
156 let command_not_found_handler = if with_command_not_found {
157 config
158 .command_not_found
159 .map(|s| s.replace("{aster_bin}", &aster_bin))
160 .unwrap_or_default()
161 } else {
162 String::new()
163 };
164
165 let script = config
166 .script_template
167 .replace("{session_id}", &session.id)
168 .replace("{aster_bin}", &aster_bin)
169 .replace("{command_not_found_handler}", &command_not_found_handler);
170
171 println!("{}", script);
172 Ok(())
173}
174
175pub async fn handle_term_log(command: String) -> Result<()> {
176 let session_id = std::env::var("ASTER_SESSION_ID").map_err(|_| {
177 anyhow!("ASTER_SESSION_ID not set. Run 'eval \"$(aster term init <shell>)\"' first.")
178 })?;
179
180 let message = Message::new(
181 Role::User,
182 chrono::Utc::now().timestamp_millis(),
183 vec![MessageContent::text(command)],
184 )
185 .with_metadata(MessageMetadata::user_only());
186
187 SessionManager::add_message(&session_id, &message).await?;
188
189 Ok(())
190}
191
192pub async fn handle_term_run(prompt: Vec<String>) -> Result<()> {
193 let prompt = prompt.join(" ");
194 let session_id = std::env::var("ASTER_SESSION_ID").map_err(|_| {
195 anyhow!(
196 "ASTER_SESSION_ID not set.\n\n\
197 Add to your shell config (~/.zshrc or ~/.bashrc):\n \
198 eval \"$(aster term init zsh)\"\n\n\
199 Then restart your terminal or run: source ~/.zshrc"
200 )
201 })?;
202
203 let working_dir = std::env::current_dir()?;
204
205 SessionManager::update_session(&session_id)
206 .working_dir(working_dir)
207 .apply()
208 .await?;
209
210 let session = SessionManager::get_session(&session_id, true).await?;
211 let user_messages_after_last_assistant: Vec<&Message> =
212 if let Some(conv) = &session.conversation {
213 conv.messages()
214 .iter()
215 .rev()
216 .take_while(|m| m.role != Role::Assistant)
217 .collect()
218 } else {
219 Vec::new()
220 };
221
222 if let Some(oldest_user) = user_messages_after_last_assistant.last() {
223 SessionManager::truncate_conversation(&session_id, oldest_user.created).await?;
224 }
225
226 let prompt_with_context = if user_messages_after_last_assistant.is_empty() {
227 prompt
228 } else {
229 let history = user_messages_after_last_assistant
230 .iter()
231 .rev() .map(|m| m.as_concat_text())
233 .collect::<Vec<_>>()
234 .join("\n");
235
236 format!(
237 "<shell_history>\n{}\n</shell_history>\n\n{}",
238 history, prompt
239 )
240 };
241
242 let config = SessionBuilderConfig {
243 session_id: Some(session_id),
244 resume: true,
245 interactive: false,
246 quiet: true,
247 ..Default::default()
248 };
249
250 let mut session = build_session(config).await;
251 session.headless(prompt_with_context).await?;
252
253 Ok(())
254}
255
256pub async fn handle_term_info() -> Result<()> {
258 let session_id = match std::env::var("ASTER_SESSION_ID") {
259 Ok(id) => id,
260 Err(_) => return Ok(()),
261 };
262
263 let session = SessionManager::get_session(&session_id, false).await.ok();
264 let total_tokens = session.as_ref().and_then(|s| s.total_tokens).unwrap_or(0) as usize;
265
266 let config = aster::config::Config::global();
267 let model_name = config
268 .get_aster_model()
269 .ok()
270 .map(|name| {
271 let short = name.rsplit('/').next().unwrap_or(&name);
272 if let Some(stripped) = short.strip_prefix("aster-") {
273 stripped.to_string()
274 } else {
275 short.to_string()
276 }
277 })
278 .unwrap_or_else(|| "?".to_string());
279
280 let context_limit = config
281 .get_aster_model()
282 .ok()
283 .and_then(|model_name| aster::model::ModelConfig::new(&model_name).ok())
284 .map(|mc| mc.context_limit())
285 .unwrap_or(128_000);
286
287 let percentage = if context_limit > 0 {
288 ((total_tokens as f64 / context_limit as f64) * 100.0).round() as usize
289 } else {
290 0
291 };
292
293 let filled = (percentage / 20).min(5);
294 let empty = 5 - filled;
295 let dots = format!("{}{}", "●".repeat(filled), "○".repeat(empty));
296
297 println!("{} {}", dots, model_name);
298
299 Ok(())
300}