Skip to content

feat: Add session save/open feature for exporting and importing current session - #1427

Draft
ashutoshpw wants to merge 1 commit into
manaflow-ai:mainfrom
ashutoshpw:feat/sessions-persistence
Draft

ashutoshpw wants to merge 1 commit into
manaflow-ai:mainfrom
ashutoshpw:feat/sessions-persistence

Conversation

@ashutoshpw

@ashutoshpw ashutoshpw commented Mar 14, 2026 •

Copy link
Copy Markdown

Summary

  • add "Save Session…" (⇧⌘S) and "Open Session…" (⇧⌘O) menu items under File for exporting/importing the full app session as a .cmux-session JSON file
  • leverage the existing SessionPersistence snapshot infrastructure (AppSessionSnapshot, Workspace.sessionSnapshot(), TabManager.restoreSessionSnapshot()) — no new persistence model needed
  • capture foreground process commands (ssh, tmux, etc.) in snapshots so restored sessions re-launch non-shell commands via Ghostty's command config field
  • opening a session file creates new windows alongside existing ones (non-destructive); scrollback is excluded to keep files small
  • add session.save and session.open v2 socket commands and cmux session save/open CLI subcommands
  • localize all 6 new menu/dialog strings across 18 languages
  • register the .cmux-session UTType in Info.plist

Testing

  • ./scripts/setup.sh
  • ./scripts/reload.sh --tag session-save-open
  • Manually verified: save a multi-window session with splits and workspaces, quit, re-open, import the file — all windows/workspaces/splits restored with correct working directories and foreground commands

Context

  • Motivated by losing a complex session layout after closing cmux — the built-in autosave only restores the last session on relaunch, not arbitrary saved snapshots
  • Exported .cmux-session files can also serve as reusable workspace templates (e.g. save a "backend dev" layout and import it whenever needed, similar to vscode "Open Workspace from file")

Issues


Summary by cubic

Add session export/import to save the full app state to a .cmux-session JSON file and reopen it later as new windows. Restores windows, workspaces, tabs, splits, and non-shell foreground commands; scrollback is excluded.

  • New Features
    • File menu: “Save Session…” (Shift+Cmd+S) and “Open Session…” (Shift+Cmd+O).
    • Uses existing snapshot pipeline; imports create new windows without touching the current ones.
    • Captures foreground commands (e.g., ssh/tmux) and restores them via Ghostty’s command.
    • CLI: cmux session save <path> and cmux session open <path>; Socket: session.save, session.open.
    • Localized strings for menu items and dialog titles/prompts across 18 languages.

Written for commit 39df922. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • New Features
    • Save and restore workspace sessions: Export your current workspace configuration to a file and restore it later using the new "Save Session…" and "Open Session…" menu items with keyboard shortcuts Cmd+Shift+S and Cmd+Shift+O.
    • CLI session management: Introduced cmux session save <path> and cmux session open <path> commands for managing sessions from the command line.

@vercel

vercel Bot commented Mar 14, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 14, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request introduces session save/open functionality to cmux. It adds new CLI commands (cmux session save/open), AppDelegate methods for exporting/importing sessions, session persistence logic with file I/O, and propagates an optional command parameter through the terminal creation pipeline. Menu items and localization strings support the new UI.

Changes

Cohort / File(s) Summary
CLI Command Implementation
CLI/cmux.swift
Adds new top-level session command with save and open subcommands. Validates paths, calls remote session methods, and returns JSON or human-readable output.
Session Persistence & Serialization
Sources/SessionPersistence.swift
Introduces SessionExportStore enum with save() and load() methods for JSON serialization/deserialization. Adds optional command field to SessionTerminalPanelSnapshot.
Session Export/Import Endpoints
Sources/TerminalController.swift
Implements v2 RPC methods v2SessionSave and v2SessionOpen to handle session export/import with file I/O, directory validation, and window creation from snapshots.
Session Export/Import UI
Sources/AppDelegate.swift
Adds saveSessionToFile() and openSessionFromFile() methods to present file panels and coordinate session persistence via SessionExportStore.
Command Parameter Propagation
Sources/GhosttyTerminalView.swift, Sources/Panels/TerminalPanel.swift, Sources/Workspace.swift
Propagates optional command parameter through terminal surface creation chain. Command is stored in snapshots and restored during session load or workspace operations.
Menu Integration
Sources/cmuxApp.swift
Adds "Save Session…" (Cmd+Shift+S) and "Open Session…" (Cmd+Shift+O) menu items that invoke session export/import methods.
Localization Strings
Resources/Localizable.xcstrings
Adds 70+ new localized string entries for session menu items, workspace placement options, update process messages, notification settings, and UI configuration labels across multiple languages.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Menu as Menu Item
    participant AppDelegate
    participant FilePanel as File Panel
    participant SessionExportStore
    participant File as File System
    participant TerminalController as RPC Handler

    User->>Menu: Click "Save Session…"
    Menu->>AppDelegate: saveSessionToFile()
    AppDelegate->>FilePanel: Present save panel
    User->>FilePanel: Select location & confirm
    FilePanel-->>AppDelegate: File path
    AppDelegate->>AppDelegate: buildSessionSnapshot(includeScrollback: false)
    AppDelegate->>SessionExportStore: save(snapshot, to: fileURL)
    SessionExportStore->>SessionExportStore: Encode snapshot as JSON
    SessionExportStore->>File: Write atomically
    File-->>SessionExportStore: Success
    SessionExportStore-->>AppDelegate: Boolean result
    AppDelegate-->>User: Confirm save complete
