From c74e03defa35f0c30cee4bb7e93ac942bf376e3d Mon Sep 17 00:00:00 2001 From: Fabio Lissi Date: Thu, 9 Apr 2026 21:45:27 -0500 Subject: [PATCH] feat(shell-plugin): add Fish shell plugin port of ZSH plugin Adds a complete Fish shell plugin under shell-plugin/fish/ that provides the same UX as the existing ZSH plugin for Fish users. Features: - :command dispatch with 40+ commands via Enter key override - fzf-powered Tab completion for @file paths and :command names - Right prompt showing active agent, model, conversation, and reasoning effort - Session overrides (model, provider, reasoning effort) without editing config - Full tab completion for the forge CLI binary Structured as a Fisher-compatible plugin (fisher install FabioLissi/forge-fish). Also available standalone via the Fisher plugin registry at: https://github.com/FabioLissi/forge-fish --- shell-plugin/fish/README.md | 105 +++++++++++ shell-plugin/fish/completions/forge.fish | 172 ++++++++++++++++++ shell-plugin/fish/conf.d/forge.fish | 130 +++++++++++++ .../fish/functions/_forge_accept_line.fish | 126 +++++++++++++ .../fish/functions/_forge_action_agent.fish | 37 ++++ .../fish/functions/_forge_action_clone.fish | 35 ++++ .../fish/functions/_forge_action_commit.fish | 11 ++ .../functions/_forge_action_commit_model.fish | 16 ++ .../_forge_action_commit_preview.fish | 21 +++ .../fish/functions/_forge_action_compact.fish | 9 + .../fish/functions/_forge_action_config.fish | 5 + .../functions/_forge_action_config_edit.fish | 31 ++++ ..._forge_action_config_reasoning_effort.fish | 22 +++ .../_forge_action_config_reload.fish | 12 ++ .../functions/_forge_action_conversation.fish | 61 +++++++ .../_forge_action_conversation_rename.fish | 47 +++++ .../fish/functions/_forge_action_copy.fish | 29 +++ .../fish/functions/_forge_action_default.fish | 60 ++++++ .../fish/functions/_forge_action_doctor.fish | 7 + .../fish/functions/_forge_action_dump.fish | 14 ++ .../fish/functions/_forge_action_editor.fish | 52 ++++++ .../fish/functions/_forge_action_env.fish | 5 + .../fish/functions/_forge_action_info.fish | 9 + .../functions/_forge_action_keyboard.fish | 75 ++++++++ .../fish/functions/_forge_action_login.fish | 10 + .../fish/functions/_forge_action_logout.fish | 10 + .../fish/functions/_forge_action_model.fish | 21 +++ .../fish/functions/_forge_action_new.fish | 17 ++ .../functions/_forge_action_provider.fish | 10 + .../_forge_action_reasoning_effort.fish | 28 +++ .../fish/functions/_forge_action_rename.fish | 14 ++ .../fish/functions/_forge_action_retry.fish | 9 + .../_forge_action_session_model.fish | 34 ++++ .../fish/functions/_forge_action_skill.fish | 5 + .../fish/functions/_forge_action_suggest.fish | 19 ++ .../_forge_action_suggest_model.fish | 16 ++ .../fish/functions/_forge_action_sync.fish | 5 + .../functions/_forge_action_sync_info.fish | 5 + .../functions/_forge_action_sync_init.fish | 5 + .../functions/_forge_action_sync_status.fish | 5 + .../fish/functions/_forge_action_tools.fish | 6 + .../functions/_forge_clear_conversation.fish | 7 + .../functions/_forge_clone_and_switch.fish | 28 +++ shell-plugin/fish/functions/_forge_exec.fish | 24 +++ .../functions/_forge_exec_interactive.fish | 23 +++ .../fish/functions/_forge_find_index.fish | 34 ++++ shell-plugin/fish/functions/_forge_fzf.fish | 4 + .../fish/functions/_forge_get_commands.fish | 7 + .../fish/functions/_forge_highlight_noop.fish | 7 + shell-plugin/fish/functions/_forge_log.fish | 21 +++ .../fish/functions/_forge_pick_model.fish | 30 +++ .../fish/functions/_forge_rprompt_info.fish | 40 ++++ .../functions/_forge_select_provider.fish | 49 +++++ .../_forge_start_background_sync.fish | 17 ++ .../_forge_start_background_update.fish | 5 + .../functions/_forge_switch_conversation.fish | 8 + .../fish/functions/_forge_tab_completion.fish | 132 ++++++++++++++ 57 files changed, 1746 insertions(+) create mode 100644 shell-plugin/fish/README.md create mode 100644 shell-plugin/fish/completions/forge.fish create mode 100644 shell-plugin/fish/conf.d/forge.fish create mode 100644 shell-plugin/fish/functions/_forge_accept_line.fish create mode 100644 shell-plugin/fish/functions/_forge_action_agent.fish create mode 100644 shell-plugin/fish/functions/_forge_action_clone.fish create mode 100644 shell-plugin/fish/functions/_forge_action_commit.fish create mode 100644 shell-plugin/fish/functions/_forge_action_commit_model.fish create mode 100644 shell-plugin/fish/functions/_forge_action_commit_preview.fish create mode 100644 shell-plugin/fish/functions/_forge_action_compact.fish create mode 100644 shell-plugin/fish/functions/_forge_action_config.fish create mode 100644 shell-plugin/fish/functions/_forge_action_config_edit.fish create mode 100644 shell-plugin/fish/functions/_forge_action_config_reasoning_effort.fish create mode 100644 shell-plugin/fish/functions/_forge_action_config_reload.fish create mode 100644 shell-plugin/fish/functions/_forge_action_conversation.fish create mode 100644 shell-plugin/fish/functions/_forge_action_conversation_rename.fish create mode 100644 shell-plugin/fish/functions/_forge_action_copy.fish create mode 100644 shell-plugin/fish/functions/_forge_action_default.fish create mode 100644 shell-plugin/fish/functions/_forge_action_doctor.fish create mode 100644 shell-plugin/fish/functions/_forge_action_dump.fish create mode 100644 shell-plugin/fish/functions/_forge_action_editor.fish create mode 100644 shell-plugin/fish/functions/_forge_action_env.fish create mode 100644 shell-plugin/fish/functions/_forge_action_info.fish create mode 100644 shell-plugin/fish/functions/_forge_action_keyboard.fish create mode 100644 shell-plugin/fish/functions/_forge_action_login.fish create mode 100644 shell-plugin/fish/functions/_forge_action_logout.fish create mode 100644 shell-plugin/fish/functions/_forge_action_model.fish create mode 100644 shell-plugin/fish/functions/_forge_action_new.fish create mode 100644 shell-plugin/fish/functions/_forge_action_provider.fish create mode 100644 shell-plugin/fish/functions/_forge_action_reasoning_effort.fish create mode 100644 shell-plugin/fish/functions/_forge_action_rename.fish create mode 100644 shell-plugin/fish/functions/_forge_action_retry.fish create mode 100644 shell-plugin/fish/functions/_forge_action_session_model.fish create mode 100644 shell-plugin/fish/functions/_forge_action_skill.fish create mode 100644 shell-plugin/fish/functions/_forge_action_suggest.fish create mode 100644 shell-plugin/fish/functions/_forge_action_suggest_model.fish create mode 100644 shell-plugin/fish/functions/_forge_action_sync.fish create mode 100644 shell-plugin/fish/functions/_forge_action_sync_info.fish create mode 100644 shell-plugin/fish/functions/_forge_action_sync_init.fish create mode 100644 shell-plugin/fish/functions/_forge_action_sync_status.fish create mode 100644 shell-plugin/fish/functions/_forge_action_tools.fish create mode 100644 shell-plugin/fish/functions/_forge_clear_conversation.fish create mode 100644 shell-plugin/fish/functions/_forge_clone_and_switch.fish create mode 100644 shell-plugin/fish/functions/_forge_exec.fish create mode 100644 shell-plugin/fish/functions/_forge_exec_interactive.fish create mode 100644 shell-plugin/fish/functions/_forge_find_index.fish create mode 100644 shell-plugin/fish/functions/_forge_fzf.fish create mode 100644 shell-plugin/fish/functions/_forge_get_commands.fish create mode 100644 shell-plugin/fish/functions/_forge_highlight_noop.fish create mode 100644 shell-plugin/fish/functions/_forge_log.fish create mode 100644 shell-plugin/fish/functions/_forge_pick_model.fish create mode 100644 shell-plugin/fish/functions/_forge_rprompt_info.fish create mode 100644 shell-plugin/fish/functions/_forge_select_provider.fish create mode 100644 shell-plugin/fish/functions/_forge_start_background_sync.fish create mode 100644 shell-plugin/fish/functions/_forge_start_background_update.fish create mode 100644 shell-plugin/fish/functions/_forge_switch_conversation.fish create mode 100644 shell-plugin/fish/functions/_forge_tab_completion.fish diff --git a/shell-plugin/fish/README.md b/shell-plugin/fish/README.md new file mode 100644 index 0000000000..b25c08b8ce --- /dev/null +++ b/shell-plugin/fish/README.md @@ -0,0 +1,105 @@ +# Forge Fish Plugin + +A Fish shell plugin that provides the same `:command` shortcuts, fzf-powered completions, and right-prompt integration as the built-in ZSH plugin — for users who run [Fish](https://fishshell.com/) as their daily driver. + +## Features + +- **`:command` dispatch** — 40+ commands routed through a single Enter key override +- **fzf integration** — Tab completion for `@file` paths and `:command` names with live preview +- **Right prompt** — shows active agent, model, conversation, and reasoning effort (plays nice with starship and other prompt themes) +- **Session overrides** — switch models, providers, and reasoning effort on the fly without touching config files +- **CLI completions** — full tab completion for the `forge` binary itself, including nested subcommands + +## Requirements + +- [Forge Code](https://github.com/antinomyhq/forgecode) installed and on your `$PATH` +- [Fish](https://fishshell.com/) 3.4+ +- [Fisher](https://github.com/jorgebucaran/fisher) (plugin manager) +- [fzf](https://github.com/junegunn/fzf) +- Optional: [fd](https://github.com/sharkdp/fd) for faster `@file` completion +- Optional: [bat](https://github.com/sharkdp/bat) for syntax-highlighted file previews + +## Install + +Via [Fisher](https://github.com/jorgebucaran/fisher): + +```fish +fisher install FabioLissi/forge-fish +``` + +To update: + +```fish +fisher update FabioLissi/forge-fish +``` + +To uninstall: + +```fish +fisher remove FabioLissi/forge-fish +``` + +## Usage + +### Quick reference + +| Shortcut | What it does | +|---|---| +| `: ` | Send a prompt to Forge in the current conversation | +| `:new` / `:n` | Start a new conversation | +| `:suggest` / `:s` | AI generates a shell command from your description | +| `:commit` | AI writes a commit message and commits | +| `:commit-preview` | Preview the commit message without committing | +| `:model` / `:m` | Switch model for this session | +| `:agent` / `:a` | Switch between agents (forge, sage, muse, etc.) | +| `:conversation` / `:c` | Browse and switch conversations | +| `:edit` / `:ed` | Open your `$EDITOR` for a multi-line prompt | +| `:info` / `:i` | Show current session info | +| `:config` | Show configuration | +| `:copy` | Copy last response to clipboard | +| `:clone` | Clone a conversation | +| `:rename` / `:rn` | Rename the current conversation | +| `:compact` | Compact conversation context | +| `:retry` / `:r` | Retry the last command | +| `:dump` / `:d` | Export conversation as JSON or HTML | +| `:tools` / `:t` | List available tools | +| `:skill` | List available skills | +| `:sync` | Sync workspace for codebase search | +| `:doctor` | Run environment diagnostics | +| `:kb` | Show keyboard shortcuts reference | + +### Tab completion + +- Type `:` and press **Tab** to browse all commands with fzf +- Type `@` and press **Tab** to pick files from the current directory +- The `forge` CLI itself gets full tab completion (try `forge config set `) + +### Right prompt + +The plugin appends Forge session info to your right prompt. If you use starship or another prompt tool, it wraps around it — your existing right prompt stays intact. + +### Session overrides + +Change settings for the current shell session without editing config files: + +```fish +:model # pick a model interactively +:reasoning-effort # set reasoning effort (low/medium/high) +:config-reload # reset all session overrides back to global config +``` + +## Plugin structure + +This plugin follows the [Fisher](https://github.com/jorgebucaran/fisher) plugin layout: + +``` +fish/ +├── completions/ # Tab completions for the forge CLI +│ └── forge.fish +├── conf.d/ # Auto-sourced on shell start +│ └── forge.fish # Right prompt and session state init +└── functions/ # All :command handlers and helpers + ├── forge_prompt.fish + ├── _forge_*.fish # Internal helper functions + └── ... +``` diff --git a/shell-plugin/fish/completions/forge.fish b/shell-plugin/fish/completions/forge.fish new file mode 100644 index 0000000000..53a28a0e3a --- /dev/null +++ b/shell-plugin/fish/completions/forge.fish @@ -0,0 +1,172 @@ +# Fish completions for forge CLI +# Auto-loaded from ~/.config/fish/completions/ + +# ── Helpers ─────────────────────────────────────────────────────────────────── +# __forge_needs_subcommand +# True when has been seen but the next positional arg is still needed. +# Works by counting positional (non-option) tokens on the command line. +function __forge_needs_subcommand + set -l parent $argv[1] + set -l cmd (commandline -opc) + set -l found_parent 0 + for tok in $cmd[2..] # skip "forge" itself + switch $tok + case '-*' + continue + case '*' + if test $found_parent -eq 0 + if test "$tok" = "$parent" + set found_parent 1 + end + else + # A positional token after parent => subcommand already present + return 1 + end + end + end + # Return true only if we found the parent and there's no sub yet + test $found_parent -eq 1 +end + +# __forge_at_depth3 +# True when "forge " have been seen and a 3rd positional +# argument is still needed (e.g. "forge config set "). +function __forge_at_depth3 + set -l parent $argv[1] + set -l child $argv[2] + set -l cmd (commandline -opc) + set -l depth 0 + set -l match_parent 0 + set -l match_child 0 + for tok in $cmd[2..] + switch $tok + case '-*' + continue + case '*' + set depth (math $depth + 1) + if test $depth -eq 1; and test "$tok" = "$parent" + set match_parent 1 + else if test $depth -eq 2; and test $match_parent -eq 1; and test "$tok" = "$child" + set match_child 1 + else if test $depth -ge 3 + return 1 # already have a 3rd token + end + end + end + test $match_parent -eq 1; and test $match_child -eq 1 +end + +# Disable file completions by default for forge +complete -c forge -f + +# ── Top-level subcommands ───────────────────────────────────────────────────── +complete -c forge -n __fish_use_subcommand -a agent -d "Manage agents" +complete -c forge -n __fish_use_subcommand -a zsh -d "Generate shell extension scripts" +complete -c forge -n __fish_use_subcommand -a list -d "List agents, models, providers, tools, or MCP servers" +complete -c forge -n __fish_use_subcommand -a banner -d "Display the banner with version information" +complete -c forge -n __fish_use_subcommand -a info -d "Show configuration, active model, and environment status" +complete -c forge -n __fish_use_subcommand -a env -d "Display environment information" +complete -c forge -n __fish_use_subcommand -a config -d "Get, set, or list configuration values" +complete -c forge -n __fish_use_subcommand -a conversation -d "Manage conversation history and state" +complete -c forge -n __fish_use_subcommand -a commit -d "Generate and optionally commit changes with AI-generated message" +complete -c forge -n __fish_use_subcommand -a mcp -d "Manage Model Context Protocol servers" +complete -c forge -n __fish_use_subcommand -a suggest -d "Generate shell commands without executing them" +complete -c forge -n __fish_use_subcommand -a provider -d "Manage API provider authentication" +complete -c forge -n __fish_use_subcommand -a cmd -d "Run or list custom commands" +complete -c forge -n __fish_use_subcommand -a workspace -d "Manage workspaces for semantic search" +complete -c forge -n __fish_use_subcommand -a data -d "Process JSONL data through LLM" +complete -c forge -n __fish_use_subcommand -a vscode -d "VS Code integration commands" +complete -c forge -n __fish_use_subcommand -a update -d "Update forge to the latest version" +complete -c forge -n __fish_use_subcommand -a setup -d "Setup zsh integration" +complete -c forge -n __fish_use_subcommand -a doctor -d "Run diagnostics on shell environment" + +# Top-level options +complete -c forge -n __fish_use_subcommand -s p -l prompt -d "Direct prompt to process" +complete -c forge -n __fish_use_subcommand -l conversation -d "Path to conversation JSON" -r -F +complete -c forge -n __fish_use_subcommand -l conversation-id -d "Conversation ID to use" +complete -c forge -n __fish_use_subcommand -s C -l directory -d "Working directory" -r -a "(__fish_complete_directories)" +complete -c forge -n __fish_use_subcommand -l sandbox -d "Isolated git worktree name" +complete -c forge -n __fish_use_subcommand -l verbose -d "Enable verbose logging" +complete -c forge -n __fish_use_subcommand -l agent -d "Agent ID to use" +complete -c forge -n __fish_use_subcommand -s e -l event -d "Event to dispatch (JSON)" +complete -c forge -n __fish_use_subcommand -s h -l help -d "Print help" +complete -c forge -n __fish_use_subcommand -s V -l version -d "Print version" + +# ── config subcommands ──────────────────────────────────────────────────────── +complete -c forge -n "__forge_needs_subcommand config" -a set -d "Set a configuration value" +complete -c forge -n "__forge_needs_subcommand config" -a get -d "Get a configuration value" +complete -c forge -n "__forge_needs_subcommand config" -a list -d "List configuration" + +# config set targets +complete -c forge -n "__forge_at_depth3 config set" -a model -d "Set default model" +complete -c forge -n "__forge_at_depth3 config set" -a provider -d "Set default provider" +complete -c forge -n "__forge_at_depth3 config set" -a commit -d "Set commit model" +complete -c forge -n "__forge_at_depth3 config set" -a suggest -d "Set suggest model" +complete -c forge -n "__forge_at_depth3 config set" -a reasoning-effort -d "Set reasoning effort" + +# config get targets +complete -c forge -n "__forge_at_depth3 config get" -a model -d "Get default model" +complete -c forge -n "__forge_at_depth3 config get" -a provider -d "Get default provider" +complete -c forge -n "__forge_at_depth3 config get" -a commit -d "Get commit model" +complete -c forge -n "__forge_at_depth3 config get" -a suggest -d "Get suggest model" +complete -c forge -n "__forge_at_depth3 config get" -a reasoning-effort -d "Get reasoning effort" + +# ── conversation subcommands ────────────────────────────────────────────────── +complete -c forge -n "__forge_needs_subcommand conversation" -a list -d "List conversations" +complete -c forge -n "__forge_needs_subcommand conversation" -a new -d "Create new conversation" +complete -c forge -n "__forge_needs_subcommand conversation" -a dump -d "Export conversation" +complete -c forge -n "__forge_needs_subcommand conversation" -a compact -d "Compact conversation" +complete -c forge -n "__forge_needs_subcommand conversation" -a retry -d "Retry last turn" +complete -c forge -n "__forge_needs_subcommand conversation" -a resume -d "Resume conversation" +complete -c forge -n "__forge_needs_subcommand conversation" -a show -d "Show conversation" +complete -c forge -n "__forge_needs_subcommand conversation" -a info -d "Show conversation info" +complete -c forge -n "__forge_needs_subcommand conversation" -a stats -d "Show conversation stats" +complete -c forge -n "__forge_needs_subcommand conversation" -a clone -d "Clone conversation" +complete -c forge -n "__forge_needs_subcommand conversation" -a delete -d "Delete conversation" +complete -c forge -n "__forge_needs_subcommand conversation" -a rename -d "Rename conversation" + +# ── list subcommands ────────────────────────────────────────────────────────── +complete -c forge -n "__forge_needs_subcommand list" -a agent -d "List agents" +complete -c forge -n "__forge_needs_subcommand list" -a provider -d "List providers" +complete -c forge -n "__forge_needs_subcommand list" -a model -d "List models" +complete -c forge -n "__forge_needs_subcommand list" -a command -d "List commands" +complete -c forge -n "__forge_needs_subcommand list" -a config -d "List config" +complete -c forge -n "__forge_needs_subcommand list" -a tool -d "List tools" +complete -c forge -n "__forge_needs_subcommand list" -a mcp -d "List MCP servers" +complete -c forge -n "__forge_needs_subcommand list" -a conversation -d "List conversations" +complete -c forge -n "__forge_needs_subcommand list" -a cmd -d "List custom commands" +complete -c forge -n "__forge_needs_subcommand list" -a skill -d "List skills" + +# ── provider subcommands ────────────────────────────────────────────────────── +complete -c forge -n "__forge_needs_subcommand provider" -a login -d "Log in to provider" +complete -c forge -n "__forge_needs_subcommand provider" -a logout -d "Log out from provider" +complete -c forge -n "__forge_needs_subcommand provider" -a list -d "List providers" + +# ── mcp subcommands ─────────────────────────────────────────────────────────── +complete -c forge -n "__forge_needs_subcommand mcp" -a import -d "Import MCP server config" +complete -c forge -n "__forge_needs_subcommand mcp" -a list -d "List MCP servers" +complete -c forge -n "__forge_needs_subcommand mcp" -a remove -d "Remove MCP server" +complete -c forge -n "__forge_needs_subcommand mcp" -a show -d "Show MCP server details" +complete -c forge -n "__forge_needs_subcommand mcp" -a reload -d "Reload MCP servers" + +# ── workspace subcommands ───────────────────────────────────────────────────── +complete -c forge -n "__forge_needs_subcommand workspace" -a sync -d "Sync workspace" +complete -c forge -n "__forge_needs_subcommand workspace" -a init -d "Initialize workspace" +complete -c forge -n "__forge_needs_subcommand workspace" -a status -d "Show workspace status" +complete -c forge -n "__forge_needs_subcommand workspace" -a info -d "Show workspace info" + +# ── commit options ──────────────────────────────────────────────────────────── +complete -c forge -n "__fish_seen_subcommand_from commit" -l max-diff -d "Maximum git diff size in bytes" +complete -c forge -n "__fish_seen_subcommand_from commit" -l preview -d "Preview commit message without committing" + +# ── zsh subcommands ─────────────────────────────────────────────────────────── +complete -c forge -n "__forge_needs_subcommand zsh" -a plugin -d "Generate shell plugin script" +complete -c forge -n "__forge_needs_subcommand zsh" -a theme -d "Generate shell theme" +complete -c forge -n "__forge_needs_subcommand zsh" -a doctor -d "Run diagnostics" +complete -c forge -n "__forge_needs_subcommand zsh" -a rprompt -d "Get rprompt information" +complete -c forge -n "__forge_needs_subcommand zsh" -a setup -d "Setup zsh integration" +complete -c forge -n "__forge_needs_subcommand zsh" -a keyboard -d "Show keyboard shortcuts" + +# Common flags across subcommands +complete -c forge -n "__fish_seen_subcommand_from list agent provider mcp config conversation" -l porcelain -d "Machine-readable output" +complete -c forge -n "__fish_seen_subcommand_from list agent provider mcp config conversation" -s h -l help -d "Print help" diff --git a/shell-plugin/fish/conf.d/forge.fish b/shell-plugin/fish/conf.d/forge.fish new file mode 100644 index 0000000000..9731399761 --- /dev/null +++ b/shell-plugin/fish/conf.d/forge.fish @@ -0,0 +1,130 @@ +# Forge Code - Fish Shell Plugin +# https://github.com/antinomyhq/forge +# Provides :command integration, right prompt, and key bindings + +# ── Fisher lifecycle events ─────────────────────────────────────────────────── +function _forge_install --on-event forge_install + # Runs once after: fisher install +end + +function _forge_update --on-event forge_update + # Runs once after: fisher update +end + +function _forge_uninstall --on-event forge_uninstall + # Clean up key bindings + bind --erase \r + bind --erase \n + bind --erase \t + + # Clean up global variables + set --erase _FORGE_BIN + set --erase _FORGE_MAX_COMMIT_DIFF + set --erase _FORGE_DELIMITER + set --erase _FORGE_PREVIEW_WINDOW + set --erase _FORGE_CONVERSATION_ID + set --erase _FORGE_PREVIOUS_CONVERSATION_ID + set --erase _FORGE_ACTIVE_AGENT + set --erase _FORGE_SESSION_MODEL + set --erase _FORGE_SESSION_PROVIDER + set --erase _FORGE_SESSION_REASONING_EFFORT + set --erase _FORGE_COMMANDS + set --erase _FORGE_FD_CMD + set --erase _FORGE_CAT_CMD + set --erase _FORGE_THEME_LOADED + set --erase _FORGE_PLUGIN_LOADED + + # Clean up :command abbreviation + abbr --erase _forge_cmd 2>/dev/null + + # Restore original right prompt if we wrapped it + if functions -q _forge_original_fish_right_prompt + functions -c _forge_original_fish_right_prompt fish_right_prompt + functions -e _forge_original_fish_right_prompt + end +end + +# ── Guard: interactive shells only ──────────────────────────────────────────── +if not status is-interactive + return +end + +# ── Global variables ────────────────────────────────────────────────────────── +set -g _FORGE_BIN (command -v forge 2>/dev/null; or echo forge) +set -g _FORGE_MAX_COMMIT_DIFF (test -n "$FORGE_MAX_COMMIT_DIFF"; and echo $FORGE_MAX_COMMIT_DIFF; or echo 100000) +set -g _FORGE_DELIMITER '\\s\\s+' +set -g _FORGE_PREVIEW_WINDOW "--preview-window=bottom:75%:wrap:border-sharp" +set -g _FORGE_CONVERSATION_ID "" +set -g _FORGE_PREVIOUS_CONVERSATION_ID "" +set -g _FORGE_ACTIVE_AGENT "" +set -g _FORGE_SESSION_MODEL "" +set -g _FORGE_SESSION_PROVIDER "" +set -g _FORGE_SESSION_REASONING_EFFORT "" +set -g _FORGE_COMMANDS "" + +# Detect fd +if command -q fdfind + set -g _FORGE_FD_CMD fdfind +else if command -q fd + set -g _FORGE_FD_CMD fd +else + set -g _FORGE_FD_CMD fd +end + +# Detect bat +if command -q bat + set -g _FORGE_CAT_CMD "bat --color=always --style=numbers,changes --line-range=:500" +else + set -g _FORGE_CAT_CMD cat +end + +# ── Key bindings ────────────────────────────────────────────────────────────── +# Override Enter to intercept :commands +bind \r _forge_accept_line +bind \n _forge_accept_line + +# Tab completion for @file and :command +bind \t _forge_tab_completion + +# ── Syntax highlighting for :commands ───────────────────────────────────────── +# Fish's built-in highlighter colors unknown commands red. Since :commands like +# :kb, :model, :suggest are intercepted by our Enter key handler (not real +# commands), they'd always appear red. We register a regex abbreviation that +# matches any `:word` token in command position. The highlighter checks +# abbreviations when deciding if a command is valid, so :commands get the +# normal command color. The abbreviation function returns the input unchanged +# so it never visually expands -- our Enter keybinding fires first anyway. +abbr --erase _forge_cmd 2>/dev/null +abbr --add _forge_cmd --position command --regex ":[a-zA-Z][-a-zA-Z0-9_]*" --function _forge_highlight_noop + +# ── Right prompt integration ────────────────────────────────────────────────── +# Wrap the existing fish_right_prompt (e.g. starship) to append forge info. +# We defer this so other conf.d scripts (starship) can set up first. +if not set -q _FORGE_THEME_LOADED + function _forge_install_rprompt --on-event fish_prompt + # Only run once + functions -e _forge_install_rprompt + + if functions -q fish_right_prompt + # Save the original right prompt + functions -c fish_right_prompt _forge_original_fish_right_prompt + + # Redefine with forge info appended + function fish_right_prompt + set -l forge_info (_forge_rprompt_info) + set -l original (_forge_original_fish_right_prompt) + if test -n "$forge_info" + echo -n "$forge_info " + end + echo -n "$original" + end + else + function fish_right_prompt + _forge_rprompt_info + end + end + end + set -g _FORGE_THEME_LOADED (date +%s) +end + +set -g _FORGE_PLUGIN_LOADED (date +%s) diff --git a/shell-plugin/fish/functions/_forge_accept_line.fish b/shell-plugin/fish/functions/_forge_accept_line.fish new file mode 100644 index 0000000000..72a02e2557 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_accept_line.fish @@ -0,0 +1,126 @@ +# Forge: _forge_accept_line - Main enter key handler +# Intercepts :commands and `: ` patterns, passes everything else through +function _forge_accept_line + set -l buf (commandline) + + # Check if the line matches `:command [args]` or `: text` + # Pattern 1: :command_name [optional args] + # Pattern 2: : freetext (prompt to forge) + # Otherwise: pass through to normal execution + + set -l user_action "" + set -l input_text "" + + if string match -rq '^:([a-zA-Z][a-zA-Z0-9_-]*)(\s+(.*))?$' -- "$buf" + set user_action (string match -r '^:([a-zA-Z][a-zA-Z0-9_-]*)' -- "$buf" | tail -1) + # Extract text after the command + set input_text (string replace -r '^:[a-zA-Z][a-zA-Z0-9_-]*\s*' '' -- "$buf") + else if string match -rq '^: (.+)$' -- "$buf" + set user_action "" + set input_text (string replace -r '^: ' '' -- "$buf") + else + # Normal command - execute normally + commandline -f execute + return + end + + # Add to history (Fish 4.x uses "append", older versions use "merge") + builtin history append -- "$buf" 2>/dev/null + or builtin history merge 2>/dev/null + + # Handle aliases + switch "$user_action" + case ask + set user_action sage + case plan + set user_action muse + end + + # Clear the command line before running actions + commandline -r "" + commandline -f repaint + + # Dispatch to action handlers + switch "$user_action" + case new n + _forge_action_new "$input_text" + case info i + _forge_action_info + case env e + _forge_action_env + case dump d + _forge_action_dump "$input_text" + case compact + _forge_action_compact + case retry r + _forge_action_retry + case agent a + _forge_action_agent "$input_text" + case conversation c + _forge_action_conversation "$input_text" + case config-provider provider p + _forge_action_provider "$input_text" + case config-model cm + _forge_action_model "$input_text" + case model m + _forge_action_session_model "$input_text" + case config-reload cr model-reset mr + _forge_action_config_reload + case reasoning-effort re + _forge_action_reasoning_effort "$input_text" + case config-reasoning-effort cre + _forge_action_config_reasoning_effort "$input_text" + case config-commit-model ccm + _forge_action_commit_model "$input_text" + case config-suggest-model csm + _forge_action_suggest_model "$input_text" + case tools t + _forge_action_tools + case config + _forge_action_config + case config-edit ce + _forge_action_config_edit + case skill + _forge_action_skill + case edit ed + _forge_action_editor "$input_text" + return + case commit + _forge_action_commit "$input_text" + case commit-preview + _forge_action_commit_preview "$input_text" + return + case suggest s + _forge_action_suggest "$input_text" + return + case clone + _forge_action_clone "$input_text" + case rename rn + _forge_action_rename "$input_text" + case conversation-rename + _forge_action_conversation_rename "$input_text" + case copy + _forge_action_copy + case workspace-sync sync + _forge_action_sync + case workspace-init sync-init + _forge_action_sync_init + case workspace-status sync-status + _forge_action_sync_status + case workspace-info sync-info + _forge_action_sync_info + case provider-login login + _forge_action_login "$input_text" + case logout + _forge_action_logout "$input_text" + case doctor + _forge_action_doctor + case keyboard-shortcuts kb + _forge_action_keyboard + case '*' + _forge_action_default "$user_action" "$input_text" + end + + commandline -r "" + commandline -f repaint +end diff --git a/shell-plugin/fish/functions/_forge_action_agent.fish b/shell-plugin/fish/functions/_forge_action_agent.fish new file mode 100644 index 0000000000..ea91d0ff2f --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_agent.fish @@ -0,0 +1,37 @@ +# Forge: _forge_action_agent - Switch agent interactively +function _forge_action_agent + set -l input_text $argv[1] + echo + + if test -n "$input_text" + set -l agent_id $input_text + set -l agent_exists ( + $_FORGE_BIN list agents --porcelain 2>/dev/null | tail -n +2 | grep -q "^$agent_id\\b"; and echo true; or echo false + ) + if test "$agent_exists" = false + _forge_log error "Agent '"(set_color --bold)"$agent_id"(set_color normal)"' not found" + return 0 + end + set -g _FORGE_ACTIVE_AGENT $agent_id + _forge_log success "Switched to agent "(set_color --bold)"$agent_id"(set_color normal) + return 0 + end + + set -l agents_output ($_FORGE_BIN list agents --porcelain 2>/dev/null) + if test -n "$agents_output" + set -l current_agent $_FORGE_ACTIVE_AGENT + set -l fzf_args --prompt="Agent ❯ " --delimiter="$_FORGE_DELIMITER" --with-nth="1,2,4,5,6" + if test -n "$current_agent" + set -l idx (printf '%s\n' $agents_output | _forge_find_index "$current_agent") + set fzf_args $fzf_args --bind="start:pos($idx)" + end + set -l selected_agent (printf '%s\n' $agents_output | _forge_fzf --header-lines=1 $fzf_args) + if test -n "$selected_agent" + set -l agent_id (echo "$selected_agent" | awk '{print $1}') + set -g _FORGE_ACTIVE_AGENT $agent_id + _forge_log success "Switched to agent "(set_color --bold)"$agent_id"(set_color normal) + end + else + _forge_log error "No agents found" + end +end diff --git a/shell-plugin/fish/functions/_forge_action_clone.fish b/shell-plugin/fish/functions/_forge_action_clone.fish new file mode 100644 index 0000000000..bd04067562 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_clone.fish @@ -0,0 +1,35 @@ +# Forge: _forge_action_clone - Clone a conversation +function _forge_action_clone + set -l input_text $argv[1] + echo + + if test -n "$input_text" + _forge_clone_and_switch $input_text + return 0 + end + + set -l conversations_output ($_FORGE_BIN conversation list --porcelain 2>/dev/null) + if test -z "$conversations_output" + _forge_log error "No conversations found" + return 0 + end + + set -l current_id $_FORGE_CONVERSATION_ID + set -l fzf_args \ + --prompt="Clone Conversation ❯ " \ + --delimiter="$_FORGE_DELIMITER" \ + --with-nth="2,3" \ + --preview="CLICOLOR_FORCE=1 $_FORGE_BIN conversation info {1}; echo; CLICOLOR_FORCE=1 $_FORGE_BIN conversation show {1}" \ + $_FORGE_PREVIEW_WINDOW + + if test -n "$current_id" + set -l idx (printf '%s\n' $conversations_output | _forge_find_index "$current_id") + set fzf_args $fzf_args --bind="start:pos($idx)" + end + + set -l selected (printf '%s\n' $conversations_output | _forge_fzf --header-lines=1 $fzf_args) + if test -n "$selected" + set -l conversation_id (echo "$selected" | sed -E 's/ .*//' | tr -d '\n') + _forge_clone_and_switch $conversation_id + end +end diff --git a/shell-plugin/fish/functions/_forge_action_commit.fish b/shell-plugin/fish/functions/_forge_action_commit.fish new file mode 100644 index 0000000000..5ff0f5c5c0 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_commit.fish @@ -0,0 +1,11 @@ +# Forge: _forge_action_commit - Generate and apply commit +function _forge_action_commit + set -l additional_context $argv[1] + echo + + if test -n "$additional_context" + env FORCE_COLOR=true CLICOLOR_FORCE=1 $_FORGE_BIN commit --max-diff $_FORGE_MAX_COMMIT_DIFF $additional_context + else + env FORCE_COLOR=true CLICOLOR_FORCE=1 $_FORGE_BIN commit --max-diff $_FORGE_MAX_COMMIT_DIFF + end +end diff --git a/shell-plugin/fish/functions/_forge_action_commit_model.fish b/shell-plugin/fish/functions/_forge_action_commit_model.fish new file mode 100644 index 0000000000..27ac57981d --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_commit_model.fish @@ -0,0 +1,16 @@ +# Forge: _forge_action_commit_model - Set commit model +function _forge_action_commit_model + set -l input_text $argv[1] + echo + + set -l commit_output (_forge_exec config get commit 2>/dev/null) + set -l current_commit_provider (printf '%s\n' $commit_output | head -n 1) + set -l current_commit_model (printf '%s\n' $commit_output | tail -n 1) + + set -l selected (_forge_pick_model "Commit Model ❯ " "$current_commit_model" "$input_text" "$current_commit_provider" 4) + if test -n "$selected" + set -l model_id (echo "$selected" | awk -F ' +' '{print $1}' | string trim) + set -l provider_id (echo "$selected" | awk -F ' +' '{print $4}' | string trim) + _forge_exec config set commit $provider_id $model_id + end +end diff --git a/shell-plugin/fish/functions/_forge_action_commit_preview.fish b/shell-plugin/fish/functions/_forge_action_commit_preview.fish new file mode 100644 index 0000000000..d1fe7566d2 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_commit_preview.fish @@ -0,0 +1,21 @@ +# Forge: _forge_action_commit_preview - Preview commit message and place in command line +function _forge_action_commit_preview + set -l additional_context $argv[1] + echo + + set -l commit_message + if test -n "$additional_context" + set commit_message (env FORCE_COLOR=true CLICOLOR_FORCE=1 $_FORGE_BIN commit --preview --max-diff $_FORGE_MAX_COMMIT_DIFF $additional_context) + else + set commit_message (env FORCE_COLOR=true CLICOLOR_FORCE=1 $_FORGE_BIN commit --preview --max-diff $_FORGE_MAX_COMMIT_DIFF) + end + + if test -n "$commit_message" + if git diff --staged --quiet + commandline -r "git commit -am '$commit_message'" + else + commandline -r "git commit -m '$commit_message'" + end + commandline -f repaint + end +end diff --git a/shell-plugin/fish/functions/_forge_action_compact.fish b/shell-plugin/fish/functions/_forge_action_compact.fish new file mode 100644 index 0000000000..f5c17737fd --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_compact.fish @@ -0,0 +1,9 @@ +# Forge: _forge_action_compact - Compact conversation +function _forge_action_compact + echo + if test -z "$_FORGE_CONVERSATION_ID" + _forge_log error "No active conversation. Start a conversation first or use :conversation to see existing ones" + return 0 + end + _forge_exec conversation compact $_FORGE_CONVERSATION_ID +end diff --git a/shell-plugin/fish/functions/_forge_action_config.fish b/shell-plugin/fish/functions/_forge_action_config.fish new file mode 100644 index 0000000000..0fd2d6b41e --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_config.fish @@ -0,0 +1,5 @@ +# Forge: _forge_action_config - Show config +function _forge_action_config + echo + $_FORGE_BIN config list +end diff --git a/shell-plugin/fish/functions/_forge_action_config_edit.fish b/shell-plugin/fish/functions/_forge_action_config_edit.fish new file mode 100644 index 0000000000..df5be726d8 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_config_edit.fish @@ -0,0 +1,31 @@ +# Forge: _forge_action_config_edit - Edit forge config in $EDITOR +function _forge_action_config_edit + echo + set -l editor_cmd (test -n "$FORGE_EDITOR"; and echo $FORGE_EDITOR; or test -n "$EDITOR"; and echo $EDITOR; or echo nano) + set -l editor_bin (echo $editor_cmd | awk '{print $1}') + + if not command -q $editor_bin + _forge_log error "Editor not found: $editor_cmd (set FORGE_EDITOR or EDITOR)" + return 1 + end + + set -l config_file "$HOME/forge/.forge.toml" + if not test -d "$HOME/forge" + mkdir -p "$HOME/forge"; or begin + _forge_log error "Failed to create ~/forge directory" + return 1 + end + end + if not test -f "$config_file" + touch "$config_file"; or begin + _forge_log error "Failed to create $config_file" + return 1 + end + end + + eval "$editor_cmd '$config_file'" /dev/tty 2>&1 + set -l exit_code $status + if test $exit_code -ne 0 + _forge_log error "Editor exited with error code $exit_code" + end +end diff --git a/shell-plugin/fish/functions/_forge_action_config_reasoning_effort.fish b/shell-plugin/fish/functions/_forge_action_config_reasoning_effort.fish new file mode 100644 index 0000000000..de4ec192ff --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_config_reasoning_effort.fish @@ -0,0 +1,22 @@ +# Forge: _forge_action_config_reasoning_effort - Set config reasoning effort (persistent) +function _forge_action_config_reasoning_effort + set -l input_text $argv[1] + echo + + set -l efforts "EFFORT\nnone\nminimal\nlow\nmedium\nhigh\nxhigh\nmax" + set -l current_effort ($_FORGE_BIN config get reasoning-effort 2>/dev/null) + + set -l fzf_args --prompt="Config Reasoning Effort ❯ " + if test -n "$input_text" + set fzf_args $fzf_args --query="$input_text" + end + if test -n "$current_effort" + set -l idx (echo -e $efforts | _forge_find_index "$current_effort" 1) + set fzf_args $fzf_args --bind="start:pos($idx)" + end + + set -l selected (echo -e $efforts | _forge_fzf --header-lines=1 $fzf_args) + if test -n "$selected" + _forge_exec config set reasoning-effort $selected + end +end diff --git a/shell-plugin/fish/functions/_forge_action_config_reload.fish b/shell-plugin/fish/functions/_forge_action_config_reload.fish new file mode 100644 index 0000000000..33d1353f56 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_config_reload.fish @@ -0,0 +1,12 @@ +# Forge: _forge_action_config_reload - Clear session overrides +function _forge_action_config_reload + echo + if test -z "$_FORGE_SESSION_MODEL" -a -z "$_FORGE_SESSION_PROVIDER" -a -z "$_FORGE_SESSION_REASONING_EFFORT" + _forge_log info "No session overrides active (already using global config)" + return 0 + end + set -g _FORGE_SESSION_MODEL "" + set -g _FORGE_SESSION_PROVIDER "" + set -g _FORGE_SESSION_REASONING_EFFORT "" + _forge_log success "Session overrides cleared — using global config" +end diff --git a/shell-plugin/fish/functions/_forge_action_conversation.fish b/shell-plugin/fish/functions/_forge_action_conversation.fish new file mode 100644 index 0000000000..d9f6b428fe --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_conversation.fish @@ -0,0 +1,61 @@ +# Forge: _forge_action_conversation - Switch conversation interactively +function _forge_action_conversation + set -l input_text $argv[1] + echo + + # Handle "-" to toggle previous conversation + if test "$input_text" = "-" + if test -z "$_FORGE_PREVIOUS_CONVERSATION_ID" + set input_text "" + else + set -l temp $_FORGE_CONVERSATION_ID + set -g _FORGE_CONVERSATION_ID $_FORGE_PREVIOUS_CONVERSATION_ID + set -g _FORGE_PREVIOUS_CONVERSATION_ID $temp + echo + _forge_exec conversation show $_FORGE_CONVERSATION_ID + _forge_exec conversation info $_FORGE_CONVERSATION_ID + _forge_log success "Switched to conversation "(set_color --bold)"$_FORGE_CONVERSATION_ID"(set_color normal) + return 0 + end + end + + # Direct ID provided + if test -n "$input_text" + set -l conversation_id $input_text + _forge_switch_conversation $conversation_id + echo + _forge_exec conversation show $conversation_id + _forge_exec conversation info $conversation_id + _forge_log success "Switched to conversation "(set_color --bold)"$conversation_id"(set_color normal) + return 0 + end + + # Interactive picker + set -l conversations_output ($_FORGE_BIN conversation list --porcelain 2>/dev/null) + if test -n "$conversations_output" + set -l current_id $_FORGE_CONVERSATION_ID + set -l fzf_args \ + --prompt="Conversation ❯ " \ + --delimiter="$_FORGE_DELIMITER" \ + --with-nth="2,3" \ + --preview="CLICOLOR_FORCE=1 $_FORGE_BIN conversation info {1}; echo; CLICOLOR_FORCE=1 $_FORGE_BIN conversation show {1}" \ + $_FORGE_PREVIEW_WINDOW + + if test -n "$current_id" + set -l idx (printf '%s\n' $conversations_output | _forge_find_index "$current_id" 1) + set fzf_args $fzf_args --bind="start:pos($idx)" + end + + set -l selected (printf '%s\n' $conversations_output | _forge_fzf --header-lines=1 $fzf_args) + if test -n "$selected" + set -l conversation_id (echo "$selected" | sed -E 's/ .*//' | tr -d '\n') + _forge_switch_conversation $conversation_id + echo + _forge_exec conversation show $conversation_id + _forge_exec conversation info $conversation_id + _forge_log success "Switched to conversation "(set_color --bold)"$conversation_id"(set_color normal) + end + else + _forge_log error "No conversations found" + end +end diff --git a/shell-plugin/fish/functions/_forge_action_conversation_rename.fish b/shell-plugin/fish/functions/_forge_action_conversation_rename.fish new file mode 100644 index 0000000000..5f9b5749de --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_conversation_rename.fish @@ -0,0 +1,47 @@ +# Forge: _forge_action_conversation_rename - Rename any conversation +function _forge_action_conversation_rename + set -l input_text $argv[1] + echo + + if test -n "$input_text" + set -l conversation_id (echo "$input_text" | awk '{print $1}') + set -l new_name (echo "$input_text" | awk '{$1=""; print $0}' | string trim) + if test "$conversation_id" = "$new_name" -o -z "$new_name" + _forge_log error "Usage: :conversation-rename " + return 0 + end + _forge_exec conversation rename $conversation_id $new_name + return 0 + end + + set -l conversations_output ($_FORGE_BIN conversation list --porcelain 2>/dev/null) + if test -z "$conversations_output" + _forge_log error "No conversations found" + return 0 + end + + set -l current_id $_FORGE_CONVERSATION_ID + set -l fzf_args \ + --prompt="Rename Conversation ❯ " \ + --delimiter="$_FORGE_DELIMITER" \ + --with-nth="2,3" \ + --preview="CLICOLOR_FORCE=1 $_FORGE_BIN conversation info {1}; echo; CLICOLOR_FORCE=1 $_FORGE_BIN conversation show {1}" \ + $_FORGE_PREVIEW_WINDOW + + if test -n "$current_id" + set -l idx (printf '%s\n' $conversations_output | _forge_find_index "$current_id" 1) + set fzf_args $fzf_args --bind="start:pos($idx)" + end + + set -l selected (printf '%s\n' $conversations_output | _forge_fzf --header-lines=1 $fzf_args) + if test -n "$selected" + set -l conversation_id (echo "$selected" | sed -E 's/ .*//' | tr -d '\n') + echo -n "Enter new name: " + read new_name + if test -n "$new_name" + _forge_exec conversation rename $conversation_id $new_name + else + _forge_log error "No name provided, rename cancelled" + end + end +end diff --git a/shell-plugin/fish/functions/_forge_action_copy.fish b/shell-plugin/fish/functions/_forge_action_copy.fish new file mode 100644 index 0000000000..e45cbeb689 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_copy.fish @@ -0,0 +1,29 @@ +# Forge: _forge_action_copy - Copy last assistant message to clipboard +function _forge_action_copy + echo + if test -z "$_FORGE_CONVERSATION_ID" + _forge_log error "No active conversation. Start a conversation first or use :conversation to see existing ones" + return 0 + end + + set -l content ($_FORGE_BIN conversation show --md $_FORGE_CONVERSATION_ID 2>/dev/null) + if test -z "$content" + _forge_log error "No assistant message found in the current conversation" + return 0 + end + + if command -q pbcopy + printf '%s\n' $content | pbcopy + else if command -q xclip + printf '%s\n' $content | xclip -selection clipboard + else if command -q xsel + printf '%s\n' $content | xsel --clipboard --input + else + _forge_log error "No clipboard utility found (pbcopy, xclip, or xsel required)" + return 0 + end + + set -l line_count (printf '%s\n' $content | wc -l | string trim) + set -l byte_count (printf '%s\n' $content | wc -c | string trim) + _forge_log success "Copied to clipboard "(set_color 888888)"[$line_count lines, $byte_count bytes]"(set_color normal) +end diff --git a/shell-plugin/fish/functions/_forge_action_default.fish b/shell-plugin/fish/functions/_forge_action_default.fish new file mode 100644 index 0000000000..8bbc18a032 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_default.fish @@ -0,0 +1,60 @@ +# Forge: _forge_action_default - Handle unknown/custom commands and direct prompts +function _forge_action_default + set -l user_action $argv[1] + set -l input_text $argv[2] + set -l command_type "" + + if test -n "$user_action" + set -l commands_list (_forge_get_commands) + if test -n "$commands_list" + set -l command_row (printf '%s\n' $commands_list | grep "^$user_action\\b") + if test -z "$command_row" + echo + _forge_log error "Command '"(set_color --bold)"$user_action"(set_color normal)"' not found" + return 0 + end + set command_type (echo "$command_row" | awk '{print $2}') + + # Handle custom commands + if test (string lower "$command_type") = custom + if test -z "$_FORGE_CONVERSATION_ID" + set -g _FORGE_CONVERSATION_ID ($_FORGE_BIN conversation new) + end + echo + if test -n "$input_text" + _forge_exec cmd execute --cid $_FORGE_CONVERSATION_ID $user_action "$input_text" + else + _forge_exec cmd execute --cid $_FORGE_CONVERSATION_ID $user_action + end + return 0 + end + end + end + + if test -z "$input_text" + if test -n "$user_action" + if test (string lower "$command_type") != agent + echo + _forge_log error "Command '"(set_color --bold)"$user_action"(set_color normal)"' not found" + return 0 + end + echo + set -g _FORGE_ACTIVE_AGENT $user_action + _forge_log info (set_color --bold white)(string upper $user_action)(set_color normal)" "(set_color 888888)"is now the active agent"(set_color normal) + end + return 0 + end + + # Create conversation if needed + if test -z "$_FORGE_CONVERSATION_ID" + set -g _FORGE_CONVERSATION_ID ($_FORGE_BIN conversation new) + end + + echo + if test -n "$user_action" + set -g _FORGE_ACTIVE_AGENT $user_action + end + _forge_exec_interactive -p "$input_text" --cid $_FORGE_CONVERSATION_ID + _forge_start_background_sync + _forge_start_background_update +end diff --git a/shell-plugin/fish/functions/_forge_action_doctor.fish b/shell-plugin/fish/functions/_forge_action_doctor.fish new file mode 100644 index 0000000000..5c056a6430 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_doctor.fish @@ -0,0 +1,7 @@ +# Forge: _forge_action_doctor - Run diagnostics +function _forge_action_doctor + echo + # Note: We call the zsh doctor for now as forge doesn't have a fish-specific doctor. + # It will still show useful general diagnostics. + $_FORGE_BIN zsh doctor 2>/dev/null; or echo "Forge Fish plugin loaded. Shell: fish "(fish --version 2>&1) +end diff --git a/shell-plugin/fish/functions/_forge_action_dump.fish b/shell-plugin/fish/functions/_forge_action_dump.fish new file mode 100644 index 0000000000..c87eebe59e --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_dump.fish @@ -0,0 +1,14 @@ +# Forge: _forge_action_dump - Dump conversation +function _forge_action_dump + set -l input_text $argv[1] + echo + if test -z "$_FORGE_CONVERSATION_ID" + _forge_log error "No active conversation. Start a conversation first or use :conversation to see existing ones" + return 0 + end + if test "$input_text" = html + _forge_exec conversation dump $_FORGE_CONVERSATION_ID --html + else + _forge_exec conversation dump $_FORGE_CONVERSATION_ID + end +end diff --git a/shell-plugin/fish/functions/_forge_action_editor.fish b/shell-plugin/fish/functions/_forge_action_editor.fish new file mode 100644 index 0000000000..7d98a962a7 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_editor.fish @@ -0,0 +1,52 @@ +# Forge: _forge_action_editor - Open editor for multi-line prompt input +function _forge_action_editor + set -l initial_text $argv[1] + echo + + set -l editor_cmd (test -n "$FORGE_EDITOR"; and echo $FORGE_EDITOR; or test -n "$EDITOR"; and echo $EDITOR; or echo nano) + set -l editor_bin (echo $editor_cmd | awk '{print $1}') + + if not command -q $editor_bin + _forge_log error "Editor not found: $editor_cmd (set FORGE_EDITOR or EDITOR)" + return 1 + end + + set -l forge_dir ".forge" + if not test -d $forge_dir + mkdir -p $forge_dir; or begin + _forge_log error "Failed to create .forge directory" + return 1 + end + end + + set -l temp_file "$forge_dir/FORGE_EDITMSG.md" + touch $temp_file; or begin + _forge_log error "Failed to create temporary file" + return 1 + end + + if test -n "$initial_text" + printf '%s\n' $initial_text >$temp_file + else + echo -n "" >$temp_file + end + + eval "$editor_cmd '$temp_file'" /dev/tty 2>&1 + set -l editor_exit_code $status + + if test $editor_exit_code -ne 0 + _forge_log error "Editor exited with error code $editor_exit_code" + return 1 + end + + set -l content (cat $temp_file | tr -d '\r') + if test -z "$content" + _forge_log info "Editor closed with no content" + commandline -r "" + commandline -f repaint + return 0 + end + + commandline -r ": $content" + commandline -f repaint +end diff --git a/shell-plugin/fish/functions/_forge_action_env.fish b/shell-plugin/fish/functions/_forge_action_env.fish new file mode 100644 index 0000000000..9922d34881 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_env.fish @@ -0,0 +1,5 @@ +# Forge: _forge_action_env - Show environment info +function _forge_action_env + echo + _forge_exec env +end diff --git a/shell-plugin/fish/functions/_forge_action_info.fish b/shell-plugin/fish/functions/_forge_action_info.fish new file mode 100644 index 0000000000..5384f11f70 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_info.fish @@ -0,0 +1,9 @@ +# Forge: _forge_action_info - Show current info +function _forge_action_info + echo + if test -n "$_FORGE_CONVERSATION_ID" + _forge_exec info --cid $_FORGE_CONVERSATION_ID + else + _forge_exec info + end +end diff --git a/shell-plugin/fish/functions/_forge_action_keyboard.fish b/shell-plugin/fish/functions/_forge_action_keyboard.fish new file mode 100644 index 0000000000..4efe0092c8 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_keyboard.fish @@ -0,0 +1,75 @@ +# Forge: _forge_action_keyboard - Show keyboard shortcuts +function _forge_action_keyboard + echo + echo "Forge Fish Shell Plugin - Commands Reference" + echo "" + echo " Keybindings" + echo " ───────────────────────────────────────────────────" + echo " Enter Execute :command or send prompt to forge" + echo " Tab Complete @file or :command with fzf" + echo "" + echo " Prompts" + echo " ───────────────────────────────────────────────────" + echo " : Send prompt to forge (current conversation)" + echo " :new / :n Start a new conversation" + echo " :edit / :ed Open editor for multi-line prompt" + echo " :suggest / :s Generate shell command from description" + echo " :retry / :r Retry the last command" + echo "" + echo " Conversations" + echo " ───────────────────────────────────────────────────" + echo " :conversation / :c Switch between conversations" + echo " :rename / :rn Rename current conversation" + echo " :conversation-rename Rename a conversation by ID" + echo " :clone Clone conversation context" + echo " :copy Copy last response to clipboard" + echo " :compact Compact the conversation context" + echo " :dump / :d Save conversation as JSON or HTML" + echo "" + echo " Models & Providers" + echo " ───────────────────────────────────────────────────" + echo " :model / :m Switch model (session only)" + echo " :config-model / :cm Switch model (global config)" + echo " :config-provider / :p Switch provider (global config)" + echo " :reasoning-effort / :re Set reasoning effort (session)" + echo " :config-reasoning-effort / :cre Set reasoning effort (global)" + echo " :config-commit-model / :ccm Set commit model" + echo " :config-suggest-model / :csm Set suggest model" + echo " :config-reload / :cr Reset all session overrides" + echo "" + echo " Agents" + echo " ───────────────────────────────────────────────────" + echo " :agent / :a Switch agent" + echo " :forge Technical development agent" + echo " :sage Research and analysis agent" + echo " :muse / :plan Planning and strategy agent" + echo "" + echo " Git" + echo " ───────────────────────────────────────────────────" + echo " :commit Commit with AI-generated message" + echo " :commit-preview Preview AI-generated commit message" + echo "" + echo " Workspace" + echo " ───────────────────────────────────────────────────" + echo " :workspace-sync / :sync Sync workspace for search" + echo " :workspace-init Initialize new workspace" + echo " :workspace-status Show sync status" + echo " :workspace-info Show workspace details" + echo "" + echo " Info & Config" + echo " ───────────────────────────────────────────────────" + echo " :info / :i Show session information" + echo " :env / :e Show environment information" + echo " :config Show configuration values" + echo " :config-edit / :ce Edit global config file" + echo " :tools / :t List available tools" + echo " :skill List available skills" + echo " :doctor Run environment diagnostics" + echo " :kb Show this reference" + echo "" + echo " Auth" + echo " ───────────────────────────────────────────────────" + echo " :login Login to a provider" + echo " :logout Logout from a provider" + echo "" +end diff --git a/shell-plugin/fish/functions/_forge_action_login.fish b/shell-plugin/fish/functions/_forge_action_login.fish new file mode 100644 index 0000000000..33cef78f18 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_login.fish @@ -0,0 +1,10 @@ +# Forge: _forge_action_login - Interactive provider login +function _forge_action_login + set -l input_text $argv[1] + echo + set -l selected (_forge_select_provider "" "" "" "$input_text") + if test -n "$selected" + set -l provider (echo "$selected" | awk '{print $2}') + _forge_exec_interactive provider login $provider + end +end diff --git a/shell-plugin/fish/functions/_forge_action_logout.fish b/shell-plugin/fish/functions/_forge_action_logout.fish new file mode 100644 index 0000000000..f697db8bf6 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_logout.fish @@ -0,0 +1,10 @@ +# Forge: _forge_action_logout - Interactive provider logout +function _forge_action_logout + set -l input_text $argv[1] + echo + set -l selected (_forge_select_provider "\\[yes\\]" "" "" "$input_text") + if test -n "$selected" + set -l provider (echo "$selected" | awk '{print $2}') + _forge_exec provider logout $provider + end +end diff --git a/shell-plugin/fish/functions/_forge_action_model.fish b/shell-plugin/fish/functions/_forge_action_model.fish new file mode 100644 index 0000000000..73a09c9fd5 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_model.fish @@ -0,0 +1,21 @@ +# Forge: _forge_action_model - Set config model interactively +function _forge_action_model + set -l input_text $argv[1] + echo + + set -l current_model ($_FORGE_BIN config get model 2>/dev/null) + set -l current_provider ($_FORGE_BIN config get provider 2>/dev/null) + + set -l selected (_forge_pick_model "Model ❯ " "$current_model" "$input_text" "$current_provider" 3) + if test -n "$selected" + set -l model_id (echo "$selected" | awk -F ' +' '{print $1}' | string trim) + set -l provider_display (echo "$selected" | awk -F ' +' '{print $3}' | string trim) + set -l provider_id (echo "$selected" | awk -F ' +' '{print $4}' | string trim) + + if test -n "$provider_display" -a "$provider_display" != "$current_provider" + _forge_exec_interactive config set provider $provider_id --model $model_id + return + end + _forge_exec config set model $model_id + end +end diff --git a/shell-plugin/fish/functions/_forge_action_new.fish b/shell-plugin/fish/functions/_forge_action_new.fish new file mode 100644 index 0000000000..8c14188709 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_new.fish @@ -0,0 +1,17 @@ +# Forge: _forge_action_new - Start a new conversation +function _forge_action_new + set -l input_text $argv[1] + _forge_clear_conversation + set -g _FORGE_ACTIVE_AGENT forge + echo + + if test -n "$input_text" + set -l new_id ($_FORGE_BIN conversation new) + _forge_switch_conversation $new_id + _forge_exec_interactive -p "$input_text" --cid $_FORGE_CONVERSATION_ID + _forge_start_background_sync + _forge_start_background_update + else + _forge_exec banner + end +end diff --git a/shell-plugin/fish/functions/_forge_action_provider.fish b/shell-plugin/fish/functions/_forge_action_provider.fish new file mode 100644 index 0000000000..538d37eb66 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_provider.fish @@ -0,0 +1,10 @@ +# Forge: _forge_action_provider - Switch provider interactively +function _forge_action_provider + set -l input_text $argv[1] + echo + set -l selected (_forge_select_provider "" "" "llm" "$input_text") + if test -n "$selected" + set -l provider_id (echo "$selected" | awk '{print $2}') + _forge_exec_interactive config set provider $provider_id + end +end diff --git a/shell-plugin/fish/functions/_forge_action_reasoning_effort.fish b/shell-plugin/fish/functions/_forge_action_reasoning_effort.fish new file mode 100644 index 0000000000..a7e82579eb --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_reasoning_effort.fish @@ -0,0 +1,28 @@ +# Forge: _forge_action_reasoning_effort - Set session reasoning effort +function _forge_action_reasoning_effort + set -l input_text $argv[1] + echo + + set -l efforts "EFFORT\nnone\nminimal\nlow\nmedium\nhigh\nxhigh\nmax" + set -l current_effort + if test -n "$_FORGE_SESSION_REASONING_EFFORT" + set current_effort $_FORGE_SESSION_REASONING_EFFORT + else + set current_effort ($_FORGE_BIN config get reasoning-effort 2>/dev/null) + end + + set -l fzf_args --prompt="Reasoning Effort ❯ " + if test -n "$input_text" + set fzf_args $fzf_args --query="$input_text" + end + if test -n "$current_effort" + set -l idx (echo -e $efforts | _forge_find_index "$current_effort" 1) + set fzf_args $fzf_args --bind="start:pos($idx)" + end + + set -l selected (echo -e $efforts | _forge_fzf --header-lines=1 $fzf_args) + if test -n "$selected" + set -g _FORGE_SESSION_REASONING_EFFORT $selected + _forge_log success "Session reasoning effort set to "(set_color --bold)"$selected"(set_color normal) + end +end diff --git a/shell-plugin/fish/functions/_forge_action_rename.fish b/shell-plugin/fish/functions/_forge_action_rename.fish new file mode 100644 index 0000000000..f5619ca908 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_rename.fish @@ -0,0 +1,14 @@ +# Forge: _forge_action_rename - Rename current conversation +function _forge_action_rename + set -l input_text $argv[1] + echo + if test -z "$_FORGE_CONVERSATION_ID" + _forge_log error "No active conversation. Start a conversation first or use :conversation to select one" + return 0 + end + if test -z "$input_text" + _forge_log error "Usage: :rename " + return 0 + end + _forge_exec conversation rename $_FORGE_CONVERSATION_ID $input_text +end diff --git a/shell-plugin/fish/functions/_forge_action_retry.fish b/shell-plugin/fish/functions/_forge_action_retry.fish new file mode 100644 index 0000000000..5a55f6014b --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_retry.fish @@ -0,0 +1,9 @@ +# Forge: _forge_action_retry - Retry last conversation turn +function _forge_action_retry + echo + if test -z "$_FORGE_CONVERSATION_ID" + _forge_log error "No active conversation. Start a conversation first or use :conversation to see existing ones" + return 0 + end + _forge_exec conversation retry $_FORGE_CONVERSATION_ID +end diff --git a/shell-plugin/fish/functions/_forge_action_session_model.fish b/shell-plugin/fish/functions/_forge_action_session_model.fish new file mode 100644 index 0000000000..8a5a587021 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_session_model.fish @@ -0,0 +1,34 @@ +# Forge: _forge_action_session_model - Set session model (non-persistent) +function _forge_action_session_model + set -l input_text $argv[1] + echo + + set -l current_model + set -l current_provider + set -l provider_index + + if test -n "$_FORGE_SESSION_MODEL" + set current_model $_FORGE_SESSION_MODEL + set provider_index 4 + else + set current_model ($_FORGE_BIN config get model 2>/dev/null) + set provider_index 3 + end + + if test -n "$_FORGE_SESSION_PROVIDER" + set current_provider $_FORGE_SESSION_PROVIDER + set provider_index 4 + else + set current_provider ($_FORGE_BIN config get provider 2>/dev/null) + set provider_index 3 + end + + set -l selected (_forge_pick_model "Session Model ❯ " "$current_model" "$input_text" "$current_provider" "$provider_index") + if test -n "$selected" + set -l model_id (echo "$selected" | awk -F ' +' '{print $1}' | string trim) + set -l provider_id (echo "$selected" | awk -F ' +' '{print $4}' | string trim) + set -g _FORGE_SESSION_MODEL $model_id + set -g _FORGE_SESSION_PROVIDER $provider_id + _forge_log success "Session model set to "(set_color --bold)"$model_id"(set_color normal)" (provider: "(set_color --bold)"$provider_id"(set_color normal)")" + end +end diff --git a/shell-plugin/fish/functions/_forge_action_skill.fish b/shell-plugin/fish/functions/_forge_action_skill.fish new file mode 100644 index 0000000000..368d4f83b4 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_skill.fish @@ -0,0 +1,5 @@ +# Forge: _forge_action_skill - List skills +function _forge_action_skill + echo + _forge_exec list skill +end diff --git a/shell-plugin/fish/functions/_forge_action_suggest.fish b/shell-plugin/fish/functions/_forge_action_suggest.fish new file mode 100644 index 0000000000..1a0c4aa3a9 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_suggest.fish @@ -0,0 +1,19 @@ +# Forge: _forge_action_suggest - Generate shell command from description +function _forge_action_suggest + set -l description $argv[1] + if test -z "$description" + _forge_log error "Please provide a command description" + return 0 + end + echo + + set -lx FORCE_COLOR true + set -lx CLICOLOR_FORCE 1 + set -l generated_command (_forge_exec suggest "$description") + if test -n "$generated_command" + commandline -r "$generated_command" + commandline -f repaint + else + _forge_log error "Failed to generate command" + end +end diff --git a/shell-plugin/fish/functions/_forge_action_suggest_model.fish b/shell-plugin/fish/functions/_forge_action_suggest_model.fish new file mode 100644 index 0000000000..726f28d1c0 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_suggest_model.fish @@ -0,0 +1,16 @@ +# Forge: _forge_action_suggest_model - Set suggest model +function _forge_action_suggest_model + set -l input_text $argv[1] + echo + + set -l suggest_output (_forge_exec config get suggest 2>/dev/null) + set -l current_suggest_provider (printf '%s\n' $suggest_output | head -n 1) + set -l current_suggest_model (printf '%s\n' $suggest_output | tail -n 1) + + set -l selected (_forge_pick_model "Suggest Model ❯ " "$current_suggest_model" "$input_text" "$current_suggest_provider" 4) + if test -n "$selected" + set -l model_id (echo "$selected" | awk -F ' +' '{print $1}' | string trim) + set -l provider_id (echo "$selected" | awk -F ' +' '{print $4}' | string trim) + _forge_exec config set suggest $provider_id $model_id + end +end diff --git a/shell-plugin/fish/functions/_forge_action_sync.fish b/shell-plugin/fish/functions/_forge_action_sync.fish new file mode 100644 index 0000000000..62853ac021 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_sync.fish @@ -0,0 +1,5 @@ +# Forge: _forge_action_sync - Workspace sync with --init +function _forge_action_sync + echo + _forge_exec_interactive workspace sync --init +end diff --git a/shell-plugin/fish/functions/_forge_action_sync_info.fish b/shell-plugin/fish/functions/_forge_action_sync_info.fish new file mode 100644 index 0000000000..470893e69a --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_sync_info.fish @@ -0,0 +1,5 @@ +# Forge: _forge_action_sync_info - Show workspace info +function _forge_action_sync_info + echo + _forge_exec workspace info "." +end diff --git a/shell-plugin/fish/functions/_forge_action_sync_init.fish b/shell-plugin/fish/functions/_forge_action_sync_init.fish new file mode 100644 index 0000000000..28c4ab24ea --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_sync_init.fish @@ -0,0 +1,5 @@ +# Forge: _forge_action_sync_init - Initialize workspace +function _forge_action_sync_init + echo + _forge_exec_interactive workspace init +end diff --git a/shell-plugin/fish/functions/_forge_action_sync_status.fish b/shell-plugin/fish/functions/_forge_action_sync_status.fish new file mode 100644 index 0000000000..44606bd90f --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_sync_status.fish @@ -0,0 +1,5 @@ +# Forge: _forge_action_sync_status - Show workspace status +function _forge_action_sync_status + echo + _forge_exec workspace status "." +end diff --git a/shell-plugin/fish/functions/_forge_action_tools.fish b/shell-plugin/fish/functions/_forge_action_tools.fish new file mode 100644 index 0000000000..e1c3594a21 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_action_tools.fish @@ -0,0 +1,6 @@ +# Forge: _forge_action_tools - List tools for active agent +function _forge_action_tools + echo + set -l agent_id (test -n "$_FORGE_ACTIVE_AGENT"; and echo $_FORGE_ACTIVE_AGENT; or echo forge) + _forge_exec list tools $agent_id +end diff --git a/shell-plugin/fish/functions/_forge_clear_conversation.fish b/shell-plugin/fish/functions/_forge_clear_conversation.fish new file mode 100644 index 0000000000..d50797c0b6 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_clear_conversation.fish @@ -0,0 +1,7 @@ +# Forge: _forge_clear_conversation - Clear current conversation, save as previous +function _forge_clear_conversation + if test -n "$_FORGE_CONVERSATION_ID" + set -g _FORGE_PREVIOUS_CONVERSATION_ID $_FORGE_CONVERSATION_ID + end + set -g _FORGE_CONVERSATION_ID "" +end diff --git a/shell-plugin/fish/functions/_forge_clone_and_switch.fish b/shell-plugin/fish/functions/_forge_clone_and_switch.fish new file mode 100644 index 0000000000..fdf4deab09 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_clone_and_switch.fish @@ -0,0 +1,28 @@ +# Forge: _forge_clone_and_switch - Clone a conversation and switch to it +function _forge_clone_and_switch + set -l clone_target $argv[1] + set -l original_conversation_id $_FORGE_CONVERSATION_ID + + _forge_log info (set_color --bold)"Cloning conversation $clone_target"(set_color normal) + + set -l clone_output ($_FORGE_BIN conversation clone "$clone_target" 2>&1) + set -l clone_exit $status + + if test $clone_exit -eq 0 + set -l new_id (printf '%s\n' $clone_output | grep -oE '[a-f0-9-]{36}' | tail -1) + if test -n "$new_id" + _forge_switch_conversation $new_id + _forge_log success "└─ Switched to conversation "(set_color --bold)"$new_id"(set_color normal) + if test "$clone_target" != "$original_conversation_id" + echo + _forge_exec conversation show $new_id + echo + _forge_exec conversation info $new_id + end + else + _forge_log error "Failed to extract new conversation ID from clone output" + end + else + _forge_log error "Failed to clone conversation: $clone_output" + end +end diff --git a/shell-plugin/fish/functions/_forge_exec.fish b/shell-plugin/fish/functions/_forge_exec.fish new file mode 100644 index 0000000000..991232a617 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_exec.fish @@ -0,0 +1,24 @@ +# Forge: _forge_exec - Execute forge command with session overrides +function _forge_exec + set -l agent_id (test -n "$_FORGE_ACTIVE_AGENT"; and echo $_FORGE_ACTIVE_AGENT; or echo forge) + set -l cmd $_FORGE_BIN --agent $agent_id $argv + + # Build env prefix for session overrides + # Fish's `set -lx` inside if-blocks is block-scoped, so we use `env` command + set -l env_args + if test -n "$_FORGE_SESSION_MODEL" + set -a env_args FORGE_SESSION__MODEL_ID=$_FORGE_SESSION_MODEL + end + if test -n "$_FORGE_SESSION_PROVIDER" + set -a env_args FORGE_SESSION__PROVIDER_ID=$_FORGE_SESSION_PROVIDER + end + if test -n "$_FORGE_SESSION_REASONING_EFFORT" + set -a env_args FORGE_REASONING__EFFORT=$_FORGE_SESSION_REASONING_EFFORT + end + + if test (count $env_args) -gt 0 + env $env_args $cmd + else + $cmd + end +end diff --git a/shell-plugin/fish/functions/_forge_exec_interactive.fish b/shell-plugin/fish/functions/_forge_exec_interactive.fish new file mode 100644 index 0000000000..f802944a14 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_exec_interactive.fish @@ -0,0 +1,23 @@ +# Forge: _forge_exec_interactive - Execute forge command interactively (with TTY) +function _forge_exec_interactive + set -l agent_id (test -n "$_FORGE_ACTIVE_AGENT"; and echo $_FORGE_ACTIVE_AGENT; or echo forge) + set -l cmd $_FORGE_BIN --agent $agent_id $argv + + # Build env prefix for session overrides + set -l env_args + if test -n "$_FORGE_SESSION_MODEL" + set -a env_args FORGE_SESSION__MODEL_ID=$_FORGE_SESSION_MODEL + end + if test -n "$_FORGE_SESSION_PROVIDER" + set -a env_args FORGE_SESSION__PROVIDER_ID=$_FORGE_SESSION_PROVIDER + end + if test -n "$_FORGE_SESSION_REASONING_EFFORT" + set -a env_args FORGE_REASONING__EFFORT=$_FORGE_SESSION_REASONING_EFFORT + end + + if test (count $env_args) -gt 0 + env $env_args $cmd /dev/tty + else + $cmd /dev/tty + end +end diff --git a/shell-plugin/fish/functions/_forge_find_index.fish b/shell-plugin/fish/functions/_forge_find_index.fish new file mode 100644 index 0000000000..12cddd0cf7 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_find_index.fish @@ -0,0 +1,34 @@ +# Forge: _forge_find_index - Find line index in porcelain output matching a value +# Usage: printf '%s\n' $data | _forge_find_index "value" [field_num] [field_num2] [value2] +# Reads multi-line data from stdin to avoid Fish list-to-string quoting issues. +function _forge_find_index + set -l value_to_find $argv[1] + set -l field_number (test -n "$argv[2]"; and echo $argv[2]; or echo 1) + set -l field_number2 $argv[3] + set -l value_to_find2 $argv[4] + + set -l index 1 + set -l line_num 0 + while read -l line + set line_num (math $line_num + 1) + if test $line_num -eq 1 + continue + end + set -l field_value (echo "$line" | awk -F ' +' "{print \$$field_number}") + if test "$field_value" = "$value_to_find" + if test -n "$field_number2" -a -n "$value_to_find2" + set -l field_value2 (echo "$line" | awk -F ' +' "{print \$$field_number2}") + if test "$field_value2" = "$value_to_find2" + echo $index + return 0 + end + else + echo $index + return 0 + end + end + set index (math $index + 1) + end + echo 1 + return 0 +end diff --git a/shell-plugin/fish/functions/_forge_fzf.fish b/shell-plugin/fish/functions/_forge_fzf.fish new file mode 100644 index 0000000000..14841150e1 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_fzf.fish @@ -0,0 +1,4 @@ +# Forge: _forge_fzf - Wrapper around fzf with forge defaults +function _forge_fzf + fzf --reverse --exact --cycle --select-1 --height 80% --no-scrollbar --ansi --color="header:bold" $argv +end diff --git a/shell-plugin/fish/functions/_forge_get_commands.fish b/shell-plugin/fish/functions/_forge_get_commands.fish new file mode 100644 index 0000000000..46f389b6a5 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_get_commands.fish @@ -0,0 +1,7 @@ +# Forge: _forge_get_commands - Get cached list of forge commands +function _forge_get_commands + if test -z "$_FORGE_COMMANDS" + set -g _FORGE_COMMANDS (env CLICOLOR_FORCE=0 $_FORGE_BIN list commands --porcelain 2>/dev/null | sed 's/Display ZSH keyboard shortcuts/Display Fish keyboard shortcuts/') + end + printf '%s\n' $_FORGE_COMMANDS +end diff --git a/shell-plugin/fish/functions/_forge_highlight_noop.fish b/shell-plugin/fish/functions/_forge_highlight_noop.fish new file mode 100644 index 0000000000..b5f5dd7d2f --- /dev/null +++ b/shell-plugin/fish/functions/_forge_highlight_noop.fish @@ -0,0 +1,7 @@ +# Forge: _forge_highlight_noop - No-op abbreviation function for syntax highlighting +# Returns the input token unchanged. Used by the _forge_cmd regex abbreviation +# so Fish's highlighter recognizes :commands as valid (showing them in white +# instead of red). The actual :command dispatch is handled by _forge_accept_line. +function _forge_highlight_noop + echo -- $argv[1] +end diff --git a/shell-plugin/fish/functions/_forge_log.fish b/shell-plugin/fish/functions/_forge_log.fish new file mode 100644 index 0000000000..1d9ed2cf2e --- /dev/null +++ b/shell-plugin/fish/functions/_forge_log.fish @@ -0,0 +1,21 @@ +# Forge: _forge_log - Logging utility +function _forge_log + set -l level $argv[1] + set -l message $argv[2..-1] + set -l timestamp (set_color 888888)"["(date '+%H:%M:%S')"]"(set_color normal) + + switch $level + case error + echo -e (set_color red)"⏺"(set_color normal)" $timestamp "(set_color red)"$message"(set_color normal) + case info + echo -e (set_color white)"⏺"(set_color normal)" $timestamp "(set_color white)"$message"(set_color normal) + case success + echo -e (set_color yellow)"⏺"(set_color normal)" $timestamp "(set_color white)"$message"(set_color normal) + case warning + echo -e (set_color bryellow)"⚠️"(set_color normal)" $timestamp "(set_color bryellow)"$message"(set_color normal) + case debug + echo -e (set_color cyan)"⏺"(set_color normal)" $timestamp "(set_color 888888)"$message"(set_color normal) + case '*' + echo -e "$message" + end +end diff --git a/shell-plugin/fish/functions/_forge_pick_model.fish b/shell-plugin/fish/functions/_forge_pick_model.fish new file mode 100644 index 0000000000..98b696935c --- /dev/null +++ b/shell-plugin/fish/functions/_forge_pick_model.fish @@ -0,0 +1,30 @@ +# Forge: _forge_pick_model - Interactive model picker via fzf +# Usage: _forge_pick_model prompt_text current_model input_text [current_provider] [provider_field] +function _forge_pick_model + set -l prompt_text $argv[1] + set -l current_model $argv[2] + set -l input_text $argv[3] + set -l current_provider $argv[4] + set -l provider_field $argv[5] + + set -l output ($_FORGE_BIN list models --porcelain 2>/dev/null) + if test -z "$output" + return 1 + end + + set -l fzf_args --delimiter="$_FORGE_DELIMITER" --prompt="$prompt_text" --with-nth="2,3,5.." + if test -n "$input_text" + set fzf_args $fzf_args --query="$input_text" + end + if test -n "$current_model" + if test -n "$current_provider" -a -n "$provider_field" + set -l idx (printf '%s\n' $output | _forge_find_index "$current_model" 1 "$provider_field" "$current_provider") + set fzf_args $fzf_args --bind="start:pos($idx)" + else + set -l idx (printf '%s\n' $output | _forge_find_index "$current_model" 1) + set fzf_args $fzf_args --bind="start:pos($idx)" + end + end + + printf '%s\n' $output | _forge_fzf --header-lines=1 $fzf_args +end diff --git a/shell-plugin/fish/functions/_forge_rprompt_info.fish b/shell-plugin/fish/functions/_forge_rprompt_info.fish new file mode 100644 index 0000000000..2cac3a56bf --- /dev/null +++ b/shell-plugin/fish/functions/_forge_rprompt_info.fish @@ -0,0 +1,40 @@ +# Forge: _forge_rprompt_info - Right prompt info showing model and agent +# Called by the fish_right_prompt wrapper installed in conf.d/forge.fish +# Builds the prompt natively in Fish (forge zsh rprompt outputs ZSH escapes) + +function _forge_rprompt_info + set -l forge_bin (test -n "$_FORGE_BIN"; and echo $_FORGE_BIN; or echo forge) + set -l parts + + # Agent info + set -l agent_name + if test -n "$_FORGE_ACTIVE_AGENT" + set agent_name (string upper $_FORGE_ACTIVE_AGENT) + else + set agent_name FORGE + end + set -a parts (set_color --bold 888888)"$agent_name"(set_color normal) + + # Model info + set -l model_id + if test -n "$_FORGE_SESSION_MODEL" + set model_id $_FORGE_SESSION_MODEL + else + set model_id ($forge_bin config get model --porcelain 2>/dev/null) + end + if test -n "$model_id" + set -a parts (set_color 888888)" $model_id"(set_color normal) + end + + # Conversation indicator + if test -n "$_FORGE_CONVERSATION_ID" + set -a parts (set_color 888888)" "(set_color normal) + end + + # Reasoning effort (only if session override) + if test -n "$_FORGE_SESSION_REASONING_EFFORT" + set -a parts (set_color yellow)" $_FORGE_SESSION_REASONING_EFFORT"(set_color normal) + end + + echo -n (string join '' $parts) +end diff --git a/shell-plugin/fish/functions/_forge_select_provider.fish b/shell-plugin/fish/functions/_forge_select_provider.fish new file mode 100644 index 0000000000..2208bf2970 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_select_provider.fish @@ -0,0 +1,49 @@ +# Forge: _forge_select_provider - Interactive provider selection via fzf +# Usage: _forge_select_provider [filter_status] [current_provider] [filter_type] [query] +function _forge_select_provider + set -l filter_status $argv[1] + set -l current_provider $argv[2] + set -l filter_type $argv[3] + set -l query $argv[4] + + set -l cmd $_FORGE_BIN list provider --porcelain + if test -n "$filter_type" + set cmd $cmd --type=$filter_type + end + + set -l output ($cmd 2>/dev/null) + if test -z "$output" + _forge_log error "No providers available" + return 1 + end + + if test -n "$filter_status" + set -l header (printf '%s\n' $output | head -n 1) + set -l filtered (printf '%s\n' $output | tail -n +2 | grep -i "$filter_status") + if test -z "$filtered" + _forge_log error "No $filter_status providers found" + return 1 + end + set output (printf "%s\n%s" "$header" "$filtered") + end + + if test -z "$current_provider" + set current_provider ($_FORGE_BIN config get provider --porcelain 2>/dev/null) + end + + set -l fzf_args --delimiter="$_FORGE_DELIMITER" --prompt="Provider ❯ " --with-nth=1,3.. + if test -n "$query" + set fzf_args $fzf_args --query="$query" + end + if test -n "$current_provider" + set -l idx (printf '%s\n' $output | _forge_find_index "$current_provider" 1) + set fzf_args $fzf_args --bind="start:pos($idx)" + end + + set -l selected (printf '%s\n' $output | _forge_fzf --header-lines=1 $fzf_args) + if test -n "$selected" + echo "$selected" + return 0 + end + return 1 +end diff --git a/shell-plugin/fish/functions/_forge_start_background_sync.fish b/shell-plugin/fish/functions/_forge_start_background_sync.fish new file mode 100644 index 0000000000..50c21675eb --- /dev/null +++ b/shell-plugin/fish/functions/_forge_start_background_sync.fish @@ -0,0 +1,17 @@ +# Forge: _forge_start_background_sync - Background workspace sync +function _forge_start_background_sync + set -l sync_enabled (test -n "$FORGE_SYNC_ENABLED"; and echo $FORGE_SYNC_ENABLED; or echo true) + if test "$sync_enabled" != true + return 0 + end + + set -l workspace_path (pwd -P) + + # Run in background: check if workspace is indexed, then sync + fish -c " + if $_FORGE_BIN workspace info '$workspace_path' >/dev/null 2>&1 + $_FORGE_BIN workspace sync '$workspace_path' >/dev/null 2>&1 + end + " & + disown +end diff --git a/shell-plugin/fish/functions/_forge_start_background_update.fish b/shell-plugin/fish/functions/_forge_start_background_update.fish new file mode 100644 index 0000000000..87a5a90d4a --- /dev/null +++ b/shell-plugin/fish/functions/_forge_start_background_update.fish @@ -0,0 +1,5 @@ +# Forge: _forge_start_background_update - Background auto-update check +function _forge_start_background_update + fish -c "$_FORGE_BIN update --no-confirm >/dev/null 2>&1" & + disown +end diff --git a/shell-plugin/fish/functions/_forge_switch_conversation.fish b/shell-plugin/fish/functions/_forge_switch_conversation.fish new file mode 100644 index 0000000000..c15a05db68 --- /dev/null +++ b/shell-plugin/fish/functions/_forge_switch_conversation.fish @@ -0,0 +1,8 @@ +# Forge: _forge_switch_conversation - Switch to a conversation, saving previous +function _forge_switch_conversation + set -l new_id $argv[1] + if test -n "$_FORGE_CONVERSATION_ID" -a "$_FORGE_CONVERSATION_ID" != "$new_id" + set -g _FORGE_PREVIOUS_CONVERSATION_ID $_FORGE_CONVERSATION_ID + end + set -g _FORGE_CONVERSATION_ID $new_id +end diff --git a/shell-plugin/fish/functions/_forge_tab_completion.fish b/shell-plugin/fish/functions/_forge_tab_completion.fish new file mode 100644 index 0000000000..e935bb005c --- /dev/null +++ b/shell-plugin/fish/functions/_forge_tab_completion.fish @@ -0,0 +1,132 @@ +# Forge: _forge_tab_completion - Tab handler for @file and :command completion +function _forge_tab_completion + set -l buf (commandline) + set -l cursor_pos (commandline -C) + + # Get the word under/before cursor + set -l tokens (commandline -co) + set -l current_token (commandline -ct) + + # @file completion + if string match -rq '^@' -- "$current_token" + set -l filter_text (string replace -r '^@' '' -- "$current_token") + set -l fzf_args \ + --preview="if test -d {}; ls -la {} 2>/dev/null; else; $_FORGE_CAT_CMD {}; end" \ + $_FORGE_PREVIEW_WINDOW + + set -l file_list ($_FORGE_FD_CMD --type f --type d --hidden --exclude .git) + + set -l selected + if test -n "$filter_text" + set selected (printf '%s\n' $file_list | _forge_fzf --query "$filter_text" $fzf_args) + else + set selected (printf '%s\n' $file_list | _forge_fzf $fzf_args) + end + + if test -n "$selected" + set selected "@[$selected]" + # Replace the current @token with the selected file + set -l before (string replace -r '@[^@]*$' '' -- (commandline -cb)) + commandline -r "$before$selected"(string replace -r '^[^ ]*' '' -- (commandline | string sub -s (math $cursor_pos + 1))) + commandline -C (math (string length "$before$selected")) + end + commandline -f repaint + return + end + + # :command completion - only if the entire buffer is `:something` + if string match -rq '^:([a-zA-Z][a-zA-Z0-9_-]*)?$' -- "$buf" + set -l filter_text (string replace -r '^:' '' -- "$buf") + set -l commands_list (_forge_get_commands) + + if test -n "$commands_list" + # If user typed a partial command, try inline completion first + if test -n "$filter_text" + # Build list of all completable names (commands + aliases) + # Each line: "name" where name is either the command or an alias + set -l all_names + for line in (printf '%s\n' $commands_list | tail -n +2) + set -l cmd_name (echo "$line" | awk '{print $1}') + set all_names $all_names $cmd_name + # Extract aliases from "[alias: x]" or "[alias: x, y]" or "[alias for: x]" + set -l aliases (echo "$line" | string match -r '\[alias(?:\s+for)?:\s*([^\]]+)\]' | tail -n 1) + if test -n "$aliases" + for a in (string split ',' -- $aliases) + set -l trimmed (string trim -- $a) + if test -n "$trimmed" + set all_names $all_names $trimmed + end + end + end + end + + # Filter to matching names + set -l matches + for name in $all_names + if string match -riq "^$filter_text" -- $name + set matches $matches $name + end + end + # Deduplicate + set matches (printf '%s\n' $matches | sort -u) + set -l match_count (count $matches) + + if test $match_count -eq 1 + # Exact single match -- complete inline + commandline -r ":$matches[1] " + commandline -C (math (string length ":$matches[1] ")) + commandline -f repaint + return + else if test $match_count -gt 1 + # Find longest common prefix among matches + set -l prefix $matches[1] + for m in $matches[2..] + set -l new_prefix "" + set -l plen (string length "$prefix") + set -l mlen (string length "$m") + set -l len (math "min($plen, $mlen)") + for idx in (seq 1 $len) + set -l pc (string lower (string sub -s $idx -l 1 "$prefix")) + set -l mc (string lower (string sub -s $idx -l 1 "$m")) + if test "$pc" = "$mc" + set new_prefix "$new_prefix"(string sub -s $idx -l 1 "$prefix") + else + break + end + end + set prefix $new_prefix + end + + # If prefix is longer than what user typed, extend it + if test (string length "$prefix") -gt (string length "$filter_text") + commandline -r ":$prefix" + commandline -C (math (string length ":$prefix")) + commandline -f repaint + return + end + + # Otherwise fall through to fzf picker + end + end + + # Bare ":" or ambiguous partial -- open fzf picker + set -l selected + if test -n "$filter_text" + set selected (printf '%s\n' $commands_list | _forge_fzf --header-lines=1 --delimiter="$_FORGE_DELIMITER" --nth=1 --query "$filter_text" --prompt="Command ❯ ") + else + set selected (printf '%s\n' $commands_list | _forge_fzf --header-lines=1 --delimiter="$_FORGE_DELIMITER" --nth=1 --prompt="Command ❯ ") + end + + if test -n "$selected" + set -l command_name (echo "$selected" | awk '{print $1}') + commandline -r ":$command_name " + commandline -C (math (string length ":$command_name ")) + end + end + commandline -f repaint + return + end + + # Default: normal tab completion + commandline -f complete +end