From ab52b20366045aff1c1de3bab2b7fc52726dd718 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 4 May 2026 12:01:06 +0530 Subject: [PATCH 1/5] fix(attachment): parse file paths containing square brackets --- crates/forge_domain/src/attachment.rs | 137 +++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 2 deletions(-) diff --git a/crates/forge_domain/src/attachment.rs b/crates/forge_domain/src/attachment.rs index 58019ffb3c..47d010510c 100644 --- a/crates/forge_domain/src/attachment.rs +++ b/crates/forge_domain/src/attachment.rs @@ -128,6 +128,49 @@ pub struct FileTag { pub symbol: Option, } +/// Recognizes a balanced `[...]` group, including any nested balanced groups. +/// +/// Used to absorb bracket pairs that appear inside file paths (such as Next.js +/// dynamic route segments like `[locale]` or `[[...slug]]`) so the outer path +/// parser does not terminate prematurely on the first `]`. +fn parse_balanced_brackets(input: &str) -> nom::IResult<&str, &str> { + use nom::Parser; + use nom::branch::alt; + use nom::bytes::complete::take_while1; + use nom::character::complete::char; + use nom::combinator::recognize; + use nom::multi::many0; + use nom::sequence::delimited; + + recognize(delimited( + char('['), + many0(alt(( + parse_balanced_brackets, + take_while1(|c: char| c != '[' && c != ']'), + ))), + char(']'), + )) + .parse(input) +} + +/// Recognizes a path segment that may contain balanced `[...]` groups. +/// +/// Stops at the first `:`, `#`, or `]` that is not nested inside a balanced +/// bracket pair. Requires at least one consumed character. +fn parse_path_segment(input: &str) -> nom::IResult<&str, &str> { + use nom::Parser; + use nom::branch::alt; + use nom::bytes::complete::take_while1; + use nom::combinator::recognize; + use nom::multi::many1; + + recognize(many1(alt(( + parse_balanced_brackets, + take_while1(|c: char| c != '[' && c != ']' && c != ':' && c != '#'), + )))) + .parse(input) +} + impl FileTag { pub fn parse(input: &str) -> nom::IResult<&str, FileTag> { use nom::bytes::complete::take_while1; @@ -154,10 +197,10 @@ impl FileTag { nom::combinator::recognize(( nom::character::complete::satisfy(|c| c.is_ascii_alphabetic()), nom::character::complete::char(':'), - take_while1(|c: char| c != ':' && c != '#' && c != ']'), + parse_path_segment, )), // Fall back to regular path parsing - take_while1(|c: char| c != ':' && c != '#' && c != ']'), + parse_path_segment, )); let mut parser = delimited( tag("@["), @@ -563,4 +606,94 @@ mod tests { assert!(paths.contains(&expected_unix)); assert!(paths.contains(&expected_windows)); } + + #[test] + fn test_attachment_parse_nextjs_dynamic_route() { + let text = String::from("Open @[/src/app/[locale]/layout.tsx]"); + let paths = Attachment::parse_all(text); + assert_eq!(paths.len(), 1); + + let expected = FileTag { + path: "/src/app/[locale]/layout.tsx".to_string(), + loc: None, + symbol: None, + }; + let actual = paths.first().unwrap(); + assert_eq!(actual, &expected); + } + + #[test] + fn test_attachment_parse_nextjs_dynamic_route_with_location() { + let text = String::from("Open @[/src/app/[locale]/layout.tsx:10:20]"); + let paths = Attachment::parse_all(text); + assert_eq!(paths.len(), 1); + + let expected = FileTag { + path: "/src/app/[locale]/layout.tsx".to_string(), + loc: Some(Location { start: Some(10), end: Some(20) }), + symbol: None, + }; + let actual = paths.first().unwrap(); + assert_eq!(actual, &expected); + } + + #[test] + fn test_attachment_parse_nextjs_catch_all_route() { + let text = String::from("Open @[/src/app/[...slug]/page.tsx]"); + let paths = Attachment::parse_all(text); + assert_eq!(paths.len(), 1); + + let expected = FileTag { + path: "/src/app/[...slug]/page.tsx".to_string(), + loc: None, + symbol: None, + }; + let actual = paths.first().unwrap(); + assert_eq!(actual, &expected); + } + + #[test] + fn test_attachment_parse_nextjs_optional_catch_all_route() { + let text = String::from("Open @[/src/app/[[...slug]]/page.tsx#Page]"); + let paths = Attachment::parse_all(text); + assert_eq!(paths.len(), 1); + + let expected = FileTag { + path: "/src/app/[[...slug]]/page.tsx".to_string(), + loc: None, + symbol: Some("Page".to_string()), + }; + let actual = paths.first().unwrap(); + assert_eq!(actual, &expected); + } + + #[test] + fn test_attachment_parse_nextjs_multiple_dynamic_segments() { + let text = String::from("Open @[/src/app/[locale]/blog/[slug]/page.tsx:5#Component]"); + let paths = Attachment::parse_all(text); + assert_eq!(paths.len(), 1); + + let expected = FileTag { + path: "/src/app/[locale]/blog/[slug]/page.tsx".to_string(), + loc: Some(Location { start: Some(5), end: None }), + symbol: Some("Component".to_string()), + }; + let actual = paths.first().unwrap(); + assert_eq!(actual, &expected); + } + + #[test] + fn test_attachment_parse_windows_dynamic_route() { + let text = String::from("Open @[C:\\project\\src\\app\\[locale]\\layout.tsx]"); + let paths = Attachment::parse_all(text); + assert_eq!(paths.len(), 1); + + let expected = FileTag { + path: "C:\\project\\src\\app\\[locale]\\layout.tsx".to_string(), + loc: None, + symbol: None, + }; + let actual = paths.first().unwrap(); + assert_eq!(actual, &expected); + } } From 8b7917b5a743aa793adac7e108ed0e1ffe4b5629 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 06:36:06 +0000 Subject: [PATCH 2/5] [autofix.ci] apply automated fixes --- crates/forge_app/src/operation.rs | 2 +- crates/forge_infra/src/mcp_client.rs | 2 +- crates/forge_main/src/ui.rs | 2 +- crates/forge_repo/src/context_engine.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/forge_app/src/operation.rs b/crates/forge_app/src/operation.rs index 47cda35742..1a88fce4f0 100644 --- a/crates/forge_app/src/operation.rs +++ b/crates/forge_app/src/operation.rs @@ -2335,7 +2335,7 @@ mod tests { let long_content = format!( "{}{}", "A".repeat(config.max_fetch_chars), - &truncated_content + truncated_content ); let fixture = ToolOperation::NetFetch { input: forge_domain::NetFetch { diff --git a/crates/forge_infra/src/mcp_client.rs b/crates/forge_infra/src/mcp_client.rs index 511771833f..23e246a1f3 100644 --- a/crates/forge_infra/src/mcp_client.rs +++ b/crates/forge_infra/src/mcp_client.rs @@ -617,7 +617,7 @@ fn resolve_http_templates( let template_data = serde_json::json!({"env": env_vars}); // Resolve templates in headers - for (_, value) in http.headers.iter_mut() { + for value in http.headers.values_mut() { // Try to render the template, but keep original value if it fails if let Ok(resolved) = handlebars.render_template(value, &template_data) { *value = resolved; diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index 7089fcd3c8..e0e012d638 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -1618,7 +1618,7 @@ impl A + Send + Sync> UI // Show failed MCP servers if !porcelain && !all_tools.mcp.get_failures().is_empty() { self.writeln("MCP FAILURES\n".dimmed().bold())?; - for (_, error) in all_tools.mcp.get_failures().iter() { + for error in all_tools.mcp.get_failures().values() { let error = style(error).red(); self.writeln(error)?; } diff --git a/crates/forge_repo/src/context_engine.rs b/crates/forge_repo/src/context_engine.rs index 97f5bf68ef..33fa674f59 100644 --- a/crates/forge_repo/src/context_engine.rs +++ b/crates/forge_repo/src/context_engine.rs @@ -107,7 +107,7 @@ impl ForgeContextEngineRepository { ) -> Result> { request.metadata_mut().insert( "authorization", - format!("Bearer {}", &**auth_token).parse()?, + format!("Bearer {}", **auth_token).parse()?, ); Ok(request) } From 4da58d840a6f30795c97a4586d0cf9f3aad68f98 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 4 May 2026 12:07:05 +0530 Subject: [PATCH 3/5] fix(attachment): support square brackets in Next.js route paths --- crates/forge_domain/src/attachment.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/forge_domain/src/attachment.rs b/crates/forge_domain/src/attachment.rs index 47d010510c..6e589249fd 100644 --- a/crates/forge_domain/src/attachment.rs +++ b/crates/forge_domain/src/attachment.rs @@ -696,4 +696,21 @@ mod tests { let actual = paths.first().unwrap(); assert_eq!(actual, &expected); } + + #[test] + fn test_attachment_parse_many_square_brackets() { + // Real-world example: deeply nested or heavily-bracketed Next.js routes + let text = + String::from("Open @[/src/app/[locale]/[version]/[...path]/[[...rest]]/page.tsx:1:10]"); + let paths = Attachment::parse_all(text); + assert_eq!(paths.len(), 1); + + let expected = FileTag { + path: "/src/app/[locale]/[version]/[...path]/[[...rest]]/page.tsx".to_string(), + loc: Some(Location { start: Some(1), end: Some(10) }), + symbol: None, + }; + let actual = paths.first().unwrap(); + assert_eq!(actual, &expected); + } } From 9f53d9c99ababe6c67e7879102ab643e28131c2d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 06:40:36 +0000 Subject: [PATCH 4/5] [autofix.ci] apply automated fixes --- crates/forge_repo/src/context_engine.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/forge_repo/src/context_engine.rs b/crates/forge_repo/src/context_engine.rs index 33fa674f59..f1a1e4b56d 100644 --- a/crates/forge_repo/src/context_engine.rs +++ b/crates/forge_repo/src/context_engine.rs @@ -105,10 +105,9 @@ impl ForgeContextEngineRepository { mut request: tonic::Request, auth_token: &ApiKey, ) -> Result> { - request.metadata_mut().insert( - "authorization", - format!("Bearer {}", **auth_token).parse()?, - ); + request + .metadata_mut() + .insert("authorization", format!("Bearer {}", **auth_token).parse()?); Ok(request) } } From 7812f299e8f4adf77ef919cba1b42fd044f02f59 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Thu, 7 May 2026 16:28:16 +0530 Subject: [PATCH 5/5] fix(bindings): re-apply keybindings after zsh-vi-mode initialization --- crates/forge_main/src/zsh/plugin.rs | 14 ++++++++++++++ shell-plugin/lib/bindings.zsh | 20 +++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index b54e3e9baa..c91ee70a9a 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -426,6 +426,20 @@ mod tests { assert_eq!(actual, expected); } + /// Regression: forge keybindings must survive zsh-vi-mode's `zvm_init` + /// by re-applying via `zvm_after_init_commands` (#2681). + #[test] + fn test_generated_plugin_registers_zvm_after_init_hook() { + use pretty_assertions::assert_eq; + + let fixture = generate_zsh_plugin().unwrap(); + let actual = fixture.contains("function _forge_apply_keybindings()") + && fixture.contains("typeset -ga zvm_after_init_commands") + && fixture.contains("zvm_after_init_commands+=('_forge_apply_keybindings')"); + let expected = true; + assert_eq!(actual, expected); + } + #[test] fn test_setup_zsh_integration_without_nerd_font_config() { use tempfile::TempDir; diff --git a/shell-plugin/lib/bindings.zsh b/shell-plugin/lib/bindings.zsh index 432d0cb421..0476dd43cf 100644 --- a/shell-plugin/lib/bindings.zsh +++ b/shell-plugin/lib/bindings.zsh @@ -35,11 +35,17 @@ function forge-bracketed-paste() { zle reset-prompt } -# Register the bracketed paste widget to fix highlighting on paste -zle -N bracketed-paste forge-bracketed-paste +# Re-applied after zsh-vi-mode's `zvm_init` precmd hook, which rebuilds the +# main/viins/vicmd keymaps and otherwise silently clobbers these bindings. +function _forge_apply_keybindings() { + zle -N bracketed-paste forge-bracketed-paste + bindkey '^M' forge-accept-line + bindkey '^J' forge-accept-line + bindkey '^I' forge-completion +} + +_forge_apply_keybindings -# Bind Enter to our custom accept-line that transforms :commands -bindkey '^M' forge-accept-line -bindkey '^J' forge-accept-line -# Update the Tab binding to use the new completion widget -bindkey '^I' forge-completion # Tab for both @ and :command completion +# Harmless no-op when zsh-vi-mode (jeffreytse/zsh-vi-mode) isn't loaded. +typeset -ga zvm_after_init_commands +zvm_after_init_commands+=('_forge_apply_keybindings')