Loading
sequenceDiagram
    actor User
    participant Menu as Menu Item
    participant AppDelegate
    participant FilePanel as File Panel
    participant SessionExportStore
    participant File as File System
    participant TerminalController as Workspace Creator

    User->>Menu: Click "Open Session…"
    Menu->>AppDelegate: openSessionFromFile()
    AppDelegate->>FilePanel: Present open panel
    User->>FilePanel: Select file & confirm
    FilePanel-->>AppDelegate: File path
    AppDelegate->>SessionExportStore: load(from: fileURL)
    SessionExportStore->>File: Read file data
    File-->>SessionExportStore: JSON bytes
    SessionExportStore->>SessionExportStore: Decode into AppSessionSnapshot
    SessionExportStore->>SessionExportStore: Validate version & windows
    SessionExportStore-->>AppDelegate: Snapshot or nil
    AppDelegate->>TerminalController: Create windows from snapshots
    TerminalController->>TerminalController: Recreate panes, panels, terminals
    TerminalController-->>AppDelegate: Windows created
    AppDelegate-->>User: Session restored
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Wiggles nose with joy
Sessions bundled, saved with care,
Commands flow through the air,
Open folders, windows gleam,
Every snapshot a perfect dream! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.83% 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 clearly and concisely summarizes the main change: adding a session save/open feature for exporting and importing sessions.
Description check ✅ Passed The description covers all required template sections: Summary (what changed and why), Testing (setup steps and manual verification), and includes context about related issues and motivation.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

CodeRabbit can generate a title for your PR based on the changes.

Add @coderabbitai placeholder anywhere in the title of your PR and CodeRabbit will replace it with a title based on the changes in the PR. You can change the placeholder by changing the reviews.auto_title_placeholder setting.

…on files

Allow users to save their entire session (all windows, workspaces, tabs,
splits, and foreground commands like ssh/tmux) to a .cmux-session JSON file
and restore it later as new windows. Accessible via File menu (Shift+Cmd+S /
Shift+Cmd+O), socket commands (session.save / session.open), and CLI
(cmux session save/open <path>).

- Add command field to SessionTerminalPanelSnapshot for restoring shell commands
- Add SessionExportStore for pretty-printed JSON export/import
- Add Save Session / Open Session menu items, socket commands, and CLI subcommands
- Add localized strings for all 18 supported languages
@ashutoshpw
ashutoshpw force-pushed the feat/sessions-persistence branch from 0576b67 to 39df922 Compare March 14, 2026 07:17
@ashutoshpw ashutoshpw changed the title Add session save/open feature for exporting and importing .cmux-sessi… feat: Add session save/open feature for exporting and importing current session Mar 14, 2026
@ashutoshpw
ashutoshpw marked this pull request as ready for review March 14, 2026 07:20

@greptile-apps greptile-apps Bot left a comment

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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

5 issues found across 9 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/AppDelegate.swift">

<violation number="1" location="Sources/AppDelegate.swift:5043">
P2: Handle `SessionExportStore.save` failure in the menu save path; currently export errors are silently ignored.</violation>

<violation number="2" location="Sources/AppDelegate.swift:5057">
P2: Avoid silent failure when opening a session file; notify the user when `SessionExportStore.load` returns `nil`.</violation>
</file>

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

<violation number="1" location="Sources/GhosttyTerminalView.swift:3105">
P1: Clear `surfaceConfig.command` when no explicit command is requested to avoid inheriting stale commands into new surfaces.</violation>
</file>

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

<violation number="1" location="Sources/Workspace.swift:321">
P1: Default terminal tabs can be persisted as command "Terminal", causing broken session restore commands.</violation>
</file>

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

<violation number="1" location="Sources/SessionPersistence.swift:227">
P1: Opening a session file can execute arbitrary commands from `terminal.command` with no trust gate. Imported snapshots should require explicit confirmation or strip executable commands by default.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

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

Comment on lines +3105 to +3112
if let command = self.command, !command.isEmpty {
command.withCString { cCommand in
surfaceConfig.command = cCommand
createSurface()
}
} else {
createSurface()
}

@cubic-dev-ai cubic-dev-ai Bot Mar 14, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Clear surfaceConfig.command when no explicit command is requested to avoid inheriting stale commands into new surfaces.

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

<comment>Clear `surfaceConfig.command` when no explicit command is requested to avoid inheriting stale commands into new surfaces.</comment>

<file context>
@@ -3098,13 +3101,24 @@ final class TerminalSurface: Identifiable, ObservableObject {
         }
 
