From 6029838057ead37b83b8a5ab317e9167b60dab2c Mon Sep 17 00:00:00 2001 From: Harley Laue Date: Wed, 1 Apr 2026 05:35:39 -0700 Subject: [PATCH] fix(zsh): use global scope for typeset to support lazy loading The usage of typeset within zsh's scope rules means that if the eval "$(forge zsh plugin)" is run within a function, the typeset's will be function local scoped. However, forge expects these to be globally scoped. This can be fixed by specifying typeset -g to be global instead of scoped to whatever scope it currently is. With this fixed, it would allow lazy loading plugin managers to work, or in my case, by manually wrapping the forge initialization into a function: forge_ai() { if [[ -z "$_FORGE_PLUGIN_LOADED" ]]; then eval "$(forge zsh plugin)" fi } This allows me to load forge when I want to, instead of it being always initialized on startup. --- crates/forge_main/src/zsh/plugin.rs | 8 ++++++-- shell-plugin/forge.theme.zsh | 6 ++++-- shell-plugin/lib/config.zsh | 32 +++++++++++++++-------------- shell-plugin/lib/highlight.zsh | 8 ++++++++ 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index 363f2afeaf..fe0e0e2032 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -44,7 +44,9 @@ pub fn generate_zsh_plugin() -> Result { output.push_str(&completions_str); // Set environment variable to indicate plugin is loaded (with timestamp) - output.push_str("\n_FORGE_PLUGIN_LOADED=$(date +%s)\n"); + // Use typeset -g so the variable is global even when eval'd inside a function + // (e.g. lazy-loading plugin managers like zinit, zplug, zsh-defer) + output.push_str("\ntypeset -g _FORGE_PLUGIN_LOADED=$(date +%s)\n"); Ok(output) } @@ -54,7 +56,9 @@ pub fn generate_zsh_theme() -> Result { let mut content = include_str!("../../../../shell-plugin/forge.theme.zsh").to_string(); // Set environment variable to indicate theme is loaded (with timestamp) - content.push_str("\n_FORGE_THEME_LOADED=$(date +%s)\n"); + // Use typeset -g so the variable is global even when eval'd inside a function + // (e.g. lazy-loading plugin managers like zinit, zplug, zsh-defer) + content.push_str("\ntypeset -g _FORGE_THEME_LOADED=$(date +%s)\n"); Ok(content) } diff --git a/shell-plugin/forge.theme.zsh b/shell-plugin/forge.theme.zsh index 065f275a0b..816844f153 100644 --- a/shell-plugin/forge.theme.zsh +++ b/shell-plugin/forge.theme.zsh @@ -1,7 +1,8 @@ #!/usr/bin/env zsh # Enable prompt substitution for RPROMPT -setopt PROMPT_SUBST +# Use emulate to ensure setopt applies globally even when sourced from a function +emulate -R zsh -c 'setopt PROMPT_SUBST' # Model and agent info with token count # Fully formatted output directly from Rust @@ -22,6 +23,7 @@ function _forge_prompt_info() { # Right prompt: agent and model with token count (uses single forge prompt command) # Set RPROMPT if empty, otherwise append to existing value +# Use typeset -g to ensure RPROMPT is set globally even when sourced from a function if [[ -z "$_FORGE_THEME_LOADED" ]]; then - RPROMPT='$(_forge_prompt_info)'"${RPROMPT:+ ${RPROMPT}}" + typeset -g RPROMPT='$(_forge_prompt_info)'"${RPROMPT:+ ${RPROMPT}}" fi diff --git a/shell-plugin/lib/config.zsh b/shell-plugin/lib/config.zsh index 0a7510b321..e4392833ef 100644 --- a/shell-plugin/lib/config.zsh +++ b/shell-plugin/lib/config.zsh @@ -1,36 +1,38 @@ #!/usr/bin/env zsh # Configuration variables for forge plugin -# Using typeset to keep variables local to plugin scope and prevent public exposure +# Using typeset -gh (global + hidden) so variables survive lazy-loading +# from within a function scope (e.g. zinit, zplug, zsh-defer) while +# staying hidden from `typeset` listings. -typeset -h _FORGE_BIN="${FORGE_BIN:-forge}" -typeset -h _FORGE_CONVERSATION_PATTERN=":" -typeset -h _FORGE_MAX_COMMIT_DIFF="${FORGE_MAX_COMMIT_DIFF:-100000}" -typeset -h _FORGE_DELIMITER='\s\s+' -typeset -h _FORGE_PREVIEW_WINDOW="--preview-window=bottom:75%:wrap:border-sharp" +typeset -gh _FORGE_BIN="${FORGE_BIN:-forge}" +typeset -gh _FORGE_CONVERSATION_PATTERN=":" +typeset -gh _FORGE_MAX_COMMIT_DIFF="${FORGE_MAX_COMMIT_DIFF:-100000}" +typeset -gh _FORGE_DELIMITER='\s\s+' +typeset -gh _FORGE_PREVIEW_WINDOW="--preview-window=bottom:75%:wrap:border-sharp" # Detect fd command - Ubuntu/Debian use 'fdfind', others use 'fd' -typeset -h _FORGE_FD_CMD="$(command -v fdfind 2>/dev/null || command -v fd 2>/dev/null || echo 'fd')" +typeset -gh _FORGE_FD_CMD="$(command -v fdfind 2>/dev/null || command -v fd 2>/dev/null || echo 'fd')" # Detect bat command - use bat if available, otherwise fall back to cat if command -v bat &>/dev/null; then - typeset -h _FORGE_CAT_CMD="bat --color=always --style=numbers,changes --line-range=:500" + typeset -gh _FORGE_CAT_CMD="bat --color=always --style=numbers,changes --line-range=:500" else - typeset -h _FORGE_CAT_CMD="cat" + typeset -gh _FORGE_CAT_CMD="cat" fi # Commands cache - loaded lazily on first use -typeset -h _FORGE_COMMANDS="" +typeset -gh _FORGE_COMMANDS="" # Hidden variables to be used only via the ForgeCLI -typeset -h _FORGE_CONVERSATION_ID -typeset -h _FORGE_ACTIVE_AGENT +typeset -gh _FORGE_CONVERSATION_ID +typeset -gh _FORGE_ACTIVE_AGENT # Previous conversation ID for :conversation - (like cd -) -typeset -h _FORGE_PREVIOUS_CONVERSATION_ID +typeset -gh _FORGE_PREVIOUS_CONVERSATION_ID # Session-scoped model and provider overrides (set via :model / :m). # When non-empty, these are passed as --model / --provider to every forge # invocation for the lifetime of the current shell session. -typeset -h _FORGE_SESSION_MODEL -typeset -h _FORGE_SESSION_PROVIDER +typeset -gh _FORGE_SESSION_MODEL +typeset -gh _FORGE_SESSION_PROVIDER diff --git a/shell-plugin/lib/highlight.zsh b/shell-plugin/lib/highlight.zsh index cdefe7c4b4..ec1f0c59de 100644 --- a/shell-plugin/lib/highlight.zsh +++ b/shell-plugin/lib/highlight.zsh @@ -3,6 +3,14 @@ # Syntax highlighting configuration for forge commands # Style the conversation pattern with appropriate highlighting # Keywords in yellow, rest in default white +# +# Use global declarations so we update the shared zsh-syntax-highlighting +# collections even when sourced from within a function (lazy-loading plugin +# managers). Patterns must remain an associative array because the pattern +# highlighter stores regex => style entries in ZSH_HIGHLIGHT_PATTERNS. + +typeset -gA ZSH_HIGHLIGHT_PATTERNS +typeset -ga ZSH_HIGHLIGHT_HIGHLIGHTERS # Style tagged files ZSH_HIGHLIGHT_PATTERNS+=('@\[[^]]#\]' 'fg=cyan,bold')