Conversation
…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>
|
@Sean529 is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR introduces v0.64.0, with the headline feature being a local Go daemon (
Confidence Score: 4/5Safe 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
Sequence DiagramsequenceDiagram
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
|
|
|
||
| // 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 | ||
| } |
There was a problem hiding this comment.
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.| <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"/> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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>
|
|
||
| enum SocketControlPasswordStore { | ||
| static let directoryName = "cmux" | ||
| static let directoryName = "jmux" |
There was a problem hiding this comment.
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>
| 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" |
There was a problem hiding this comment.
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>
| private static let legacyKeychainService = "com.jmux.app.socket-control" | |
| private static let legacyKeychainService = "com.cmuxterm.app.socket-control" |
| return v2Result(id: id, self.v2WorkspaceRemoteTerminalSessionEnd(params: params)) | ||
|
|
||
| // Local daemon session management | ||
| case "session.local.status": |
There was a problem hiding this comment.
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>
| rm -rf "$TMPDIR_TEST" | ||
| fi | ||
| # Clean up test socket and state file | ||
| rm -f "$SOCKET_PATH" "$STATE_PATH" 2>/dev/null || true |
There was a problem hiding this comment.
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>
| <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"/> |
There was a problem hiding this comment.
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>
| <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"/> |
|
|
||
| while true { | ||
| let count = read(fd, &buffer, buffer.count) | ||
| if count <= 0 { |
There was a problem hiding this comment.
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>
| /// process spawn when no launch agent is configured. | ||
| func ensureRunning() async { | ||
| // Fast path: daemon already confirmed running. | ||
| if isRunning, Self.probeSync() { |
There was a problem hiding this comment.
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>
| ) | ||
| return resolvedAppSupport | ||
| .appendingPathComponent("cmux", isDirectory: true) | ||
| .appendingPathComponent("jmux", isDirectory: true) |
There was a problem hiding this comment.
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>
| local waited=0 | ||
| while [ ! -S "$SOCKET_PATH" ] && [ $waited -lt $max_wait ]; do | ||
| sleep 0.2 | ||
| waited=$((waited + 1)) |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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 | 🟠 MajorHistory 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 existingcmux-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 | 🟡 MinorKeep a legacy loopback alias fallback for restored sessions.
Line 1733 renames the alias host, but persisted URLs using
cmux-loopback.localtest.meare 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 | 🟠 MajorRestore the configured modifier-hint predicate.
Hardcoding
falsehere disables sidebar/titlebar shortcut hints entirely and also breaks the close-button suppression wired toshowsModifierShortcutHints. This still needs to compare the normalized flags against the configuredselectWorkspaceByNumbershortcut 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 matchKeyboardShortcutSettings.shortcut(for: .selectWorkspaceByNumber).modifierFlags, andTabItemView’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 | 🟠 MajorAdd 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 existingcom.cmuxterm.appconfig 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 | 🟠 MajorPreserve existing trust state when moving store path.
Line 16 changes the persistence directory to
jmux, but there is no fallback/migration from the oldcmuxstore. 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 | 🟠 MajorDon't rename
cmux-cliin isolation.The target/product refs in this same project are still wired as
cmux(B9000004 /* cmux */and the app'sCopy CLIphase), so changing onlyPRODUCT_NAMEhere makes the produced executable name diverge from the file the app copies. Either keep the tool product namedcmux, 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-clitarget.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 | 🟠 MajorDon't rename the legacy password stores in place.
configuredPassword()now reads from the newApplication Support/jmux/...location, andlegacyKeychainServiceno 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 | 🟠 MajorDon'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 | 🟠 MajorUse
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 (requiringNSNull()instead), causingv2Encode()to fail theisValidJSONObject()guard and return anencode_errorresponse 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 | 🟠 MajorReject 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 = trueAlso 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 | 🟠 MajorPreserve the legacy password stores during this namespace migration.
The resolver now only looks under the
jmuxApplication Support directory and thecom.jmux.app.socket-controlkeychain service. Any upgraded install that still has its socket password under the oldcmuxnamespace 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 | 🟠 MajorKeep 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 | 🟠 MajorTagged debug sockets are no longer discoverable without
CMUX_TAG.This adds
/tmp/jmux-debug-<tag>.sockto the candidate list, but the directory scan below still only picks upcmux*.sock. Any implicit connection that relies on discovery instead of an in-processCMUX_TAGwill miss the livejmux-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 | 🟠 MajorLocalization catalog is incomplete: update the three missing close-tab warning keys and correct the stale
about.appNamedefaultValue.Three new user-facing strings added in Lines 4919-4922 (
settings.app.warnBeforeCloseTab,settings.app.warnBeforeCloseTab.subtitleOn,settings.app.warnBeforeCloseTab.subtitleOff) are missing fromResources/Localizable.xcstrings. Without these catalog entries, the UI will fall back to the inline EnglishdefaultValuestrings with no Japanese translation support.Additionally, Line 2836 still carries
defaultValue: "jmux", but the catalog showsabout.appNamehas 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.appNamedefaultValue from"jmux"to"cmux"in the code- Add
settings.app.warnBeforeCloseTab,settings.app.warnBeforeCloseTab.subtitleOn, andsettings.app.warnBeforeCloseTab.subtitleOfftoResources/Localizable.xcstringswith 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 | 🟠 MajorKeep the saved session id until the daemon proves the session is gone.
These branches clear
daemonSessionIDfor 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 afterlistSessionsAsync()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 | 🟠 MajorPick the restored terminal pane as the reattach target.
This chooses
focusedPaneIdor 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 ofexistingTerminalPanelIdsbefore 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 | 🟠 MajorSerialize reattach before the first
await.
daemonSessionBindingis only set after the async work completes. Because this method isasyncon@MainActor, another caller can enter while the first reattach is suspended onlistSessionsAsync()orTask.detachedand 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 | 🟠 MajorUse the active daemon binding state, not persisted ID, to guard detach-on-close.
Line 358 restores
daemonSessionIDbefore reattach completes. If the workspace closes between restore and successful reattach (while replay/local terminals exist butdaemonSessionBindingis still nil), line 8218 will incorrectly calldetachFromDaemon()on non-daemon panels.Line 8218–8220 should check
daemonSessionBinding != nilinstead ofdaemonSessionID != nilto 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 | 🟠 MajorSerialize saves instead of spawning one per mutation.
NotifyChange()firesSave()in a fresh goroutine, butSave()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 | 🟠 MajorPropagate shell exit into window/session status.
When the last pane exits, this loop only marks
p.Statusas dead.Session.Snapshot()still returns the cacheds.Status, andsession.listuses 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 | 🟠 MajorThe replay/live handoff can double-deliver bytes.
readLoop()appendsdataintop.ringbefore it snapshotsp.clients, whileAttach()snapshots the ring before it registers the new client. If PTY output lands in that gap, the same bytes are included inreplayand then sent again as the first livepty.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 | 🟠 MajorSend the attach ACK before starting replay.
Line 337 schedules
pty.replayon the same connection beforehandleClienthas written thesession.attachresponse, 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 flipsisRunningtofalse.Sources/DaemonSessionBinding.swift-513-527 (1)
513-527:⚠️ Potential issue | 🟠 MajorDon'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, socat <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 | 🟠 MajorBuffer raw bytes fully before decoding to UTF-8.
Line 521 decodes each socket read chunk to
Stringindependently. 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 accumulatedresponsestring incomplete. The partial JSON then fails to parse inrpcSync(line 200), causingsession.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 | 🟡 MinorFix relative link path (currently points to
docs/docs/...)Because this file is already inside
docs/, prefixing withdocs/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 | 🟡 MinorLocalize 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.appNametoResources/Localizable.xcstrings(English + Japanese).
As per coding guidelines: "All user-facing strings must be localized usingString(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 | 🟡 MinorPluralize
sidebar.detachedSessions.panesfor Ukrainian (and other count-sensitive locales).Line 1250/1262 uses a single
%dstring 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 | 🟡 MinorSession files from previous versions will be lost on upgrade.
Changing the default session storage path from
com.cmuxterm.app/cmuxtocom.jmux.app/jmuxmeans 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 howlegacyPersistedWindowGeometryDefaultsKeysare 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 | 🟡 MinorUnread tooltips still regress to the old
cmuxbrand.The zero-unread branch now shows
jmux, but as soon asdisplayedUnreadCount > 0the tooltip switches back tocmux:. 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 | 🟡 MinorDifferentiate malformed IDs from missing IDs.
session_idandpane_idhave 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 stableinvalid_paramscontract.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
hasSessionIDpattern tosession.local.detachandsession.local.close.Based on learnings:
v2SurfaceSplitSized(params:)andv2WorkspaceClearTags(params:)returninvalid_paramswhen 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 | 🟡 MinorReject malformed optional params instead of silently dropping them.
v2RawString/v2StrictIntcollapse “missing” and “wrong type” into the samenil, sosession.local.newwill ignore badname/shell/cols/rowsvalues and create a default session instead of returninginvalid_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:)andv2WorkspaceClearTags(params:)returninvalid_paramswhen 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 | 🟡 MinorAdd 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 | 🟡 MinorUpdate the help text for the renamed default socket path.
The runtime defaults here moved from
cmuxtojmux, 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 | 🟡 MinorWait timeout logic doesn't match the stated max_wait.
The loop increments
waitedby 1 each iteration but sleeps for 0.2 seconds. Withmax_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=5and usesleep 1for 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 | 🟡 MinorLocalize these
LocalizedErrordescriptions.These messages can surface directly to users, but they're hard-coded English literals. Please move them to
String(localized:..., defaultValue:...)keys inResources/Localizable.xcstringswith the same English/Japanese coverage as the new launch-agent errors.As per coding guidelines,
**/*.{swift,swiftui}: All user-facing strings must be localized usingString(localized: "key.name", defaultValue: "English text")and keys must go inResources/Localizable.xcstringswith 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
⛔ Files ignored due to path filters (1)
daemon/local/go.sumis excluded by!**/*.sum
📒 Files selected for processing (47)
CHANGELOG.mdCLI/cmux.swiftGhosttyTabs.xcodeproj/project.pbxprojGhosttyTabs.xcodeproj/xcshareddata/xcschemes/jmux-ci.xcschemeGhosttyTabs.xcodeproj/xcshareddata/xcschemes/jmux-unit.xcschemeGhosttyTabs.xcodeproj/xcshareddata/xcschemes/jmux.xcschemeResources/Info.plistResources/InfoPlist.xcstringsResources/Localizable.xcstringsSources/AppDelegate.swiftSources/AppIconDockTilePlugin.swiftSources/CmuxDirectoryTrust.swiftSources/ContentView.swiftSources/DaemonSessionBinding.swiftSources/GhosttyConfig.swiftSources/GhosttyTerminalView.swiftSources/KeyboardShortcutSettings.swiftSources/LocalDaemonManager.swiftSources/Panels/BrowserPanel.swiftSources/Panels/TerminalPanel.swiftSources/PostHogAnalytics.swiftSources/SessionPersistence.swiftSources/SocketControlSettings.swiftSources/TabManager.swiftSources/TerminalController.swiftSources/TerminalNotificationStore.swiftSources/Workspace.swiftSources/cmuxApp.swiftcmuxTests/DaemonSessionTests.swiftdaemon/local/README.mddaemon/local/cmd/cmux-local/main.godaemon/local/cmd/cmuxd-local/main.godaemon/local/com.cmux.daemon-local.plistdaemon/local/go.moddaemon/local/integration_test.godaemon/local/persistence.godaemon/local/ringbuffer.godaemon/local/ringbuffer_test.godaemon/local/rpc.godaemon/local/session.godaemon/local/test_e2e.shdocs/20260404_Git开发与同步工作流.mddocs/cmux-shortcuts.htmldocs/llms.txtscripts/reload.shscripts/reloadp.shscripts/setup.sh
|
Closing — contains fork-specific jmux customizations not intended for upstream. |
Release v0.64.0
Added
Changed
Fixed
Removed
🤖 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
Written for commit 0e61be2. Summary will update on new commits.
Summary by CodeRabbit
Release Notes v0.64.0
New Features
Changed
Bug Fixes
Removed