+        let applyCommandAndCreate = {
+            if let command = self.command, !command.isEmpty {
+                command.withCString { cCommand in
+                    surfaceConfig.command = cCommand
</file context>
Suggested change
if let command = self.command, !command.isEmpty {
command.withCString { cCommand in
surfaceConfig.command = cCommand
createSurface()
}
} else {
createSurface()
}
if let command = self.command, !command.isEmpty {
command.withCString { cCommand in
surfaceConfig.command = cCommand
createSurface()
}
} else {
surfaceConfig.command = nil
createSurface()
}
Fix with Cubic

Comment thread Sources/Workspace.swift
Comment on lines +321 to +324
let shellNames: Set<String> = [
"bash", "zsh", "fish", "sh", "dash", "tcsh", "csh", "ksh",
"nu", "nushell", "pwsh", "elvish", "login",
]

@cubic-dev-ai cubic-dev-ai Bot Mar 14, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Default terminal tabs can be persisted as command "Terminal", causing broken session restore commands.

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

<comment>Default terminal tabs can be persisted as command "Terminal", causing broken session restore commands.</comment>

<file context>
@@ -316,9 +316,20 @@ extension Workspace {
             )
+            let snapshotCommand: String? = {
+                let title = panelTitles[panelId] ?? ""
+                let shellNames: Set<String> = [
+                    "bash", "zsh", "fish", "sh", "dash", "tcsh", "csh", "ksh",
+                    "nu", "nushell", "pwsh", "elvish", "login",
</file context>
Suggested change
let shellNames: Set<String> = [
"bash", "zsh", "fish", "sh", "dash", "tcsh", "csh", "ksh",
"nu", "nushell", "pwsh", "elvish", "login",
]
let shellNames: Set<String> = [
"bash", "zsh", "fish", "sh", "dash", "tcsh", "csh", "ksh",
"nu", "nushell", "pwsh", "elvish", "login", "terminal",
]
Fix with Cubic

struct SessionTerminalPanelSnapshot: Codable, Sendable {
var workingDirectory: String?
var scrollback: String?
var command: String?

@cubic-dev-ai cubic-dev-ai Bot Mar 14, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Opening a session file can execute arbitrary commands from terminal.command with no trust gate. Imported snapshots should require explicit confirmation or strip executable commands by default.

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

<comment>Opening a session file can execute arbitrary commands from `terminal.command` with no trust gate. Imported snapshots should require explicit confirmation or strip executable commands by default.</comment>

<file context>
@@ -224,6 +224,7 @@ struct SessionGitBranchSnapshot: Codable, Sendable {
 struct SessionTerminalPanelSnapshot: Codable, Sendable {
     var workingDirectory: String?
     var scrollback: String?
+    var command: String?
 }
 
</file context>
Fix with Cubic

Comment thread Sources/AppDelegate.swift
panel.title = String(localized: "menu.file.openSession.panelTitle", defaultValue: "Open Session")
panel.prompt = String(localized: "menu.file.openSession.panelPrompt", defaultValue: "Open")
guard panel.runModal() == .OK, let url = panel.url else { return }
guard let snapshot = SessionExportStore.load(from: url) else { return }

@cubic-dev-ai cubic-dev-ai Bot Mar 14, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Avoid silent failure when opening a session file; notify the user when SessionExportStore.load returns nil.

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

<comment>Avoid silent failure when opening a session file; notify the user when `SessionExportStore.load` returns `nil`.</comment>

<file context>
@@ -5026,6 +5027,44 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
+        panel.title = String(localized: "menu.file.openSession.panelTitle", defaultValue: "Open Session")
+        panel.prompt = String(localized: "menu.file.openSession.panelPrompt", defaultValue: "Open")
+        guard panel.runModal() == .OK, let url = panel.url else { return }
+        guard let snapshot = SessionExportStore.load(from: url) else { return }
+        for windowSnapshot in snapshot.windows.prefix(SessionPersistencePolicy.maxWindowsPerSnapshot) {
+            createMainWindow(sessionWindowSnapshot: windowSnapshot)
</file context>
Suggested change
guard let snapshot = SessionExportStore.load(from: url) else { return }
guard let snapshot = SessionExportStore.load(from: url) else {
NSSound.beep()
return
}
Fix with Cubic

Comment thread Sources/AppDelegate.swift
panel.title = String(localized: "menu.file.saveSession.panelTitle", defaultValue: "Save Session")
panel.prompt = String(localized: "menu.file.saveSession.panelPrompt", defaultValue: "Save")
guard panel.runModal() == .OK, let url = panel.url else { return }
SessionExportStore.save(snapshot, to: url)

@cubic-dev-ai cubic-dev-ai Bot Mar 14, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Handle SessionExportStore.save failure in the menu save path; currently export errors are silently ignored.

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

<comment>Handle `SessionExportStore.save` failure in the menu save path; currently export errors are silently ignored.</comment>

<file context>
@@ -5026,6 +5027,44 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
+        panel.title = String(localized: "menu.file.saveSession.panelTitle", defaultValue: "Save Session")
+        panel.prompt = String(localized: "menu.file.saveSession.panelPrompt", defaultValue: "Save")
+        guard panel.runModal() == .OK, let url = panel.url else { return }
+        SessionExportStore.save(snapshot, to: url)
+    }
+
</file context>
Suggested change
SessionExportStore.save(snapshot, to: url)
guard SessionExportStore.save(snapshot, to: url) else {
NSSound.beep()
return
}
Fix with Cubic

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 2088-2118: The session save/open handlers currently only use
subArgs.first and ignore any trailing tokens; update both cases (the "save" and
"open" branches that reference subArgs, resolvePath, client.sendV2, jsonOutput,
formatIDs, idFormat) to reject unexpected extra arguments by validating
subArgs.count == 1 (or that there are no additional tokens) and throw a CLIError
with a clear usage message if extra tokens are present, before resolving the
path and calling client.sendV2.

In `@Resources/Localizable.xcstrings`:
- Around line 32761-32873: The menu labels menu.file.openSession and
menu.file.saveSession use mixed ellipsis forms; normalize them to a single
Unicode ellipsis character "…" (no leading space) across all locale
stringUnit.value entries: search for variants " ...", " …", and "..." under both
keys and replace them with a single trailing "…" so every localization uses the
same convention.

In `@Sources/AppDelegate.swift`:
- Line 5038: Replace the hard-coded filename stem "session" with a localized
string; update the assignment to use String(localized:..., defaultValue:...) for
the stem and then append ".\(SessionExportStore.fileExtension)". Specifically
change the use of panel.nameFieldStringValue so it builds the filename from
String(localized: "session", defaultValue: "session") (or your chosen
localization key) combined with SessionExportStore.fileExtension.
- Line 5057: The guard silently returns when SessionExportStore.load(from: url)
fails; change the call site in AppDelegate.swift to surface parse errors to the
user by handling the failure instead of returning: modify
SessionExportStore.load(from:) to throw or return a Result/optional+error, then
replace the guard let snapshot = SessionExportStore.load(from: url) else {
return } with a do/catch or Result switch that captures the parsing error and
presents a user-facing message (e.g., NSAlert or your app’s error UI) describing
the import failure and the underlying error, referencing the
SessionExportStore.load(from:) call and the local snapshot variable so callers
clearly log and display the parsing error.
- Around line 5046-5060: openSessionFromFile currently restores windows
immediately after loading a snapshot, which can relaunch foreground commands;
update openSessionFromFile to detect whether the loaded snapshot or any
windowSnapshot contains relaunchable/foreground commands (inspect the
snapshot/windowSnapshot properties that describe processes or a relaunchable
flag), and if so present a modal confirmation dialog to the user listing the
risk and requiring explicit consent before calling createMainWindow for those
window snapshots; if the user cancels, abort restoration and do not call
createMainWindow for any relaunchable windows (still allow restoring
non-relaunchable windows only after explicit consent).

In `@Sources/SessionPersistence.swift`:
- Around line 443-449: The load(from:) function reads the entire session file
into memory without size limits, risking OOM on large .cmux-session files;
before creating Data(contentsOf:), check the file size using FileManager or URL
resourceValues (e.g., values(forKey: .fileSizeKey) or attributesOfItem) and
reject files over a reasonable cap (choose a constant max size) by returning
nil, then proceed to decode AppSessionSnapshot and validate
SessionSnapshotSchema.currentVersion and non-empty windows as before.

In `@Sources/TerminalController.swift`:
- Around line 2177-2181: The code registers RPC handlers for "session.save" and
"session.open" (v2SessionSave and v2SessionOpen) but never advertises them in
the system.capabilities list, causing clients to think these methods are
unsupported; update the capability discovery where system.capabilities is
assembled to include the strings "session.save" and "session.open" (alongside
existing capability entries) so the advertised capabilities reflect the
registered handlers (ensure names match exactly the case used in the switch:
"session.save" and "session.open").
- Around line 6283-6297: The restore currently returns .ok even when no windows
were actually created; after the v2MainSync loop that calls
AppDelegate.shared?.createMainWindow(sessionWindowSnapshot:) and collects
windowIds, check if windowIds.count is zero and return an error result instead
of .ok (e.g., return a failure/error enum case with a descriptive message like
"no windows restored" and include filePath for context); keep the existing
successful .ok response when windowIds.count > 0 and still include
"windows_created" and "window_ids" as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f32332e6-fac2-45eb-85da-0b252b2e6e32

📥 Commits

Reviewing files that changed from the base of the PR and between 6c644d9 and 39df922.

📒 Files selected for processing (9)
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/TerminalPanel.swift
  • Sources/SessionPersistence.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift

Comment thread CLI/cmux.swift
Comment on lines +2088 to +2118
switch subcommand {
case "save":
guard let rawPath = subArgs.first, !rawPath.isEmpty else {
throw CLIError(message: "session save requires a file path. Usage: cmux session save <path>")
}
let absolutePath = resolvePath(rawPath)
let payload = try client.sendV2(method: "session.save", params: ["path": absolutePath])
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
} else {
let savedPath = (payload["path"] as? String) ?? absolutePath
print("OK path=\(savedPath)")
}

case "open":
guard let rawPath = subArgs.first, !rawPath.isEmpty else {
throw CLIError(message: "session open requires a file path. Usage: cmux session open <path>")
}
let absolutePath = resolvePath(rawPath)
let payload = try client.sendV2(method: "session.open", params: ["path": absolutePath])
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
} else {
let windowCount = (payload["windows_created"] as? Int) ?? 0
let savedPath = (payload["path"] as? String) ?? absolutePath
print("OK windows_created=\(windowCount) path=\(savedPath)")
}

default:
throw CLIError(message: "Unknown session subcommand: \(subcommand). Usage: cmux session save <path> | cmux session open <path>")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reject unexpected trailing arguments in session save/open.

At Line 2090 and Line 2103, only the first token after the subcommand is used as path; extra tokens are silently ignored. That can mask typos/misuse while still performing file operations.

♻️ Proposed fix
         case "save":
             guard let rawPath = subArgs.first, !rawPath.isEmpty else {
                 throw CLIError(message: "session save requires a file path. Usage: cmux session save <path>")
             }
+            if subArgs.count > 1 {
+                let extras = subArgs.dropFirst().joined(separator: " ")
+                throw CLIError(message: "session save: unexpected argument(s): \(extras)")
+            }
             let absolutePath = resolvePath(rawPath)
             let payload = try client.sendV2(method: "session.save", params: ["path": absolutePath])

         case "open":
             guard let rawPath = subArgs.first, !rawPath.isEmpty else {
                 throw CLIError(message: "session open requires a file path. Usage: cmux session open <path>")
             }
+            if subArgs.count > 1 {
+                let extras = subArgs.dropFirst().joined(separator: " ")
+                throw CLIError(message: "session open: unexpected argument(s): \(extras)")
+            }
             let absolutePath = resolvePath(rawPath)
             let payload = try client.sendV2(method: "session.open", params: ["path": absolutePath])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
switch subcommand {
case "save":
guard let rawPath = subArgs.first, !rawPath.isEmpty else {
throw CLIError(message: "session save requires a file path. Usage: cmux session save <path>")
}
let absolutePath = resolvePath(rawPath)
let payload = try client.sendV2(method: "session.save", params: ["path": absolutePath])
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
} else {
let savedPath = (payload["path"] as? String) ?? absolutePath
print("OK path=\(savedPath)")
}
case "open":
guard let rawPath = subArgs.first, !rawPath.isEmpty else {
throw CLIError(message: "session open requires a file path. Usage: cmux session open <path>")
}
let absolutePath = resolvePath(rawPath)
let payload = try client.sendV2(method: "session.open", params: ["path": absolutePath])
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
} else {
let windowCount = (payload["windows_created"] as? Int) ?? 0
let savedPath = (payload["path"] as? String) ?? absolutePath
print("OK windows_created=\(windowCount) path=\(savedPath)")
}
default:
throw CLIError(message: "Unknown session subcommand: \(subcommand). Usage: cmux session save <path> | cmux session open <path>")
}
switch subcommand {
case "save":
guard let rawPath = subArgs.first, !rawPath.isEmpty else {
throw CLIError(message: "session save requires a file path. Usage: cmux session save <path>")
}
if subArgs.count > 1 {
let extras = subArgs.dropFirst().joined(separator: " ")
throw CLIError(message: "session save: unexpected argument(s): \(extras)")
}
let absolutePath = resolvePath(rawPath)
let payload = try client.sendV2(method: "session.save", params: ["path": absolutePath])
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
} else {
let savedPath = (payload["path"] as? String) ?? absolutePath
print("OK path=\(savedPath)")
}
case "open":
guard let rawPath = subArgs.first, !rawPath.isEmpty else {
throw CLIError(message: "session open requires a file path. Usage: cmux session open <path>")
}
if subArgs.count > 1 {
let extras = subArgs.dropFirst().joined(separator: " ")
throw CLIError(message: "session open: unexpected argument(s): \(extras)")
}
let absolutePath = resolvePath(rawPath)
let payload = try client.sendV2(method: "session.open", params: ["path": absolutePath])
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
} else {
let windowCount = (payload["windows_created"] as? Int) ?? 0
let savedPath = (payload["path"] as? String) ?? absolutePath
print("OK windows_created=\(windowCount) path=\(savedPath)")
}
default:
throw CLIError(message: "Unknown session subcommand: \(subcommand). Usage: cmux session save <path> | cmux session open <path>")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 2088 - 2118, The session save/open handlers
currently only use subArgs.first and ignore any trailing tokens; update both
cases (the "save" and "open" branches that reference subArgs, resolvePath,
client.sendV2, jsonOutput, formatIDs, idFormat) to reject unexpected extra
arguments by validating subArgs.count == 1 (or that there are no additional
tokens) and throw a CLIError with a clear usage message if extra tokens are
present, before resolving the path and calling client.sendV2.

Comment on lines +32761 to +32873
"menu.file.openSession": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Open Session…"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "セッションを開く…"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "打开会话..."
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "開啟工作階段..."
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "세션 열기…"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Sitzung öffnen …"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Abrir sesión…"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Ouvrir une session..."
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Apri sessione…"
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Åbn session…"
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Otwórz sesję…"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Открыть сессию..."
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Otvori sesiju…"
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "فتح جلسة…"
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Åpne økt …"
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Abrir Sessão…"
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "เปิดเซสชัน..."
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "Oturumu Aç…"
}
}
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the suffix style used by the new dialog-launching File menu items.
python - <<'PY'
import json
from pathlib import Path

strings = json.loads(Path("Resources/Localizable.xcstrings").read_text(encoding="utf-8"))["strings"]

def suffix_kind(value: str) -> str:
    if value.endswith(" …"):
        return "space+ellipsis"
    if value.endswith("…"):
        return "ellipsis"
    if value.endswith("..."):
        return "three-dots"
    return "other"

for key in ("menu.file.openSession", "menu.file.saveSession"):
    print(f"\n{key}")
    for locale, entry in sorted(strings[key]["localizations"].items()):
        value = entry["stringUnit"]["value"]
        print(f"{locale:8} {suffix_kind(value):14} {value}")
PY

Repository: manaflow-ai/cmux

Length of output: 1465


Normalize ellipsis style across File-menu commands.

menu.file.openSession and menu.file.saveSession currently mix three suffix styles—…, …, and ...—across locales (18 total). Use one consistent ellipsis convention across both commands and all locales before shipping.

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

In `@Resources/Localizable.xcstrings` around lines 32761 - 32873, The menu labels
menu.file.openSession and menu.file.saveSession use mixed ellipsis forms;
normalize them to a single Unicode ellipsis character "…" (no leading space)
across all locale stringUnit.value entries: search for variants " ...", " …",
and "..." under both keys and replace them with a single trailing "…" so every
localization uses the same convention.

Comment thread Sources/AppDelegate.swift
panel.allowedContentTypes = [
UTType(filenameExtension: SessionExportStore.fileExtension) ?? .json,
]
panel.nameFieldStringValue = "session.\(SessionExportStore.fileExtension)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Localize the save panel’s default filename stem.

"session" is user-visible text in the save dialog and should be localized like the other dialog strings.

🌐 Suggested fix
-        panel.nameFieldStringValue = "session.\(SessionExportStore.fileExtension)"
+        let defaultFilename = String(
+            localized: "menu.file.saveSession.defaultFilename",
+            defaultValue: "session"
+        )
+        panel.nameFieldStringValue = "\(defaultFilename).\(SessionExportStore.fileExtension)"

As per coding guidelines “All user-facing strings must be localized. Use String(localized:..., defaultValue: ...) for every string shown in the UI.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
panel.nameFieldStringValue = "session.\(SessionExportStore.fileExtension)"
let defaultFilename = String(
localized: "menu.file.saveSession.defaultFilename",
defaultValue: "session"
)
panel.nameFieldStringValue = "\(defaultFilename).\(SessionExportStore.fileExtension)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` at line 5038, Replace the hard-coded filename stem
"session" with a localized string; update the assignment to use
String(localized:..., defaultValue:...) for the stem and then append
".\(SessionExportStore.fileExtension)". Specifically change the use of
panel.nameFieldStringValue so it builds the filename from String(localized:
"session", defaultValue: "session") (or your chosen localization key) combined
with SessionExportStore.fileExtension.

Comment thread Sources/AppDelegate.swift
Comment on lines +5046 to +5060
@objc func openSessionFromFile(_ sender: Any?) {
let panel = NSOpenPanel()
panel.allowedContentTypes = [
UTType(filenameExtension: SessionExportStore.fileExtension) ?? .json,
]
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.allowsMultipleSelection = false
panel.title = String(localized: "menu.file.openSession.panelTitle", defaultValue: "Open Session")
panel.prompt = String(localized: "menu.file.openSession.panelPrompt", defaultValue: "Open")
guard panel.runModal() == .OK, let url = panel.url else { return }
guard let snapshot = SessionExportStore.load(from: url) else { return }
for windowSnapshot in snapshot.windows.prefix(SessionPersistencePolicy.maxWindowsPerSnapshot) {
createMainWindow(sessionWindowSnapshot: windowSnapshot)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Require explicit confirmation before importing sessions that can relaunch commands.

openSessionFromFile(_:) restores snapshot windows immediately after load. Since session files can contain relaunchable foreground commands, importing an untrusted file can execute unexpected commands.

🔒 Suggested safeguard
     `@objc` func openSessionFromFile(_ sender: Any?) {
         let panel = NSOpenPanel()
         panel.allowedContentTypes = [
             UTType(filenameExtension: SessionExportStore.fileExtension) ?? .json,
         ]
         panel.canChooseFiles = true
         panel.canChooseDirectories = false
         panel.allowsMultipleSelection = false
         panel.title = String(localized: "menu.file.openSession.panelTitle", defaultValue: "Open Session")
         panel.prompt = String(localized: "menu.file.openSession.panelPrompt", defaultValue: "Open")
         guard panel.runModal() == .OK, let url = panel.url else { return }
         guard let snapshot = SessionExportStore.load(from: url) else { return }
+
+        let warning = NSAlert()
+        warning.alertStyle = .warning
+        warning.messageText = String(
+            localized: "menu.file.openSession.confirm.title",
+            defaultValue: "Open Session?"
+        )
+        warning.informativeText = String(
+            localized: "menu.file.openSession.confirm.message",
+            defaultValue: "Opening a session may relaunch commands saved in this file. Only open trusted session files."
+        )
+        warning.addButton(withTitle: String(localized: "menu.file.openSession.panelPrompt", defaultValue: "Open"))
+        warning.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
+        guard warning.runModal() == .alertFirstButtonReturn else { return }
+
         for windowSnapshot in snapshot.windows.prefix(SessionPersistencePolicy.maxWindowsPerSnapshot) {
             createMainWindow(sessionWindowSnapshot: windowSnapshot)
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 5046 - 5060, openSessionFromFile
currently restores windows immediately after loading a snapshot, which can
relaunch foreground commands; update openSessionFromFile to detect whether the
loaded snapshot or any windowSnapshot contains relaunchable/foreground commands
(inspect the snapshot/windowSnapshot properties that describe processes or a
relaunchable flag), and if so present a modal confirmation dialog to the user
listing the risk and requiring explicit consent before calling createMainWindow
for those window snapshots; if the user cancels, abort restoration and do not
call createMainWindow for any relaunchable windows (still allow restoring
non-relaunchable windows only after explicit consent).

Comment thread Sources/AppDelegate.swift
panel.title = String(localized: "menu.file.openSession.panelTitle", defaultValue: "Open Session")
panel.prompt = String(localized: "menu.file.openSession.panelPrompt", defaultValue: "Open")
guard panel.runModal() == .OK, let url = panel.url else { return }
guard let snapshot = SessionExportStore.load(from: url) else { return }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Surface import errors instead of silently returning on parse failure.

If parsing fails, the current flow exits with no user feedback, which makes the feature appear broken.

🛠️ Suggested user-facing error path
-        guard let snapshot = SessionExportStore.load(from: url) else { return }
+        guard let snapshot = SessionExportStore.load(from: url) else {
+            let alert = NSAlert()
+            alert.alertStyle = .warning
+            alert.messageText = String(
+                localized: "menu.file.openSession.error.title",
+                defaultValue: "Couldn’t Open Session"
+            )
+            alert.informativeText = String(
+                localized: "menu.file.openSession.error.message",
+                defaultValue: "The selected session file is invalid or unreadable."
+            )
+            alert.addButton(withTitle: String(localized: "common.ok", defaultValue: "OK"))
+            alert.runModal()
+            return
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
guard let snapshot = SessionExportStore.load(from: url) else { return }
guard let snapshot = SessionExportStore.load(from: url) else {
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = String(
localized: "menu.file.openSession.error.title",
defaultValue: "Couldn't Open Session"
)
alert.informativeText = String(
localized: "menu.file.openSession.error.message",
defaultValue: "The selected session file is invalid or unreadable."
)
alert.addButton(withTitle: String(localized: "common.ok", defaultValue: "OK"))
alert.runModal()
return
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` at line 5057, The guard silently returns when
SessionExportStore.load(from: url) fails; change the call site in
AppDelegate.swift to surface parse errors to the user by handling the failure
instead of returning: modify SessionExportStore.load(from:) to throw or return a
Result/optional+error, then replace the guard let snapshot =
SessionExportStore.load(from: url) else { return } with a do/catch or Result
switch that captures the parsing error and presents a user-facing message (e.g.,
NSAlert or your app’s error UI) describing the import failure and the underlying
error, referencing the SessionExportStore.load(from:) call and the local
snapshot variable so callers clearly log and display the parsing error.

Comment on lines +443 to +449
static func load(from fileURL: URL) -> AppSessionSnapshot? {
guard let data = try? Data(contentsOf: fileURL) else { return nil }
let decoder = JSONDecoder()
guard let snapshot = try? decoder.decode(AppSessionSnapshot.self, from: data) else { return nil }
guard snapshot.version == SessionSnapshotSchema.currentVersion else { return nil }
guard !snapshot.windows.isEmpty else { return nil }
return snapshot

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add an import size guard before decoding session files.

Line 444 reads the full file into memory with no cap. A very large .cmux-session file can spike memory and hang the app during import.

💡 Suggested hardening patch
 enum SessionExportStore {
     static let fileExtension = "cmux-session"
+    private static let maxImportBytes = 8 * 1024 * 1024
 
@@
     static func load(from fileURL: URL) -> AppSessionSnapshot? {
-        guard let data = try? Data(contentsOf: fileURL) else { return nil }
+        guard let values = try? fileURL.resourceValues(forKeys: [.fileSizeKey]),
+              let fileSize = values.fileSize,
+              fileSize > 0,
+              fileSize <= maxImportBytes else { return nil }
+        guard let data = try? Data(contentsOf: fileURL, options: .mappedIfSafe) else { return nil }
         let decoder = JSONDecoder()
         guard let snapshot = try? decoder.decode(AppSessionSnapshot.self, from: data) else { return nil }
         guard snapshot.version == SessionSnapshotSchema.currentVersion else { return nil }
         guard !snapshot.windows.isEmpty else { return nil }
         return snapshot
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static func load(from fileURL: URL) -> AppSessionSnapshot? {
guard let data = try? Data(contentsOf: fileURL) else { return nil }
let decoder = JSONDecoder()
guard let snapshot = try? decoder.decode(AppSessionSnapshot.self, from: data) else { return nil }
guard snapshot.version == SessionSnapshotSchema.currentVersion else { return nil }
guard !snapshot.windows.isEmpty else { return nil }
return snapshot
static func load(from fileURL: URL) -> AppSessionSnapshot? {
guard let values = try? fileURL.resourceValues(forKeys: [.fileSizeKey]),
let fileSize = values.fileSize,
fileSize > 0,
fileSize <= maxImportBytes else { return nil }
guard let data = try? Data(contentsOf: fileURL, options: .mappedIfSafe) else { return nil }
let decoder = JSONDecoder()
guard let snapshot = try? decoder.decode(AppSessionSnapshot.self, from: data) else { return nil }
guard snapshot.version == SessionSnapshotSchema.currentVersion else { return nil }
guard !snapshot.windows.isEmpty else { return nil }
return snapshot
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/SessionPersistence.swift` around lines 443 - 449, The load(from:)
function reads the entire session file into memory without size limits, risking
OOM on large .cmux-session files; before creating Data(contentsOf:), check the
file size using FileManager or URL resourceValues (e.g., values(forKey:
.fileSizeKey) or attributesOfItem) and reject files over a reasonable cap
(choose a constant max size) by returning nil, then proceed to decode
AppSessionSnapshot and validate SessionSnapshotSchema.currentVersion and
non-empty windows as before.

Comment on lines +2177 to +2181
// Session export/import
case "session.save":
return v2Result(id: id, self.v2SessionSave(params: params))
case "session.open":
return v2Result(id: id, self.v2SessionOpen(params: params))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add session.save/session.open to system.capabilities

Lines 2178-2181 register handlers, but capability discovery won’t advertise them. Clients that gate on system.capabilities will treat these methods as unsupported.

Suggested patch
@@
             "surface.send_key",
             "surface.read_text",
             "surface.clear_history",
             "surface.trigger_flash",
+            "session.save",
+            "session.open",
             "pane.list",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TerminalController.swift` around lines 2177 - 2181, The code
registers RPC handlers for "session.save" and "session.open" (v2SessionSave and
v2SessionOpen) but never advertises them in the system.capabilities list,
causing clients to think these methods are unsupported; update the capability
discovery where system.capabilities is assembled to include the strings
"session.save" and "session.open" (alongside existing capability entries) so the
advertised capabilities reflect the registered handlers (ensure names match
exactly the case used in the switch: "session.save" and "session.open").

Comment on lines +6283 to +6297
var windowIds: [String] = []
v2MainSync {
for windowSnapshot in snapshot.windows.prefix(SessionPersistencePolicy.maxWindowsPerSnapshot) {
if let windowId = AppDelegate.shared?.createMainWindow(sessionWindowSnapshot: windowSnapshot) {
windowIds.append(windowId.uuidString)
}
}
}

return .ok([
"path": filePath,
"windows_created": windowIds.count,
"window_ids": windowIds
])
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t return success when restore creates zero windows

If restore attempts windows but createMainWindow(sessionWindowSnapshot:) fails for all, the method still returns .ok. That masks restore failure for automation and CLI callers.

Suggested patch
-        var windowIds: [String] = []
+        var windowIds: [String] = []
+        let requestedCount = min(snapshot.windows.count, SessionPersistencePolicy.maxWindowsPerSnapshot)
         v2MainSync {
             for windowSnapshot in snapshot.windows.prefix(SessionPersistencePolicy.maxWindowsPerSnapshot) {
                 if let windowId = AppDelegate.shared?.createMainWindow(sessionWindowSnapshot: windowSnapshot) {
                     windowIds.append(windowId.uuidString)
                 }
             }
         }
 
+        if requestedCount > 0 && windowIds.isEmpty {
+            return .err(
+                code: "internal_error",
+                message: "Failed to restore any windows from session file",
+                data: ["path": filePath]
+            )
+        }
+
         return .ok([
             "path": filePath,
+            "windows_requested": requestedCount,
             "windows_created": windowIds.count,
             "window_ids": windowIds
         ])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var windowIds: [String] = []
v2MainSync {
for windowSnapshot in snapshot.windows.prefix(SessionPersistencePolicy.maxWindowsPerSnapshot) {
if let windowId = AppDelegate.shared?.createMainWindow(sessionWindowSnapshot: windowSnapshot) {
windowIds.append(windowId.uuidString)
}
}
}
return .ok([
"path": filePath,
"windows_created": windowIds.count,
"window_ids": windowIds
])
}
var windowIds: [String] = []
let requestedCount = min(snapshot.windows.count, SessionPersistencePolicy.maxWindowsPerSnapshot)
v2MainSync {
for windowSnapshot in snapshot.windows.prefix(SessionPersistencePolicy.maxWindowsPerSnapshot) {
if let windowId = AppDelegate.shared?.createMainWindow(sessionWindowSnapshot: windowSnapshot) {
windowIds.append(windowId.uuidString)
}
}
}
if requestedCount > 0 && windowIds.isEmpty {
return .err(
code: "internal_error",
message: "Failed to restore any windows from session file",
data: ["path": filePath]
)
}
return .ok([
"path": filePath,
"windows_requested": requestedCount,
"windows_created": windowIds.count,
"window_ids": windowIds
])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TerminalController.swift` around lines 6283 - 6297, The restore
currently returns .ok even when no windows were actually created; after the
v2MainSync loop that calls
AppDelegate.shared?.createMainWindow(sessionWindowSnapshot:) and collects
windowIds, check if windowIds.count is zero and return an error result instead
of .ok (e.g., return a failure/error enum case with a descriptive message like
"no windows restored" and include filePath for context); keep the existing
successful .ok response when windowIds.count > 0 and still include
"windows_created" and "window_ids" as before.

This branch has not been deployed

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

Labels

area: workspaces Workspaces, sessions, restore after relaunch, worktrees

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants