Skip to content

Release v0.64.0#2597

Closed
Sean529 wants to merge 6 commits intomanaflow-ai:mainfrom
Sean529:release/v0.64.0
Closed

Release v0.64.0#2597
Sean529 wants to merge 6 commits intomanaflow-ai:mainfrom
Sean529:release/v0.64.0

Conversation

@Sean529
Copy link
Copy Markdown

@Sean529 Sean529 commented Apr 4, 2026

Release v0.64.0

Added

Changed

  • Pane focus shortcuts changed to Cmd+Shift+H/J/K/L (was Cmd+Option+Arrow)
  • Open Browser shortcut moved to Cmd+Ctrl+Option+L to avoid conflict

Fixed

  • Suppress fallback text during IME composition
  • Fix split divider drags escaping to adjacent panes
  • Fix browser portal sync flickering during split drag
  • Fix external insertText escape handling
  • Fix paste from Raycast and other apps using alternate plain-text UTIs
  • Fix terminal clipboard rich text and image fallbacks
  • Fix macOS Tahoe glass window compatibility (macOS 26 (Tahoe): Transparent/glass background effect not working #2459)
  • Fix CLI stealing app focus when running commands
  • Preserve symlink aliases for external file opens
  • Fix terminal Cmd scroll bug

Removed

  • Remove copy-on-select setting

🤖 Generated with Claude Code


Summary by cubic

Adds a local daemon for tmux-like detach/reattach so terminal sessions persist after quit and across reboots, with a sidebar to view and reattach. Updates shortcuts (pane focus Cmd+Shift+H/J/K/L; Open Browser Cmd+Ctrl+Option+L) and removes the copy-on-select setting.

  • New Features

  • Bug Fixes

    • Suppress fallback text during IME composition.
    • Fix split divider drags and browser portal flicker while dragging.
    • Improve paste compatibility (Raycast and other plain-text UTIs) and terminal clipboard fallbacks.
    • Fix macOS Tahoe glass window compatibility and prevent CLI commands from stealing app focus.
    • Preserve symlink aliases on external file open and fix Cmd-scroll behavior in terminal.

Written for commit 0e61be2. Summary will update on new commits.

Summary by CodeRabbit

Release Notes v0.64.0

  • New Features

    • Local daemon support for terminal session detach/reattach functionality
    • Sidebar UI for managing detached sessions
    • Launchd auto-start capability
    • On-disk session persistence
    • Keyboard shortcuts cheatsheet page
    • Editable workspace descriptions
  • Changed

    • Pane focus shortcuts remapped to Cmd+Shift+HJKL
    • "Open Browser" shortcut moved to avoid conflicts
  • Bug Fixes

    • Input, clipboard, drag/split, and browser portal syncing issues resolved
    • Terminal clipboard and scroll behavior improvements
    • macOS Tahoe compatibility fixes
    • CLI focus and symlink alias handling fixes
  • Removed

    • "copy-on-select" setting

Sean529 and others added 6 commits April 4, 2026 00:23
…roll bug

- Fix: Prevent unintended Ghostty scroll logic when pressing Cmd during text selection drag by filtering mouse updates.
- Feat: Add 'Warn Before Closing Tab' toggle in app settings.
- Docs: Add new comprehensive cmux-shortcuts.html HTML cheat sheet.
- Chore: Disable shortcut hints behavior in ContentView.
…focus shortcuts

Implement a complete client-server architecture for persistent terminal sessions:
- Go daemon (daemon/local/) manages PTY lifecycle, ring buffer replay, and JSON-RPC
- Swift GUI integration with DaemonSessionBinding (FIFO bridge for I/O) and LocalDaemonManager
- Sidebar UI for detached sessions with reattach support
- Session disk persistence with atomic saves and restore on daemon restart
- launchd integration for daemon auto-start
- Secure socket path (~/.local/state/cmux/) with legacy symlink compat
- Thread-safe DaemonSessionBinding with proper RPC ID correlation
- Async main-thread-safe socket I/O throughout Swift code
- 35 Swift unit tests, 34 Go tests, and E2E test script
- Change default pane focus shortcuts to Cmd+Shift+HJKL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…icts

- Default pane focus: Cmd+Shift+H/J/K/L (was Cmd+Option+Arrow)
- Move triggerFlash to Cmd+Ctrl+Option+H (was Cmd+Shift+H, conflicted)
- Move openBrowser to Cmd+Ctrl+Option+L (was Cmd+Shift+L, conflicted)
- Update shortcuts cheatsheet HTML to match

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 4, 2026

@Sean529 is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a local tmux-like daemon system for terminal session persistence, implements a comprehensive branding migration from "cmux" to "jmux" across the entire codebase, adds detached-session UI to the sidebar, remaps keyboard shortcuts, bumps the version to 0.64.0, and includes extensive daemon tests plus documentation.

Changes

Cohort / File(s) Summary
Branding: Bundle Identifiers & Keychain
CLI/cmux.swift, GhosttyTabs.xcodeproj/project.pbxproj, GhosttyTabs.xcodeproj/xcshareddata/xcschemes/..., Sources/AppDelegate.swift, Sources/AppIconDockTilePlugin.swift, Sources/CmuxDirectoryTrust.swift, Sources/GhosttyConfig.swift, Sources/GhosttyTerminalView.swift, Sources/PostHogAnalytics.swift, Sources/SocketControlSettings.swift, Sources/TabManager.swift, Sources/TerminalNotificationStore.swift, scripts/reload.sh, scripts/reloadp.sh
Renamed bundle identifiers from com.cmuxterm.app to com.jmux.app, updated socket paths (/tmp/cmux*.sock/tmp/jmux*.sock), notification names, keychain services, application directory names, and all related namespace references across Swift and shell scripts.
Branding: Resources & Localization
Resources/Info.plist, Resources/InfoPlist.xcstrings, Resources/Localizable.xcstrings, CHANGELOG.md
Updated Info.plist UTI identifiers, exception domains, microphone/camera descriptions, disabled Sparkle auto-update checks. Updated localized strings from "cmux" to "jmux" across all supported languages (en, ja, uk, zh, ko). Added new localized strings for detached sessions feature.
Daemon Lifecycle Management
Sources/LocalDaemonManager.swift, Sources/AppDelegate.swift
New LocalDaemonManager singleton that ensures daemon is running via socket probe/launchd bootstrap/direct spawn, manages lifecycle with ensureRunning()/stop(), publishes detached session list with 5-second refresh polling, and supports launch agent installation/uninstallation. AppDelegate now calls manager on startup and periodically refreshes detached sessions.
Daemon Session Binding
Sources/DaemonSessionBinding.swift, Sources/Workspace.swift, Sources/TerminalPanel.swift, Sources/SessionPersistence.swift
New DaemonSessionBinding class manages Unix socket connection and JSON-RPC communication with daemon, with attach()/detach()/sendInput()/resize()/close() lifecycle. New DaemonPTYBridge creates named pipes (FIFOs) to wire daemon PTY I/O to ghostty surface. Workspace now supports reattaching via reattachDaemonSessionIfNeeded() and detaching via detachFromDaemon(). SessionPersistence adds daemonSessionID field for restore.
Detached Sessions UI
Sources/ContentView.swift, Resources/Localizable.xcstrings
New SwiftUI components (DetachedSessionItem, SidebarDetachedSessionsSection, SidebarDetachedSessionRow) render detached sessions in sidebar with name/shell/pane count/created time. Reattach button creates new workspace and triggers daemon reattach. Added corresponding localized strings for header, pane count, tooltip, and accessibility labels.
Keyboard Shortcut Remapping
Sources/KeyboardShortcutSettings.swift, Sources/AppDelegate.swift
Remapped pane focus shortcuts: command+option+arrowcommand+shift+hjkl (left/right/up/down). Moved "triggerFlash" to command+option+control+h, moved "openBrowser" to command+option+control+l to avoid conflicts. Updated comment documenting default focus navigation.
Shortcut Hints Policy
Sources/ContentView.swift
Changed ShortcutHintModifierPolicy.shouldShowHints() to always return false, disabling the shortcut-hint visibility logic in the sidebar.
Version & Build Configuration
GhosttyTabs.xcodeproj/project.pbxproj
Bumped CURRENT_PROJECT_VERSION from 78 to 79 and MARKETING_VERSION from 0.63.1 to 0.64.0. Added new build file entries for DaemonSessionBinding.swift, LocalDaemonManager.swift, and DaemonSessionTests.swift.
Terminal Socket Control
Sources/TerminalController.swift
Added new command dispatcher routes for V2 local daemon socket methods: session.local.status, session.local.list, session.local.new, session.local.attach, session.local.detach, session.local.close. Each handler parses params, calls LocalDaemonManager RPC helpers off-main, and returns standardized V2 response payloads with error handling.
Browser & Support Paths
Sources/Panels/BrowserPanel.swift
Updated BrowserProfileStore and BrowserHistoryStore default bundle ID fallback from "cmux" to "jmux". Updated loopback proxy alias host from "cmux-loopback.localtest.me" to "jmux-loopback.localtest.me".
Mouse Input Handling
Sources/GhosttyTerminalView.swift
Modified GhosttyNSView.flagsChanged() to skip updating cached mouse position and hover state when left mouse button is pressed, reducing unnecessary refreshes during mouse operations.
Local Daemon Server Implementation
daemon/local/session.go, daemon/local/rpc.go, daemon/local/persistence.go
Comprehensive Go daemon implementation: session.go manages Pane/Window/Session types with PTY spawning, ring-buffer output, event streams, and lifecycle; rpc.go provides newline-delimited JSON-RPC framing and request/response helpers; persistence.go implements snapshot serialization, atomic file operations, and session restoration with stale PID detection.
Ring Buffer Data Structure
daemon/local/ringbuffer.go
New fixed-capacity circular byte buffer with concurrent-safe writes, wraparound handling, and chronological byte sequence reconstruction via Bytes() method.
Local Daemon Executables
daemon/local/cmd/cmuxd-local/main.go, daemon/local/cmd/cmux-local/main.go
Server (cmuxd-local) listens on Unix socket, manages client connections, routes RPC requests to session/window/pane handlers, and notifies state persister. CLI (cmux-local) connects to daemon, implements commands (new/attach/ls/kill) with raw terminal mode for attach, detach hotkey support (Ctrl-b d), and window resize synchronization.
Daemon Launch Configuration
daemon/local/com.cmux.daemon-local.plist, daemon/local/go.mod
LaunchAgent plist template with placeholders for binary path/home directory, configured for auto-restart with 5-second throttle and file descriptor limit. Go module definition with dependencies on creack/pty, golang.org/x/term, and golang.org/x/sys.
Daemon Tests
cmuxTests/DaemonSessionTests.swift, daemon/local/integration_test.go, daemon/local/ringbuffer_test.go
Swift unit tests for path computation, error formatting, RPC ID allocation, session ID sanitization, FIFO creation/teardown, and snapshot persistence. Go integration tests covering session lifecycle, pane ring-buffer behavior, client attachment, window/pane management, persistence/restoration, and dimension updates. Ring buffer concurrent-access stress tests.
Daemon Documentation & Testing
daemon/local/README.md, daemon/local/test_e2e.sh
README documents build commands, socket location, CLI workflows, JSON-RPC protocol, and key architecture modules. End-to-end bash test script validates session create/list/close, persistence across restart, concurrent sessions, resize, CLI compatibility, and error handling.
Documentation & Setup
docs/cmux-shortcuts.html, docs/20260404_Git开发与同步工作流.md, docs/llms.txt, scripts/setup.sh
Added keyboard shortcuts cheatsheet HTML page with dark theme, search/filter, category navigation, and inline scrolling. Added Git workflow documentation in Chinese. Updated setup.sh to pass Zig version string to GhosttyKit build.

Sequence Diagram

sequenceDiagram
    participant App as App (Swift)
    participant LDM as LocalDaemonManager
    participant Daemon as Daemon (cmuxd-local)
    participant Socket as Unix Socket
    participant PTY as PTY

    rect rgba(100, 150, 200, 0.5)
    Note over App,Daemon: Daemon Startup
    App->>LDM: ensureRunning()
    LDM->>Socket: probeSync() ping
    alt Daemon not running
        LDM->>Daemon: Launch via launchd/spawn
        Daemon->>Daemon: Initialize, restore sessions
    end
    LDM->>Socket: Verify running
    end

    rect rgba(100, 200, 150, 0.5)
    Note over App,PTY: Detached Session Reattach Flow
    App->>LDM: Refresh detachedSessions list
    LDM->>Socket: RPC session.list
    Daemon->>Daemon: Collect running/detached
    Daemon-->>LDM: Return sessions
    LDM-->>App: Update `@Published` detachedSessions
    
    App->>App: User clicks reattach button
    App->>App: Create new workspace
    App->>App: Set daemonSessionID
    App->>App: Call reattachDaemonSessionIfNeeded()
    end

    rect rgba(200, 150, 100, 0.5)
    Note over App,PTY: Attachment & PTY I/O
    App->>App: Create DaemonSessionBinding
    App->>Socket: RPC session.attach with cols/rows
    Daemon->>PTY: Retrieve existing PTY
    Daemon->>Daemon: Queue pty.replay event
    Daemon-->>Socket: Send replay + session attached ack
    
    App->>App: Create DaemonPTYBridge (FIFO)
    App->>App: Read replay, send pty.output events
    
    App->>Socket: User types: pty.input RPC
    Daemon->>PTY: Write to PTY stdin
    PTY->>Daemon: PTY output ready
    Daemon->>Socket: Send pty.output event
    App->>App: Display terminal output
    end

    rect rgba(150, 100, 200, 0.5)
    Note over App,Daemon: Detach Flow
    App->>App: User closes workspace
    App->>Socket: RPC session.detach
    Daemon->>Daemon: Untrack attachment, keep PTY alive
    Daemon-->>Socket: Detach ack
    App->>App: Close FIFOs, cleanup bridge
    Daemon->>Daemon: PTY continues running
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #1479: Both modify UI files (ContentView.swift, VerticalTabsSidebar.swift) and localization resources (Localizable.xcstrings), with overlapping sidebar/detached-session feature wiring.
  • PR #1296: Both modify CLI socket control wiring (CLI/cmux.swift) and socket path/keychain resolution logic for daemon communication.
  • PR #2087: Both modify the CLI implementation (CLI/cmux.swift)—this PR renames cmux→jmux and updates socket identifiers while related PR adds CLI subcommands to the same dispatcher.

Poem

🐰 A daemon springs forth with sessions to keep,
Detached work persisting in FIFO deep,
From cmux we dance now to jmux's gleaming shore,
Ring buffers spinning forevermore!
Reattach and rejoice—our terminals soar! 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.80% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Release v0.64.0' directly matches the main objective of this pull request, which is a release of version 0.64.0 with multiple features and fixes.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all required template sections (Summary with what changed and why, Testing placeholder, Demo Video placeholder, and Checklist) with detailed information about added features, changed behavior, fixes, and removals.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch release/v0.64.0

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 4, 2026

Greptile Summary

This PR introduces v0.64.0, with the headline feature being a local Go daemon (cmuxd-local) for tmux-style session detach/reattach — terminal PTYs survive app restarts via launchd and disk persistence. It also ships editable workspace descriptions, a Claude binary path setting, a keyboard shortcuts cheatsheet, localized tab context menu strings, and several bug fixes.

  • P1 — attach handshake race (daemon/local/cmd/cmuxd-local/main.go): In handleSessionAttach the pty.replay goroutine is launched before handleClient writes the RPC response. On a multi-core machine the goroutine can win the FrameWriter mutex, sending the replay event before the response; the Swift client then reads the replay frame as the response, fails response[\"ok\"] as? Bool == true, and throws attachFailed. The response must be written synchronously before the goroutine is started.
  • P2 — fork artefacts: docs/20260404_Git开发与同步工作流.md is a personal Chinese-language workflow guide referencing git@github.com:Sean529/cmux.git and should not be in this repo. The three new jmux-*.xcscheme files reference jmux.app/jmuxTests targets from the contributor's private fork rather than cmux equivalents.

Confidence Score: 4/5

Safe to merge after fixing the attach-handshake race and removing the two fork artefacts.

One P1 defect (replay/response ordering race) can cause non-deterministic attach failures in production on multi-core machines. Two P2 items (personal doc + jmux schemes) are cleanup issues that don't affect runtime but should not land in the upstream repo.

daemon/local/cmd/cmuxd-local/main.go (P1 race in handleSessionAttach), docs/20260404_Git开发与同步工作流.md and GhosttyTabs.xcodeproj/xcshareddata/xcschemes/jmux-*.xcscheme (fork artefacts to remove)

Important Files Changed

Filename Overview
daemon/local/cmd/cmuxd-local/main.go New Go daemon entry point handling session lifecycle over a Unix socket; contains a P1 race between the pty.replay goroutine and the attach RPC response write.
daemon/local/session.go New PTY session/window/pane model with consistent lock hierarchy (sm→session→window→pane); logic looks correct.
daemon/local/persistence.go Atomic state file save/load with PID staleness check and periodic auto-save; implementation is clean and correct.
daemon/local/rpc.go Newline-delimited JSON framing with 4 MB frame limit and thread-safe FrameWriter; looks correct.
Sources/DaemonSessionBinding.swift Swift bridge to daemon PTY over Unix socket; careful NSLock + read-thread lifecycle management; attach handshake is fragile due to the server-side race described in the Go daemon comment.
Sources/LocalDaemonManager.swift MainActor daemon lifecycle manager with launchd integration and socket-path fallback; implementation is solid.
docs/20260404_Git开发与同步工作流.md Personal fork Git workflow document (Chinese) referencing Sean529's personal repo URL — should not be committed to upstream.
GhosttyTabs.xcodeproj/xcshareddata/xcschemes/jmux-ci.xcscheme New Xcode scheme referencing fork-renamed targets (jmux.app / jmuxTests) rather than cmux equivalents — fork artefact that should be excluded.
CHANGELOG.md Adds well-structured v0.64.0 entry covering all PR changes.
Sources/SessionPersistence.swift In-app session snapshot persistence with ANSI-safe scrollback truncation; no issues found.

Sequence Diagram

sequenceDiagram
    participant App as cmux (Swift)
    participant DSB as DaemonSessionBinding
    participant Sock as Unix Socket
    participant Daemon as cmuxd-local (Go)
    participant PTY as PTY/Shell

    App->>DSB: attach(cols, rows)
    DSB->>Sock: connect()
    DSB->>Daemon: session.attach RPC (JSON)
    Daemon->>PTY: Pane.Attach() — register writer
    Note over Daemon: launch pty.replay goroutine ⚠️
    Daemon-->>DSB: RPC response {ok:true} (or replay races ahead)
    Daemon-->>DSB: pty.replay event (ring buffer snapshot)
    DSB->>DSB: startReadThread()
    loop streaming I/O
        PTY-->>Daemon: PTY output
        Daemon-->>DSB: pty.output event
        DSB->>App: outputHandler(data)
        App->>DSB: sendInput(data)
        DSB->>Daemon: pty.input RPC
        Daemon->>PTY: WriteInput(data)
    end
    App->>DSB: detach()
    DSB->>Daemon: session.detach RPC
    Note over PTY: PTY keeps running in daemon
    DSB->>Sock: shutdown + close
Loading

Comments Outside Diff (1)

  1. docs/20260404_Git开发与同步工作流.md, line 1-46 (link)

    P2 Personal fork workflow document committed to upstream repo

    This file is a Chinese-language personal Git workflow guide referencing git@github.com:Sean529/cmux.git — the PR author's personal fork. It describes how to set that fork as origin and the upstream project as upstream. This document does not belong in the manaflow-ai/cmux upstream repository and should be removed before merging.

Reviews (1): Last reviewed commit: "Bump version to 0.64.0" | Re-trigger Greptile

Comment on lines +335 to +348

// Queue replay event.
go func() {
_ = cs.writer.WriteEvent(local.RPCEvent{
Event: "pty.replay",
SessionID: sess.ID,
PaneID: pane.ID,
DataBase64: base64.StdEncoding.EncodeToString(replay),
ReplayDone: true,
})
}()

return resp
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Replay event can race ahead of the attach response

The pty.replay goroutine is launched before handleClient calls cs.writer.WriteResponse(resp). Because FrameWriter serialises writes under a mutex, either write can win. If the goroutine wins, the Swift client's synchronous readJSONLine call in DaemonSessionBinding.attach() reads the replay event as the response, fails the response["ok"] as? Bool == true guard, and throws attachFailed — causing the attach to fail non-deterministically on multi-core machines.

The fix is to write the RPC response synchronously inside handleSessionAttach before launching the goroutine, then return a sentinel that tells handleClient not to write a second response:

// Write the ok response first, then queue the replay.
if err := cs.writer.WriteResponse(resp); err != nil {
    return local.ErrorResponse(req.ID, "write_failed", err.Error())
}
go func() {
    _ = cs.writer.WriteEvent(local.RPCEvent{
        Event:      "pty.replay",
        SessionID:  sess.ID,
        PaneID:     pane.ID,
        DataBase64: base64.StdEncoding.EncodeToString(replay),
        ReplayDone: true,
    })
}()
// Return a nil/already-written sentinel so handleClient skips its write.

Comment on lines +6 to +9
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="A5001050" BuildableName="jmux.app" BlueprintName="GhosttyTabs" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
</BuildActionEntry>
<BuildActionEntry buildForTesting="YES" buildForRunning="NO" buildForProfiling="NO" buildForArchiving="NO" buildForAnalyzing="NO">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="F1000004A1B2C3D4E5F60718" BuildableName="cmuxTests.xctest" BlueprintName="cmuxTests" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="F1000004A1B2C3D4E5F60718" BuildableName="jmuxTests.xctest" BlueprintName="jmuxTests" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Fork-renamed Xcode schemes added to the upstream repo

Three new scheme files (jmux.xcscheme, jmux-ci.xcscheme, jmux-unit.xcscheme) reference jmux.app, jmuxTests.xctest, and jmuxUITests.xctest — names belonging to the contributor's personal fork, not to cmux. These are fork artefacts that do not belong in the upstream project and will cause confusion for other contributors.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

11 issues found across 48 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/SocketControlSettings.swift">

<violation number="1" location="Sources/SocketControlSettings.swift:64">
P2: Password file directory changed to "jmux" without any fallback to the legacy "cmux" path, so existing password files won’t be loaded after upgrade.</violation>

<violation number="2" location="Sources/SocketControlSettings.swift:68">
P2: Legacy keychain migration now points at the new service identifier, so existing passwords stored under the old `com.cmuxterm.app.socket-control` service will never be found or migrated.</violation>
</file>

<file name="Sources/TerminalController.swift">

<violation number="1" location="Sources/TerminalController.swift:2088">
P2: New `session.local.*` v2 methods are implemented in dispatch but not advertised in `system.capabilities`, causing capability-discovery mismatch.</violation>
</file>

<file name="daemon/local/test_e2e.sh">

<violation number="1" location="daemon/local/test_e2e.sh:44">
P2: `cleanup` expands `SOCKET_PATH`/`STATE_PATH` under `set -u` before they are guaranteed to be initialized, so an early exit can trigger an unbound variable error in the EXIT trap and mask the original failure.</violation>

<violation number="2" location="daemon/local/test_e2e.sh:181">
P3: wait_for_socket only waits about 1s (5 iterations × 0.2s) while reporting a 5s timeout, which can cause flaky test failures when daemon startup takes between 1–5s.</violation>
</file>

<file name="Sources/Workspace.swift">

<violation number="1" location="Sources/Workspace.swift:5687">
P2: Existing terminal panels are closed before the daemon attach succeeds; if attach fails, the daemon panel is torn down and the original restored terminals are never recreated, so the workspace can lose all terminal surfaces and restored session state on attach failure.</violation>
</file>

<file name="GhosttyTabs.xcodeproj/xcshareddata/xcschemes/jmux.xcscheme">

<violation number="1" location="GhosttyTabs.xcodeproj/xcshareddata/xcschemes/jmux.xcscheme:13">
P2: Scheme buildable metadata was renamed to `jmuxUITests` while the referenced target ID still maps to `cmuxUITests` in the project, creating scheme/project inconsistency that can break or destabilize test/run configuration.</violation>
</file>

<file name="Sources/DaemonSessionBinding.swift">

<violation number="1" location="Sources/DaemonSessionBinding.swift:414">
P2: Read loop exits on EOF/error without closing `_socketFD` or clearing `_readThread`, leaving a leaked fd and stale thread state if the daemon disconnects unexpectedly.</violation>
</file>

<file name="Sources/LocalDaemonManager.swift">

<violation number="1" location="Sources/LocalDaemonManager.swift:75">
P2: ensureRunning() runs on the @MainActor but calls probeSync() directly; probeSync uses blocking socket I/O with a 5s timeout (rpcSync/sendSocketCommand). This can block the main actor and freeze UI when the daemon socket is slow or absent. Use the existing off-main pattern (Task.detached/async wrapper) for these probes.</violation>
</file>

<file name="Sources/SessionPersistence.swift">

<violation number="1" location="Sources/SessionPersistence.swift:427">
P2: Changing the snapshot directory from `cmux` to `jmux` without any legacy fallback means upgraded users won’t find existing session snapshots saved under the old path, so session restore silently fails after upgrade.</violation>
</file>

<file name="daemon/local/cmd/cmux-local/main.go">

<violation number="1" location="daemon/local/cmd/cmux-local/main.go:380">
P1: Write the `session.attach` response before queuing `pty.replay`. The replay goroutine can currently win the write lock and send an event first, which makes the client parse the event as the RPC response and intermittently fail attach.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

doneCh := make(chan int, 3)

// Read events from daemon (pty.replay, pty.output, session.exited)
go func() {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 4, 2026

Choose a reason for hiding this comment

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

P1: Write the session.attach response before queuing pty.replay. The replay goroutine can currently win the write lock and send an event first, which makes the client parse the event as the RPC response and intermittently fail attach.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At daemon/local/cmd/cmux-local/main.go, line 380:

<comment>Write the `session.attach` response before queuing `pty.replay`. The replay goroutine can currently win the write lock and send an event first, which makes the client parse the event as the RPC response and intermittently fail attach.</comment>

<file context>
@@ -0,0 +1,523 @@
+	doneCh := make(chan int, 3)
+
+	// Read events from daemon (pty.replay, pty.output, session.exited)
+	go func() {
+		for {
+			line, err := reader.ReadString('\n')
</file context>
Fix with Cubic


enum SocketControlPasswordStore {
static let directoryName = "cmux"
static let directoryName = "jmux"
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 4, 2026

Choose a reason for hiding this comment

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

P2: Password file directory changed to "jmux" without any fallback to the legacy "cmux" path, so existing password files won’t be loaded after upgrade.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/SocketControlSettings.swift, line 64:

<comment>Password file directory changed to "jmux" without any fallback to the legacy "cmux" path, so existing password files won’t be loaded after upgrade.</comment>

<file context>
@@ -61,11 +61,11 @@ enum SocketControlMode: String, CaseIterable, Identifiable {
 
 enum SocketControlPasswordStore {
-    static let directoryName = "cmux"
+    static let directoryName = "jmux"
     static let fileName = "socket-control-password"
     private static let keychainMigrationDefaultsKey = "socketControlPasswordMigrationVersion"
</file context>
Fix with Cubic

private static let keychainMigrationDefaultsKey = "socketControlPasswordMigrationVersion"
private static let keychainMigrationVersion = 1
private static let legacyKeychainService = "com.cmuxterm.app.socket-control"
private static let legacyKeychainService = "com.jmux.app.socket-control"
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 4, 2026

Choose a reason for hiding this comment

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

P2: Legacy keychain migration now points at the new service identifier, so existing passwords stored under the old com.cmuxterm.app.socket-control service will never be found or migrated.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/SocketControlSettings.swift, line 68:

<comment>Legacy keychain migration now points at the new service identifier, so existing passwords stored under the old `com.cmuxterm.app.socket-control` service will never be found or migrated.</comment>

<file context>
@@ -61,11 +61,11 @@ enum SocketControlMode: String, CaseIterable, Identifiable {
     private static let keychainMigrationDefaultsKey = "socketControlPasswordMigrationVersion"
     private static let keychainMigrationVersion = 1
-    private static let legacyKeychainService = "com.cmuxterm.app.socket-control"
+    private static let legacyKeychainService = "com.jmux.app.socket-control"
     private static let legacyKeychainAccount = "local-socket-password"
     private struct LazyKeychainFallbackCache {
</file context>
Suggested change
private static let legacyKeychainService = "com.jmux.app.socket-control"
private static let legacyKeychainService = "com.cmuxterm.app.socket-control"
Fix with Cubic

return v2Result(id: id, self.v2WorkspaceRemoteTerminalSessionEnd(params: params))

// Local daemon session management
case "session.local.status":
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 4, 2026

Choose a reason for hiding this comment

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

P2: New session.local.* v2 methods are implemented in dispatch but not advertised in system.capabilities, causing capability-discovery mismatch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/TerminalController.swift, line 2088:

<comment>New `session.local.*` v2 methods are implemented in dispatch but not advertised in `system.capabilities`, causing capability-discovery mismatch.</comment>

<file context>
@@ -2084,6 +2084,20 @@ class TerminalController {
             return v2Result(id: id, self.v2WorkspaceRemoteTerminalSessionEnd(params: params))
 
+        // Local daemon session management
+        case "session.local.status":
+            return v2Result(id: id, self.v2SessionLocalStatus(params: params))
+        case "session.local.list":
</file context>
Fix with Cubic

rm -rf "$TMPDIR_TEST"
fi
# Clean up test socket and state file
rm -f "$SOCKET_PATH" "$STATE_PATH" 2>/dev/null || true
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 4, 2026

Choose a reason for hiding this comment

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

P2: cleanup expands SOCKET_PATH/STATE_PATH under set -u before they are guaranteed to be initialized, so an early exit can trigger an unbound variable error in the EXIT trap and mask the original failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At daemon/local/test_e2e.sh, line 44:

<comment>`cleanup` expands `SOCKET_PATH`/`STATE_PATH` under `set -u` before they are guaranteed to be initialized, so an early exit can trigger an unbound variable error in the EXIT trap and mask the original failure.</comment>

<file context>
@@ -0,0 +1,597 @@
+    rm -rf "$TMPDIR_TEST"
+  fi
+  # Clean up test socket and state file
+  rm -f "$SOCKET_PATH" "$STATE_PATH" 2>/dev/null || true
+}
+
</file context>
Fix with Cubic

<Testables>
<TestableReference skipped="NO">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="CB450DF0F0B3839599082C4D" BuildableName="cmuxUITests.xctest" BlueprintName="cmuxUITests" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="CB450DF0F0B3839599082C4D" BuildableName="jmuxUITests.xctest" BlueprintName="jmuxUITests" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 4, 2026

Choose a reason for hiding this comment

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

P2: Scheme buildable metadata was renamed to jmuxUITests while the referenced target ID still maps to cmuxUITests in the project, creating scheme/project inconsistency that can break or destabilize test/run configuration.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At GhosttyTabs.xcodeproj/xcshareddata/xcschemes/jmux.xcscheme, line 13:

<comment>Scheme buildable metadata was renamed to `jmuxUITests` while the referenced target ID still maps to `cmuxUITests` in the project, creating scheme/project inconsistency that can break or destabilize test/run configuration.</comment>

<file context>
@@ -3,28 +3,28 @@
     <Testables>
       <TestableReference skipped="NO">
-        <BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="CB450DF0F0B3839599082C4D" BuildableName="cmuxUITests.xctest" BlueprintName="cmuxUITests" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
+        <BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="CB450DF0F0B3839599082C4D" BuildableName="jmuxUITests.xctest" BlueprintName="jmuxUITests" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
       </TestableReference>
     </Testables>
</file context>
Suggested change
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="CB450DF0F0B3839599082C4D" BuildableName="jmuxUITests.xctest" BlueprintName="jmuxUITests" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="CB450DF0F0B3839599082C4D" BuildableName="cmuxUITests.xctest" BlueprintName="cmuxUITests" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
Fix with Cubic


while true {
let count = read(fd, &buffer, buffer.count)
if count <= 0 {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 4, 2026

Choose a reason for hiding this comment

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

P2: Read loop exits on EOF/error without closing _socketFD or clearing _readThread, leaving a leaked fd and stale thread state if the daemon disconnects unexpectedly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/DaemonSessionBinding.swift, line 414:

<comment>Read loop exits on EOF/error without closing `_socketFD` or clearing `_readThread`, leaving a leaked fd and stale thread state if the daemon disconnects unexpectedly.</comment>

<file context>
@@ -0,0 +1,891 @@
+
+        while true {
+            let count = read(fd, &buffer, buffer.count)
+            if count <= 0 {
+                throw DaemonSessionError.readFailed
+            }
</file context>
Fix with Cubic

/// process spawn when no launch agent is configured.
func ensureRunning() async {
// Fast path: daemon already confirmed running.
if isRunning, Self.probeSync() {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 4, 2026

Choose a reason for hiding this comment

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

P2: ensureRunning() runs on the @mainactor but calls probeSync() directly; probeSync uses blocking socket I/O with a 5s timeout (rpcSync/sendSocketCommand). This can block the main actor and freeze UI when the daemon socket is slow or absent. Use the existing off-main pattern (Task.detached/async wrapper) for these probes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/LocalDaemonManager.swift, line 75:

<comment>ensureRunning() runs on the @MainActor but calls probeSync() directly; probeSync uses blocking socket I/O with a 5s timeout (rpcSync/sendSocketCommand). This can block the main actor and freeze UI when the daemon socket is slow or absent. Use the existing off-main pattern (Task.detached/async wrapper) for these probes.</comment>

<file context>
@@ -0,0 +1,550 @@
+    /// process spawn when no launch agent is configured.
+    func ensureRunning() async {
+        // Fast path: daemon already confirmed running.
+        if isRunning, Self.probeSync() {
+            return
+        }
</file context>
Fix with Cubic

)
return resolvedAppSupport
.appendingPathComponent("cmux", isDirectory: true)
.appendingPathComponent("jmux", isDirectory: true)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 4, 2026

Choose a reason for hiding this comment

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

P2: Changing the snapshot directory from cmux to jmux without any legacy fallback means upgraded users won’t find existing session snapshots saved under the old path, so session restore silently fails after upgrade.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/SessionPersistence.swift, line 427:

<comment>Changing the snapshot directory from `cmux` to `jmux` without any legacy fallback means upgraded users won’t find existing session snapshots saved under the old path, so session restore silently fails after upgrade.</comment>

<file context>
@@ -414,14 +417,14 @@ enum SessionPersistenceStore {
         )
         return resolvedAppSupport
-            .appendingPathComponent("cmux", isDirectory: true)
+            .appendingPathComponent("jmux", isDirectory: true)
             .appendingPathComponent("session-\(safeBundleId).json", isDirectory: false)
     }
</file context>
Fix with Cubic

local waited=0
while [ ! -S "$SOCKET_PATH" ] && [ $waited -lt $max_wait ]; do
sleep 0.2
waited=$((waited + 1))
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 4, 2026

Choose a reason for hiding this comment

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

P3: wait_for_socket only waits about 1s (5 iterations × 0.2s) while reporting a 5s timeout, which can cause flaky test failures when daemon startup takes between 1–5s.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At daemon/local/test_e2e.sh, line 181:

<comment>wait_for_socket only waits about 1s (5 iterations × 0.2s) while reporting a 5s timeout, which can cause flaky test failures when daemon startup takes between 1–5s.</comment>

<file context>
@@ -0,0 +1,597 @@
+  local waited=0
+  while [ ! -S "$SOCKET_PATH" ] && [ $waited -lt $max_wait ]; do
+    sleep 0.2
+    waited=$((waited + 1))
+  done
+  if [ ! -S "$SOCKET_PATH" ]; then
</file context>
Fix with Cubic

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.

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (3)
Sources/Panels/BrowserPanel.swift (2)

473-479: ⚠️ Potential issue | 🟠 Major

History namespace rename needs legacy migration to avoid silent data loss.

Line 473, Lines 905-909, and Line 1511 switch storage namespaces to jmux, but current migration logic won’t discover existing cmux-namespaced browser history files. Upgrading users can appear to “lose” history/profile history.

💡 Suggested fix (add legacy namespace candidates during migration)
@@
-    private func migrateLegacyTaggedHistoryFileIfNeeded(to targetURL: URL) {
+    private func migrateLegacyTaggedHistoryFileIfNeeded(to targetURL: URL) {
         let fm = FileManager.default
         guard !fm.fileExists(atPath: targetURL.path) else { return }
-        guard let legacyURL = Self.legacyTaggedHistoryFileURL(),
-              legacyURL != targetURL,
-              fm.fileExists(atPath: legacyURL.path) else {
+        guard let legacyURL = Self.legacyTaggedHistoryFileURLs(for: targetURL).first(where: {
+            $0 != targetURL && fm.fileExists(atPath: $0.path)
+        }) else {
             return
         }
@@
-    nonisolated private static func legacyTaggedHistoryFileURL() -> URL? {
-        guard let bundleId = Bundle.main.bundleIdentifier else { return nil }
-        let namespace = normalizedBrowserHistoryNamespace(bundleIdentifier: bundleId)
-        guard namespace != bundleId else { return nil }
+    nonisolated private static func legacyTaggedHistoryFileURLs(for targetURL: URL) -> [URL] {
         let fm = FileManager.default
         guard let appSupport = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
-            return nil
+            return []
         }
-        let dir = appSupport.appendingPathComponent(bundleId, isDirectory: true)
-        return dir.appendingPathComponent("browser_history.json", isDirectory: false)
+        let legacyNamespaces = ["cmux", "com.cmuxterm.app.debug", "com.cmuxterm.app.staging"]
+        return legacyNamespaces.map { namespace in
+            appSupport
+                .appendingPathComponent(namespace, isDirectory: true)
+                .appendingPathComponent("browser_history.json", isDirectory: false)
+        }
     }

Also applies to: 905-912, 1511-1527

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserPanel.swift` around lines 473 - 479, The namespace
change to "jmux" can miss existing "cmux"-namespaced files; update the logic
that computes/reads browser history paths (e.g.,
BrowserHistoryStore.normalizedBrowserHistoryNamespaceForBundleIdentifier and any
callers that build profilesDir using profileID.uuidString.lowercased()) to
consider legacy namespace candidates during lookup/migration: when resolving or
migrating browser_history.json, check for the file under the new namespace first
and fall back to the legacy "cmux" namespace (and any other historical
candidates) and, if found, move/rename it into the new namespace so existing
data is preserved; ensure the migration runs where path construction is used
(the places around the current profile/path builders) so no silent data loss
occurs.

1733-1739: ⚠️ Potential issue | 🟡 Minor

Keep a legacy loopback alias fallback for restored sessions.

Line 1733 renames the alias host, but persisted URLs using cmux-loopback.localtest.me are no longer normalized by display/rewrite helpers. Add a legacy alias set so old sessions keep restoring cleanly.

💡 Suggested compatibility patch
-    private static let remoteLoopbackProxyAliasHost = "jmux-loopback.localtest.me"
+    private static let remoteLoopbackProxyAliasHost = "jmux-loopback.localtest.me"
+    private static let legacyRemoteLoopbackProxyAliasHosts: Set<String> = [
+        "cmux-loopback.localtest.me"
+    ]
-        guard host == BrowserInsecureHTTPSettings.normalizeHost(remoteLoopbackProxyAliasHost) else { return url }
+        let aliasHosts = Set(
+            [remoteLoopbackProxyAliasHost]
+                .compactMap(BrowserInsecureHTTPSettings.normalizeHost)
+        ).union(
+            Set(legacyRemoteLoopbackProxyAliasHosts.compactMap(BrowserInsecureHTTPSettings.normalizeHost))
+        )
+        guard aliasHosts.contains(host) else { return url }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserPanel.swift` around lines 1733 - 1739, The
display/rewrite helpers now only recognize remoteLoopbackProxyAliasHost
("jmux-loopback.localtest.me") so persisted URLs using the old alias
"cmux-loopback.localtest.me" are no longer normalized; add a legacy alias
constant (e.g., remoteLoopbackProxyLegacyAliasHost =
"cmux-loopback.localtest.me") or include the legacy host in the
remoteLoopbackHosts/alias checks and update any normalization logic that
references remoteLoopbackProxyAliasHost to accept the legacy alias as well so
restored sessions using the old name continue to be rewritten/normalized by
BrowserPanel's helpers.
Sources/ContentView.swift (1)

10122-10126: ⚠️ Potential issue | 🟠 Major

Restore the configured modifier-hint predicate.

Hardcoding false here disables sidebar/titlebar shortcut hints entirely and also breaks the close-button suppression wired to showsModifierShortcutHints. This still needs to compare the normalized flags against the configured selectWorkspaceByNumber shortcut modifiers.

Suggested fix
    static func shouldShowHints(
        for modifierFlags: NSEvent.ModifierFlags,
        defaults: UserDefaults = .standard
    ) -> Bool {
-        return false
+        guard ShortcutHintDebugSettings.showHintsOnCommandHoldEnabled(defaults: defaults) else {
+            return false
+        }
+        let normalizedFlags = modifierFlags
+            .intersection(.deviceIndependentFlagsMask)
+            .subtracting([.numericPad, .function, .capsLock])
+        return normalizedFlags == KeyboardShortcutSettings
+            .shortcut(for: .selectWorkspaceByNumber)
+            .modifierFlags
    }

Based on learnings, ShortcutHintModifierPolicy.shouldShowHints(for:) should match KeyboardShortcutSettings.shortcut(for: .selectWorkspaceByNumber).modifierFlags, and TabItemView’s close-button suppression depends on that same signal.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 10122 - 10126, Restore the original
predicate in ShortcutHintModifierPolicy.shouldShowHints(for:defaults:) so it
returns whether the provided modifierFlags (normalized/sanitized the same way
the app compares shortcuts) matches the configured shortcut modifiers for
selectWorkspaceByNumber; specifically compare the incoming flags to
KeyboardShortcutSettings.shortcut(for: .selectWorkspaceByNumber).modifierFlags
and return that boolean so TabItemView's close-button suppression tied to
showsModifierShortcutHints works again.
🟠 Major comments (22)
Sources/GhosttyConfig.swift-10-10 (1)

10-10: ⚠️ Potential issue | 🟠 Major

Add legacy bundle-id fallback for config path migration.

Line 10 changes the release identifier to com.jmux.app, but path resolution no longer accounts for existing com.cmuxterm.app config directories. That can drop user config on upgrade.

🛠️ Suggested compatibility patch
-    private static let cmuxReleaseBundleIdentifier = "com.jmux.app"
+    private static let primaryReleaseBundleIdentifier = "com.jmux.app"
+    private static let legacyReleaseBundleIdentifier = "com.cmuxterm.app"
@@
-        let releasePaths = paths(for: cmuxReleaseBundleIdentifier)
+        let releaseBundleIds = [primaryReleaseBundleIdentifier, legacyReleaseBundleIdentifier]
+        let releasePaths = releaseBundleIds.flatMap { paths(for: $0) }
@@
-        if currentBundleIdentifier == cmuxReleaseBundleIdentifier {
+        if releaseBundleIds.contains(currentBundleIdentifier) {
             return releasePaths
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyConfig.swift` at line 10, The static release bundle identifier
cmuxReleaseBundleIdentifier was changed to "com.jmux.app" but path resolution
must also accept the legacy "com.cmuxterm.app" to avoid dropping user config;
update the code that resolves or constructs the config directory (the logic that
uses cmuxReleaseBundleIdentifier) to first check for an existing config folder
for "com.cmuxterm.app" and if found use or migrate it (or prefer it over the new
path), otherwise fall back to using cmuxReleaseBundleIdentifier
("com.jmux.app"); ensure the legacy string "com.cmuxterm.app" appears as a
fallback constant and that any migration copies or moves existing files rather
than creating a new empty config directory.
Sources/CmuxDirectoryTrust.swift-16-17 (1)

16-17: ⚠️ Potential issue | 🟠 Major

Preserve existing trust state when moving store path.

Line 16 changes the persistence directory to jmux, but there is no fallback/migration from the old cmux store. Existing trusted directories will be silently lost after upgrade.

💡 Suggested migration-safe approach
-        let appSupport = FileManager.default.urls(
+        let appSupport = FileManager.default.urls(
             for: .applicationSupportDirectory, in: .userDomainMask
         ).first!.appendingPathComponent("jmux")
+        let legacyAppSupport = FileManager.default.urls(
+            for: .applicationSupportDirectory, in: .userDomainMask
+        ).first!.appendingPathComponent("cmux")
         storePath = appSupport.appendingPathComponent("trusted-directories.json").path
+        let legacyStorePath = legacyAppSupport
+            .appendingPathComponent("trusted-directories.json")
+            .path
@@
-        if let data = fm.contents(atPath: storePath),
+        let seedPath: String = {
+            if fm.fileExists(atPath: storePath) { return storePath }
+            if fm.fileExists(atPath: legacyStorePath) { return legacyStorePath }
+            return storePath
+        }()
+
+        if let data = fm.contents(atPath: seedPath),
            let paths = try? JSONDecoder().decode([String].self, from: data) {
             trustedPaths = Set(paths)
+            if seedPath != storePath { save() } // migrate forward
         } else {
             trustedPaths = []
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/CmuxDirectoryTrust.swift` around lines 16 - 17, The change to use a
new persistence directory ("jmux") will lose existing trust entries from the old
"cmux" store; update the CmuxDirectoryTrust initialization/migration logic (the
code that sets storePath and loads/saves trusted-directories.json) to detect the
old location (the previous appSupport/.../cmux/trusted-directories.json), and if
that file exists and the new file does not, copy or migrate its contents into
the new storePath before loading; ensure migration is idempotent (only migrate
once), preserve file permissions, and handle errors by logging via the same
logger used in CmuxDirectoryTrust so existing trust state is retained after
upgrade.
GhosttyTabs.xcodeproj/project.pbxproj-1157-1159 (1)

1157-1159: ⚠️ Potential issue | 🟠 Major

Don't rename cmux-cli in isolation.

The target/product refs in this same project are still wired as cmux (B9000004 /* cmux */ and the app's Copy CLI phase), so changing only PRODUCT_NAME here makes the produced executable name diverge from the file the app copies. Either keep the tool product named cmux, or rename the product reference/copy wiring in the same PR.

Minimal fix
-				PRODUCT_NAME = jmux;
+				PRODUCT_NAME = cmux;

Apply that to both the Debug and Release configurations of the cmux-cli target.

Also applies to: 1176-1178

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@GhosttyTabs.xcodeproj/project.pbxproj` around lines 1157 - 1159, The
PRODUCT_NAME and PRODUCT_MODULE_NAME for the cmux-cli target were changed to
"jmux"/"cmux_cli" while the project still references the tool as "cmux" (e.g.
the target/product ref B9000004 /* cmux */ and the app's "Copy CLI" build
phase), causing a mismatch; either revert PRODUCT_NAME/PRODUCT_MODULE_NAME back
to "cmux" for both Debug and Release configurations of the cmux-cli target, or
update the product reference and the app's "Copy CLI" phase to the new product
name consistently across the project so the built executable and the copy phase
use the same name.
Sources/SocketControlSettings.swift-64-69 (1)

64-69: ⚠️ Potential issue | 🟠 Major

Don't rename the legacy password stores in place.

configuredPassword() now reads from the new Application Support/jmux/... location, and legacyKeychainService no longer points at the pre-rename keychain item. Upgraders with an existing socket password in the old file path or old keychain service will suddenly look unconfigured, so password-mode sockets stop accepting clients until the password is re-entered. Keep the old path/service readable and migrate forward into the new namespace instead of replacing the legacy lookup keys.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/SocketControlSettings.swift` around lines 64 - 69,
configuredPassword() must continue to read the pre-rename legacy stores and
perform an explicit migration into the new Application Support/jmux/...
namespace rather than changing the legacy identifiers; update the logic that
references legacyKeychainService and legacyKeychainAccount so it still attempts
to read the old keychain item and also check the old file path (using
directoryName and fileName) when the new location is empty, then on successful
read write the password into the new location and set
keychainMigrationDefaultsKey to keychainMigrationVersion to mark migration
complete; do not overwrite or rename the legacy keys in place—only migrate their
contents into the new namespace.
Sources/AppDelegate.swift-2558-2577 (1)

2558-2577: ⚠️ Potential issue | 🟠 Major

Don't gate daemon reattach on a fixed 1s sleep.

Line 2566 makes auto-reattach depend on startup speed, but this file restores windows/workspaces asynchronously after launch. Any context that appears after that timeout can miss reattachDaemonSessionIfNeeded(), which makes the new restore path flaky on slower launches. Trigger reattach from actual restore completion and/or window registration once the daemon is running instead of using a fixed delay.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 2558 - 2577, The code currently uses
a fixed Task.sleep(1_000_000_000) before iterating mainWindowContexts and
calling workspace.reattachDaemonSessionIfNeeded(), which can miss contexts
restored after that timeout; remove the hard sleep and instead trigger reattach
from the actual restore/window-registration completion or from the daemon
startup completion callback: have LocalDaemonManager.shared.ensureRunning()
either await a completion notification or expose a method like
LocalDaemonManager.shared.notifyWhenReady(completion:) or send a
Notification/async Stream; then from the window/workspace registration path
(where mainWindowContexts and tabManager.tabs get populated) call
workspace.reattachDaemonSessionIfNeeded() or call a new
LocalDaemonManager.shared.reattachAllPersistedSessions(for: mainWindowContexts)
so reattach runs when both daemon is running and each window/workspace is
registered (referencing ensureRunning, mainWindowContexts, tabManager.tabs,
workspace.reattachDaemonSessionIfNeeded, and startPeriodicRefresh).
Sources/TerminalController.swift-4049-4067 (1)

4049-4067: ⚠️ Potential issue | 🟠 Major

Use v2OrNull() for optional version value and guard on both daemon probe and handshake.

When rpcSync(method: "hello") fails or omits "version", the code puts a nil value into the response dictionary. JSONSerialization rejects nil in dictionaries (requiring NSNull() instead), causing v2Encode() to fail the isValidJSONObject() guard and return an encode_error response instead of properly reporting daemon unavailability.

Suggested fix
     private func v2SessionLocalStatus(params _: [String: Any]) -> V2CallResult {
-        // Probe daemon readiness off-main via nonisolated static method.
-        let running = LocalDaemonManager.probeSync()
-
-        if !running {
-            return .ok([
-                "daemon_running": false,
-                "version": NSNull(),
-            ])
-        }
-
-        // Fetch version off-main.
-        let versionResponse = LocalDaemonManager.rpcSync(method: "hello")
-        let version = (versionResponse?["result"] as? [String: Any])?["version"] as? String
-
-        return .ok([
-            "daemon_running": true,
-            "version": version as Any,
-        ])
+        guard
+            LocalDaemonManager.probeSync(),
+            let versionResponse = LocalDaemonManager.rpcSync(method: "hello"),
+            let version = (versionResponse["result"] as? [String: Any])?["version"] as? String
+        else {
+            return .ok([
+                "daemon_running": false,
+                "version": NSNull(),
+            ])
+        }
+
+        return .ok([
+            "daemon_running": true,
+            "version": version,
+        ])
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TerminalController.swift` around lines 4049 - 4067,
v2SessionLocalStatus currently probes daemon with LocalDaemonManager.probeSync
and then trusts LocalDaemonManager.rpcSync(method: "hello") to provide a non-nil
"version", which can insert nil into the response and break JSON encoding;
update v2SessionLocalStatus to use v2OrNull() for the "version" field and add a
guard that treats the session as unavailable if either probeSync fails or the
handshake (rpcSync/"hello") yields no version (use NSNull via v2OrNull() when
absent), so the returned dictionary never contains Swift nil and encodes
correctly; locate and modify v2SessionLocalStatus, LocalDaemonManager.probeSync,
and the rpcSync/"hello" handling to implement this change.
Sources/ContentView.swift-12280-12287 (1)

12280-12287: ⚠️ Potential issue | 🟠 Major

Reject detached sessions without a real session_id.

The current fallback gives malformed rows a random SwiftUI identity, but the action still flows through to workspace.daemonSessionID = "". On malformed or version-skewed daemon payloads, that creates a blank workspace that can never reattach.

Suggested fix
 private struct DetachedSessionItem: Identifiable {
     let id: String
     let raw: [String: Any]
 
-    init(_ dict: [String: Any]) {
-        self.id = dict["session_id"] as? String ?? UUID().uuidString
+    init?(_ dict: [String: Any]) {
+        guard let rawID = dict["session_id"] as? String else { return nil }
+        let sessionID = rawID.trimmingCharacters(in: .whitespacesAndNewlines)
+        guard !sessionID.isEmpty else { return nil }
+        self.id = sessionID
         self.raw = dict
     }
 }
 
 private struct SidebarDetachedSessionsSection: View {
@@
     private var items: [DetachedSessionItem] {
-        sessions.map { DetachedSessionItem($0) }
+        sessions.compactMap(DetachedSessionItem.init)
     }
@@
     private var sessionID: String {
-        session["session_id"] as? String ?? ""
+        (session["session_id"] as? String)?
+            .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
     }
@@
     private func reattachSession() {
-        guard !isReattaching else { return }
+        guard !isReattaching, !sessionID.isEmpty else { return }
         isReattaching = true

Also applies to: 12294-12296, 12334-12336, 12422-12430

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 12280 - 12287, The
DetachedSessionItem initializer should reject malformed payloads instead of
inventing an id: make init(_ dict: [String: Any]) failable (init?) and return
nil when dict["session_id"] is missing or empty, then update callers that build
DetachedSessionItem arrays to use compactMap so malformed rows are dropped
(preventing workspace.daemonSessionID = "" from being set); apply the same
failable-init + compactMap pattern to the other analogous structs/initializers
referenced in the comment ranges so no entries without a real session_id can
flow into workspace.daemonSessionID.
CLI/cmux.swift-531-535 (1)

531-535: ⚠️ Potential issue | 🟠 Major

Preserve the legacy password stores during this namespace migration.

The resolver now only looks under the jmux Application Support directory and the com.jmux.app.socket-control keychain service. Any upgraded install that still has its socket password under the old cmux namespace will fail CLI auth until that secret is rewritten. Please keep the previous file/keychain locations as fallbacks while preferring the new namespace.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 531 - 535, SocketPasswordResolver currently only
uses the new namespace constants (service, account, directoryName, fileName) and
will miss upgraded installs; update the resolver so reads first prefer the new
locations but fall back to the legacy cmux locations (e.g. legacy service
"com.cmux.app.socket-control" and legacy directoryName "cmux" with same
account/fileName) when retrieving the password, and when persisting the password
write the password to the new namespace and also mirror it to the legacy
key/file so existing installs continue to work until they are fully migrated;
change logic inside SocketPasswordResolver to try new keychain/file paths then
legacy ones, and ensure writes update both places.
CLI/cmux.swift-7333-7336 (1)

7333-7336: ⚠️ Potential issue | 🟠 Major

Keep reading the old theme override path for one migration window.

Changing the bundle-id constant moves the managed override location from ~/Library/Application Support/com.cmuxterm.app/... to ~/Library/Application Support/com.jmux.app/..., and the search helpers below no longer include the old path. Users with an existing managed theme override will lose it after upgrade unless you migrate or fall back to the previous bundle-id directory.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 7333 - 7336, The constant
cmuxThemeOverrideBundleIdentifier was changed to "com.jmux.app" which breaks
lookup of existing managed overrides; update the theme-path/lookup logic (the
code that uses cmuxThemeOverrideBundleIdentifier and the related
cmuxThemesBlockStart/cmuxThemesBlockEnd read helpers and notification handling)
to also check the previous bundle id "com.cmuxterm.app" as a fallback during a
migration window: when reading/searching for an existing managed theme, first
look in the new "com.jmux.app" location then fallback to "com.cmuxterm.app" (and
if you encounter files in the old location, either migrate them to the new
location or continue honoring the old file while logging/migrating once), and
ensure the reload notification handling still works for either path so users
don’t lose their overrides after upgrade.
CLI/cmux.swift-709-713 (1)

709-713: ⚠️ Potential issue | 🟠 Major

Tagged debug sockets are no longer discoverable without CMUX_TAG.

This adds /tmp/jmux-debug-<tag>.sock to the candidate list, but the directory scan below still only picks up cmux*.sock. Any implicit connection that relies on discovery instead of an in-process CMUX_TAG will miss the live jmux-debug-* socket and fall back to the wrong path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 709 - 713, The candidate list adds
"/tmp/jmux-debug-<tag>.sock" when CMUX_TAG is present but the subsequent
discovery scan only matches "cmux*.sock", so jmux-debug sockets are missed;
update the discovery logic (the code that builds/filters candidates and the
directory scan that currently looks for "cmux*.sock") to also include the
"jmux-debug-*.sock" pattern (and any sanitized slug variant) so that jmux-debug
sockets are discovered implicitly—locate uses of candidates, sanitizeTagSlug,
and the directory scanning routine and add the jmux-debug pattern to the
glob/filter set.
Sources/cmuxApp.swift-2836-2836 (1)

2836-2836: ⚠️ Potential issue | 🟠 Major

Localization catalog is incomplete: update the three missing close-tab warning keys and correct the stale about.appName defaultValue.

Three new user-facing strings added in Lines 4919-4922 (settings.app.warnBeforeCloseTab, settings.app.warnBeforeCloseTab.subtitleOn, settings.app.warnBeforeCloseTab.subtitleOff) are missing from Resources/Localizable.xcstrings. Without these catalog entries, the UI will fall back to the inline English defaultValue strings with no Japanese translation support.

Additionally, Line 2836 still carries defaultValue: "jmux", but the catalog shows about.appName has been updated to 'cmux' in both en and ja. The code's defaultValue should match the catalog value for consistency.

Add all four keys to the catalog with proper English and Japanese translations:

  • Update about.appName defaultValue from "jmux" to "cmux" in the code
  • Add settings.app.warnBeforeCloseTab, settings.app.warnBeforeCloseTab.subtitleOn, and settings.app.warnBeforeCloseTab.subtitleOff to Resources/Localizable.xcstrings with en and ja values
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/cmuxApp.swift` at line 2836, The code uses Text(String(localized:
"about.appName", defaultValue: "jmux")) but the localization catalog has been
updated to "cmux" and three new strings used by settings are missing from the
catalog; change the defaultValue in the about.appName call to "cmux" and add the
following keys to Resources/Localizable.xcstrings with English and Japanese
entries: settings.app.warnBeforeCloseTab,
settings.app.warnBeforeCloseTab.subtitleOn, and
settings.app.warnBeforeCloseTab.subtitleOff so the UI no longer falls back to
inline defaults and matches the catalog.
Sources/Workspace.swift-5615-5618 (1)

5615-5618: ⚠️ Potential issue | 🟠 Major

Keep the saved session id until the daemon proves the session is gone.

These branches clear daemonSessionID for client-side startup, bridge setup, pane creation, and attach failures. That turns a recoverable reattach problem into permanent session loss even when the daemon session still exists. Only drop the id after listSessionsAsync() confirms the session no longer exists.

Also applies to: 5650-5657, 5663-5666, 5681-5683, 5705-5710

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 5615 - 5618, The current guard branches
(e.g., the one checking manager.isRunning that clears daemonSessionID)
prematurely wipe daemonSessionID on transient failures; instead, stop clearing
daemonSessionID in those early-return paths (keep the saved id) and only clear
it after verifying the session truly no longer exists via listSessionsAsync()
(i.e., call listSessionsAsync(), check for the session id, and clear
daemonSessionID only if the session is absent). Update the same pattern wherever
daemonSessionID is cleared on startup/bridge/pane/attach failures (references:
daemonSessionID, manager.isRunning, listSessionsAsync(), and the
attach/bridge/pane setup functions) so session id is preserved until
listSessionsAsync() confirms removal.
Sources/Workspace.swift-5661-5684 (1)

5661-5684: ⚠️ Potential issue | 🟠 Major

Pick the restored terminal pane as the reattach target.

This chooses focusedPaneId or the first pane before it looks at where the restored terminal actually lives. If the snapshot focus lands on a browser pane, the daemon-backed terminal gets inserted there and the original terminal pane is then closed, so the saved layout shifts on relaunch. Prefer the pane owning one of existingTerminalPanelIds before falling back.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 5661 - 5684, Current logic picks
bonsplitController.focusedPaneId or first pane before considering where the
restored terminal lives, causing the daemon-backed terminal to be inserted into
the wrong pane; update the pane selection so it prefers the pane that currently
contains one of existingTerminalPanelIds before falling back to focusedPaneId or
the first pane. Specifically, compute a preferredPaneId by finding the pane that
owns any id in existingTerminalPanelIds (use panels dictionary/panel.paneId or
equivalent) and then change the guard that sets paneId (used by
newTerminalSurface(inPane:focus:daemonBridgeCommand:)) to use preferredPaneId if
present, otherwise use bonsplitController.focusedPaneId, otherwise
bonsplitController.allPaneIds.first.
Sources/Workspace.swift-5610-5623 (1)

5610-5623: ⚠️ Potential issue | 🟠 Major

Serialize reattach before the first await.

daemonSessionBinding is only set after the async work completes. Because this method is async on @MainActor, another caller can enter while the first reattach is suspended on listSessionsAsync() or Task.detached and create a second bridge/panel pair.

🔐 One simple guard
+    private var isReattachingDaemonSession = false
+
     func reattachDaemonSessionIfNeeded() async {
         guard let sessionID = daemonSessionID else { return }
         guard daemonSessionBinding == nil else { return }
+        guard !isReattachingDaemonSession else { return }
+        isReattachingDaemonSession = true
+        defer { isReattachingDaemonSession = false }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 5610 - 5623, The
reattachDaemonSessionIfNeeded method can race because it awaits
listSessionsAsync before marking that a reattach is in progress; add a
synchronous guard/marker at the top of reattachDaemonSessionIfNeeded (before the
first await) to serialize concurrent callers — e.g. check and set a new
in-progress flag or assign a temporary placeholder to daemonSessionBinding
immediately after reading daemonSessionID so subsequent calls return early; then
proceed with LocalDaemonManager.shared.listSessionsAsync() and, on success,
replace the placeholder with the real binding or clear the marker on failure.
Ensure references to daemonSessionID, daemonSessionBinding, and
LocalDaemonManager.shared/listSessionsAsync are used to locate and update the
logic.
Sources/Workspace.swift-358-358 (1)

358-358: ⚠️ Potential issue | 🟠 Major

Use the active daemon binding state, not persisted ID, to guard detach-on-close.

Line 358 restores daemonSessionID before reattach completes. If the workspace closes between restore and successful reattach (while replay/local terminals exist but daemonSessionBinding is still nil), line 8218 will incorrectly call detachFromDaemon() on non-daemon panels.

Line 8218–8220 should check daemonSessionBinding != nil instead of daemonSessionID != nil to guard the detach path. TerminalPanel.detachFromDaemon() itself has no guards, so the workspace-level check is the only safety gate; use the active binding, not the persisted ID.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` at line 358, Restore is currently setting
daemonSessionID from the snapshot and later the workspace close path checks
daemonSessionID to decide whether to call detachFromDaemon(), but the correct
guard is the live binding. Change the close/detach logic to check
daemonSessionBinding != nil instead of daemonSessionID != nil so we only call
TerminalPanel.detachFromDaemon() when an active daemonSessionBinding exists;
update any conditional(s) that currently reference daemonSessionID in the
detach-on-close flow to reference daemonSessionBinding and leave
TerminalPanel.detachFromDaemon() unchanged.
daemon/local/persistence.go-503-522 (1)

503-522: ⚠️ Potential issue | 🟠 Major

Serialize saves instead of spawning one per mutation.

NotifyChange() fires Save() in a fresh goroutine, but Save() snapshots and writes without any ordering. Two quick mutations can persist out of order—for example, a pre-close snapshot can win the race after a later close has already removed the file—so restart can resurrect sessions that were already closed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@daemon/local/persistence.go` around lines 503 - 522, NotifyChange currently
starts a new goroutine that calls Save() directly, causing unordered concurrent
writes; change StatePersister to serialize saves by introducing a single
background save worker (e.g., a notify channel or a runLoop goroutine) that
coalesces notifications and invokes Save() sequentially, and have NotifyChange
send a signal to that worker instead of spawning go sp.Save(); update
StatePersister initialization to start the worker and ensure shutdown semantics
if needed so Save(), NotifyChange, and the new worker/runLoop coordinate
serialized persistence.
daemon/local/session.go-143-166 (1)

143-166: ⚠️ Potential issue | 🟠 Major

Propagate shell exit into window/session status.

When the last pane exits, this loop only marks p.Status as dead. Session.Snapshot() still returns the cached s.Status, and session.list uses that snapshot directly, so naturally exited daemon sessions keep showing up as "running" and reattachable.

daemon/local/session.go-114-123 (1)

114-123: ⚠️ Potential issue | 🟠 Major

The replay/live handoff can double-deliver bytes.

readLoop() appends data into p.ring before it snapshots p.clients, while Attach() snapshots the ring before it registers the new client. If PTY output lands in that gap, the same bytes are included in replay and then sent again as the first live pty.output, which will duplicate terminal output right after reattach.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@daemon/local/session.go` around lines 114 - 123, The replay/live handoff
double-delivery happens because readLoop() calls p.ring.Write(data) before it
snapshots p.clients, while Attach() snapshots the ring before registering the
new client; fix by making the handoff atomic from the perspective of new clients
— either (A) modify readLoop so it snapshots p.clients (the slice built from
p.clients) before calling p.ring.Write(data), ensuring newly attached clients
will get only live data, or (B) change Attach so it registers the new
FrameWriter into p.clients first and then snapshots the ring/replay (so the
newly registered client will not receive the same bytes twice); use symbols
readLoop, p.ring.Write, p.clients, Attach, FrameWriter and replay/pty.output to
locate and implement the change.
daemon/local/cmd/cmuxd-local/main.go-336-345 (1)

336-345: ⚠️ Potential issue | 🟠 Major

Send the attach ACK before starting replay.

Line 337 schedules pty.replay on the same connection before handleClient has written the session.attach response, so the first frame can be the event instead of the ACK. DaemonSessionBinding.attach() does a blocking first-read for the response, which makes attaches intermittently fail even when the daemon attached successfully.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@daemon/local/cmd/cmuxd-local/main.go` around lines 336 - 345, The replay
event is being queued (cs.writer.WriteEvent of local.RPCEvent with Event
"pty.replay") before the attach ACK is sent, causing
DaemonSessionBinding.attach() (which blocks waiting for the attach response) to
sometimes read the replay frame instead; fix by ensuring the attach ACK (the
session.attach response written in handleClient) is sent/flushed before
scheduling the pty.replay goroutine — e.g., move the goroutine that calls
cs.writer.WriteEvent(... "pty.replay" ...) to run only after handleClient has
written the session.attach response or add an explicit sync/flush point so the
attach ACK is guaranteed to be on the wire before emitting the replay event.
Sources/LocalDaemonManager.swift-232-242 (1)

232-242: ⚠️ Potential issue | 🟠 Major

stop() doesn't stop launchd/external daemons yet.

This issues shutdown, but the new Go RPC router never implements that method. When the daemon was started by launchd or was already running before the app launched, daemonProcess?.terminate() is a no-op and the background daemon keeps running while the UI flips isRunning to false.

Sources/DaemonSessionBinding.swift-513-527 (1)

513-527: ⚠️ Potential issue | 🟠 Major

Don't drop daemon exit events.

The daemon sends pane.exited / session.exited, but this switch ignores them. That leaves the bridge's output FIFO open, so cat <output_fifo> never gets EOF and daemon-backed terminals stay hung after the shell has already exited.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/DaemonSessionBinding.swift` around lines 513 - 527, The switch
currently only handles "pty.replay"/"pty.output" and drops daemon exit events;
add explicit cases for "pane.exited" and "session.exited" (checking the same
method variable) and in those cases perform output stream cleanup by closing the
output FIFO/handle so readers receive EOF; call an existing cleanup function or
add one (e.g., closeOutputFIFO() or similar) that closes the FileHandle/Writer
used by outputHandler (or otherwise signals EOF) and invoke it from those new
cases to avoid leaving the FIFO open.
Sources/LocalDaemonManager.swift-505-523 (1)

505-523: ⚠️ Potential issue | 🟠 Major

Buffer raw bytes fully before decoding to UTF-8.

Line 521 decodes each socket read chunk to String independently. When a multibyte UTF-8 character (e.g., Japanese) is split across socket reads, the incomplete chunk fails UTF-8 decode and is silently dropped, leaving the accumulated response string incomplete. The partial JSON then fails to parse in rpcSync (line 200), causing session.list, hello, and similar commands to fail for sessions/windows with non-ASCII names.

Accumulate all raw bytes until a newline is found, then decode the complete line once as UTF-8.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/LocalDaemonManager.swift` around lines 505 - 523, The loop in
LocalDaemonManager that reads socket chunks currently decodes each chunk into a
String (using `buffer` and `response`) which drops incomplete multibyte UTF‑8
sequences; instead, keep a raw byte accumulator (e.g., a Data/Array<UInt8> named
like `rawResponseBytes`) and append each read() result to it, check for a
newline byte in the raw buffer, and only then decode the complete line to UTF‑8
once and convert to String; update the code paths that use `response` (the read
loop in LocalDaemonManager and callers like `rpcSync`) to use the newly decoded
String so multibyte characters aren’t lost.
🟡 Minor comments (11)
docs/llms.txt-4-4 (1)

4-4: ⚠️ Potential issue | 🟡 Minor

Fix relative link path (currently points to docs/docs/...)

Because this file is already inside docs/, prefixing with docs/ breaks the link target.

Suggested fix
-- [20260404_Git开发与同步工作流.md](docs/20260404_Git开发与同步工作流.md)
+- [20260404_Git开发与同步工作流.md](20260404_Git开发与同步工作流.md)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/llms.txt` at line 4, In docs/llms.txt the Markdown link target wrongly
includes a redundant "docs/" prefix; update the link text
"[20260404_Git开发与同步工作流.md](docs/20260404_Git开发与同步工作流.md)" to remove the extra
directory prefix so it reads
"[20260404_Git开发与同步工作流.md](20260404_Git开发与同步工作流.md)" ensuring the relative path
is correct from inside the docs/ folder.
Sources/TabManager.swift-3402-3408 (1)

3402-3408: ⚠️ Potential issue | 🟡 Minor

Localize fallback window title strings.

Line 3402 and Line 3408 use bare user-facing literals ("jmux"). These should go through localization keys.

🔧 Suggested fix
 private func windowTitle(for tab: Workspace?) -> String {
-    guard let tab else { return "jmux" }
+    let fallbackTitle = String(localized: "window.title.appName", defaultValue: "jmux")
+    guard let tab else { return fallbackTitle }
@@
-    return trimmedDirectory.isEmpty ? "jmux" : trimmedDirectory
+    return trimmedDirectory.isEmpty ? fallbackTitle : trimmedDirectory
 }

Also add window.title.appName to Resources/Localizable.xcstrings (English + Japanese).
As per coding guidelines: "All user-facing strings must be localized using String(localized: "key.name", defaultValue: "English text") ... Never use bare string literals..."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TabManager.swift` around lines 3402 - 3408, Replace the two bare
"jmux" literals used as fallback window titles in TabManager.swift (the early
guard return for missing tab and the final ternary return after computing
trimmedDirectory) with a localized string call (use String(localized:
"window.title.appName", defaultValue: "jmux")) so the fallback goes through
localization APIs; also add the key window.title.appName to
Resources/Localizable.xcstrings for English and Japanese with appropriate
translations.
Resources/Localizable.xcstrings-1244-1266 (1)

1244-1266: ⚠️ Potential issue | 🟡 Minor

Pluralize sidebar.detachedSessions.panes for Ukrainian (and other count-sensitive locales).

Line 1250/1262 uses a single %d string form. For Ukrainian, plural categories vary by count, so this will produce grammatically wrong UI for many values. Please switch this key to a pluralized string-catalog entry with count variants.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Localizable.xcstrings` around lines 1244 - 1266, The key
"sidebar.detachedSessions.panes" currently uses a single "%d" form which breaks
Ukrainian plural rules; replace it with a pluralized string-catalog entry that
uses a count variable (e.g., "count") and provides CLDR plural categories for
each locale: English should have "one" and "other" (e.g., "%d pane" / "%d
panes"), Japanese can map all to "other" (still "%dペイン"), and Ukrainian must
include "one", "few", "many" and "other" forms with appropriate localized
endings; update the Resources localization entry for
"sidebar.detachedSessions.panes" to the pluralized format (stringsdict/xcstrings
plural form) so the runtime selects the correct variant by count.
Sources/SessionPersistence.swift-420-428 (1)

420-428: ⚠️ Potential issue | 🟡 Minor

Session files from previous versions will be lost on upgrade.

Changing the default session storage path from com.cmuxterm.app/cmux to com.jmux.app/jmux means existing users upgrading to this version will have their session snapshots stored at the old path (~/Library/Application Support/cmux/session-*.json) while the app now looks only in the new path (~/Library/Application Support/jmux/session-*.json).

The load function in SessionPersistenceStore does not check for sessions at legacy paths. Consider adding a fallback in SessionPersistenceStore.load() to check the old path if no sessions exist at the new location, similar to how legacyPersistedWindowGeometryDefaultsKeys are migrated in AppDelegate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/SessionPersistence.swift` around lines 420 - 428, Session files will
be lost because the new default path uses "com.jmux.app"/"jmux" but
SessionPersistenceStore.load() never checks legacy locations; update
SessionPersistenceStore.load() to fall back to the old path
("com.cmuxterm.app"/"cmux") when no sessions are found in the new path, using
the same safeBundleId/safe filename logic used in the current path generation
(session-<safeBundleId>.json), and when legacy files are found either load and
return them or move them into the new
resolvedAppSupport/appendingPathComponent("jmux") location to complete
migration; ensure the fallback only triggers when the new path is empty and
preserve existing behavior otherwise.
Sources/AppDelegate.swift-12265-12269 (1)

12265-12269: ⚠️ Potential issue | 🟡 Minor

Unread tooltips still regress to the old cmux brand.

The zero-unread branch now shows jmux, but as soon as displayedUnreadCount > 0 the tooltip switches back to cmux:. That leaves the status item branding inconsistent in normal use.

💡 Suggested change
-                    ? "cmux: " + String(localized: "statusMenu.tooltip.unread.one", defaultValue: "1 unread notification")
-                    : "cmux: " + String(localized: "statusMenu.tooltip.unread.other", defaultValue: "\(displayedUnreadCount) unread notifications")
+                    ? "jmux: " + String(localized: "statusMenu.tooltip.unread.one", defaultValue: "1 unread notification")
+                    : "jmux: " + String(localized: "statusMenu.tooltip.unread.other", defaultValue: "\(displayedUnreadCount) unread notifications")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 12265 - 12269, The tooltip branches
for button.toolTip use "cmux" for displayedUnreadCount > 0 causing inconsistent
branding; update the two non-zero branches to use "jmux" (or the correct brand
string) instead of "cmux:" while preserving the localized message construction
using String(localized: ...) and the displayedUnreadCount variable so all three
branches (zero, one, other) consistently show "jmux" branding; locate this logic
around button.toolTip and change the literal "cmux:" occurrences to "jmux"
(keeping the concatenation and localization calls intact).
Sources/TerminalController.swift-4100-4106 (1)

4100-4106: ⚠️ Potential issue | 🟡 Minor

Differentiate malformed IDs from missing IDs.

session_id and pane_id have the same ambiguity here: a present non-string/blank value is either reported as “missing” or ignored. These routes should reject malformed IDs explicitly so callers get a stable invalid_params contract.

Suggested fix
     private func v2SessionLocalAttach(params: [String: Any]) -> V2CallResult {
+        let hasSessionID = params.keys.contains("session_id")
         guard let sessionId = v2RawString(params, "session_id") else {
-            return .err(code: "invalid_params", message: "Missing session_id", data: nil)
+            let message = hasSessionID ? "session_id must be a string" : "Missing session_id"
+            return .err(code: "invalid_params", message: message, data: nil)
         }
+        if params.keys.contains("pane_id"), v2RawString(params, "pane_id") == nil {
+            return .err(code: "invalid_params", message: "pane_id must be a string", data: nil)
+        }

Apply the same hasSessionID pattern to session.local.detach and session.local.close.

Based on learnings: v2SurfaceSplitSized(params:) and v2WorkspaceClearTags(params:) return invalid_params when a key is present but invalid.

Also applies to: 4121-4123, 4137-4139

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TerminalController.swift` around lines 4100 - 4106, The guard for
session_id currently treats any non-string/blank value as "missing"; update the
code to use the same hasSessionID validation pattern used elsewhere so that if
the key exists but is malformed you return .err(code: "invalid_params", message:
"Invalid session_id", data: nil) rather than "Missing session_id". Apply the
same explicit validation to pane_id: if pane_id key is present but v2RawString
returns nil/blank, return an invalid_params error (do not silently ignore);
otherwise populate rpcParams as before. Make these changes for the handlers
referenced (session.local.detach and session.local.close) and mirror the
behavior used by v2SurfaceSplitSized(params:) and v2WorkspaceClearTags(params:)
so callers get a stable invalid_params contract.
Sources/TerminalController.swift-4077-4086 (1)

4077-4086: ⚠️ Potential issue | 🟡 Minor

Reject malformed optional params instead of silently dropping them.

v2RawString / v2StrictInt collapse “missing” and “wrong type” into the same nil, so session.local.new will ignore bad name / shell / cols / rows values and create a default session instead of returning invalid_params.

Suggested fix
     private func v2SessionLocalNew(params: [String: Any]) -> V2CallResult {
         let name = v2RawString(params, "name")
         let shell = v2RawString(params, "shell")
         let cols = v2StrictInt(params, "cols")
         let rows = v2StrictInt(params, "rows")
+
+        if params.keys.contains("name"), name == nil {
+            return .err(code: "invalid_params", message: "name must be a string", data: nil)
+        }
+        if params.keys.contains("shell"), shell == nil {
+            return .err(code: "invalid_params", message: "shell must be a string", data: nil)
+        }
+        if params.keys.contains("cols"), cols == nil {
+            return .err(code: "invalid_params", message: "cols must be an integer", data: nil)
+        }
+        if params.keys.contains("rows"), rows == nil {
+            return .err(code: "invalid_params", message: "rows must be an integer", data: nil)
+        }

Based on learnings: v2SurfaceSplitSized(params:) and v2WorkspaceClearTags(params:) return invalid_params when a key is present but invalid.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TerminalController.swift` around lines 4077 - 4086, The code
currently uses v2RawString and v2StrictInt to fetch optional params (name,
shell, cols, rows) and then only adds non-nil values to rpcParams, which hides
the difference between missing keys and present-but-wrong-type keys; change the
logic in the session.local.new handler to explicitly check
params.keys.contains("name"/"shell"/"cols"/"rows") and if a key is present but
the corresponding v2RawString/v2StrictInt returned nil, return an invalid_params
error immediately; otherwise, when the value is non-nil add it to rpcParams as
before (keep the rpcParams construction and the symbols v2RawString,
v2StrictInt, and rpcParams unchanged).
Sources/ContentView.swift-12389-12404 (1)

12389-12404: ⚠️ Potential issue | 🟡 Minor

Add a VoiceOver label to the icon-only reattach button.

.help(...) won't give this control an accessibility name, so assistive tech will announce the SF Symbol instead of the action. Reuse the existing localized tooltip string as an explicit .accessibilityLabel(...).

Suggested fix
             .buttonStyle(.plain)
             .foregroundStyle(isHovered ? .primary : .secondary)
             .help(String(localized: "sidebar.detachedSessions.reattach.tooltip", defaultValue: "Reattach session"))
+            .accessibilityLabel(
+                Text(String(localized: "sidebar.detachedSessions.reattach.tooltip", defaultValue: "Reattach session"))
+            )
             .disabled(isReattaching)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 12389 - 12404, The reattach Button
currently only uses .help(...) so VoiceOver will read the SF Symbol; update the
Button to provide an explicit accessibility label by adding
.accessibilityLabel(String(localized:
"sidebar.detachedSessions.reattach.tooltip", defaultValue: "Reattach session"))
to the Button chain (next to .help and .disabled) so assistive tech announces
the localized action; keep existing behavior for isReattaching,
reattachSession(), ProgressView and Image system icon unchanged.
CLI/cmux.swift-663-668 (1)

663-668: ⚠️ Potential issue | 🟡 Minor

Update the help text for the renamed default socket path.

The runtime defaults here moved from cmux to jmux, but Line 13419 still documents ~/Library/Application Support/cmux/cmux.sock. That will send users debugging socket issues to the wrong location.

Also applies to: 1295-1297

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 663 - 668, The help text and any hard-coded
documentation strings that still reference the old cmux paths must be updated to
the new jmux names: replace occurrences of the cmux application-support
directory and socket filename (e.g., "~/Library/Application
Support/cmux/cmux.sock" and "cmux.sock") with the new values that match the
runtime constants (appSupportDirectoryName and stableSocketFileName), e.g.
"~/Library/Application Support/jmux/jmux.sock"; also update any references to
the legacyDefaultSocketPath, fallbackSocketPath, and stagingSocketPath mentions
in help output or docs so they point to the correct jmux socket filenames/paths
rather than cmux.
daemon/local/test_e2e.sh-176-187 (1)

176-187: ⚠️ Potential issue | 🟡 Minor

Wait timeout logic doesn't match the stated max_wait.

The loop increments waited by 1 each iteration but sleeps for 0.2 seconds. With max_wait=5, this results in a maximum wait of ~1 second (5 × 0.2s), not 5 seconds as the error message implies.

🔧 Proposed fix
 wait_for_socket() {
-  local max_wait=5
+  local max_wait=25
   local waited=0
   while [ ! -S "$SOCKET_PATH" ] && [ $waited -lt $max_wait ]; do
     sleep 0.2
     waited=$((waited + 1))
   done
   if [ ! -S "$SOCKET_PATH" ]; then
-    echo "ERROR: daemon socket did not appear at $SOCKET_PATH within ${max_wait}s"
+    echo "ERROR: daemon socket did not appear at $SOCKET_PATH within 5s"
     return 1
   fi
 }

Alternatively, keep max_wait=5 and use sleep 1 for clearer semantics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@daemon/local/test_e2e.sh` around lines 176 - 187, The wait_for_socket() loop
currently sleeps 0.2s but increments waited by 1 so max_wait=5 yields ~1s total;
fix by making the units consistent: either change sleep to 1 and keep
waited++/max_wait semantics, or keep sleep 0.2 and set max_wait to 25 (or
increment waited by 0.2 using a float-aware approach). Update the variables
max_wait and/or sleep call in wait_for_socket() and ensure the error message
referencing $SOCKET_PATH and ${max_wait}s remains correct.
Sources/DaemonSessionBinding.swift-853-889 (1)

853-889: ⚠️ Potential issue | 🟡 Minor

Localize these LocalizedError descriptions.

These messages can surface directly to users, but they're hard-coded English literals. Please move them to String(localized:..., defaultValue:...) keys in Resources/Localizable.xcstrings with the same English/Japanese coverage as the new launch-agent errors.

As per coding guidelines, **/*.{swift,swiftui}: All user-facing strings must be localized using String(localized: "key.name", defaultValue: "English text") and keys must go in Resources/Localizable.xcstrings with translations for English and Japanese.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/DaemonSessionBinding.swift` around lines 853 - 889, Replace all
hard-coded English literals in the LocalizedError implementations with localized
lookups: change each string returned in DaemonBridgeError.errorDescription and
DaemonSessionError.errorDescription to use String(localized: "key",
defaultValue: "English text") (one key per case, e.g., keys for
fifoDirectoryCreationFailed, mkfifoFailed.path, socketCreateFailed,
socketPathTooLong, connectFailed, serializationFailed, writeFailed, readFailed,
parseFailed, attachFailed). Add corresponding entries with English and Japanese
values to Resources/Localizable.xcstrings using those keys, and include the
errno/path placeholders in the defaultValue where applicable so mkfifoFailed and
connectFailed/attachFailed still interpolate variables correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8c45b12-15b9-412e-b091-9c2c25dd8970

📥 Commits

Reviewing files that changed from the base of the PR and between 77939a2 and 0e61be2.

⛔ Files ignored due to path filters (1)
  • daemon/local/go.sum is excluded by !**/*.sum
📒 Files selected for processing (47)
  • CHANGELOG.md
  • CLI/cmux.swift
  • GhosttyTabs.xcodeproj/project.pbxproj
  • GhosttyTabs.xcodeproj/xcshareddata/xcschemes/jmux-ci.xcscheme
  • GhosttyTabs.xcodeproj/xcshareddata/xcschemes/jmux-unit.xcscheme
  • GhosttyTabs.xcodeproj/xcshareddata/xcschemes/jmux.xcscheme
  • Resources/Info.plist
  • Resources/InfoPlist.xcstrings
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/AppIconDockTilePlugin.swift
  • Sources/CmuxDirectoryTrust.swift
  • Sources/ContentView.swift
  • Sources/DaemonSessionBinding.swift
  • Sources/GhosttyConfig.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/KeyboardShortcutSettings.swift
  • Sources/LocalDaemonManager.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/TerminalPanel.swift
  • Sources/PostHogAnalytics.swift
  • Sources/SessionPersistence.swift
  • Sources/SocketControlSettings.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • Sources/TerminalNotificationStore.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift
  • cmuxTests/DaemonSessionTests.swift
  • daemon/local/README.md
  • daemon/local/cmd/cmux-local/main.go
  • daemon/local/cmd/cmuxd-local/main.go
  • daemon/local/com.cmux.daemon-local.plist
  • daemon/local/go.mod
  • daemon/local/integration_test.go
  • daemon/local/persistence.go
  • daemon/local/ringbuffer.go
  • daemon/local/ringbuffer_test.go
  • daemon/local/rpc.go
  • daemon/local/session.go
  • daemon/local/test_e2e.sh
  • docs/20260404_Git开发与同步工作流.md
  • docs/cmux-shortcuts.html
  • docs/llms.txt
  • scripts/reload.sh
  • scripts/reloadp.sh
  • scripts/setup.sh

@Sean529
Copy link
Copy Markdown
Author

Sean529 commented Apr 5, 2026

Closing — contains fork-specific jmux customizations not intended for upstream.

@Sean529 Sean529 closed this Apr 5, 2026
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