Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
7988 Sources/Panels/BrowserPanelView.swift
7355 cmuxTests/WorkspaceUnitTests.swift
7218 cmuxTests/WorkspaceRemoteConnectionTests.swift
6317 cmuxTests/SessionPersistenceTests.swift
6360 cmuxTests/SessionPersistenceTests.swift
6220 cmuxTests/GhosttyConfigTests.swift
6183 Sources/TabManager.swift
6151 CLI/cmux_open.swift
Expand Down Expand Up @@ -45,8 +45,8 @@
2257 Sources/TerminalNotificationStore.swift
2257 Sources/TerminalWindowPortal.swift
2126 cmuxTests/CmuxConfigTests.swift
2104 Sources/SessionPersistence.swift
2091 cmuxTests/ShortcutAndCommandPaletteTests.swift
2082 Sources/SessionPersistence.swift
1949 Sources/Panels/BrowserWebAuthnSupport.swift
1940 Sources/KeyboardShortcutSettingsFileStore.swift
1869 cmuxTests/NotificationAndMenuBarTests.swift
Expand Down Expand Up @@ -88,7 +88,7 @@
1021 cmuxUITests/TerminalCmdClickUITests.swift
1009 cmuxTests/CmuxTopSnapshotScopeTests.swift
1006 cmuxTests/CmuxSSHURLRequestTests.swift
959 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift
974 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift
951 Sources/App/TerminalDirectoryOpenSupport.swift
947 Sources/TerminalNotificationPolicy.swift
945 Sources/SessionIndexRegisteredAgents.swift
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ public struct AppCatalogSection: SettingCatalogSection {
userDefaultsKey: "defaultWorkspacePath"
)

/// When false, cmux does not automatically restore the previous launch's
/// windows, workspaces, and terminal content on startup; each launch opens a
/// fresh workspace instead. The explicit "Restore Previous Launch" command is
/// unaffected. Defaults to `true` (the historical always-restore behavior).
public let restorePreviousSession = DefaultsKey<Bool>(
id: "app.restorePreviousSession",
defaultValue: true,
userDefaultsKey: "restorePreviousSession"
)

public let presentationMode = DefaultsKey<WorkspacePresentationMode>(
id: "app.minimalMode",
defaultValue: .standard,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extension Array where Element == CuratedSettingEntry {
.init(section: .app, id: "new-workspace-placement", title: "New Workspace Placement", synonyms: "app.newWorkspacePlacement new tab insert position order top bottom end"),
.init(section: .app, id: "workspace-inherit-working-directory", title: "Inherit Workspace Working Directory", synonyms: "app.workspaceInheritWorkingDirectory workspace cwd directory inherit current focused working-directory"),
.init(section: .app, id: "default-workspace-path", title: "Default Workspace Directory", synonyms: "app.defaultWorkspacePath default working directory new workspace path cwd pinned root folder"),
.init(section: .app, id: "restore-previous-session", title: "Restore Previous Session", synonyms: "app.restorePreviousSession session restore reopen previous launch startup fresh workspace remember scrollback persist memory"),
.init(section: .app, id: "minimal-mode", title: "Minimal Mode", synonyms: "app.minimalMode presentation compact chrome layout simple titlebar controls"),
.init(section: .app, id: "keep-workspace-open", title: "Keep Workspace Open When Closing Last Surface", synonyms: "app.keepWorkspaceOpenWhenClosingLastSurface close last pane surface keep tab workspace"),
.init(section: .app, id: "focus-pane-first-click", title: "Focus Pane on First Click", synonyms: "app.focusPaneOnFirstClick click to focus focus follows mouse first click mouse activation"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public struct AppSection: View {
@State private var fileDrop: DefaultsValueModel<FileDropDefaultBehavior>
@State private var preferredEditor: DefaultsValueModel<String>
@State private var defaultWorkspacePath: DefaultsValueModel<String>
@State private var restorePreviousSession: DefaultsValueModel<Bool>
@State private var openSupported: DefaultsValueModel<Bool>
@State private var openMarkdown: DefaultsValueModel<Bool>
@State private var markdownFontSize: DefaultsValueModel<Int>
Expand Down Expand Up @@ -82,6 +83,7 @@ public struct AppSection: View {
_fileDrop = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.fileDropDefaultBehavior))
_preferredEditor = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.preferredEditor))
_defaultWorkspacePath = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.defaultWorkspacePath))
_restorePreviousSession = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.restorePreviousSession))
_openSupported = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.openSupportedFilesInCmux))
_openMarkdown = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.openMarkdownInCmuxViewer))
_markdownFontSize = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.markdown.fontSize))
Expand Down Expand Up @@ -129,7 +131,7 @@ public struct AppSection: View {
mainCard
}
.task {
startSettingsObservation([language, appearance, appIcon, placement, inheritDir, minimalMode, keepWorkspaceOpen, firstClick, fileDrop, preferredEditor, defaultWorkspacePath, openSupported, openMarkdown, markdownFontSize, markdownFontFamily, markdownMaxWidth, canvasPaneGap, canvasSnapping, fileEditorWordWrap, iMessage, reorder, dockBadge, menuBarOnly, showInMenuBar, paneRing, paneFlash, soundName, soundCommand, customSoundFile, telemetry, confirmQuit, warnCloseTab, warnCloseX, hideCloseButton, renameSelects, paletteAllSurfaces])
startSettingsObservation([language, appearance, appIcon, placement, inheritDir, minimalMode, keepWorkspaceOpen, firstClick, fileDrop, preferredEditor, defaultWorkspacePath, restorePreviousSession, openSupported, openMarkdown, markdownFontSize, markdownFontFamily, markdownMaxWidth, canvasPaneGap, canvasSnapping, fileEditorWordWrap, iMessage, reorder, dockBadge, menuBarOnly, showInMenuBar, paneRing, paneFlash, soundName, soundCommand, customSoundFile, telemetry, confirmQuit, warnCloseTab, warnCloseX, hideCloseButton, renameSelects, paletteAllSurfaces])
if languageAtAppear == nil { languageAtAppear = language.current }; if telemetryAtAppear == nil { telemetryAtAppear = telemetry.current }
}
}
Expand Down Expand Up @@ -239,6 +241,19 @@ public struct AppSection: View {
}
SettingsCardDivider()

// Restore Previous Session
SettingsCardRow(
configurationReview: .json("app.restorePreviousSession"),
String(localized: "settings.app.restorePreviousSession", defaultValue: "Restore Previous Session"),
subtitle: String(localized: "settings.app.restorePreviousSession.subtitle", defaultValue: "Reopen the previous launch's windows, workspaces, and terminal content on startup. Turn off to start each launch with a fresh workspace. The Restore Previous Launch command still works.")
) {
Toggle("", isOn: Binding(get: { restorePreviousSession.current }, set: { restorePreviousSession.set($0) }))
.labelsHidden()
.controlSize(.small)
.accessibilityIdentifier("SettingsRestorePreviousSessionToggle")
}
SettingsCardDivider()

// Minimal Mode
SettingsCardRow(
configurationReview: .json("app.minimalMode"),
Expand Down
34 changes: 34 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -198186,6 +198186,40 @@
}
}
},
"settings.app.restorePreviousSession": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Restore Previous Session"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "前回のセッションを復元"
}
}
}
},
"settings.app.restorePreviousSession.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Reopen the previous launch's windows, workspaces, and terminal content on startup. Turn off to start each launch with a fresh workspace. The Restore Previous Launch command still works."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "起動時に前回のウィンドウ、ワークスペース、ターミナルの内容を復元します。オフにすると、毎回新しいワークスペースで起動します。「前回の起動を復元」コマンドは引き続き使用できます。"
}
}
}
},
"settings.app.preferredEditor": {
"extractionState": "manual",
"localizations": {
Expand Down
4 changes: 4 additions & 0 deletions Sources/CmuxSettingsJSONPathSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ enum AppSettingsFileMapping {
jsonKey: "commandPaletteSearchesAllSurfaces",
defaultsKey: app.commandPaletteSearchesAllSurfaces.userDefaultsKey
),
.init(
jsonKey: "restorePreviousSession",
defaultsKey: app.restorePreviousSession.userDefaultsKey
),
]

static let stringSettings: [SettingsFileStringMapping] = [
Expand Down
1 change: 1 addition & 0 deletions Sources/KeyboardShortcutSettingsFileStore+Template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ extension CmuxSettingsFileStore {
"forkConversationDefaultDestination": AgentConversationForkDefaultSettings.defaultDestination.rawValue,
"workspaceInheritWorkingDirectory": SettingCatalog().app.workspaceInheritWorkingDirectory.defaultValue,
"defaultWorkspacePath": SettingCatalog().app.defaultWorkspacePath.defaultValue,
"restorePreviousSession": SettingCatalog().app.restorePreviousSession.defaultValue,
"minimalMode": false,
"keepWorkspaceOpenWhenClosingLastSurface": !SettingCatalog().app.keepWorkspaceOpenWhenClosingLastSurface.defaultValue,
"focusPaneOnFirstClick": PaneFirstClickFocusSettings.defaultEnabled,
Expand Down
24 changes: 23 additions & 1 deletion Sources/SessionPersistence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,32 @@ enum SessionRestorePolicy {
return false
}

/// UserDefaults key backing `app.restorePreviousSession` (mirrors
/// `AppCatalogSection.restorePreviousSession.userDefaultsKey`). Kept as a
/// literal so this file need not depend on CmuxSettings, matching the
/// `AgentSessionAutoResumeSettings` pattern.
static let restorePreviousSessionDefaultsKey = "restorePreviousSession"

/// Reads `app.restorePreviousSession`, defaulting to `true` when unset so
/// the historical always-restore behavior is preserved.
static func restorePreviousSessionEnabled(defaults: UserDefaults = .standard) -> Bool {
guard defaults.object(forKey: restorePreviousSessionDefaultsKey) != nil else {
return true
}
return defaults.bool(forKey: restorePreviousSessionDefaultsKey)
}

static func shouldAttemptRestore(
arguments: [String] = CommandLine.arguments,
environment: [String: String] = ProcessInfo.processInfo.environment
environment: [String: String] = ProcessInfo.processInfo.environment,
restorePreviousSessionEnabled: Bool = SessionRestorePolicy.restorePreviousSessionEnabled()
) -> Bool {
// User opted out of automatic restore (Settings → App, or
// `app.restorePreviousSession: false` in cmux.json). The explicit
// "Restore Previous Launch" command still works — it does not call here.
if !restorePreviousSessionEnabled {
return false
}
if environment["CMUX_DISABLE_SESSION_RESTORE"] == "1" {
return false
}
Expand Down
43 changes: 43 additions & 0 deletions cmuxTests/SessionPersistenceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6314,4 +6314,47 @@ extension SessionPersistenceTests {
XCTAssertNil(MarkdownPanelFileLinkResolver.resolve(rawPath: "notes.txt", relativeToMarkdownFile: openedFile.path))
XCTAssertNil(MarkdownPanelFileLinkResolver.resolve(rawPath: "https://example.com/notes.md", relativeToMarkdownFile: openedFile.path))
}

// MARK: - app.restorePreviousSession

func testRestorePreviousSessionEnabledDefaultsToTrueWhenUnset() throws {
let defaults = try XCTUnwrap(UserDefaults(suiteName: "cmux-restore-pref-\(UUID().uuidString)"))
defer { defaults.removeObject(forKey: SessionRestorePolicy.restorePreviousSessionDefaultsKey) }

XCTAssertNil(defaults.object(forKey: SessionRestorePolicy.restorePreviousSessionDefaultsKey))
XCTAssertTrue(SessionRestorePolicy.restorePreviousSessionEnabled(defaults: defaults))
}

func testRestorePreviousSessionEnabledReflectsStoredValue() throws {
let defaults = try XCTUnwrap(UserDefaults(suiteName: "cmux-restore-pref-\(UUID().uuidString)"))
defer { defaults.removeObject(forKey: SessionRestorePolicy.restorePreviousSessionDefaultsKey) }

defaults.set(false, forKey: SessionRestorePolicy.restorePreviousSessionDefaultsKey)
XCTAssertFalse(SessionRestorePolicy.restorePreviousSessionEnabled(defaults: defaults))

defaults.set(true, forKey: SessionRestorePolicy.restorePreviousSessionDefaultsKey)
XCTAssertTrue(SessionRestorePolicy.restorePreviousSessionEnabled(defaults: defaults))
}

func testShouldAttemptRestoreSkipsWhenRestorePreviousSessionDisabled() {
// Even with an otherwise-restorable launch (no extra args, no test env),
// the user opting out of restore must short-circuit to no restore.
XCTAssertFalse(
SessionRestorePolicy.shouldAttemptRestore(
arguments: ["cmux"],
environment: [:],
restorePreviousSessionEnabled: false
)
)
}

func testShouldAttemptRestoreProceedsWhenRestorePreviousSessionEnabled() {
XCTAssertTrue(
SessionRestorePolicy.shouldAttemptRestore(
arguments: ["cmux"],
environment: [:],
restorePreviousSessionEnabled: true
)
)
}
}
19 changes: 19 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,25 @@ Pins the default working directory for new workspaces instead of always using th

Editable in **Settings → General → Default Workspace Directory**.

## `app.restorePreviousSession`

Controls whether cmux automatically restores the previous launch on startup — its windows, workspaces, working directories, and terminal content.

```json
{
"app": {
"restorePreviousSession": false
}
}
```

- `true` (the default) preserves the historical behavior: reopening cmux brings back exactly where you left off.
- `false` starts every launch with a single fresh workspace (seeded by `app.defaultWorkspacePath`), so reopening the app no longer replays the previous session's terminals.
- The explicit **Restore Previous Launch** command (File menu, Command Palette, and its keyboard shortcut) is unaffected — you can still pull back the last session on demand.
- Equivalent to launching with the `CMUX_DISABLE_SESSION_RESTORE=1` environment variable, but persisted.

Editable in **Settings → General → Restore Previous Session**.

## `terminal.agentHibernation`

Opt-in Agent Hibernation. cmux kills idle background agent processes to free RAM and CPU, then resumes each one with its saved session when you visit its tab. See [agent-hooks.md](agent-hooks.md#agent-hibernation) for the full behavior, including the confirmation settle window and how resume works.
Expand Down
5 changes: 5 additions & 0 deletions web/data/cmux.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@
"default": "",
"description": "Default working directory for new workspaces. Supports ~ and $VAR/${VAR} expansion. When set, it takes precedence over workspaceInheritWorkingDirectory (the inherited last-used directory). An invalid or nonexistent path falls back to the last-used directory (or the home directory). Leave empty to keep the inherited last-used behavior."
},
"restorePreviousSession": {
"type": "boolean",
"default": true,
"description": "When true, cmux restores the previous launch's windows, workspaces, and terminal content on startup. Set false to start each launch with a fresh workspace (the explicit 'Restore Previous Launch' command still works)."
},
"minimalMode": {
"type": "boolean",
"default": false,
Expand Down
Loading