[codex] ExternalTool abstraction layer#2294
Conversation
Introduces a central shell abstraction that replaces hardcoded
Command::new("cmd") / Command::new("sh") across the execution path.
- Shell detection at startup (pwsh -> powershell -> cmd -> sh)
- Correct quoting per shell (PowerShell uses -NoProfile -Command)
- Scope guards restore crossterm raw mode on all spawn paths (Hmbown#1690)
- Direct program+args builder for sandbox ExecEnv integration
Files:
- crates/tui/src/shell_dispatcher.rs (new, 12 tests)
- crates/tui/src/main.rs (register module)
- crates/tui/src/eval.rs (exec_shell delegates to dispatcher)
- crates/tui/src/sandbox/mod.rs (CommandSpec::shell uses dispatcher)
- crates/tui/src/tools/shell.rs (raw mode guards on all spawn paths)
Closes Hmbown#1690
Refs Hmbown#1779
… logging - find_exe(): fall back to known install dirs when PATH lookup fails (C:\Program Files\PowerShell\7, System32\WindowsPowerShell\v1.0) - Encoding prefix: use idiomatic [Console]::OutputEncoding for PowerShell instead of cmd.exe chcp 65001 >NUL - Execution logging: write exec via <ShellKind> entries to SHELL_DISPATCHER_LOG when set - System prompt: use ShellDispatcher detection instead of raw $SHELL so model knows it has PowerShell and generates native cmdlets - display_command(): strip PowerShell encoding prefix for display - Add tests for find_exe PATH + known-dir fallback Refs Hmbown#1779
There was a problem hiding this comment.
Code Review
This pull request introduces a unified ExternalTool trait to abstract external command execution (such as git, gh, cargo, python, node, and rustc) and a ShellDispatcher to manage shell-specific execution details. While these changes centralize subprocess handling, several critical regressions were identified in the review. Specifically, eager clipboard initialization on startup and synchronous waiting on external clipboard commands can block or hang the TUI event loop, particularly on headless Linux/WSL2 hosts or Windows. Additionally, replacing lossy UTF-8 decoding with strict read_line in the Python REPL makes it fragile to non-UTF-8 output, removing -c core.quotepath=false breaks unicode filename support in git status, removing the wl-copy fallback breaks Linux Wayland clipboard support, removing the always_load parameter breaks user-configured tool loading, and using blocking .status() for URL opening can freeze the TUI thread.
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
This is the second stacked PR on top of #2290.
Scope:
ExternalToolValidation:
cargo fmt --allcargo check -p codewhale-tui --all-features --lockedThis PR is intentionally stacked on
fix/1779-windows-shell-dispatchand is not based onmain.Paulo Aboim Pinto
Greptile Summary
This PR introduces two cross-cutting abstractions:
ExternalTool(a trait unifying subprocess resolution and command construction forgit,gh,rustc,cargo,python,node) andShellDispatcher(a singleton that detects the user's shell at startup and builds correctly-structuredCommandobjects for every platform/shell variant). All directCommand::new(\"git\")/Command::new(\"gh\")call sites across the TUI crate are migrated to these new types.ExternalTooltrait (dependencies.rs): each concrete tool holds its ownstatic OnceLockcache and exposes both synccommand()and asynctokio_command()factory methods.ShellDispatcher(shell_dispatcher.rs): detects shell kind from$SHELL/ PATH probing;build_commandhandles per-shell quoting (PowerShell, cmd, POSIX);run_foregroundsaves/restores crossterm raw mode.Command::new(...)withExternalTool::command()or convenience wrappers.Confidence Score: 3/5
The abstraction layer is sound and production migrations are correct, but two Cmd-shell tests assert on get_args() which does not capture raw_arg() content on Windows.
The Cmd-shell tests would panic on Windows CI because build_command routes the payload through raw_arg(), which is invisible to get_args(). The rest of the migration is correct and well-tested on Unix paths.
crates/tui/src/shell_dispatcher.rs — the Cmd-shell unit tests need platform guards or alternative assertion strategies.
Important Files Changed
Comments Outside Diff (3)
crates/tui/src/tools/git.rs, line 66-77 (link)The refactor drops
-c core.quotepath=falsethat was previously prepended to everygit statusinvocation. Without that flag, git uses its defaultcore.quotepath=truesetting, which encodes non-ASCII filenames as octal escape sequences. Users with CJK, Cyrillic, Arabic, or emoji paths in their workspace will see"\344\270\255\346\226\207..."instead of the real path. The test that explicitly verified this (git_status_reports_unquoted_unicode_paths) was also removed in this PR — the fix and its verification vanished together. The args should prepend-c core.quotepath=falsebefore extending with the user-provided args.crates/tui/src/tools/git.rs, line 74-81 (link)-c core.quotepath=falseflag that forced git to emit raw UTF-8 filenames was removed here. Without it, git's defaultcore.quotepath=truesetting encodes non-ASCII path components as octal escape sequences (e.g."\344\270\255\346\226\207..."for CJK paths). Any user with Unicode filenames will see garbled output. The test that exercised this behaviour (git_status_reports_unquoted_unicode_paths) was also deleted in this PR.crates/tui/src/shell_dispatcher.rs, line 1936-1943 (link)raw_arginvisibilitybuild_commandforShellKind::Cmdusescmd.raw_arg(shell_command)on Windows (gated#[cfg(windows)]), butget_args()only returns args added viaarg()/args()— it does not surfaceraw_argcontent. So on a real Windows CI run:args == ["/C"] // length 1, not 2; args.contains(&"echo hello") == falseBoth
cmd_build_command_uses_c_flag(assertsargs.contains(&"echo hello")) andbuild_command_quotes_spaces_for_cmd(assertsargs.len() == 2andargs[1].starts_with("git ")) would panic. Since this PR is explicitly stacked on the Windows shell-dispatch fix branch, broken Windows tests are a regression. Both tests should be gated with#[cfg(not(windows))], or the assertions should use an alternative that capturesraw_argcontent.Reviews (7): Last reviewed commit: "fix(tui): keep core native tools active ..." | Re-trigger Greptile