Skip to content
Draft
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
193 changes: 92 additions & 101 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ derive_setters = "0.1.9"
dirs = "6.0.0"
dissimilar = "1.0.9"
dotenvy = "0.15.7"
fzf-wrapped = "0.1.4"
futures = "0.3.32"
gh-workflow = "0.8.1"
glob = "0.3.3"
Expand Down Expand Up @@ -130,6 +129,7 @@ rmcp = { version = "0.10.0", features = [
] }
open = "5.3.2"
nucleo = "0.5.0"
nucleo-picker = "0.11.1"
gray_matter = "0.3.2"
num-format = "0.4"
humantime = "2.1.0"
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ Install the ZSH plugin once with `forge setup`, then use `:` commands directly a
: refactor the auth module # Send a prompt to the active agent
:commit # AI-powered git commit
:suggest "find large log files" # Translate description → shell command in your buffer
:conversation # Browse saved conversations with fzf preview
:conversation # Browse saved conversations with interactive picker
```

See the full [ZSH Plugin reference below](#zsh-plugin-the--prefix-system) for all commands and aliases.
Expand All @@ -235,7 +235,7 @@ When you install the ZSH plugin (`forge setup`), you get a `:` prefix command sy
```zsh
: <prompt> # Send a prompt to the active agent
:sage <prompt> # Send a prompt to a specific agent by name (sage, muse, forge, or any custom agent)
:agent <name> # Switch the active agent; opens fzf picker if no name given
:agent <name> # Switch the active agent; opens interactive picker if no name given
```

### Agents
Expand Down Expand Up @@ -275,13 +275,13 @@ Forge saves every conversation. You can switch between them like switching direc
```zsh
:new # Start a fresh conversation (saves current for :conversation -)
:new <initial prompt> # Start a new conversation and immediately send a prompt
:conversation # Open fzf picker: browse and switch conversations with preview
:conversation # Open interactive picker: browse and switch conversations with preview
:conversation <id> # Switch directly to a conversation by ID
:conversation - # Toggle between current and previous conversation (like cd -)
:clone # Branch the current conversation (try a different direction)
:clone <id> # Clone a specific conversation by ID
:rename <name> # Rename the current conversation
:conversation-rename # Rename a conversation via fzf picker
:conversation-rename # Rename a conversation via interactive picker
:retry # Retry the last prompt (useful if the AI misunderstood)
:copy # Copy the last AI response to clipboard as markdown
:dump # Export conversation as JSON
Expand Down Expand Up @@ -377,11 +377,11 @@ After running `:sync`, the AI can search your codebase by meaning rather than ex
|---|---|---|
| `: <prompt>` | | Send prompt to active agent |
| `:new` | `:n` | Start new conversation |
| `:conversation` | `:c` | Browse/switch conversations (fzf) |
| `:conversation` | `:c` | Browse/switch conversations (interactive picker) |
| `:conversation -` | | Toggle to previous conversation |
| `:clone` | | Branch current conversation |
| `:rename <name>` | `:rn` | Rename current conversation |
| `:conversation-rename` | | Rename conversation (fzf picker) |
| `:conversation-rename` | | Rename conversation (interactive picker) |
| `:retry` | `:r` | Retry last prompt |
| `:copy` | | Copy last response to clipboard |
| `:dump` | `:d` | Export conversation as JSON |
Expand All @@ -392,7 +392,7 @@ After running `:sync`, the AI can search your codebase by meaning rather than ex
| `:edit` | `:ed` | Compose prompt in $EDITOR |
| `:sage <prompt>` | `:ask` | Q&A / code understanding agent |
| `:muse <prompt>` | `:plan` | Planning agent |
| `:agent <name>` | `:a` | Switch active agent (fzf picker if no name given) |
| `:agent <name>` | `:a` | Switch active agent (interactive picker if no name given) |
| `:model <id>` | `:m` | Set model for this session only |
| `:config-model <id>` | `:cm` | Set default model (persistent) |
| `:reasoning-effort <lvl>` | `:re` | Set reasoning effort for session |
Expand Down
6 changes: 5 additions & 1 deletion crates/forge_main/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ forge_spinner.workspace = true
forge_select.workspace = true

