feat(windows): custom title bar (Variant B — macOS traffic lights, top-left)#292
feat(windows): custom title bar (Variant B — macOS traffic lights, top-left)#292nothingness0db wants to merge 23 commits into
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.
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.
📝 WalkthroughWalkthroughThis 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 ChangesShell Integration and Cross-Platform Shell Discovery
Command Execution Refactoring
Handler Layer Consolidation
Frontend Platform Support and UI Improvements
Cleanup and Documentation
🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
|
f0cd42d to
90808f0
Compare
90808f0 to
1b6d57b
Compare
❌ Deploy Preview for grand-selkie-225f87 failed. Why did it fail? →
|
There was a problem hiding this comment.
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 winLooks like a scratch/working file committed by accident.
This
给你/directory contains manual patch notes (and siblingpty.rs.new,shell_init.rs.new,MIGRATION-GUIDE.mddrafts) 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 valueShell label exposes raw command string including flags/full path.
shell_labelreturns the command verbatim, so on Windows the user-facing label becomes e.g.C:\Program Files\PowerShell\7\pwsh.exe -NoLogo -NoProfilerather than a friendly name likePowerShell. Consider deriving a display name from the executable file stem (stripping the directory and CLI flags) for thelabel, while keepingcommandintact 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 winTests only exercise the missing-project fallback path.
Because
getQueryDatais stubbed to always returnundefined, both tests hit the!projectbranch (the 5 namespace-wide invalidations). The new per-profile invalidation branch (Lines 56-65 infileWatcher.ts) — arguably the core of this change — is never covered. Consider adding a case wheregetQueryDatareturns 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 winGuard the
onResizedlistener against the unmount race; avoid shadowingwindow.
onResized(...)resolves asynchronously and assignsunlistenonly after the promise settles. If the component unmounts before that, the cleanup runs whileunlistenis stillundefined, leaving the resize listener registered (leak). Also, the localconst windowshadows the globalwindow, 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
⛔ Files ignored due to path filters (1)
src/generated/types.tsis 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.gitignoreCLAUDE.mdmessages/en.jsonmessages/zh.jsonsrc-tauri/crates/infra/scripts/default_init_fish.fishsrc-tauri/crates/infra/scripts/default_init_pwsh.ps1src-tauri/crates/infra/scripts/shellIntegration-bash.shsrc-tauri/crates/infra/scripts/shellIntegration-rc.zshsrc-tauri/crates/infra/scripts/shellIntegration.fishsrc-tauri/crates/infra/scripts/shellIntegration.ps1src-tauri/crates/infra/src/config.rssrc-tauri/crates/infra/src/filesystem.rssrc-tauri/crates/infra/src/git.rssrc-tauri/crates/infra/src/lib.rssrc-tauri/crates/infra/src/no_window.rssrc-tauri/crates/infra/src/pty.rssrc-tauri/crates/infra/src/shell_detect.rssrc-tauri/crates/infra/src/shell_init.rssrc-tauri/crates/repo/src/project_group.rssrc-tauri/migrations/2026-05-21-000000_add_fk_indexes/down.sqlsrc-tauri/migrations/2026-05-21-000000_add_fk_indexes/up.sqlsrc-tauri/src/handler/shell.rssrc-tauri/src/handler/topbar.rssrc-tauri/src/handler/updater.rssrc-tauri/src/helper.rssrc-tauri/src/lib.rssrc-tauri/tests/common/mod.rssrc-tauri/tests/integration_git.rssrc/App.tsxsrc/features/git/ProjectTopBar.tsxsrc/features/home/HomePage.tsxsrc/features/projects/FileTreePanel.tsxsrc/features/projects/ProfileLayout.tsxsrc/features/projects/components/ProjectTemplatesEditor.tsxsrc/features/projects/hooks.tssrc/features/settings/GlobalTerminalTemplatesSettings.tsxsrc/features/settings/SettingsPage.tsxsrc/features/settings/ShellPicker.tsxsrc/features/terminal/Terminal.tsxsrc/features/terminal/keybindings.tssrc/features/watcher/fileWatcher.test.tssrc/features/watcher/fileWatcher.tssrc/layout/AppSidebar.tsxsrc/layout/WindowControls.tsxsrc/shared/lib/platform.tssrc/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
| export function isWindowsPlatform() { | ||
| return /win/i.test(platformString); | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: https://stackoverflow.com/questions/26994491/set-useragent-in-wkwebview
- 2: https://v2.tauri.app/reference/javascript/os/
- 3: https://v2.tauri.app/plugin/os-info/
- 4: https://docs.rs/tauri-plugin-os/latest/tauri_plugin_os/fn.platform.html
🏁 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'
doneRepository: 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 -nRepository: 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 || trueRepository: AkaraChen/2code
Length of output: 1261
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '250,330p' src/features/topbar/controls.tsx | cat -nRepository: 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.
| 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).
| 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"); |
There was a problem hiding this comment.
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.
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 viatrafficLightPosition. 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,ProjectTopBarreverts to a single-row layout.Screenshot
Test plan
⤡when maximizedHomePage,SettingsPage,ProjectDetailPagestay draggableSummary by CodeRabbit
Release Notes
New Features
Improvements
Chores