Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,7 @@ Some commands change settings for the current session only. Others persist to yo

# View & edit config
:info # Show current session info (model, agent, conversation ID)
:env # Show environment and provider info
:config # List current configuration values
:config # Display effective resolved configuration in TOML format
:config-edit # Open config file in $EDITOR (alias: :ce)
:tools # List available tools for the current agent
:skill # List available skills
Expand Down
2 changes: 2 additions & 0 deletions crates/forge_display/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ console.workspace = true
regex.workspace = true
termimad.workspace = true
syntect.workspace = true
terminal-colorsaurus = "1.0.3"
two-face = "0.5.1"

[dev-dependencies]
insta.workspace = true
Expand Down
32 changes: 27 additions & 5 deletions crates/forge_display/src/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
use syntect::util::as_24_bit_terminal_escaped;
use terminal_colorsaurus::{QueryOptions, ThemeMode, theme_mode};
use two_face::theme::EmbeddedThemeName;

/// Loads and caches syntax highlighting resources.
#[derive(Clone)]
Expand All @@ -14,20 +16,40 @@ pub struct SyntaxHighlighter {

impl Default for SyntaxHighlighter {
fn default() -> Self {
// Use two-face's extended syntax set which includes TOML, Rust, Python, etc.
Self {
syntax_set: Arc::new(SyntaxSet::load_defaults_newlines()),
theme_set: Arc::new(ThemeSet::load_defaults()),
syntax_set: Arc::new(two_face::syntax::extra_newlines()),
theme_set: Arc::new(two_face::theme::extra().into()),
}
}
}

impl SyntaxHighlighter {
fn highlight(&self, code: &str, lang: &str) -> String {
/// Detects whether the terminal is using a dark or light background.
fn is_dark_theme() -> bool {
match theme_mode(QueryOptions::default()) {
Ok(ThemeMode::Light) => false,
Ok(ThemeMode::Dark) | Err(_) => true,
}
}

/// Syntax-highlights `code` for the given language token (e.g. `"toml"`,
/// `"rust"`), returning an ANSI-escaped string ready for terminal output.
///
/// The theme is chosen automatically based on the terminal background
/// (dark → `base16-ocean.dark`, light → `InspiredGitHub`). Falls back to
/// plain text if the language is unrecognised.
pub fn highlight(&self, code: &str, lang: &str) -> String {
let syntax = self
.syntax_set
.find_syntax_by_token(lang)
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
let theme = &self.theme_set.themes["base16-ocean.dark"];
let theme_name = if Self::is_dark_theme() {
EmbeddedThemeName::Base16OceanDark
} else {
EmbeddedThemeName::InspiredGithub
};
let theme = &self.theme_set.themes[theme_name.as_name()];
let mut hl = HighlightLines::new(syntax, theme);

code.lines()
Expand Down Expand Up @@ -113,7 +135,7 @@ impl CodeBlockParser {

/// Get the extracted code blocks.
#[cfg(test)]
pub fn blocks(&self) -> &[CodeBlock] {
pub(crate) fn blocks(&self) -> &[CodeBlock] {
&self.blocks
}

Expand Down
3 changes: 2 additions & 1 deletion crates/forge_display/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
mod code;
pub mod code;
pub mod diff;
pub mod grep;
pub mod markdown;

pub use code::SyntaxHighlighter;
pub use diff::DiffFormat;
pub use grep::GrepFormat;
pub use markdown::MarkdownFormat;
1 change: 1 addition & 0 deletions crates/forge_main/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ tracing.workspace = true
chrono.workspace = true
serde_json.workspace = true
serde.workspace = true
toml_edit.workspace = true
strum.workspace = true
strum_macros.workspace = true

Expand Down
9 changes: 2 additions & 7 deletions crates/forge_main/src/built_in_commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
"description": "Print session information [alias: i]"
},
{
"command": "env",
"description": "Display environment information [alias: e]"
"command": "config",
"description": "Display effective resolved configuration in TOML format"
},
{
"command": "config-model",
"description": "Switch the models [alias: cm]"
},
{
Expand All @@ -35,10 +34,6 @@
"command": "config-suggest-model",
"description": "Set the model used for command suggestion generation [alias: csm]"
},
{
"command": "config",
"description": "List current configuration values"
},
{
"command": "config-edit",
"description": "Open the global forge config file (~/forge/.forge.toml) in an editor [alias: ce]"
Expand Down
3 changes: 0 additions & 3 deletions crates/forge_main/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,6 @@ pub enum TopLevelCommand {
porcelain: bool,
},

/// Display environment information.
Env,

/// Get, set, or list configuration values.
Config(ConfigCommandGroup),

Expand Down
5 changes: 0 additions & 5 deletions crates/forge_main/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,6 @@ impl ForgeCommandManager {
"/compact" => Ok(SlashCommand::Compact),
"/new" => Ok(SlashCommand::New),
"/info" => Ok(SlashCommand::Info),
"/env" => Ok(SlashCommand::Env),
"/usage" => Ok(SlashCommand::Usage),
"/exit" => Ok(SlashCommand::Exit),
"/update" => Ok(SlashCommand::Update),
Expand Down Expand Up @@ -361,9 +360,6 @@ pub enum SlashCommand {
/// Display usage information (tokens & requests).
#[strum(props(usage = "Shows usage information (tokens & requests)"))]
Usage,
/// Display environment information.
#[strum(props(usage = "Display environment information"))]
Env,
/// Exit the application without any further action.
#[strum(props(usage = "Exit the application"))]
Exit,
Expand Down Expand Up @@ -464,7 +460,6 @@ impl SlashCommand {
SlashCommand::Message(_) => "message",
SlashCommand::Update => "update",
SlashCommand::Info => "info",
SlashCommand::Env => "env",
SlashCommand::Usage => "usage",
SlashCommand::Exit => "exit",
SlashCommand::Forge => "forge",
Expand Down
86 changes: 13 additions & 73 deletions crates/forge_main/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,10 +555,6 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
self.on_info(porcelain, conversation_id).await?;
return Ok(());
}
TopLevelCommand::Env => {
self.on_env().await?;
return Ok(());
}
TopLevelCommand::Banner => {
banner::display(true)?;
return Ok(());
Expand Down Expand Up @@ -1447,69 +1443,24 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI

/// Lists current configuration values
async fn on_show_config(&mut self, porcelain: bool) -> anyhow::Result<()> {
let model = self
.get_agent_model(None)
.await
.map(|m| m.as_str().to_string());
let model = model.unwrap_or_else(|| markers::EMPTY.to_string());
let provider = self
.get_provider(None)
.await
.ok()
.map(|p| p.id.to_string())
.unwrap_or_else(|| markers::EMPTY.to_string());
let commit_config = self.api.get_commit_config().await.ok().flatten();
let commit_provider = commit_config
.as_ref()
.map(|c| c.provider.to_string())
.unwrap_or_else(|| markers::EMPTY.to_string());
let commit_model = commit_config
.as_ref()
.map(|c| c.model.as_str().to_string())
.unwrap_or_else(|| markers::EMPTY.to_string());

let suggest_config = self.api.get_suggest_config().await.ok().flatten();
let suggest_provider = suggest_config
.as_ref()
.map(|c| c.provider.to_string())
.unwrap_or_else(|| markers::EMPTY.to_string());
let suggest_model = suggest_config
.as_ref()
.map(|c| c.model.as_str().to_string())
.unwrap_or_else(|| markers::EMPTY.to_string());
// Get the effective resolved config
let config = &self.config;

let reasoning_effort = self
.api
.get_reasoning_effort()
.await
.ok()
.flatten()
.map(|e| e.to_string())
.unwrap_or_else(|| markers::EMPTY.to_string());

let info = Info::new()
.add_title("SESSION")
.add_key_value("Model", model)
.add_key_value("Provider", provider)
.add_title("COMMIT")
.add_key_value("Model", commit_model)
.add_key_value("Provider", commit_provider)
.add_title("SUGGEST")
.add_key_value("Model", suggest_model)
.add_key_value("Provider", suggest_provider)
.add_title("REASONING")
.add_key_value("Effort", reasoning_effort);
// Serialize to TOML pretty format
let config_toml = toml_edit::ser::to_string_pretty(config)
.map_err(|e| anyhow::anyhow!("Failed to serialize config: {}", e))?;

if porcelain {
self.writeln(
Porcelain::from(&info)
.into_long()
.drop_col(0)
.uppercase_headers(),
)?;
// For porcelain mode, output raw TOML without highlighting
self.writeln(config_toml)?;
} else {
self.writeln(info)?;
// For human-readable mode, add a title and syntax-highlight the TOML
self.writeln("\nCONFIGURATION\n".bold().dimmed())?;
let highlighted =
forge_display::SyntaxHighlighter::default().highlight(&config_toml, "toml");
self.writeln(highlighted)?;
}

Ok(())
}

Expand Down Expand Up @@ -1680,14 +1631,6 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
Ok(())
}

async fn on_env(&mut self) -> anyhow::Result<()> {
let env = self.api.environment();
let config = &self.config;
let info = Info::from(&env).extend(Info::from(config));
self.writeln(info)?;
Ok(())
}

/// Generate ZSH plugin script
async fn on_zsh_plugin(&self) -> anyhow::Result<()> {
let plugin = crate::zsh::generate_zsh_plugin()?;
Expand Down Expand Up @@ -1990,9 +1933,6 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
SlashCommand::Info => {
self.on_info(false, self.state.conversation_id).await?;
}
SlashCommand::Env => {
self.on_env().await?;
}
SlashCommand::Usage => {
self.on_usage().await?;
}
Expand Down
6 changes: 0 additions & 6 deletions shell-plugin/lib/actions/core.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,6 @@ function _forge_action_info() {
fi
}

# Action handler: Show environment info
function _forge_action_env() {
echo
_forge_exec env
}

# Action handler: Dump conversation
function _forge_action_dump() {
local input_text="$1"
Expand Down
5 changes: 1 addition & 4 deletions shell-plugin/lib/dispatcher.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,6 @@ function forge-accept-line() {
info|i)
_forge_action_info
;;
env|e)
_forge_action_env
;;
dump|d)
_forge_action_dump "$input_text"
;;
Expand Down Expand Up @@ -190,7 +187,7 @@ function forge-accept-line() {
tools|t)
_forge_action_tools
;;
config)
config|env|e)
_forge_action_config
;;
config-edit|ce)
Expand Down
Loading