Repository navigation
Conversation
Add a Profiles feature that lets users save their current workspace layout as a named profile (e.g. "Work", "Personal") and restore it later in the current window or a new one. - ProfileStore: save/load/delete/rename/list profiles as JSON in ~/Library/Application Support/cmux/profiles/ - Menu: File > Workspace > Profiles submenu with save, load, delete - Sidebar badge: shows active profile name inline next to help button - Auto-save: active profiles auto-save on the existing 8s timer and at app termination (with scrollback) - Socket commands: profile.list/save/load/delete (v1 and v2) - 18 unit tests covering model, persistence, and TabManager integration
|
@andrekat is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughThis PR introduces a comprehensive profile management system for saving, loading, renaming, and deleting user session profiles. It adds filesystem-based persistence using JSON encoding, terminal command interfaces (v1 and v2), UI menu integration with dialogs, autosave on app termination, and extensive test coverage for the new functionality. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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 Tip You can make CodeRabbit's review stricter and more nitpicky using the `assertive` profile, if that's what you prefer.Change the |
There was a problem hiding this comment.
8 issues found across 8 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/cmuxApp.swift">
<violation number="1" location="Sources/cmuxApp.swift:937">
P2: Profiles menu content is derived from ProfileStore.list() without any observable state. Save/delete actions don't update any @State/@Published value, so the SwiftUI command menu may not refresh immediately, leaving saved/deleted profiles stale until unrelated state changes occur.</violation>
</file>
<file name="cmuxTests/ProfileStoreTests.swift">
<violation number="1" location="cmuxTests/ProfileStoreTests.swift:123">
P2: Tests write/delete profiles using the real ProfileStore location (Application Support) with fixed names, so they can overwrite or delete a user’s real profiles and be flaky if data already exists. There’s no test-specific directory override or setup/teardown to isolate storage.</violation>
</file>
<file name="Sources/ProfileStore.swift">
<violation number="1" location="Sources/ProfileStore.swift:149">
P2: rename deletes the original profile before a successful save and ignores delete failure, which can cause data loss or duplicate profiles if save/delete fails.</violation>
<violation number="2" location="Sources/ProfileStore.swift:184">
P2: Profile filenames are derived from a lossy sanitized version of `Profile.name`, but overwrite checks compare the raw name. Distinct names that sanitize to the same filename can overwrite each other or be loaded/deleted incorrectly.</violation>
</file>
<file name="Sources/AppDelegate.swift">
<violation number="1" location="Sources/AppDelegate.swift:2374">
P2: Profile autosave now runs synchronous snapshot creation and JSON file writes on the main thread during each autosave tick, which can cause UI hitches for large scrollback or multiple windows.</violation>
<violation number="2" location="Sources/AppDelegate.swift:3231">
P2: Autosave for active profiles is added to normal termination and timer paths, but the existing fallback shutdown/update lifecycle paths (update relaunch, power-off, session resign while terminating) still only save the session snapshot. In those nonstandard termination paths, profile changes can be lost even though the session snapshot is preserved. Consider invoking autosaveActiveProfiles in those fallback handlers as well.</violation>
</file>
<file name="Sources/TerminalController.swift">
<violation number="1" location="Sources/TerminalController.swift:2328">
P2: system.capabilities does not advertise the new profile.* v2 methods, so clients using capability discovery will not see profile.list/save/load/delete even though they are implemented.</violation>
<violation number="2" location="Sources/TerminalController.swift:10922">
P2: `profile_save`/`profile_load` run on the socket worker thread but mutate AppKit/TabManager state without hopping to the main thread. This differs from the v2 implementations which wrap the same operations in `v2MainSync`, and can trigger AppKit thread-safety violations or workspace races. Wrap these v1 operations in a main-thread hop (e.g., `DispatchQueue.main.sync`) before calling `ProfileStore.saveCurrentSession`, `AppDelegate.createMainWindow`, and `TabManager.restoreSessionSnapshot`/`setActiveProfileName`.</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-aiwith guidance or docs links (includingllms.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.
|
|
||
| @ViewBuilder | ||
| private func profilesMenuContent(manager: TabManager) -> some View { | ||
| let profiles = ProfileStore.list() |
There was a problem hiding this comment.
P2: Profiles menu content is derived from ProfileStore.list() without any observable state. Save/delete actions don't update any @State/@published value, so the SwiftUI command menu may not refresh immediately, leaving saved/deleted profiles stale until unrelated state changes occur.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/cmuxApp.swift, line 937:
<comment>Profiles menu content is derived from ProfileStore.list() without any observable state. Save/delete actions don't update any @State/@Published value, so the SwiftUI command menu may not refresh immediately, leaving saved/deleted profiles stale until unrelated state changes occur.</comment>
<file context>
@@ -928,6 +930,182 @@ struct cmuxApp: App {
+
+ @ViewBuilder
+ private func profilesMenuContent(manager: TabManager) -> some View {
+ let profiles = ProfileStore.list()
+
+ Menu(String(localized: "menu.file.profiles", defaultValue: "Profiles")) {
</file context>
| var profile = Profile(name: "SaveLoad", snapshot: snapshot) | ||
| profile.updatedAt = Date() | ||
|
|
||
| XCTAssertTrue(ProfileStore.save(profile)) |
There was a problem hiding this comment.
P2: Tests write/delete profiles using the real ProfileStore location (Application Support) with fixed names, so they can overwrite or delete a user’s real profiles and be flaky if data already exists. There’s no test-specific directory override or setup/teardown to isolate storage.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmuxTests/ProfileStoreTests.swift, line 123:
<comment>Tests write/delete profiles using the real ProfileStore location (Application Support) with fixed names, so they can overwrite or delete a user’s real profiles and be flaky if data already exists. There’s no test-specific directory override or setup/teardown to isolate storage.</comment>
<file context>
@@ -0,0 +1,333 @@
+ var profile = Profile(name: "SaveLoad", snapshot: snapshot)
+ profile.updatedAt = Date()
+
+ XCTAssertTrue(ProfileStore.save(profile))
+
+ let loaded = ProfileStore.load(name: "SaveLoad")
</file context>
| } | ||
|
|
||
| // Delete old file first (the filename is derived from the name). | ||
| delete(name: oldName) |
There was a problem hiding this comment.
P2: rename deletes the original profile before a successful save and ignores delete failure, which can cause data loss or duplicate profiles if save/delete fails.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/ProfileStore.swift, line 149:
<comment>rename deletes the original profile before a successful save and ignores delete failure, which can cause data loss or duplicate profiles if save/delete fails.</comment>
<file context>
@@ -0,0 +1,196 @@
+ }
+
+ // Delete old file first (the filename is derived from the name).
+ delete(name: oldName)
+
+ profile.name = trimmedNew
</file context>
| let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| guard !trimmed.isEmpty else { return "" } | ||
| // Replace characters that are problematic in filenames. | ||
| let sanitized = trimmed.replacingOccurrences( |
There was a problem hiding this comment.
P2: Profile filenames are derived from a lossy sanitized version of Profile.name, but overwrite checks compare the raw name. Distinct names that sanitize to the same filename can overwrite each other or be loaded/deleted incorrectly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/ProfileStore.swift, line 184:
<comment>Profile filenames are derived from a lossy sanitized version of `Profile.name`, but overwrite checks compare the raw name. Distinct names that sanitize to the same filename can overwrite each other or be loaded/deleted incorrectly.</comment>
<file context>
@@ -0,0 +1,196 @@
+ let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmed.isEmpty else { return "" }
+ // Replace characters that are problematic in filenames.
+ let sanitized = trimmed.replacingOccurrences(
+ of: "[/\\\\:*?\"<>|]",
+ with: "_",
</file context>
| @@ -2371,12 +2371,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent | |||
| func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { | |||
There was a problem hiding this comment.
P2: Autosave for active profiles is added to normal termination and timer paths, but the existing fallback shutdown/update lifecycle paths (update relaunch, power-off, session resign while terminating) still only save the session snapshot. In those nonstandard termination paths, profile changes can be lost even though the session snapshot is preserved. Consider invoking autosaveActiveProfiles in those fallback handlers as well.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/AppDelegate.swift, line 3231:
<comment>Autosave for active profiles is added to normal termination and timer paths, but the existing fallback shutdown/update lifecycle paths (update relaunch, power-off, session resign while terminating) still only save the session snapshot. In those nonstandard termination paths, profile changes can be lost even though the session snapshot is preserved. Consider invoking autosaveActiveProfiles in those fallback handlers as well.</comment>
<file context>
@@ -3224,6 +3227,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
+ /// Auto-saves any active profiles so changes are persisted without explicit user action.
+ private func autosaveActiveProfiles(includeScrollback: Bool = false) {
+ let contexts = Array(mainWindowContexts.values)
+ for context in contexts {
</file context>
| func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { | ||
| isTerminatingApp = true | ||
| _ = saveSessionSnapshot(includeScrollback: true, removeWhenEmpty: false) | ||
| autosaveActiveProfiles(includeScrollback: true) |
There was a problem hiding this comment.
P2: Profile autosave now runs synchronous snapshot creation and JSON file writes on the main thread during each autosave tick, which can cause UI hitches for large scrollback or multiple windows.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/AppDelegate.swift, line 2374:
<comment>Profile autosave now runs synchronous snapshot creation and JSON file writes on the main thread during each autosave tick, which can cause UI hitches for large scrollback or multiple windows.</comment>
<file context>
@@ -2371,12 +2371,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
isTerminatingApp = true
_ = saveSessionSnapshot(includeScrollback: true, removeWhenEmpty: false)
+ autosaveActiveProfiles(includeScrollback: true)
return .terminateNow
}
</file context>
| return v2Result(id: id, self.v2MarkdownOpen(params: params)) | ||
|
|
||
| // Profiles | ||
| case "profile.list": |
There was a problem hiding this comment.
P2: system.capabilities does not advertise the new profile.* v2 methods, so clients using capability discovery will not see profile.list/save/load/delete even though they are implemented.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/TerminalController.swift, line 2328:
<comment>system.capabilities does not advertise the new profile.* v2 methods, so clients using capability discovery will not see profile.list/save/load/delete even though they are implemented.</comment>
<file context>
@@ -2313,6 +2324,16 @@ class TerminalController {
return v2Result(id: id, self.v2MarkdownOpen(params: params))
+ // Profiles
+ case "profile.list":
+ return v2Result(id: id, self.v2ProfileList(params: params))
+ case "profile.save":
</file context>
| guard let tabManager else { | ||
| return "ERROR: TabManager not available" | ||
| } | ||
| guard let profile = ProfileStore.saveCurrentSession(name: name, tabManager: tabManager) else { |
There was a problem hiding this comment.
P2: profile_save/profile_load run on the socket worker thread but mutate AppKit/TabManager state without hopping to the main thread. This differs from the v2 implementations which wrap the same operations in v2MainSync, and can trigger AppKit thread-safety violations or workspace races. Wrap these v1 operations in a main-thread hop (e.g., DispatchQueue.main.sync) before calling ProfileStore.saveCurrentSession, AppDelegate.createMainWindow, and TabManager.restoreSessionSnapshot/setActiveProfileName.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/TerminalController.swift, line 10922:
<comment>`profile_save`/`profile_load` run on the socket worker thread but mutate AppKit/TabManager state without hopping to the main thread. This differs from the v2 implementations which wrap the same operations in `v2MainSync`, and can trigger AppKit thread-safety violations or workspace races. Wrap these v1 operations in a main-thread hop (e.g., `DispatchQueue.main.sync`) before calling `ProfileStore.saveCurrentSession`, `AppDelegate.createMainWindow`, and `TabManager.restoreSessionSnapshot`/`setActiveProfileName`.</comment>
<file context>
@@ -10758,6 +10898,93 @@ class TerminalController {
+ guard let tabManager else {
+ return "ERROR: TabManager not available"
+ }
+ guard let profile = ProfileStore.saveCurrentSession(name: name, tabManager: tabManager) else {
+ return "ERROR: Failed to save profile '\(name)'"
+ }
</file context>
* feat(cmux): add tools cmux profiles save / view / restore Snapshot cmux workspace layouts (windows, panes, surfaces, geometry, cwd, last shell-history command) into named JSON profiles under ~/.genesis-tools/cmux/profiles/, then recreate them on demand. Geometry fidelity uses the new pane.list pixel_frame fields (cmux 0.63.0+); restore is non-destructive (always creates new prefixed workspaces). Why: cmux's built-in autosave only restores layout on relaunch and offers no named profile system (PR manaflow-ai/cmux#1617 was closed unmerged). This tool bridges the gap so you can switch between curated work setups on the fly. * docs(cmux): add comprehensive Cmux.md command reference Covers every cmux CLI verb and JSON-RPC method tools cmux uses, with the sharp edges I hit during implementation: - pane.list and surface.split both ignore the workspace/surface params and operate on the focused workspace/pane — must use the CLI for splits and focus-switch for cross-workspace reads. - new-workspace --name is best-effort; cmux often overrides with auto title. - cmux --json is inconsistent across commands (some return JSON, some plain text "OK <ref>"). Each verb noted. - tab-action's allow-list is unwritten; known-good action is "rename". - pane geometry (pixel_frame, columns, rows) requires cmux 0.63.0+. Includes a step-by-step trace of what tools cmux profiles save / restore actually invokes, useful as a debugging reference. * fix(cmux/save): propagate interactive overwrite confirmation to store.write After user confirmed "Overwrite?" interactively, the `force: false` from the original flag was still passed to `store.write`, triggering ProfileExistsError every time. Track `forceWrite` separately and set it to `true` after the user confirms. https://claude.ai/code/session_011bfXscyS4Q7VYaLtHtviLK * fix(cmux/snapshot): ListPaneSurfacesResponse was wrapping a bare array + use identify.focused for scope cmux list-pane-surfaces returns a bare JSON array, not {surfaces:[]}. The old type caused surfacesInfo.surfaces to always be undefined, making the spread crash at runtime. Also fixes scope=window/workspace in multi-window sessions: instead of all.find((ws)=>ws.selected) which ambiguously picks the first selected workspace across all windows, we now call `cmux identify --json` to get the globally focused workspace_ref and derive the target window from that. https://claude.ai/code/session_011bfXscyS4Q7VYaLtHtviLK * fix(cmux/cli): switch to Bun.spawn and reject signal-terminated processes Replaces node:child_process.spawn with Bun.spawn (per CLAUDE.md). Also fixes the bug where a signal-killed cmux process (code===null) was resolved as exit code 0, causing runCmuxJSON to attempt JSON parsing on potentially truncated output. Also uses SafeJSON per project convention. https://claude.ai/code/session_011bfXscyS4Q7VYaLtHtviLK * fix(cmux/shell-probe): handle paths with spaces and bare ~ or … titles The user@host:path regex used \S+ which breaks on directory paths containing spaces. Switching to .+ captures the full remainder of the title after the colon. Also adds the bare ~ and … edge cases (without trailing slash) to the condition so they reach expandHome(), which already handles them. https://claude.ai/code/session_011bfXscyS4Q7VYaLtHtviLK * fix(cmux/profiles): add suggestCommand to non-interactive refusals Per repo convention (CLAUDE.md), non-interactive failure paths should pair the error with the exact rerun command. delete and restore were only printing a generic error message. https://claude.ai/code/session_011bfXscyS4Q7VYaLtHtviLK * fix(cmux/restore): shell-quote cwd path before sending cd command Raw interpolation `cd ${surface.cwd}\n` breaks on paths with spaces and treats shell metacharacters literally (e.g. /tmp/a b or /tmp/x;echo). Now uses POSIX single-quote escaping: cd -- '<escaped-path>'. https://claude.ai/code/session_011bfXscyS4Q7VYaLtHtviLK * fix(cmux/edit): switch to Bun.spawn and guard empty EDITOR env value Drops node:child_process.spawn in favour of Bun.spawn (per CLAUDE.md). Also adds a length guard before accessing parts[0] so a whitespace-only $EDITOR value safely falls back to vi instead of spawning undefined. https://claude.ai/code/session_011bfXscyS4Q7VYaLtHtviLK * fix(cmux/resize): re-read layout after MAX_ITERATIONS to get accurate convergence report The loop measured deltas before applying resize calls, so when iteration hit MAX_ITERATIONS the final pass's resize effects were never measured. converged could be false even when the last pass pushed every pane within TOLERANCE_CELLS. A final paneList read is now done after the loop exits without converging so the logged deltas and returned converged flag reflect the actual post-resize state. https://claude.ai/code/session_011bfXscyS4Q7VYaLtHtviLK * fix(cmux/store): normalize profile.name and version at write time write(name, profile) could persist a file whose internal profile.name differed from the filename key, creating inconsistent behaviour in list/view/restore. Now the written JSON always has name===name and version===PROFILE_VERSION regardless of what the caller passed in. Also uses SafeJSON per project convention. https://claude.ai/code/session_011bfXscyS4Q7VYaLtHtviLK * fix(cmux/docs): add language labels to all unlabeled fenced code blocks Fixes MD040 lint warnings. Plain-text diagrams and error snippets are now tagged as `text`; shell walkthroughs are tagged as `bash`. https://claude.ai/code/session_011bfXscyS4Q7VYaLtHtviLK * style(cmux): apply biome import-order and trailing-comma formatting https://claude.ai/code/session_011bfXscyS4Q7VYaLtHtviLK * fix(cmux): per-pane screen replay, correct surface placement, split-time resize Three bugs the smoke save+restore comparison surfaced, all rooted in cmux 0.63.2 quirks where v1 RPC routing falls back to the focused workspace/pane instead of using explicit params: 1. Surfaces stacking into one pane. `surface.create { pane }` ignores the param and creates the surface in the focused pane (verified: ask for pane:54/workspace:17, get pane:5/ workspace:2). Switched to `cmux new-surface --pane <ref>` CLI which is v2-routed. 2. Pane sizes never converging. Post-hoc `convergeToTarget` loop tried to move borders that don't exist in the binary split tree; cmux silently rejects with "no adjacent border". Replaced with split-time resize: each `new-split` is followed immediately by one `resize-pane` call, while only two panes share that border. Direction choice picks the pane that has a neighbor in that direction (-L/-U on NEW, -R/-D on OLD) so cmux never errors. `--amount` is empirically pixels not cells, so deltas multiply by `cell_width_px` / `cell_height_px`. 3. Wrong "command pre-typed at restored prompt". The previous global $HISTFILE hint stamped the same string (typically the `tools cmux profiles save` invocation) on every pane. Replaced with per-pane `cmux capture-pane` parsing, only matching prompts with directory/git/host context (oh-my-zsh, [user@host dir]$, user@host:dir$); bare `❯`/`$` are rejected to avoid Claude/Forge input boxes capturing chat text. Plus a new feature: visible terminal content is captured per-pane and replayed on restore via `printf '\033[2J\033[3J\033[H'; printf %s '<base64>' | base64 -d`, so each restored pane shows its saved screen with a fresh shell prompt below. Cmux.md updated with the four V1 routing bugs we hit (`pane.list`, `surface.split`, `surface.create`, `surface.read_text`), the broken-in-0.63.2 `capture-pane --scrollback` flag, the ANSI-stripping (monochrome replay), and the pixel-based `--amount`. `convergeToTarget` and the `resize.ts` module removed; tests grew from 20 to 30, all passing. * fix(cmux/snapshot): list-pane-surfaces does return a {surfaces:[]} wrapper Empirically verified — `cmux --json list-pane-surfaces --workspace <ws> --pane <p>` on cmux 0.63.2 returns: { "pane_ref": "...", "workspace_ref": "...", "window_ref": "...", "surfaces": [...] } The earlier "fix" that retyped this as a bare `SurfaceListEntry[]` array was wrong against the live CLI — `[...surfacesInfo]` then threw `Spread syntax requires ...iterable[Symbol.iterator] to be a function` and aborted save with no profile written. Restore the wrapper interface and the `.surfaces` access. * feat(cmux/save): clack multiselect step — pick what to capture per pane After the scope picker (and before the name prompt) the interactive save flow now asks which per-pane data to record: ◆ What should be captured per pane? ◇ Working directory — from each pane's tab title ◇ Visible screen contents — saved scrollback redrawn on restore ◇ Last shell command — pre-typed (not executed) at the new prompt All three default-checked. The prompt is skipped when any of `--no-cwd`, `--no-screen`, `--no-history` is explicitly passed (those are unambiguous intent) and in non-interactive mode (where flags are the only input). Closes the gap user flagged: saving used to silently capture everything with no chance to opt out short of remembering the right `--no-*` flag. * fix(cmux/snapshot): skip screen capture for the surface running save The pane where the user invokes \`tools cmux profiles save\` has the save command itself echoed at the bottom of its visible content, plus the running clack prompts. Capturing that pane's screen would record the save invocation as the "saved scrollback" and replay it verbatim into the restored pane — meta-circular. Get the caller surface_ref from \`cmux identify\`'s \`caller\` field, plumb it through to captureSurface, and pass screen:false for that one surface only. Other panes still capture screen normally; cwd and command capture are unaffected for the caller too. * fix(cmux): address remaining PR #154 review comments - cli.ts: parse cmux --json subprocess output in strict mode (rejects comments / malformed JSON) - shell-probe.ts: only treat user@host: title suffix as cwd when it looks like a path; titles like "user@host:build" no longer round-trip as `cd build` on restore - snapshot.ts: throw a clear error when --window/--workspace ref does not match anything (instead of silently writing an empty profile) - save.ts: reject incompatible --window/--workspace + --scope combos up front; validate positional profile names with the same rules used by the interactive prompt - edit.ts: description mentions \$VISUAL and \$EDITOR; ProfileNotFoundError is caught locally and prints a clean message + exits 1 - restore.ts: pass full error object to logger.warn (preserves stack) - Cmux.md: fix broken anchor for "the BIG GOTCHA" heading
Summary
What changed? Adds a Profiles feature that lets users save their current workspace layout as a named profile (e.g. "Work", "Personal") and restore it later — either in the current window or a new one. Think of it like VS Code workspaces: a preset of terminals, splits, and browser panels you can switch between.
Why? Power users often have distinct contexts — a "Work" setup with API server, frontend, and test workspaces, and a "Personal" setup with side projects. Today, these have to be manually recreated each time. Profiles make this a one-click operation.
What's included
Sources/ProfileStore.swiftProfilemodel +ProfileStorepersistence layer (save/load/delete/rename/list as JSON in~/Library/Application Support/cmux/profiles/)Sources/cmuxApp.swiftSources/ContentView.swift?help buttonSources/AppDelegate.swiftSources/TabManager.swiftactiveProfileNamepublished property — persists across workspace changesSources/TerminalController.swiftprofile.list,profile.save,profile.load,profile.delete(v1 + v2)cmuxTests/ProfileStoreTests.swiftSocket/CLI API
v1:
v2 JSON:
How it works
Profiles are a thin layer on top of the existing
SessionTabManagerSnapshot— the same system powering session restore on app restart. Saving callstabManager.sessionSnapshot(), loading callstabManager.restoreSessionSnapshot(). Auto-save piggybacks on the existing 8-second autosave timer.Testing
ProfileStoreTestscovering Codable round-trip, save/load/delete/rename, list sorting, filename sanitization, active profile state, snapshot field preservation, edge cases. All passing.Demo Video
Screenshot (badge visible in sidebar footer):
The active profile badge sits inline next to the
?button in the sidebar footer, showing the currently loaded profile name (e.g. "Work"). It appears when a profile is loaded and persists across workspace additions/removals since changes auto-save back to the profile.Checklist
Summary by cubic
Add Profiles to save the current workspace layout as a named profile and restore it in the current window or a new one. Makes switching between work/personal setups a one-click action.
ProfileStore: save/load/delete/rename/list profiles as JSON in ~/Library/Application Support/cmux/profiles/ (max 24).profile_list,profile_save,profile_load [--new-window],profile_delete; v2profile.list|save|load|deleteJSON APIs.TabManager: newactiveProfileNamepublished property.Written for commit d7b2d1f. Summary will update on new commits.
Summary by CodeRabbit
New Features
Tests