Skip to content

feat(windows): custom title bar (Variant B — macOS traffic lights, top-left)#292

Open
nothingness0db wants to merge 23 commits into
AkaraChen:devfrom
nothingness0db:feat/windows-custom-titlebar-mac-topleft
Open

feat(windows): custom title bar (Variant B — macOS traffic lights, top-left)#292
nothingness0db wants to merge 23 commits into
AkaraChen:devfrom
nothingness0db:feat/windows-custom-titlebar-mac-topleft

Conversation

@nothingness0db
Copy link
Copy Markdown
Contributor

@nothingness0db nothingness0db commented May 24, 2026

Summary

Windows only. macOS untouched.

One of two competing variants for the Windows custom title bar — the other is the Windows-style chrome icons at top-right.

This variant mirrors the macOS overlay layout exactly: three 12px traffic lights (red close, yellow minimize, green maximize) fixed at (left: 16px, top: 18px) — the same position macOS uses via trafficLightPosition. The dots stay as solid colours by default and reveal symbols (×, , +) on hover, like native macOS. The Windows sidebar header is bumped from 52px to 80px (matching macOS) so the "2Code" label drops to its own row beneath the dots instead of being shoved aside. With the controls out of the top-right corner, ProjectTopBar reverts to a single-row layout.
PixPin_2026-05-24_18-52-08

Screenshot

Test plan

  • Windows: black system title bar gone
  • Windows: three round (not square) traffic lights at top-left
  • Windows: red / yellow / green from left to right (macOS order)
  • Windows: hovering any dot reveals × − + symbols in all three
  • Windows: maximize symbol swaps to when maximized
  • Windows: "2Code" label sits below the dots, not displaced sideways
  • Windows: top areas of HomePage, SettingsPage, ProjectDetailPage stay draggable
  • macOS: layout unchanged

Summary by CodeRabbit

Release Notes

  • New Features

    • Added shell detection and enumeration across Bash, Zsh, Fish, and PowerShell.
    • Implemented shell integration support for improved terminal experience.
    • Added custom window controls for Windows platform.
  • Improvements

    • Enhanced git query performance with increased caching duration.
    • Added database indexes for faster lookups.
  • Chores

    • Removed deprecated internal command definitions.

Eira Hazel and others added 22 commits May 19, 2026 04:40
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.
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
…iles

Improves CodeRabbit docstring coverage from 37.5% to 90%+, passing the 80% threshold.
- bash: capture Python activation exit code before negation so failures report the real code
- zsh: unset VSCODE_INJECTION after injection block to prevent nested shells re-entering it
- fish: use set -g instead of set -l for env-var guard so it actually persists across prompts
- pty: preserve existing_args in Bash/Zsh injection branches (e.g. bash -l keeps -l)
- shell_init: handle executable paths with spaces in detect_shell_type (Windows Program Files paths)
- shell_init: map sh to Unknown instead of Bash to avoid --init-file on dash/BusyBox
- shell: use silent_command for where pwsh probe to suppress console flash on Windows
- Unify exe extraction: extract_exe() in shell_init shared by detect_shell_type and parse_shell_command, eliminating two diverging parsers
- Add supports_integration field to AvailableShell; cmd.exe and wsl.exe marked false
- Show "No shell integration" label in ShellPicker for unsupported shells
- Clean up 2code-init-* temp dirs on app exit
- Add tauri-plugin-clipboard-manager to CLAUDE.md plugin list
- Run cargo fmt
Fish and pwsh shells were missing the 2code common init (notify hook,
claude wrapper, PATH setup), causing the notify feature and claude
wrapper to be absent in those shells.

Fix: setup_2code_home() writes notify.sh, claude wrapper, and
claude-settings.json from Rust at session creation time (idempotent,
only writes if files don't exist). Fish/pwsh init scripts then just
set env vars and update PATH using shell-native syntax (fish_add_path,
$env:PATH), avoiding the need to reimplement bash heredoc logic.

Tests added for both fish and pwsh injection content.
handler/shell.rs is now a thin layer (9 lines) that delegates to
infra::shell_detect. AvailableShell struct and all detection logic
(find_pwsh_path, load_*_shells, push_shell) live in infra.
…g indexes

- Gate git-status and file-tree queries with isActive so hidden profiles
  stop polling every second while another profile is active
- Increase staleTime from 0 to 5s for git branch and git status queries
  to avoid redundant IPC calls on profile switch
- Scope file watcher query invalidation to the triggering project's
  profiles instead of invalidating all git queries globally
- Add missing FK indexes on profiles.project_id and
  pty_sessions.profile_id to eliminate full-table scans

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…split correctly

The previous fallback condition `if end_idx > parts.len()` could never be
true — the loop bumps end_idx up to parts.len() at most. When given a
shell command like `C:\Program Files\nu\nu --login` (path with spaces,
no .exe extension, followed by a flag), the dead fallback meant the flag
was swallowed into the returned exe string.

Track whether the loop matched an extension/exists() candidate with an
explicit found flag, and only run the flag-position fallback when the
loop exhausted without a match.

Adds 5 unit tests for extract_exe covering the regression case plus
simple basename, Unix path with args, Windows path with .exe, and the
existing no-extension/no-flag path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ndow

Use a Set instead of a single string so events from multiple projects
within the same 1s debounce window are all invalidated, not just the last.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- bash shellIntegration: use constant printf format for PS2 so `%` in
  the continuation prompt doesn't get parsed as a format specifier
- fish shellIntegration: hex-escape ASCII control bytes (0x01..0x1F)
  in __vsc_escape_value so multi-line commands and other control bytes
  don't break OSC 633 parsing (matches bash/zsh)
- pwsh shellIntegration: detect Windows via $PSVersionTable when
  $IsWindows isn't defined (Windows PowerShell 5.1)
- shell_detect: read $SHELL on macOS instead of hardcoding /bin/zsh
- shell_init: write claude.cmd on Windows so PowerShell can resolve
  the wrapper from PATH; fall back to USERPROFILE when HOME is unset;
  skip POSIX-flavoured project_init_scripts for fish/pwsh; include
  process PID in temp dir prefix
- lib.rs: scope shell-integration temp-dir cleanup to this process's
  PID prefix so concurrent app instances don't wipe each other's dirs
- ProjectTopBar: extract GitBranchLabelProps interface
- ShellPicker: render both "(default)" and "(no integration)" suffixes
  when both apply

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Apply rustfmt formatting across the codebase for consistent style
- Fix indentation in no_window.rs (spaces to tabs)
- Reorder imports in helper.rs for consistency
- Adjust line breaks in filesystem.rs, git.rs, project_group.rs, topbar.rs
- Extract GitBranchLabelProps interface in ProjectTopBar.tsx
- Update ShellPicker.tsx to show both '(default)' and '(no integration)' suffixes

These changes are purely cosmetic and do not affect functionality.
…ration

Resolved 13 conflicts (4 shell scripts, no_window/config/git/pty/shell_init
in infra, handler shell/topbar/updater, generated types.ts):

- Kept our `silent_command` helper name over upstream's
  `command_without_windows_console` (shorter, same semantics). Renamed the
  two test files (tests/common/mod.rs, tests/integration_git.rs) that
  upstream auto-merged in to use our name.
- Kept our shell integration scripts (more robust: PS5 compat,
  safe printf, fish `-g` scope fix).
- Kept our shell_init.rs/shell_detect.rs (more complete: fish/pwsh init
  scripts, supports_integration flag, multiple rounds of CodeRabbit review).
- Kept our pty.rs (preserves args for bash/zsh, reuses extract_exe).
- Took upstream's types.ts as base and added back AvailableShell.supports_integration.
- Auto-merged upstream's `#[cfg(target_os = "windows")]` on topbar.rs
  `windows_commands` field.

Fixed pre-existing test failures surfaced by the merge:
- queryKeys.test.ts: added `profile-notes` namespace (upstream-added).
- fileWatcher.test.ts: updated `onmessage` callers to pass an event with
  `project_id` and added `getQueryData` to the queryClient mock (broken
  since our 83ea03b perf commit).
The cast in loadWatcher said `() => void` but the merged production code
calls `event.project_id`, so passing an event arg failed tsc (caught by
the Linux smoke build).
Drop the opaque Windows system title bar so the app extends to the
top edge, mirroring the macOS overlay layout. Render compact Win11-style
min/max/close buttons (36×28, red close hover) in a fixed top-right
overlay. ProjectTopBar splits into two rows on Windows so its action
icons don't clash with the new controls. macOS layout untouched.
Swap the hand-rolled min/max/restore/close SVGs for the VscChrome*
icons from react-icons (the same set VS Code uses), so the controls
inherit a tested icon geometry instead of relying on hand-tuned paths.
Swap the Win11 chrome buttons for macOS-style traffic lights — three
12px circles (red close, yellow minimize, green maximize) at the top
right. Symbols (×, −, +) reveal on hover, mirroring the macOS pattern.
Title row right padding shrinks from 118px to 80px to match the
smaller controls. Two-row Windows layout from the previous commit is
kept so the topbar action icons sit cleanly below the title row.
Place the macOS-style traffic lights at (16, 18) from the window's
top-left, matching the macOS overlay position. Bump the Windows
sidebar header to 80px (same as macOS) so the "2Code" label has its
own row beneath the traffic lights instead of being shoved aside.
ProjectTopBar reverts to the single-row layout — with controls out of
the top-right corner there's no longer any reason to split it.
borderRadius="full" on a Box rendered as a button was being clobbered
by Chakra v3 prop processing plus the button user-agent padding, so
the dots came out square. Switch to a css block with explicit 50%
radius and zero padding.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 24, 2026

📝 Walkthrough

Walkthrough

This PR introduces comprehensive shell integration and cross-platform support improvements. It adds shell discovery and enumeration, refactors PTY shell command construction with injection-based initialization, renames the subprocess helper to silent_command, updates all command execution throughout the codebase, consolidates handler logic to the infra layer, adds Windows window controls, optimizes file-watcher per-project, enhances project UI with active-state gating, and removes obsolete OPSX automation tooling.

Changes

Shell Integration and Cross-Platform Shell Discovery

Layer / File(s) Summary
Shell discovery and enumeration
src-tauri/crates/infra/src/shell_detect.rs
New module enumerates available shells on Unix (from /etc/shells and common paths) and Windows (PowerShell, cmd.exe, Git Bash), detects default shell from environment with OS-specific fallbacks, marks integration support, and exports AvailableShell struct.
Shell type detection and executable extraction
src-tauri/crates/infra/src/shell_init.rs
New extract_exe helper robustly parses shell command strings. detect_shell_type maps executable name to ShellType (zsh/bash/fish/pwsh/unknown), treating plain sh as unknown.
Shell injection preparation and 2code home setup
src-tauri/crates/infra/src/shell_init.rs
New setup_2code_home writes integration assets to ~/.2code. prepare_shell_injection creates temp directory with PID suffix and dispatches per-shell: zsh writes .zshrc with VS Code + common/project init; bash/fish/pwsh write integration script with common init only.
PTY shell command parsing and injection
src-tauri/crates/infra/src/pty.rs
Refactored shell command construction: parse_shell_command extracts executable and tokenizes args. build_injected_command builds injection-specific command (bash --init-file, zsh env, fish/pwsh source) while preserving existing args.
VS Code shell integration script updates
src-tauri/crates/infra/scripts/shellIntegration-*.{sh,zsh,fish,ps1}
Updated bash, zsh, fish, PowerShell integration scripts: bash refactored activation status capture; zsh clears injection flag after sourcing user rc; fish uses global escape-value helper and global guard; PowerShell improves Windows 10 detection.
Default shell initialization scripts
src-tauri/crates/infra/scripts/default_init_{fish,pwsh}.{fish,ps1}
New Fish and PowerShell scripts set _2CODE_* environment variables and update PATH/session.

Command Execution Refactoring

Layer / File(s) Summary
Silent command helper rename and inline Windows logic
src-tauri/crates/infra/src/no_window.rs
Exports renamed silent_command() inlining Windows creation_flags(0x08000000) behavior; called throughout codebase instead of former command_without_windows_console.
Git and GitHub command execution refactoring
src-tauri/crates/infra/src/git.rs
Routes all git/gh subprocess calls (avatar, repo discovery, diff/status/log/commit/branch/worktree/blob/PR status) through silent_command. Adds/updates doc comments without changing signatures or parsing.
Config, topbar, and updater command refactoring
src-tauri/crates/infra/src/config.rs, src-tauri/src/handler/topbar.rs, src-tauri/src/handler/updater.rs
Switch to silent_command for shell execution, app launch, and gh token fetch. Add doc comments for clarity.
Test utility and integration test command refactoring
src-tauri/tests/common/mod.rs, src-tauri/tests/integration_git.rs
Updated to use silent_command for git repo setup, staging, and commit operations in tests.

Handler Layer Consolidation

Layer / File(s) Summary
Shell handler consolidation to infra layer
src-tauri/src/handler/shell.rs
Re-exports AvailableShell from infra::shell_detect and delegates list_available_shells to infra loader, removing ~200 LOC of local discovery.
Infra module exports and library registration
src-tauri/crates/infra/src/lib.rs, src-tauri/src/helper.rs
Add pub mod shell_detect export and adjust import ordering.
Database foreign key indexes
src-tauri/migrations/2026-05-21-000000_add_fk_indexes/
Migration adds indexes on profiles(project_id) and pty_sessions(profile_id) for lookup performance.

Frontend Platform Support and UI Improvements

Layer / File(s) Summary
Platform detection utilities
src/shared/lib/platform.ts
New module exports isMacPlatform(), isWindowsPlatform(), isLinuxPlatform() helpers.
Windows window controls component
src/layout/WindowControls.tsx, src/App.tsx
New component renders fixed "traffic light" buttons (close/minimize/maximize); tracks maximized state, updates on resize, invokes Tauri window APIs. Conditional render in App when Windows.
Shell picker UI improvements
src/features/settings/ShellPicker.tsx, messages/{en,zh}.json
Shell option labels now include comma-separated suffixes (default marker, "no integration" when unsupported). Added i18n strings.
File watcher per-project query invalidation
src/features/watcher/fileWatcher.ts, src/features/watcher/fileWatcher.test.ts
Accumulates project IDs during debounce and selectively invalidates git/fs-tree queries per project/profile, reducing unnecessary invalidations. Tests updated with project-aware events.
Project UI active-state gating and drag regions
src/features/git/ProjectTopBar.tsx, src/features/projects/FileTreePanel.tsx, src/features/projects/ProfileLayout.tsx, src/features/home/HomePage.tsx, src/features/settings/SettingsPage.tsx, src/layout/AppSidebar.tsx, src/features/projects/hooks.ts
Refactored ProjectTopBar with titleContent/controlsContent, file-tree toggle with motion animation, active-state gating of data fetching, Tauri drag regions for window chrome, platform-aware sidebar sizing, query stale-time increases (0 → 5000ms).

Cleanup and Documentation

Layer / File(s) Summary
Remove OPSX command and skill files
.claude/commands/opsx/*, .claude/skills/openspec-*, .claude/settings.json, .gitignore
Delete 18 OPSX/skill automation files, remove openspec bash permission, ignore .claude/ directory.
Shell integration migration documentation
给你/MIGRATION-GUIDE.md, 给你/PATCH-service-pty.md, 给你/pty.rs.new, 给你/shell_init.rs.new
Comprehensive migration guide, patch notes, and preview modules documenting shell integration porting from VS Code.
Misc documentation updates
CLAUDE.md, src/features/projects/components/ProjectTemplatesEditor.tsx, src/features/settings/GlobalTerminalTemplatesSettings.tsx, src/features/terminal/Terminal.tsx, src/features/terminal/keybindings.ts, src/shared/lib/queryKeys.test.ts, src-tauri/crates/infra/src/filesystem.rs, src-tauri/crates/repo/src/project_group.rs
Update plugin list, add JSDoc comments to components and utilities, update test expectations, formatting cleanup.
App shutdown cleanup for temp directories
src-tauri/src/lib.rs
Add Windows-only window decoration disable for custom title bar and extend shutdown to cleanup 2code-init-{pid}-* temp directories.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • AkaraChen/2code#142: Both PRs refactor PTY shell command construction in src-tauri/crates/infra/src/pty.rs to improve shell string parsing and injected command building.
  • AkaraChen/2code#275: Both PRs modify core shell integration infrastructure (infra/no_window::silent_command, infra/shell_init, infra/pty) for shell injection and session management.
  • AkaraChen/2code#70: Both PRs update subprocess usage in src-tauri/src/handler/topbar.rs for app launching, with main PR switching to silent_command wrapper.

Poem

🐇 Whiskers twitch with glee,
Shells discovered far and wide,
Cross-platform harmony!
PTY injections flow with pride,
Windows controls dance with flair,
Shell scripts floating in the air 🎉

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/windows-custom-titlebar-mac-topleft

@nothingness0db nothingness0db force-pushed the feat/windows-custom-titlebar-mac-topleft branch from f0cd42d to 90808f0 Compare May 27, 2026 11:01
@nothingness0db nothingness0db force-pushed the feat/windows-custom-titlebar-mac-topleft branch from 90808f0 to 1b6d57b Compare May 30, 2026 02:28
@netlify
Copy link
Copy Markdown

netlify Bot commented May 30, 2026

Deploy Preview for grand-selkie-225f87 failed. Why did it fail? →

Name Link
🔨 Latest commit 1b6d57b
🔍 Latest deploy log https://app.netlify.com/projects/grand-selkie-225f87/deploys/6a1a4b677058180008744bf5

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
给你/PATCH-service-pty.md (1)

1-60: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Looks like a scratch/working file committed by accident.

This 给你/ directory contains manual patch notes (and sibling pty.rs.new, shell_init.rs.new, MIGRATION-GUIDE.md drafts) that aren't part of the build. These appear to be temporary working artifacts and should be removed from the PR (or gitignored) rather than merged into the repo.

🤖 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 `@给你/PATCH-service-pty.md` around lines 1 - 60, This is a stray working patch
file and related drafts that shouldn't be merged; remove the committed scratch
artifacts (the file 给你/PATCH-service-pty.md and any sibling drafts like
pty.rs.new, shell_init.rs.new, MIGRATION-GUIDE.md) from the PR (delete them from
the branch or add to .gitignore and remove from the index) so only real source
changes remain—no code edits needed to functions like session::create_session,
infra::shell_init::detect_shell_type, prepare_shell_injection, or
restore_session in this patch file.
🧹 Nitpick comments (3)
src-tauri/crates/infra/src/shell_detect.rs (1)

16-18: 💤 Low value

Shell label exposes raw command string including flags/full path.

shell_label returns the command verbatim, so on Windows the user-facing label becomes e.g. C:\Program Files\PowerShell\7\pwsh.exe -NoLogo -NoProfile rather than a friendly name like PowerShell. Consider deriving a display name from the executable file stem (stripping the directory and CLI flags) for the label, while keeping command intact for execution.

🤖 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_detect.rs` around lines 16 - 18, shell_label
currently returns the full raw command (including path and flags); change it to
derive a user-friendly label by extracting only the executable name (strip
directory and CLI flags) and using its file stem as the display name while
leaving the original command unchanged for execution; implement this in
shell_label by splitting the command on whitespace to get the first token (the
executable path), convert that token to a Path, take file_stem (fall back to
file_name or the original token if missing), convert to a String
(to_string_lossy), and return that friendly name (e.g., "pwsh" or "PowerShell")
instead of the verbatim command.
src/features/watcher/fileWatcher.test.ts (1)

46-100: ⚡ Quick win

Tests only exercise the missing-project fallback path.

Because getQueryData is stubbed to always return undefined, both tests hit the !project branch (the 5 namespace-wide invalidations). The new per-profile invalidation branch (Lines 56-65 in fileWatcher.ts) — arguably the core of this change — is never covered. Consider adding a case where getQueryData returns a project with profiles and asserting the profile-scoped query keys.

🤖 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/features/watcher/fileWatcher.test.ts` around lines 46 - 100, The tests
currently only hit the missing-project fallback because getQueryData is stubbed
to return undefined; update the test(s) (e.g., in fileWatcher.test.ts around the
loadWatcher usage) to add a case where getQueryData returns a project object
with profiles (e.g., { id: "test-project", profiles: [{ id: "p1" }, { id: "p2"
}] }) so the per-profile invalidation branch in fileWatcher.ts (the block
guarded by getQueryData and iterating profiles) is exercised, then assert
invalidateQueriesMock is called with the profile-scoped query keys (replace the
existing namespace-only expectations with assertions that include the
profile-specific queryKey entries produced by the code under test); ensure you
stub getQueryData (or the module it comes from) only for that test and restore
behavior for others.
src/layout/WindowControls.tsx (1)

75-92: ⚡ Quick win

Guard the onResized listener against the unmount race; avoid shadowing window.

onResized(...) resolves asynchronously and assigns unlisten only after the promise settles. If the component unmounts before that, the cleanup runs while unlisten is still undefined, leaving the resize listener registered (leak). Also, the local const window shadows the global window, which is a readability hazard.

This component is mounted at the app root and rarely unmounts, so impact is limited today, but the pattern is worth hardening.

♻️ Proposed fix
 	useEffect(() => {
-		const window = getCurrentWindow();
-		let unlisten: (() => void) | undefined;
-
-		window.isMaximized().then(setIsMaximized);
-
-		window
-			.onResized(() => {
-				window.isMaximized().then(setIsMaximized);
-			})
-			.then((fn) => {
-				unlisten = fn;
-			});
-
-		return () => {
-			unlisten?.();
-		};
+		const appWindow = getCurrentWindow();
+		let unlisten: (() => void) | undefined;
+		let cancelled = false;
+
+		appWindow.isMaximized().then(setIsMaximized).catch(() => {});
+
+		appWindow
+			.onResized(() => {
+				appWindow.isMaximized().then(setIsMaximized).catch(() => {});
+			})
+			.then((fn) => {
+				if (cancelled) fn();
+				else unlisten = fn;
+			});
+
+		return () => {
+			cancelled = true;
+			unlisten?.();
+		};
 	}, []);
🤖 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/layout/WindowControls.tsx` around lines 75 - 92, The effect currently
shadows the global window and can leak the onResized listener if the promise
resolves after unmount; rename the local getCurrentWindow() result (e.g., const
currentWindow = getCurrentWindow()) to avoid shadowing, add a cancelled flag
(let cancelled = false) and a local cleanup holder (let unlisten: (() => void) |
undefined), call currentWindow.isMaximized().then(...) as before, then call
currentWindow.onResized(...).then(fn => { if (cancelled) fn(); else unlisten =
fn; }); and in the returned cleanup set cancelled = true and call unlisten?.();
also keep using setIsMaximized for state updates — this ensures the listener is
removed if the promise resolves after unmount and avoids shadowing the global
window.
🤖 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/shared/lib/platform.ts`:
- Around line 7-9: The Windows detection is too broad and relies on deprecated
navigator.platform; update isWindowsPlatform to avoid false matches by either
(preferred) using `@tauri-apps/plugin-os` OS detection API (call the plugin's
platform/os function and return true when it reports "Windows") or (minimal
change) stop using navigator.platform and tighten the regex to explicitly match
common Windows tokens (e.g., /\bWindows\b|\bWin32\b|\bWin64\b|\bWOW64\b/i
against navigator.userAgent) in the isWindowsPlatform function, and make the
same change to the duplicate detection in src/features/terminal/templates.ts
(refer to isWindowsPlatform and the template detection logic to locate
replacements).

In `@给你/shell_init.rs.new`:
- Around line 1-15: This scratch file 给你/shell_init.rs.new (and its sibling
files 给你/pty.rs.new, 给你/PATCH-service-pty.md, 给你/MIGRATION-GUIDE.md) is a
duplicate working copy that references scripts via
include_str!("../scripts/...") outside the canonical
src-tauri/crates/infra/scripts/ tree; remove the entire 给你/ directory from the
PR (delete the files) or add that directory to .gitignore so only the canonical
modules (e.g., src-tauri/crates/infra/src/shell_init.rs and its scripts) remain,
ensuring no duplicated logic or non-compiling include_str! paths are committed.

---

Outside diff comments:
In `@给你/PATCH-service-pty.md`:
- Around line 1-60: This is a stray working patch file and related drafts that
shouldn't be merged; remove the committed scratch artifacts (the file
给你/PATCH-service-pty.md and any sibling drafts like pty.rs.new,
shell_init.rs.new, MIGRATION-GUIDE.md) from the PR (delete them from the branch
or add to .gitignore and remove from the index) so only real source changes
remain—no code edits needed to functions like session::create_session,
infra::shell_init::detect_shell_type, prepare_shell_injection, or
restore_session in this patch file.

---

Nitpick comments:
In `@src-tauri/crates/infra/src/shell_detect.rs`:
- Around line 16-18: shell_label currently returns the full raw command
(including path and flags); change it to derive a user-friendly label by
extracting only the executable name (strip directory and CLI flags) and using
its file stem as the display name while leaving the original command unchanged
for execution; implement this in shell_label by splitting the command on
whitespace to get the first token (the executable path), convert that token to a
Path, take file_stem (fall back to file_name or the original token if missing),
convert to a String (to_string_lossy), and return that friendly name (e.g.,
"pwsh" or "PowerShell") instead of the verbatim command.

In `@src/features/watcher/fileWatcher.test.ts`:
- Around line 46-100: The tests currently only hit the missing-project fallback
because getQueryData is stubbed to return undefined; update the test(s) (e.g.,
in fileWatcher.test.ts around the loadWatcher usage) to add a case where
getQueryData returns a project object with profiles (e.g., { id: "test-project",
profiles: [{ id: "p1" }, { id: "p2" }] }) so the per-profile invalidation branch
in fileWatcher.ts (the block guarded by getQueryData and iterating profiles) is
exercised, then assert invalidateQueriesMock is called with the profile-scoped
query keys (replace the existing namespace-only expectations with assertions
that include the profile-specific queryKey entries produced by the code under
test); ensure you stub getQueryData (or the module it comes from) only for that
test and restore behavior for others.

In `@src/layout/WindowControls.tsx`:
- Around line 75-92: The effect currently shadows the global window and can leak
the onResized listener if the promise resolves after unmount; rename the local
getCurrentWindow() result (e.g., const currentWindow = getCurrentWindow()) to
avoid shadowing, add a cancelled flag (let cancelled = false) and a local
cleanup holder (let unlisten: (() => void) | undefined), call
currentWindow.isMaximized().then(...) as before, then call
currentWindow.onResized(...).then(fn => { if (cancelled) fn(); else unlisten =
fn; }); and in the returned cleanup set cancelled = true and call unlisten?.();
also keep using setIsMaximized for state updates — this ensures the listener is
removed if the promise resolves after unmount and avoids shadowing the global
window.
🪄 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: 42a5bac0-b97b-4843-a483-83a0ad8f09fb

📥 Commits

Reviewing files that changed from the base of the PR and between 15dfbff and 1b6d57b.

⛔ Files ignored due to path filters (1)
  • src/generated/types.ts is excluded by !**/generated/**
📒 Files selected for processing (74)
  • .claude/commands/opsx/apply.md
  • .claude/commands/opsx/archive.md
  • .claude/commands/opsx/bulk-archive.md
  • .claude/commands/opsx/continue.md
  • .claude/commands/opsx/explore.md
  • .claude/commands/opsx/ff.md
  • .claude/commands/opsx/new.md
  • .claude/commands/opsx/onboard.md
  • .claude/commands/opsx/sync.md
  • .claude/commands/opsx/verify.md
  • .claude/settings.json
  • .claude/skills/dead-code-cleanup/SKILL.md
  • .claude/skills/openspec-apply-change/SKILL.md
  • .claude/skills/openspec-archive-change/SKILL.md
  • .claude/skills/openspec-bulk-archive-change/SKILL.md
  • .claude/skills/openspec-continue-change/SKILL.md
  • .claude/skills/openspec-explore/SKILL.md
  • .claude/skills/openspec-ff-change/SKILL.md
  • .claude/skills/openspec-new-change/SKILL.md
  • .claude/skills/openspec-onboard/SKILL.md
  • .claude/skills/openspec-sync-specs/SKILL.md
  • .claude/skills/openspec-verify-change/SKILL.md
  • .claude/skills/release-changelog.skill
  • .claude/skills/release-changelog/SKILL.md
  • .gitignore
  • CLAUDE.md
  • messages/en.json
  • messages/zh.json
  • src-tauri/crates/infra/scripts/default_init_fish.fish
  • src-tauri/crates/infra/scripts/default_init_pwsh.ps1
  • src-tauri/crates/infra/scripts/shellIntegration-bash.sh
  • src-tauri/crates/infra/scripts/shellIntegration-rc.zsh
  • src-tauri/crates/infra/scripts/shellIntegration.fish
  • src-tauri/crates/infra/scripts/shellIntegration.ps1
  • src-tauri/crates/infra/src/config.rs
  • src-tauri/crates/infra/src/filesystem.rs
  • src-tauri/crates/infra/src/git.rs
  • src-tauri/crates/infra/src/lib.rs
  • src-tauri/crates/infra/src/no_window.rs
  • src-tauri/crates/infra/src/pty.rs
  • src-tauri/crates/infra/src/shell_detect.rs
  • src-tauri/crates/infra/src/shell_init.rs
  • src-tauri/crates/repo/src/project_group.rs
  • src-tauri/migrations/2026-05-21-000000_add_fk_indexes/down.sql
  • src-tauri/migrations/2026-05-21-000000_add_fk_indexes/up.sql
  • src-tauri/src/handler/shell.rs
  • src-tauri/src/handler/topbar.rs
  • src-tauri/src/handler/updater.rs
  • src-tauri/src/helper.rs
  • src-tauri/src/lib.rs
  • src-tauri/tests/common/mod.rs
  • src-tauri/tests/integration_git.rs
  • src/App.tsx
  • src/features/git/ProjectTopBar.tsx
  • src/features/home/HomePage.tsx
  • src/features/projects/FileTreePanel.tsx
  • src/features/projects/ProfileLayout.tsx
  • src/features/projects/components/ProjectTemplatesEditor.tsx
  • src/features/projects/hooks.ts
  • src/features/settings/GlobalTerminalTemplatesSettings.tsx
  • src/features/settings/SettingsPage.tsx
  • src/features/settings/ShellPicker.tsx
  • src/features/terminal/Terminal.tsx
  • src/features/terminal/keybindings.ts
  • src/features/watcher/fileWatcher.test.ts
  • src/features/watcher/fileWatcher.ts
  • src/layout/AppSidebar.tsx
  • src/layout/WindowControls.tsx
  • src/shared/lib/platform.ts
  • src/shared/lib/queryKeys.test.ts
  • 给你/MIGRATION-GUIDE.md
  • 给你/PATCH-service-pty.md
  • 给你/pty.rs.new
  • 给你/shell_init.rs.new
💤 Files with no reviewable changes (23)
  • .claude/skills/openspec-ff-change/SKILL.md
  • .claude/skills/openspec-verify-change/SKILL.md
  • .claude/commands/opsx/apply.md
  • .claude/commands/opsx/ff.md
  • .claude/skills/openspec-continue-change/SKILL.md
  • .claude/commands/opsx/continue.md
  • .claude/commands/opsx/onboard.md
  • .claude/skills/openspec-apply-change/SKILL.md
  • .claude/skills/dead-code-cleanup/SKILL.md
  • .claude/skills/openspec-onboard/SKILL.md
  • .claude/settings.json
  • .claude/skills/openspec-archive-change/SKILL.md
  • .claude/commands/opsx/explore.md
  • .claude/skills/openspec-bulk-archive-change/SKILL.md
  • .claude/skills/release-changelog/SKILL.md
  • .claude/commands/opsx/archive.md
  • .claude/commands/opsx/verify.md
  • .claude/commands/opsx/sync.md
  • .claude/skills/openspec-new-change/SKILL.md
  • .claude/commands/opsx/bulk-archive.md
  • .claude/skills/openspec-sync-specs/SKILL.md
  • .claude/commands/opsx/new.md
  • .claude/skills/openspec-explore/SKILL.md

Comment on lines +7 to +9
export function isWindowsPlatform() {
return /win/i.test(platformString);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does the macOS WebKit / Tauri wry navigator.userAgent or navigator.platform contain the string "Darwin"? And does @tauri-apps/plugin-os provide a synchronous platform() detection API in Tauri v2?

💡 Result:

In macOS WebKit, including when used within a Tauri wry-based application, the navigator.platform property typically returns "MacIntel" rather than "Darwin". While the underlying kernel is Darwin, navigator.platform is standardized to reflect the hardware/OS platform (e.g., "MacIntel" for macOS on Intel/Apple Silicon) rather than the kernel name. Similarly, the default navigator.userAgent string for WebKit on macOS includes "Macintosh; Intel Mac OS X" but does not typically include the literal string "Darwin". "Darwin" is more commonly associated with the system's CFNetwork or low-level Unix-like identifiers rather than the browser navigator API [1]. Regarding Tauri v2, the @tauri-apps/plugin-os provides a synchronous platform detection API [2][3]. You can import and use the platform function from @tauri-apps/plugin-os to retrieve the current operating system, which returns one of the platform strings (e.g., "macos", "linux", "windows") [2][3]. Because the plugin's JavaScript API relies on underlying Rust commands, it is technically an asynchronous operation; however, it is exposed as a simple function call in the guest bindings [2][3]. If you require a strictly synchronous operation in the backend, the equivalent Rust function tauri_plugin_os::platform() is available and executes synchronously [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "isWindowsPlatform\(" -S src || true
rg -n "WindowControls" -S src || true

# Inspect the file under review
ls -la src/shared/lib/platform.ts 2>/dev/null || true
if [ -f src/shared/lib/platform.ts ]; then
  wc -l src/shared/lib/platform.ts
  sed -n '1,120p' src/shared/lib/platform.ts | nl -ba
fi

# Inspect the referenced usage location
# (App.tsx Line 79 mentioned in the comment; locate exact lines containing isWindowsPlatform)
for f in $(rg -l "isWindowsPlatform\(\)" -S src/App.tsx src || true); do
  echo "---- $f ----"
  wc -l "$f" || true
  sed -n '1,220p' "$f" | nl -ba | sed -n '1,220p'
done

Repository: AkaraChen/2code

Length of output: 887


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== platform.ts =="
sed -n '1,120p' src/shared/lib/platform.ts | cat -n

echo "== App.tsx around usage =="
sed -n '60,110p' src/App.tsx | cat -n

echo "== AppSidebar.tsx around usage =="
sed -n '80,150p' src/layout/AppSidebar.tsx | cat -n

echo "== templates.ts isWindowsPlatform() =="
sed -n '1,120p' src/features/terminal/templates.ts | cat -n

Repository: AkaraChen/2code

Length of output: 6732


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== usages of isWindowsPlatform from shared/lib/platform.ts =="
rg -n "from \"\.{1,2}/shared/lib/platform\"|from \"`@/shared/lib/platform`\"|isWindowsPlatform\(" src || true

echo "== direct uses of navigator.platform / navigator.userAgent =="
rg -n "navigator\.platform|navigator\.userAgent" src || true

echo "== existing `@tauri-apps/plugin-os` usage =="
rg -n "`@tauri-apps/plugin-os`" -S src || true

Repository: AkaraChen/2code

Length of output: 1261


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '250,330p' src/features/topbar/controls.tsx | cat -n

Repository: AkaraChen/2code

Length of output: 1983


Narrow the Windows detection regex (and avoid navigator.platform)

src/shared/lib/platform.ts uses /win/i against ${navigator.platform} ${navigator.userAgent} to decide whether to render <WindowControls /> in src/App.tsx. While macOS WebKit/Tauri typically reports navigator.platform like MacIntel and the UA like Macintosh; ... (so the specific “Darwin contains win” scenario is unlikely), /win/i is still fragile and can match unintended tokens; navigator.platform is also deprecated.

Prefer using @tauri-apps/plugin-os for OS detection, or tighten the regex at minimum:

♻️ Safer regex (minimal change)
 export function isWindowsPlatform() {
-	return /win/i.test(platformString);
+	return /win(dows|32|64)/i.test(platformString);
 }

(There’s the same /win/i approach in src/features/terminal/templates.ts as well.)

📝 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.

Suggested change
export function isWindowsPlatform() {
return /win/i.test(platformString);
}
export function isWindowsPlatform() {
return /win(dows|32|64)/i.test(platformString);
}
🤖 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/shared/lib/platform.ts` around lines 7 - 9, The Windows detection is too
broad and relies on deprecated navigator.platform; update isWindowsPlatform to
avoid false matches by either (preferred) using `@tauri-apps/plugin-os` OS
detection API (call the plugin's platform/os function and return true when it
reports "Windows") or (minimal change) stop using navigator.platform and tighten
the regex to explicitly match common Windows tokens (e.g.,
/\bWindows\b|\bWin32\b|\bWin64\b|\bWOW64\b/i against navigator.userAgent) in the
isWindowsPlatform function, and make the same change to the duplicate detection
in src/features/terminal/templates.ts (refer to isWindowsPlatform and the
template detection logic to locate replacements).

Comment thread 给你/shell_init.rs.new
Comment on lines +1 to +15
use std::path::{Path, PathBuf};

use model::error::AppError;

// 2code's own init script (zsh-specific stuff like keybindings, claude wrapper)
const DEFAULT_INIT: &str = include_str!("../scripts/default_init.sh");

// VS Code shell integration scripts (MIT licensed, from microsoft/vscode)
const VSC_BASH: &str = include_str!("../scripts/shellIntegration-bash.sh");
const VSC_ZSH_RC: &str = include_str!("../scripts/shellIntegration-rc.zsh");
const VSC_ZSH_ENV: &str = include_str!("../scripts/shellIntegration-env.zsh");
const VSC_ZSH_PROFILE: &str = include_str!("../scripts/shellIntegration-profile.zsh");
const VSC_ZSH_LOGIN: &str = include_str!("../scripts/shellIntegration-login.zsh");
const VSC_FISH: &str = include_str!("../scripts/shellIntegration.fish");
const VSC_PWSH: &str = include_str!("../scripts/shellIntegration.ps1");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove this scratch file (and the 给你/ directory) from the PR.

This file is a working copy of the canonical src-tauri/crates/infra/src/shell_init.rs (also changed in this PR). Its include_str!("../scripts/...") paths resolve to <repo>/scripts/, not src-tauri/crates/infra/scripts/, so it isn't part of the cargo build and could not compile in place. Committing 给你/shell_init.rs.new, 给你/pty.rs.new, 给你/PATCH-service-pty.md, and 给你/MIGRATION-GUIDE.md duplicates logic and adds confusion. Please delete the directory (or add it to .gitignore) so only the canonical modules remain.

🤖 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 `@给你/shell_init.rs.new` around lines 1 - 15, This scratch file
给你/shell_init.rs.new (and its sibling files 给你/pty.rs.new,
给你/PATCH-service-pty.md, 给你/MIGRATION-GUIDE.md) is a duplicate working copy that
references scripts via include_str!("../scripts/...") outside the canonical
src-tauri/crates/infra/scripts/ tree; remove the entire 给你/ directory from the
PR (delete the files) or add that directory to .gitignore so only the canonical
modules (e.g., src-tauri/crates/infra/src/shell_init.rs and its scripts) remain,
ensuring no duplicated logic or non-compiling include_str! paths are committed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant