7 Commits

Author SHA1 Message Date
Dark-Alex-17 2ec2aec4c0 style: updated the previous conversation marker a tad
CI / All (macos-latest) (push) Waiting to run
CI / All (windows-latest) (push) Waiting to run
CI / All (ubuntu-latest) (push) Failing after 26s
2026-07-02 16:49:38 -06:00
Dark-Alex-17 c2cb4ac433 feat: Session-specific, file-backed history in the REPL
CI / All (macos-latest) (push) Waiting to run
CI / All (windows-latest) (push) Waiting to run
CI / All (ubuntu-latest) (push) Failing after 25s
2026-07-02 16:44:55 -06:00
Dark-Alex-17 605a9170b0 feat: Replay session output when a user re-enters a session so all output can be seen again 2026-07-02 16:35:10 -06:00
Dark-Alex-17 385bd3eda2 fix: Overrode the default JSON content-type for MCP OAuth so its properly application/x-www-form-urlencoded
CI / All (macos-latest) (push) Waiting to run
CI / All (windows-latest) (push) Waiting to run
CI / All (ubuntu-latest) (push) Failing after 26s
2026-07-02 15:53:29 -06:00
Dark-Alex-17 6c3d96ac83 feat: Added confirmation message after MCP Oauth succeeds when invoked from --auth-mcp
CI / All (macos-latest) (push) Waiting to run
CI / All (windows-latest) (push) Waiting to run
CI / All (ubuntu-latest) (push) Failing after 26s
2026-07-02 15:22:22 -06:00
Dark-Alex-17 aa1fe7f7aa fmt: applied formatting 2026-07-02 15:22:00 -06:00
Dark-Alex-17 5e50828108 fix: typo in mcp file name 2026-07-02 15:20:57 -06:00
7 changed files with 148 additions and 10 deletions
+7
View File
@@ -133,6 +133,13 @@ impl MessageContent {
} }
} }
pub fn as_text(&self) -> Option<&str> {
match self {
MessageContent::Text(text) => Some(text),
_ => None,
}
}
pub fn merge_prompt(&mut self, replace_fn: impl Fn(&str) -> String) { pub fn merge_prompt(&mut self, replace_fn: impl Fn(&str) -> String) {
match self { match self {
MessageContent::Text(text) => *text = replace_fn(text), MessageContent::Text(text) => *text = replace_fn(text),
+1
View File
@@ -135,6 +135,7 @@ const RAGS_DIR_NAME: &str = "rags";
const FUNCTIONS_DIR_NAME: &str = "functions"; const FUNCTIONS_DIR_NAME: &str = "functions";
const FUNCTIONS_BIN_DIR_NAME: &str = "bin"; const FUNCTIONS_BIN_DIR_NAME: &str = "bin";
const AGENTS_DIR_NAME: &str = "agents"; const AGENTS_DIR_NAME: &str = "agents";
const REPL_HISTORY_DIR_NAME: &str = "repl-history";
const GLOBAL_TOOLS_DIR_NAME: &str = "tools"; const GLOBAL_TOOLS_DIR_NAME: &str = "tools";
const GLOBAL_TOOLS_UTILS_DIR_NAME: &str = "utils"; const GLOBAL_TOOLS_UTILS_DIR_NAME: &str = "utils";
const BASH_PROMPT_UTILS_FILE_NAME: &str = "prompt-utils.sh"; const BASH_PROMPT_UTILS_FILE_NAME: &str = "prompt-utils.sh";
+16
View File
@@ -8,6 +8,8 @@ use super::{
SKILLS_DIR_NAME, WORKSPACE_MEMORY_DIR_NAME, SKILLS_DIR_NAME, WORKSPACE_MEMORY_DIR_NAME,
}; };
use crate::client::ProviderModels; use crate::client::ProviderModels;
use crate::config::REPL_HISTORY_DIR_NAME;
use crate::config::session::Session;
use crate::utils::{get_env_name, list_file_names, normalize_env_name}; use crate::utils::{get_env_name, list_file_names, normalize_env_name};
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
@@ -320,6 +322,20 @@ pub fn workspace_memory_dir_for(workspace_root: &Path) -> PathBuf {
.join(MEMORY_DIR_NAME) .join(MEMORY_DIR_NAME)
} }
pub fn repl_history_dir() -> PathBuf {
cache_path().join(REPL_HISTORY_DIR_NAME)
}
pub fn repl_history_file(session: &Option<Session>) -> PathBuf {
let history_key = if let Some(session) = &session {
format!("session_{}", session.name().replace('/', "_"))
} else {
"default".to_string()
};
repl_history_dir().join(history_key)
}
pub fn log_config() -> Result<(LevelFilter, Option<PathBuf>)> { pub fn log_config() -> Result<(LevelFilter, Option<PathBuf>)> {
let log_level = env::var(get_env_name("log_level")) let log_level = env::var(get_env_name("log_level"))
.ok() .ok()
+8
View File
@@ -163,6 +163,14 @@ impl Session {
self.messages.is_empty() && self.compressed_messages.is_empty() self.messages.is_empty() && self.compressed_messages.is_empty()
} }
pub fn messages(&self) -> &[Message] {
&self.messages
}
pub fn compressed_messages(&self) -> &[Message] {
&self.compressed_messages
}
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
&self.name &self.name
} }
+1
View File
@@ -159,6 +159,7 @@ async fn main() -> Result<()> {
let url = spec.url.as_deref().expect("validated: remote spec has url"); let url = spec.url.as_deref().expect("validated: remote spec has url");
mcp::oauth::run_mcp_oauth_flow(server_name, url, spec.oauth_client_id.as_deref()).await?; mcp::oauth::run_mcp_oauth_flow(server_name, url, spec.oauth_client_id.as_deref()).await?;
println!("Authentication saved. '{server_name}' is now available for use.");
return Ok(()); return Ok(());
} }
+5 -1
View File
@@ -1,4 +1,4 @@
use crate::client::oauth::{OAuthProvider, load_oauth_tokens, run_oauth_flow}; use crate::client::oauth::{OAuthProvider, TokenRequestFormat, load_oauth_tokens, run_oauth_flow};
use crate::config::paths; use crate::config::paths;
use anyhow::{Context, Result, anyhow}; use anyhow::{Context, Result, anyhow};
use chrono::Utc; use chrono::Utc;
@@ -63,6 +63,10 @@ impl OAuthProvider for McpOAuthProvider {
&self.scopes &self.scopes
} }
fn token_request_format(&self) -> TokenRequestFormat {
TokenRequestFormat::FormUrlEncoded
}
fn uses_localhost_redirect(&self) -> bool { fn uses_localhost_redirect(&self) -> bool {
false false
} }
+110 -9
View File
@@ -6,7 +6,10 @@ use self::completer::ReplCompleter;
use self::highlighter::ReplHighlighter; use self::highlighter::ReplHighlighter;
use self::prompt::ReplPrompt; use self::prompt::ReplPrompt;
use crate::client::{call_chat_completions, call_chat_completions_streaming, init_client, oauth}; use crate::client::{
Message, MessageRole, call_chat_completions, call_chat_completions_streaming, init_client,
oauth,
};
use crate::config::{ use crate::config::{
AgentVariables, AppConfig, AssertState, Input, LastMessage, RequestContext, StateFlags, AgentVariables, AppConfig, AssertState, Input, LastMessage, RequestContext, StateFlags,
macro_execute, macro_execute,
@@ -29,9 +32,9 @@ use log::warn;
use parking_lot::RwLock; use parking_lot::RwLock;
use reedline::CursorConfig; use reedline::CursorConfig;
use reedline::{ use reedline::{
ColumnarMenu, EditCommand, EditMode, Emacs, KeyCode, KeyModifiers, Keybindings, Reedline, ColumnarMenu, EditCommand, EditMode, Emacs, FileBackedHistory, KeyCode, KeyModifiers,
ReedlineEvent, ReedlineMenu, ValidationResult, Validator, Vi, default_emacs_keybindings, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, ValidationResult, Validator, Vi,
default_vi_insert_keybindings, default_vi_normal_keybindings, default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
}; };
use reedline::{MenuBuilder, Signal}; use reedline::{MenuBuilder, Signal};
use std::sync::LazyLock; use std::sync::LazyLock;
@@ -318,6 +321,58 @@ Type ".help" for additional help.
} }
} }
{
let (messages_snapshot, compressed_count) = {
let ctx = self.ctx.read();
if let Some(session) = &ctx.session {
let msgs: Vec<Message> = session
.messages()
.iter()
.filter(|m| !m.role.is_system())
.cloned()
.collect();
let compressed = session.compressed_messages().len();
(msgs, compressed)
} else {
(vec![], 0)
}
};
if !messages_snapshot.is_empty() || compressed_count > 0 {
let app = Arc::clone(&self.ctx.read().app.config);
if compressed_count > 0 {
println!(
"{}",
dimmed_text(&format!(
"({compressed_count} earlier messages not shown; compressed for context)"
))
);
println!();
}
for message in &messages_snapshot {
match message.role {
MessageRole::User => {
if let Some(text) = message.content.as_text() {
println!("{}", dimmed_text("You:"));
println!("{text}");
println!();
}
}
MessageRole::Assistant => {
if let Some(text) = message.content.as_text() {
app.print_markdown(text)?;
println!();
}
}
_ => {}
}
}
println!("{}", dimmed_text("─── ↑ previous conversation ↑ ───"));
println!();
}
}
loop { loop {
if self.abort_signal.aborted_ctrld() { if self.abort_signal.aborted_ctrld() {
break; break;
@@ -393,6 +448,14 @@ Type ".help" for additional help.
editor = editor.with_buffer_editor(command, temp_file); editor = editor.with_buffer_editor(command, temp_file);
} }
if app.save_shell_history {
let ctx = ctx.read();
let history_path = paths::repl_history_file(&ctx.session);
if let Ok(history) = FileBackedHistory::with_file(1000, history_path) {
editor = editor.with_history(Box::new(history));
}
}
Ok(editor) Ok(editor)
} }
@@ -564,11 +627,9 @@ pub async fn run_repl_command(
.and_then(|c| c.mcp_servers.get(server_name)) .and_then(|c| c.mcp_servers.get(server_name))
.cloned(); .cloned();
match server_spec { match server_spec {
None => bail!( None => {
"MCP server '{}' not found in config. \ bail!("MCP server '{}' not found in mcp.json.", server_name)
Check your mcp_config.json.", }
server_name
),
Some(spec) if !spec.is_remote() => bail!( Some(spec) if !spec.is_remote() => bail!(
"MCP server '{}' uses stdio transport; \ "MCP server '{}' uses stdio transport; \
OAuth is only supported for http/sse servers.", OAuth is only supported for http/sse servers.",
@@ -686,6 +747,46 @@ pub async fn run_repl_command(
session.set_autonaming(false); session.set_autonaming(false);
} }
} }
if let Some(session) = &ctx.session {
let messages_snapshot: Vec<Message> = session
.messages()
.iter()
.filter(|m| !m.role.is_system())
.cloned()
.collect();
let compressed_count = session.compressed_messages().len();
if !messages_snapshot.is_empty() || compressed_count > 0 {
if compressed_count > 0 {
println!(
"{}",
dimmed_text(&format!(
"({compressed_count} earlier messages not shown — compressed for context)"
))
);
println!();
}
for message in &messages_snapshot {
match message.role {
MessageRole::User => {
if let Some(text) = message.content.as_text() {
println!("{}", dimmed_text("You:"));
println!("{text}");
println!();
}
}
MessageRole::Assistant => {
if let Some(text) = message.content.as_text() {
app.print_markdown(text)?;
println!();
}
}
_ => {}
}
}
println!("{}", dimmed_text("─── ↑ previous conversation ↑ ───"));
println!();
}
}
} }
".install" => { ".install" => {
let trimmed = args.map(str::trim).unwrap_or(""); let trimmed = args.map(str::trim).unwrap_or("");