merge.workspace = true
nucleo.workspace = true
nucleo-picker.workspace = true
libc = "0.2"
forge_fs.workspace = true
tokio.workspace = true
tokio-stream.workspace = true
Expand All @@ -40,9 +43,10 @@ crossterm = "0.29.0"
nu-ansi-term.workspace = true
tracing.workspace = true
chrono.workspace = true
serde_json.workspace = true
serde.workspace = true
serde_json.workspace = true
toml_edit.workspace = true
regex.workspace = true
strum.workspace = true
strum_macros.workspace = true

Expand Down
35 changes: 35 additions & 0 deletions crates/forge_main/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,41 @@ pub enum TopLevelCommand {

/// Run diagnostics on shell environment (alias for `zsh doctor`).
Doctor,

/// Interactive fuzzy item picker.
Select(SelectArgs),
}

/// Arguments for the `forge select` command.
#[derive(Parser, Debug, Clone)]
pub struct SelectArgs {
/// Prompt text displayed before the picker.
#[arg(long, short = 'p')]
pub prompt: Option<String>,

/// Initial query text pre-filled in the search box.
#[arg(long, short = 'q')]
pub query: Option<String>,

/// Allow selecting multiple items.
#[arg(long, short = 'm')]
pub multi: bool,

/// Shell command used to render a preview for the selected item.
#[arg(long)]
pub preview: Option<String>,

/// Regex delimiter used to split fields in each input line.
#[arg(long)]
pub delimiter: Option<String>,

/// Comma-separated field list used for display text.
#[arg(long = "with-nth")]
pub with_nth: Option<String>,

/// Preview window layout hint.
#[arg(long = "preview-window")]
pub preview_window: Option<String>,
}

/// Command group for custom command management.
Expand Down
2 changes: 1 addition & 1 deletion crates/forge_main/src/completer/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use reedline::{Completer, Span, Suggestion};
use crate::model::{ForgeCommand, ForgeCommandManager};

/// A display wrapper for `ForgeCommand` that renders the name and description
/// side-by-side for fzf.
/// side-by-side for the interactive picker.
#[derive(Clone)]
struct CommandRow(ForgeCommand);

Expand Down
3 changes: 2 additions & 1 deletion crates/forge_main/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ mod utils;
mod vscode;
mod zsh;

mod select_cmd;
mod update;

use std::sync::LazyLock;

pub use cli::{Cli, ListCommand, ListCommandGroup, TopLevelCommand};
pub use cli::{Cli, ListCommand, ListCommandGroup, SelectArgs, TopLevelCommand};
pub use sandbox::Sandbox;
pub use title_display::*;
pub use ui::UI;
Expand Down
8 changes: 5 additions & 3 deletions crates/forge_main/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use clap::Parser;
use forge_api::ForgeAPI;
use forge_config::ForgeConfig;
use forge_domain::TitleFormat;
use forge_main::{Cli, Sandbox, TitleDisplayExt, UI, tracker};
use forge_main::{Cli, Sandbox, TitleDisplayExt, TopLevelCommand, UI, tracker};

/// Enables ENABLE_VIRTUAL_TERMINAL_PROCESSING on the stdout console handle.
///
Expand Down Expand Up @@ -90,8 +90,10 @@ async fn run() -> Result<()> {
// Initialize and run the UI
let mut cli = Cli::parse();

// Check if there's piped input
if !std::io::stdin().is_terminal() {
// Check if there's piped input, but skip for `forge select` since that
// command uses stdin for its item list.
let is_select = matches!(cli.subcommands, Some(TopLevelCommand::Select(_)));
if !is_select && !std::io::stdin().is_terminal() {
let mut stdin_content = String::new();
std::io::stdin().read_to_string(&mut stdin_content)?;
let trimmed_content = stdin_content.trim();
Expand Down
Loading
Loading