fix(windows): 隐藏 PTY 黑窗 + 非 mac 剪贴板键位 + 修复终端模板对话框冻结#275
Conversation
Wrap all std::process::Command spawns in a silent_command helper that sets CREATE_NO_WINDOW on Windows, eliminating the brief black console window flash when invoking git, gh, sh, powershell, and open from the git, config, topbar, and updater modules.
Add copy-selection-or-interrupt and paste-clipboard keybindings for Windows and Linux. Ctrl+C copies the current selection or, when no selection exists, passes through to xterm so SIGINT (^C) still works. Ctrl+V reads the clipboard and writes it to the PTY. Mac is unchanged since Cmd+C/V is handled by the system.
Splitting `draft` and `isOpen` keeps the Dialog mounted while it closes so Chakra's exit animation can release the pointer-events lock — previously the page froze after creating a template. Also wires the global templates trash button to a remove handler instead of opening the edit dialog.
📝 WalkthroughWalkthroughThis PR adds a cross-platform silent_command helper and replaces direct Command::new subprocess construction across infra and handlers; implements multi-shell detection and injection for PTY sessions and adds comprehensive shell-integration scripts; adds terminal clipboard copy/paste actions and refactors dialog state; and updates manifests and plugin registration for clipboard support. ChangesBackend: Silent Command Infrastructure
Shell injection and PTY wiring
Shell integration scripts
Frontend: Terminal Clipboard and Dialog State Management
Misc: manifests, plugin, and ignore
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Replace navigator.clipboard with tauri-plugin-clipboard-manager so Ctrl+C/V hit the native OS clipboard API instead of WebView2's JS shim, which is unreliable on Windows.
Extend shell init injection from zsh-only to bash, fish, and PowerShell by vendoring VS Code's MIT-licensed shell integration scripts. Shell init (shell_init.rs): - Split monolithic default_init.sh into common (POSIX-sh) and zsh-only parts - New ShellType enum with detect_shell_type() for automatic shell detection - New ShellInjection enum describing per-shell injection strategy: - Bash: --init-file flag - Zsh: ZDOTDIR redirection (existing approach, now with .zshrc/.zprofile/.zlogin) - Fish: --init-command source - Pwsh: -noexit -command dot-source - Unknown: no injection (graceful fallback) - prepare_shell_injection() dispatches to per-shell prepare functions - All shells (including fish/pwsh) now receive project init scripts from 2code.json - Delete legacy prepare_init_dir() (no remaining callers) PTY layer (pty.rs): - build_injected_command() replaces old command_builder(), constructs shell-specific args (e.g. --init-file for bash, -i for zsh) - parse_shell_command() handles Windows paths with spaces in shell commands (e.g. "C:\Program Files\...\pwsh.exe -NoLogo -NoProfile") - Set TERM_PROGRAM=vscode and VSCODE_INJECTION=1 env vars so vendored VS Code scripts activate correctly - New tests for parse_shell_command() Windows shell detection (handler/shell.rs): - find_pwsh_path() probes standard install locations + PATH fallback - Default shell prefers pwsh 7 over powershell 5.1 - load_windows_shells() now lists: pwsh, powershell, cmd, Git Bash, WSL Vendor scripts (scripts/): - shellIntegration-bash.sh, shellIntegration-rc.zsh, shellIntegration-env.zsh, shellIntegration-profile.zsh, shellIntegration-login.zsh (zsh), shellIntegration.fish, shellIntegration.ps1 - default_init_common.sh: POSIX-sh notify hook, claude wrapper, PATH setup - default_init_zsh.sh: zle keybindings, unsetopt PROMPT_SP Other: - .gitignore: add tauri.conf.dev.json for local dev config override
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src-tauri/crates/infra/scripts/shellIntegration-bash.sh`:
- Around line 104-108: The activation failure reporting currently records the
negated return code because the `! builtin eval "$VSCODE_PYTHON_BASH_ACTIVATE"`
inverts `$?`; change the logic so you capture the actual exit code immediately
after `builtin eval "$VSCODE_PYTHON_BASH_ACTIVATE"` (e.g. into
`__vsc_activation_status`) and then test/negate that value for the `if`
condition; update the `printf` to use the captured `__vsc_activation_status` so
the real Python activation exit code (from `VSCODE_PYTHON_BASH_ACTIVATE`) is
logged instead of 0.
In `@src-tauri/crates/infra/scripts/shellIntegration-rc.zsh`:
- Around line 21-33: After the injected zsh startup block finishes (the branch
that sets HISTFILE, VSCODE_ZDOTDIR, ZDOTDIR and sources . $USER_ZDOTDIR/.zshrc),
unset the VSCODE_INJECTION environment variable so child/nested zsh sessions do
not re-enter the injection path; specifically, add an unset or export -n for
VSCODE_INJECTION immediately after the sourcing step in the branch that checks
[[ "$VSCODE_INJECTION" == "1" ]] to ensure VSCODE_INJECTION is cleared for
subsequent shells while leaving the rest of the HISTFILE/ZDOTDIR/VSCODE_ZDOTDIR
logic intact.
In `@src-tauri/crates/infra/scripts/shellIntegration.fish`:
- Around line 52-83: The guard variable __vsc_applied_env_vars is declared
global at top but set locally inside function __vsc_apply_env_vars (using set
-l), which shadows the global and leaves the global flag at 0 so the guard never
blocks repeated execution; change the assignment in __vsc_apply_env_vars to set
the global flag instead (use set -g __vsc_applied_env_vars 1) so the initial
check (if test $__vsc_applied_env_vars -eq 1) correctly prevents re-applying
VSCODE_ENV_REPLACE/PREPEND/APPEND on subsequent prompts.
In `@src-tauri/crates/infra/src/pty.rs`:
- Around line 183-194: The Bash and Zsh branches (ShellInjection::Bash and
ShellInjection::Zsh) drop the caller's existing_args when constructing the
CommandBuilder, so preserve and re-add existing_args (from
parse_shell_command()) to the CommandBuilder created by
CommandBuilder::new(program) before adding the injection flags; for Bash, add
existing_args then append "--init-file" and init_file.to_string_lossy(), and for
Zsh add existing_args then append "-i" (or other injection behavior), ensuring
the original flags such as "-l" or "-f" are retained.
In `@src-tauri/crates/infra/src/shell_init.rs`:
- Around line 30-40: The current detect_shell_type splits on whitespace which
misparses unquoted Windows paths like "C:\Program Files\...\pwsh.exe -NoLogo";
update detect_shell_type to first extract the executable token robustly: if
shell_cmd starts with a quote, take the content up to the closing quote;
otherwise search for the earliest occurrence of an executable filename by
looking for a path separator and a known executable extension (e.g.,
case-insensitive ".exe", ".com", ".bat", ".cmd", ".pwsh", ".sh") and take the
substring up to that extension, falling back to the original split_whitespace()
behavior only if no match is found; then continue using
Path::new(...).file_stem() on that extracted exe to compute basename and
classification.
- Around line 42-45: The match currently maps "sh" to ShellType::Bash which
causes Bash-specific init flags to be used for /bin/sh; change the match in
shell_init.rs so that "bash" alone maps to ShellType::Bash and remove "sh" from
that arm, and add an explicit "sh" mapping (e.g., ShellType::Sh or
ShellType::Posix) or map it to the generic/unknown variant used by your code;
then update downstream code that handles ShellType (the code that constructs the
invocation using ShellType) to treat ShellType::Sh/Posix as a plain POSIX sh and
avoid Bash-only flags/--init-file usage so that dash/BusyBox sh do not receive
incompatible arguments.
In `@src-tauri/src/handler/shell.rs`:
- Around line 68-72: The Windows-only "where pwsh" probe currently uses
std::process::Command::new("where").arg("pwsh").output(), which can cause a
console flash; replace that call with the project's no-window helper (e.g., use
no_window_command("where").arg("pwsh").output() or the actual helper name
provided in the repo) so the probe is spawned without a visible window; keep the
same .arg("pwsh") and .output().ok()? handling and ensure the helper returns a
Command or equivalent so the rest of the code is unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0df1ffa5-5e14-4623-8255-a7107f2df939
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.locksrc-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (19)
package.jsonsrc-tauri/.gitignoresrc-tauri/Cargo.tomlsrc-tauri/capabilities/default.jsonsrc-tauri/crates/infra/scripts/default_init_common.shsrc-tauri/crates/infra/scripts/default_init_zsh.shsrc-tauri/crates/infra/scripts/shellIntegration-bash.shsrc-tauri/crates/infra/scripts/shellIntegration-env.zshsrc-tauri/crates/infra/scripts/shellIntegration-login.zshsrc-tauri/crates/infra/scripts/shellIntegration-profile.zshsrc-tauri/crates/infra/scripts/shellIntegration-rc.zshsrc-tauri/crates/infra/scripts/shellIntegration.fishsrc-tauri/crates/infra/scripts/shellIntegration.ps1src-tauri/crates/infra/src/pty.rssrc-tauri/crates/infra/src/shell_init.rssrc-tauri/crates/service/src/pty.rssrc-tauri/src/handler/shell.rssrc-tauri/src/lib.rssrc/features/terminal/Terminal.tsx
💤 Files with no reviewable changes (1)
- src-tauri/crates/infra/scripts/default_init_common.sh
✅ Files skipped from review due to trivial changes (4)
- src-tauri/.gitignore
- package.json
- src-tauri/capabilities/default.json
- src-tauri/Cargo.toml
| if [ -n "${VSCODE_PYTHON_BASH_ACTIVATE:-}" ] && [ "$TERM_PROGRAM" = "vscode" ]; then | ||
| # Prevent crashing by negating exit code | ||
| if ! builtin eval "$VSCODE_PYTHON_BASH_ACTIVATE"; then | ||
| __vsc_activation_status=$? | ||
| builtin printf '\x1b[0m\x1b[7m * \x1b[0;103m VS Code Python bash activation failed with exit code %d \x1b[0m' "$__vsc_activation_status" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -t f "shellIntegration-bash.sh" -x cat -n {} | head -120Repository: AkaraChen/2code
Length of output: 4547
🏁 Script executed:
# Verify bash semantics: does $? get the negated status or original?
cat > /tmp/test_bash_semantics.sh << 'EOF'
#!/bin/bash
echo "=== Test 1: Capturing $? after negated failing command ==="
if ! (exit 5); then
result=$?
echo "Inside 'then': \$? = $result (should be 0 from negation)"
fi
echo ""
echo "=== Test 2: Capturing exit code before negation ==="
(exit 5)
exit_code=$?
if [ "$exit_code" -ne 0 ]; then
echo "Captured before negation: exit_code = $exit_code (should be 5)"
fi
echo ""
echo "=== Test 3: Using the suggested pattern ==="
(exit 7)
capture=$?
if [ "$capture" -ne 0 ]; then
echo "Captured status: $capture (should be 7)"
fi
EOF
bash /tmp/test_bash_semantics.shRepository: AkaraChen/2code
Length of output: 352
Capture the Python activation exit code before negating it.
Currently, $? in the then branch reflects the negated status from ! builtin eval ..., so activation failures are reported as exit code 0 instead of the actual error code. Capture the exit code before the negation to preserve diagnostic information.
Suggested fix
- if ! builtin eval "$VSCODE_PYTHON_BASH_ACTIVATE"; then
- __vsc_activation_status=$?
+ builtin eval "$VSCODE_PYTHON_BASH_ACTIVATE"
+ __vsc_activation_status=$?
+ if [ "$__vsc_activation_status" -ne 0 ]; then
builtin printf '\x1b[0m\x1b[7m * \x1b[0;103m VS Code Python bash activation failed with exit code %d \x1b[0m' "$__vsc_activation_status"
fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if [ -n "${VSCODE_PYTHON_BASH_ACTIVATE:-}" ] && [ "$TERM_PROGRAM" = "vscode" ]; then | |
| # Prevent crashing by negating exit code | |
| if ! builtin eval "$VSCODE_PYTHON_BASH_ACTIVATE"; then | |
| __vsc_activation_status=$? | |
| builtin printf '\x1b[0m\x1b[7m * \x1b[0;103m VS Code Python bash activation failed with exit code %d \x1b[0m' "$__vsc_activation_status" | |
| if [ -n "${VSCODE_PYTHON_BASH_ACTIVATE:-}" ] && [ "$TERM_PROGRAM" = "vscode" ]; then | |
| # Prevent crashing by negating exit code | |
| builtin eval "$VSCODE_PYTHON_BASH_ACTIVATE" | |
| __vsc_activation_status=$? | |
| if [ "$__vsc_activation_status" -ne 0 ]; then | |
| builtin printf '\x1b[0m\x1b[7m * \x1b[0;103m VS Code Python bash activation failed with exit code %d \x1b[0m' "$__vsc_activation_status" | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src-tauri/crates/infra/scripts/shellIntegration-bash.sh` around lines 104 -
108, The activation failure reporting currently records the negated return code
because the `! builtin eval "$VSCODE_PYTHON_BASH_ACTIVATE"` inverts `$?`; change
the logic so you capture the actual exit code immediately after `builtin eval
"$VSCODE_PYTHON_BASH_ACTIVATE"` (e.g. into `__vsc_activation_status`) and then
test/negate that value for the `if` condition; update the `printf` to use the
captured `__vsc_activation_status` so the real Python activation exit code (from
`VSCODE_PYTHON_BASH_ACTIVATE`) is logged instead of 0.
| if [[ "$VSCODE_INJECTION" == "1" ]]; then | ||
| HISTFILE=$USER_ZDOTDIR/.zsh_history | ||
| fi | ||
|
|
||
| # Only fix up ZDOTDIR if shell integration was injected (not manually installed) and has not been called yet | ||
| if [[ "$VSCODE_INJECTION" == "1" ]]; then | ||
| if [[ $options[norcs] = off && -f $USER_ZDOTDIR/.zshrc ]]; then | ||
| VSCODE_ZDOTDIR=$ZDOTDIR | ||
| ZDOTDIR=$USER_ZDOTDIR | ||
| # A user's custom HISTFILE location might be set when their .zshrc file is sourced below | ||
| . $USER_ZDOTDIR/.zshrc | ||
| fi | ||
| fi |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find src-tauri/crates/infra/scripts -name "shellIntegration*" -type fRepository: AkaraChen/2code
Length of output: 449
🏁 Script executed:
# Read the zsh script to confirm current behavior
cat -n src-tauri/crates/infra/scripts/shellIntegration-rc.zshRepository: AkaraChen/2code
Length of output: 12849
🏁 Script executed:
# Read the bash script to see if it clears VSCODE_INJECTION
cat -n src-tauri/crates/infra/scripts/shellIntegration-bash.shRepository: AkaraChen/2code
Length of output: 18098
Unset VSCODE_INJECTION once the injected startup path is done.
The bash variant clears this flag at line 58, but the zsh path leaves it exported for child shells. Nested zsh sessions will detect it as "1" and re-enter the special startup block, unexpectedly re-sourcing ~/.zshrc and re-applying history/dotdir configuration.
Suggested fix
if [[ "$VSCODE_INJECTION" == "1" ]]; then
if [[ $options[norcs] = off && -f $USER_ZDOTDIR/.zshrc ]]; then
VSCODE_ZDOTDIR=$ZDOTDIR
ZDOTDIR=$USER_ZDOTDIR
# A user's custom HISTFILE location might be set when their .zshrc file is sourced below
. $USER_ZDOTDIR/.zshrc
fi
+ builtin unset VSCODE_INJECTION
fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if [[ "$VSCODE_INJECTION" == "1" ]]; then | |
| HISTFILE=$USER_ZDOTDIR/.zsh_history | |
| fi | |
| # Only fix up ZDOTDIR if shell integration was injected (not manually installed) and has not been called yet | |
| if [[ "$VSCODE_INJECTION" == "1" ]]; then | |
| if [[ $options[norcs] = off && -f $USER_ZDOTDIR/.zshrc ]]; then | |
| VSCODE_ZDOTDIR=$ZDOTDIR | |
| ZDOTDIR=$USER_ZDOTDIR | |
| # A user's custom HISTFILE location might be set when their .zshrc file is sourced below | |
| . $USER_ZDOTDIR/.zshrc | |
| fi | |
| fi | |
| if [[ "$VSCODE_INJECTION" == "1" ]]; then | |
| HISTFILE=$USER_ZDOTDIR/.zsh_history | |
| fi | |
| # Only fix up ZDOTDIR if shell integration was injected (not manually installed) and has not been called yet | |
| if [[ "$VSCODE_INJECTION" == "1" ]]; then | |
| if [[ $options[norcs] = off && -f $USER_ZDOTDIR/.zshrc ]]; then | |
| VSCODE_ZDOTDIR=$ZDOTDIR | |
| ZDOTDIR=$USER_ZDOTDIR | |
| # A user's custom HISTFILE location might be set when their .zshrc file is sourced below | |
| . $USER_ZDOTDIR/.zshrc | |
| fi | |
| builtin unset VSCODE_INJECTION | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src-tauri/crates/infra/scripts/shellIntegration-rc.zsh` around lines 21 - 33,
After the injected zsh startup block finishes (the branch that sets HISTFILE,
VSCODE_ZDOTDIR, ZDOTDIR and sources . $USER_ZDOTDIR/.zshrc), unset the
VSCODE_INJECTION environment variable so child/nested zsh sessions do not
re-enter the injection path; specifically, add an unset or export -n for
VSCODE_INJECTION immediately after the sourcing step in the branch that checks
[[ "$VSCODE_INJECTION" == "1" ]] to ensure VSCODE_INJECTION is cleared for
subsequent shells while leaving the rest of the HISTFILE/ZDOTDIR/VSCODE_ZDOTDIR
logic intact.
| set -g __vsc_applied_env_vars 0 | ||
| function __vsc_apply_env_vars | ||
| if test $__vsc_applied_env_vars -eq 1; | ||
| return | ||
| end | ||
| set -l __vsc_applied_env_vars 1 | ||
| # Apply EnvironmentVariableCollections if needed | ||
| if test -n "$VSCODE_ENV_REPLACE" | ||
| set ITEMS (string split : $VSCODE_ENV_REPLACE) | ||
| for B in $ITEMS | ||
| set split (string split -m1 = $B) | ||
| set -gx "$split[1]" (echo -e "$split[2]") | ||
| end | ||
| set -e VSCODE_ENV_REPLACE | ||
| end | ||
| if test -n "$VSCODE_ENV_PREPEND" | ||
| set ITEMS (string split : $VSCODE_ENV_PREPEND) | ||
| for B in $ITEMS | ||
| set split (string split -m1 = $B) | ||
| set -gx "$split[1]" (echo -e "$split[2]")"$$split[1]" # avoid -p as it adds a space | ||
| end | ||
| set -e VSCODE_ENV_PREPEND | ||
| end | ||
| if test -n "$VSCODE_ENV_APPEND" | ||
| set ITEMS (string split : $VSCODE_ENV_APPEND) | ||
| for B in $ITEMS | ||
| set split (string split -m1 = $B) | ||
| set -gx "$split[1]" "$$split[1]"(echo -e "$split[2]") # avoid -a as it adds a space | ||
| end | ||
| set -e VSCODE_ENV_APPEND | ||
| end | ||
| end |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's confirm the file exists and read the relevant section
cat -n src-tauri/crates/infra/scripts/shellIntegration.fish | head -90 | tail -40Repository: AkaraChen/2code
Length of output: 1607
🏁 Script executed:
# Let's verify fish shell's variable scoping behavior with a simple test
fish <<'EOF'
# Simulate the problematic pattern
set -g test_var 0
function test_func
if test $test_var -eq 1
echo "Guard worked: early return"
return
end
set -l test_var 1
echo "Function executed with local shadow"
end
# First call
echo "First call:"
test_func
# Second call - will it return early?
echo "Second call:"
test_func
# Check global state
echo "Global test_var is: $test_var"
EOFRepository: AkaraChen/2code
Length of output: 104
🌐 Web query:
fish shell set -l vs set -g variable scoping shadow global
💡 Result:
In fish, -l (local) sets a variable scope that ends when the current block ends; -g (global) sets a variable scope that’s available to all functions running in the same shell. When you reference a variable name, fish uses the smallest (narrowest) scoped variable of that name; so a locally-scoped variable will shadow a global one with the same name within its scope.[1][2] Key behaviors 1) set -l NAME VALUE (or set --local) - “Sets a locally-scoped variable in this block. It is erased when the block ends.” Outside a block, it behaves like function scope.[1] - In the language docs, local variables are “specific to the current block of commands” and “automatically erased when a specific block goes out of scope.” Outside of a block, local is like function scope.[2] 2) set -g NAME VALUE (or set --global) - “Sets a globally-scoped variable. Global variables are available to all functions running in the same shell. They can be modified or erased.”[1] 3) Shadowing / precedence rules - fish can have multiple variables with the same name but different scopes. - When you use a variable, “the smallest scoped variable of that name will be used. If a local variable exists, it will be used instead of the global or universal variable of the same name.”[2] Practical implication for “set -l vs set -g” - If you do set -g foo 1, then later inside a function/block do set -l foo 2, any use of $foo inside that block will resolve to 2 (the local), not the global.[2] - After the block ends, the local foo is erased, so $foo resolves back to the global foo value (assuming no other narrower scope is active).[1][2] Notes about function scope vs local flag - If scope is not explicitly set, fish uses a different default (function-scope) behavior: “If a variable’s scope is not explicitly set… the variable will be local to the currently executing function. Note that this is different from using the -l or --local flag, in which case the variable will be local to the most-inner currently executing block.”[1] Example pattern (block-local shadowing) - Use set -l inside conditionals/loops/inner blocks to prevent temporarily overriding a global outside that block, since locals will shadow globals only within the active block.[1][2] Sources: fish set documentation and fish language variable scope/shadowing rules.[1][2]
Citations:
Fix the env-var application guard by using global scope.
Line 57 uses set -l __vsc_applied_env_vars 1, which creates a local variable that shadows the global flag initialized at line 52. The global remains 0, so the guard check at line 54 always fails. This allows the prepend/append/replace logic to run on every prompt, repeatedly duplicating environment variable values.
Suggested fix
- set -l __vsc_applied_env_vars 1
+ set -g __vsc_applied_env_vars 1📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| set -g __vsc_applied_env_vars 0 | |
| function __vsc_apply_env_vars | |
| if test $__vsc_applied_env_vars -eq 1; | |
| return | |
| end | |
| set -l __vsc_applied_env_vars 1 | |
| # Apply EnvironmentVariableCollections if needed | |
| if test -n "$VSCODE_ENV_REPLACE" | |
| set ITEMS (string split : $VSCODE_ENV_REPLACE) | |
| for B in $ITEMS | |
| set split (string split -m1 = $B) | |
| set -gx "$split[1]" (echo -e "$split[2]") | |
| end | |
| set -e VSCODE_ENV_REPLACE | |
| end | |
| if test -n "$VSCODE_ENV_PREPEND" | |
| set ITEMS (string split : $VSCODE_ENV_PREPEND) | |
| for B in $ITEMS | |
| set split (string split -m1 = $B) | |
| set -gx "$split[1]" (echo -e "$split[2]")"$$split[1]" # avoid -p as it adds a space | |
| end | |
| set -e VSCODE_ENV_PREPEND | |
| end | |
| if test -n "$VSCODE_ENV_APPEND" | |
| set ITEMS (string split : $VSCODE_ENV_APPEND) | |
| for B in $ITEMS | |
| set split (string split -m1 = $B) | |
| set -gx "$split[1]" "$$split[1]"(echo -e "$split[2]") # avoid -a as it adds a space | |
| end | |
| set -e VSCODE_ENV_APPEND | |
| end | |
| end | |
| set -g __vsc_applied_env_vars 0 | |
| function __vsc_apply_env_vars | |
| if test $__vsc_applied_env_vars -eq 1; | |
| return | |
| end | |
| set -g __vsc_applied_env_vars 1 | |
| # Apply EnvironmentVariableCollections if needed | |
| if test -n "$VSCODE_ENV_REPLACE" | |
| set ITEMS (string split : $VSCODE_ENV_REPLACE) | |
| for B in $ITEMS | |
| set split (string split -m1 = $B) | |
| set -gx "$split[1]" (echo -e "$split[2]") | |
| end | |
| set -e VSCODE_ENV_REPLACE | |
| end | |
| if test -n "$VSCODE_ENV_PREPEND" | |
| set ITEMS (string split : $VSCODE_ENV_PREPEND) | |
| for B in $ITEMS | |
| set split (string split -m1 = $B) | |
| set -gx "$split[1]" (echo -e "$split[2]")"$$split[1]" # avoid -p as it adds a space | |
| end | |
| set -e VSCODE_ENV_PREPEND | |
| end | |
| if test -n "$VSCODE_ENV_APPEND" | |
| set ITEMS (string split : $VSCODE_ENV_APPEND) | |
| for B in $ITEMS | |
| set split (string split -m1 = $B) | |
| set -gx "$split[1]" "$$split[1]"(echo -e "$split[2]") # avoid -a as it adds a space | |
| end | |
| set -e VSCODE_ENV_APPEND | |
| end | |
| end |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src-tauri/crates/infra/scripts/shellIntegration.fish` around lines 52 - 83,
The guard variable __vsc_applied_env_vars is declared global at top but set
locally inside function __vsc_apply_env_vars (using set -l), which shadows the
global and leaves the global flag at 0 so the guard never blocks repeated
execution; change the assignment in __vsc_apply_env_vars to set the global flag
instead (use set -g __vsc_applied_env_vars 1) so the initial check (if test
$__vsc_applied_env_vars -eq 1) correctly prevents re-applying
VSCODE_ENV_REPLACE/PREPEND/APPEND on subsequent prompts.
| ShellInjection::Bash { init_file } => { | ||
| // bash --init-file /path/to/shellIntegration-bash.sh | ||
| let mut cmd = CommandBuilder::new(program); | ||
| cmd.arg("--init-file"); | ||
| cmd.arg(init_file.to_string_lossy().as_ref()); | ||
| cmd | ||
| } | ||
| ShellInjection::Zsh { .. } => { | ||
| // zsh -i (ZDOTDIR is set via env var, scripts are in the dir) | ||
| let mut cmd = CommandBuilder::new(program); | ||
| cmd.arg("-i"); | ||
| cmd |
There was a problem hiding this comment.
Preserve the caller's shell flags for Bash/Zsh injection.
parse_shell_command() already keeps existing_args, but these two branches drop them completely. A configured shell like bash -l or zsh -f will silently change behavior as soon as injection is enabled.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src-tauri/crates/infra/src/pty.rs` around lines 183 - 194, The Bash and Zsh
branches (ShellInjection::Bash and ShellInjection::Zsh) drop the caller's
existing_args when constructing the CommandBuilder, so preserve and re-add
existing_args (from parse_shell_command()) to the CommandBuilder created by
CommandBuilder::new(program) before adding the injection flags; for Bash, add
existing_args then append "--init-file" and init_file.to_string_lossy(), and for
Zsh add existing_args then append "-i" (or other injection behavior), ensuring
the original flags such as "-l" or "-f" are retained.
| pub fn detect_shell_type(shell_cmd: &str) -> ShellType { | ||
| // Take the basename of the first token (the executable) | ||
| let exe = shell_cmd | ||
| .split_whitespace() | ||
| .next() | ||
| .unwrap_or(shell_cmd); | ||
| let basename = Path::new(exe) | ||
| .file_stem() | ||
| .and_then(|s| s.to_str()) | ||
| .unwrap_or(exe) | ||
| .to_lowercase(); |
There was a problem hiding this comment.
Handle executable paths with spaces before classifying the shell.
This logic breaks on commands like C:\Program Files\PowerShell\7\pwsh.exe -NoLogo -NoProfile: the first whitespace token becomes C:\Program, so the basename resolves to program and the function returns Unknown. The service layer then falls back to ShellInjection::None, which disables integration for the preferred Windows pwsh path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src-tauri/crates/infra/src/shell_init.rs` around lines 30 - 40, The current
detect_shell_type splits on whitespace which misparses unquoted Windows paths
like "C:\Program Files\...\pwsh.exe -NoLogo"; update detect_shell_type to first
extract the executable token robustly: if shell_cmd starts with a quote, take
the content up to the closing quote; otherwise search for the earliest
occurrence of an executable filename by looking for a path separator and a known
executable extension (e.g., case-insensitive ".exe", ".com", ".bat", ".cmd",
".pwsh", ".sh") and take the substring up to that extension, falling back to the
original split_whitespace() behavior only if no match is found; then continue
using Path::new(...).file_stem() on that extracted exe to compute basename and
classification.
| match basename.as_str() { | ||
| "zsh" => ShellType::Zsh, | ||
| "bash" | "sh" => ShellType::Bash, | ||
| "fish" => ShellType::Fish, |
There was a problem hiding this comment.
Don't route generic sh through the Bash injection path.
/bin/sh is offered in shell discovery, but this branch classifies it as Bash. Downstream that becomes sh --init-file ..., which is Bash-specific and can break startup on systems where sh is dash/BusyBox instead of Bash.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src-tauri/crates/infra/src/shell_init.rs` around lines 42 - 45, The match
currently maps "sh" to ShellType::Bash which causes Bash-specific init flags to
be used for /bin/sh; change the match in shell_init.rs so that "bash" alone maps
to ShellType::Bash and remove "sh" from that arm, and add an explicit "sh"
mapping (e.g., ShellType::Sh or ShellType::Posix) or map it to the
generic/unknown variant used by your code; then update downstream code that
handles ShellType (the code that constructs the invocation using ShellType) to
treat ShellType::Sh/Posix as a plain POSIX sh and avoid Bash-only
flags/--init-file usage so that dash/BusyBox sh do not receive incompatible
arguments.
| // Fallback: check PATH via `where` | ||
| let output = std::process::Command::new("where") | ||
| .arg("pwsh") | ||
| .output() | ||
| .ok()?; |
There was a problem hiding this comment.
Use the no-window helper for the where pwsh probe.
This Windows-only lookup is still spawned with plain Command::new, so listing shells can reintroduce the same console flash the no-window helper was meant to eliminate.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src-tauri/src/handler/shell.rs` around lines 68 - 72, The Windows-only "where
pwsh" probe currently uses
std::process::Command::new("where").arg("pwsh").output(), which can cause a
console flash; replace that call with the project's no-window helper (e.g., use
no_window_command("where").arg("pwsh").output() or the actual helper name
provided in the repo) so the probe is spawned without a visible window; keep the
same .arg("pwsh") and .output().ok()? handling and ensure the helper returns a
Command or equivalent so the rest of the code is unchanged.
|
根据 CodeRabbit 的 review 意见,修复了以下问题:
@coderabbitai review |
|
✅ Actions performedReview triggered.
|
|
根据 review 意见继续更新: 代码质量修复(上次 push 后)
功能 bug 修复
@coderabbitai review |
|
我来对更新后的代码进行新一轮完整 review。 (=^・ω・^=)🐇 ✅ Actions performedReview triggered.
|
嗨,三个 Windows 相关的体验修复打包成一个 PR,按 commit 分开了,可以单独 review。
总览
1. 72abdd3 — 隐藏 Windows 子进程黑窗
Windows 上
std::process::Command::new默认不带CREATE_NO_WINDOW,每次 spawn 一个无界面的子进程都会闪一下 console 窗。git diff/log/commit history、gh 调用、PowerShell 启动脚本执行……这些点都会闪。做法:新增
crates/infra/src/no_window.rs,里头一个silent_command(program)工厂,Windows 上设CREATE_NO_WINDOW (0x08000000),非 Windows 上等价于Command::new。然后把infra/git.rs、infra/config.rs、handler/topbar.rs、handler/updater.rs里所有Command::new(...)替换过去(运行时调用 + 单元测试 helper 一起,大概 40+ 处)。零行为变化,只是 Windows 上不闪了。
2. fe5f240 — 非 mac 平台的剪贴板键位
Mac 用户惯性是 Cmd+C/V,xterm.js 默认就这套。Windows/Linux 用户期望 Ctrl+C 复制、Ctrl+V 粘贴,但 Ctrl+C 又是 SIGINT,必须区分场景。
新增两个 action:
copy-selection-or-interrupt:有选区时复制并吞掉事件;无选区时return true让 xterm 把 ^C 原样透传给 PTY(保留 SIGINT 行为)paste-clipboard:从剪贴板读文本,writeToPty只在 Mac 之外平台触发(
isMacPlatform()判断)。keybindings.test.ts有覆盖 Win32 命中、MacIntel 不命中两种情况。3. c15d12f — 终端模板对话框冻结
主问题:创建一个全局终端模板(设置→终端模板→添加模板)后,整个页面点不动了,只能刷新。原因是:
```tsx
{draft ? (
<TerminalTemplateDraftDialog draft={draft} isOpen={isOpen} ... />
) : null}
```
保存时 `handleSave` → `closeDialog` → `setDraft(null)`,这一帧 Dialog 直接从 DOM 卸载。但 Chakra v3 的 `Dialog` 打开时给 `` 加了 `overflow: hidden` 和 pointer-events 锁,依赖 `onOpenChange(false)` 触发的退出动画来清理。直接 unmount 让清理 effect 没机会跑 → body 还锁着 + backdrop 残留 → 整页冻结。
修法:拆 `draft` 和 `isOpen` 两个 state。`draft` 永远非 null(默认空 draft,打开时 reset),`isOpen` 独立控制开/关。`closeDialog` 只翻 `isOpen=false`,Dialog 留在 DOM 里走 Chakra 的正常退出动画 → body 锁自然释放。`ProjectTemplatesEditor`(项目级模板编辑)同样写法、同样问题、一起改了。
顺手修的另一个 bug:全局模板列表行内的删除按钮,`onClick` 绑的是 `openEditDialog(template.id)`,复制粘贴 bug —— 点删除其实打开编辑框。改成调一个新的 `removeTemplate(id)`,直接 mutate 删除。`ProjectTemplatesEditor` 那边行内删除写法是对的(直接 filter onChange),没动。
没做的:Windows PowerShell 补全(PSReadLine 注入)
借这个 PR 顺便说明一下我之前折腾了但放弃的方向,免得你想问。
Windows 自带 PowerShell 5.1 的 PSReadLine 是 2.0.0,没有 prediction / ghost text。我之前尝试做一套和 macOS `ZDOTDIR`/`.zshenv` 等价的机制:
结果:跑起来出问题。三个 tab 并存时 PTY 会陷入循环喷 prompt,日志里能看到三个 session_id 各自高频 `read: bytes from PTY n=1 / n=36 / n=39`,对应屏幕上是 `PS E:\workspace\open-source\2code>` 一行接一行无限刷。怀疑是某个东西(PSReadLine 注入 / DSR 应答 / xterm 自动回复某个序列)形成了反馈环,但没排查彻底。
考虑到:
所以这条线整体回退了。如果你对 Windows 补全有想法可以聊聊,我那份代码还能从 git reflog 翻出来当起点。
测试
CI 等价命令本地都跑过:
UI 验证:
Summary by CodeRabbit
New Features
Improvements