From f0b69f3e1740c15c99908436e89d3da6264a2107 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. Most of the typesets are regular arrays, however, zsh-syntax-highlighting's ZSH_HIGHLIGHT_PATTERNS is an associative array, so we need to use -gA instead for that variable. --- crates/forge_main/src/zsh/plugin.rs | 8 ++++-- shell-plugin/lib/config.zsh | 44 +++++++++++++++-------------- shell-plugin/lib/highlight.zsh | 8 ++++++ 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index 5e96121952..9882c15469 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) } @@ -55,7 +57,9 @@ pub fn generate_zsh_theme() -> Result { super::normalize_script(include_str!("../../../../shell-plugin/forge.theme.zsh")); // 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/lib/config.zsh b/shell-plugin/lib/config.zsh index b28c1914a8..4f8af7fe1e 100644 --- a/shell-plugin/lib/config.zsh +++ b/shell-plugin/lib/config.zsh @@ -1,49 +1,51 @@ #!/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 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 # Session-scoped reasoning effort override (set via :reasoning-effort / :re). # When non-empty, exported as FORGE_REASONING__EFFORT for every forge invocation. -typeset -h _FORGE_SESSION_REASONING_EFFORT +typeset -gh _FORGE_SESSION_REASONING_EFFORT # Terminal context capture settings # Master switch for terminal context capture (preexec/precmd hooks) -typeset -h _FORGE_TERM="${FORGE_TERM:-true}" +typeset -gh _FORGE_TERM="${FORGE_TERM:-true}" # Maximum number of commands to keep in the ring buffer (metadata: cmd + exit code) -typeset -h _FORGE_TERM_MAX_COMMANDS="${FORGE_TERM_MAX_COMMANDS:-5}" +typeset -gh _FORGE_TERM_MAX_COMMANDS="${FORGE_TERM_MAX_COMMANDS:-5}" # OSC 133 semantic prompt marker emission: "auto", "on", or "off" -typeset -h _FORGE_TERM_OSC133="${FORGE_TERM_OSC133:-auto}" +typeset -gh _FORGE_TERM_OSC133="${FORGE_TERM_OSC133:-auto}" # Ring buffer arrays for context capture -typeset -ha _FORGE_TERM_COMMANDS=() -typeset -ha _FORGE_TERM_EXIT_CODES=() -typeset -ha _FORGE_TERM_TIMESTAMPS=() +typeset -gha _FORGE_TERM_COMMANDS=() +typeset -gha _FORGE_TERM_EXIT_CODES=() +typeset -gha _FORGE_TERM_TIMESTAMPS=() 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')