Skip to content

cmux Island MVP (notch-anchored agent session overlay) - #2764

Closed
jukben wants to merge 28 commits into
manaflow-ai:mainfrom
jukben:island-mvp
Closed

jukben wants to merge 28 commits into
manaflow-ai:mainfrom
jukben:island-mvp

Conversation

@jukben

@jukben jukben commented Apr 9, 2026 •

Copy link
Copy Markdown

Sorry, I didn't want to open the PR yet, I'm just playing around at this moment 🙏

Summary

  • Adds the MVP of cmux Island — a notch-anchored Dynamic Island style overlay that lists active AI-agent sessions running inside cmux and routes clicks to the corresponding workspace + terminal split.
  • Reads sessions from existing cmux set-status entries with known agent keys (claude_code, codex, copilot_cli, opencode, gemini_cli, cursor, amp, droid). Zero new CLI surface — existing hook integrations in docs/notifications.md light up automatically.
  • Opt-in from Settings → Island (off by default). Mirrored in ~/.config/cmux/settings.json under "island": { "enabled": true } via the existing CmuxSettingsFileStore pipeline.
  • New module at Sources/Island/ with a narrow IslandStateProvider / IslandFocusSink seam so the feature can be extracted to a companion cmuxIsland.app target later without touching call sites.

Closes #2590 (MVP scope only — approvals, companion-app extraction, and multi-display are explicit Phase 2+ non-goals documented in the spec).

Architecture

Sources/Island/
├── IslandSettings.swift          UserDefaults key + default
├── IslandSession.swift           IslandAgentKind / Phase / Session value types
├── IslandStateProvider.swift     read seam + IslandStateSource + InMemoryIslandStateSource
├── IslandStateStore.swift        debounced Combine projection + TabManagerIslandStateSource
├── IslandFocusSink.swift         write seam + TabManagerIslandFocusSink
├── IslandJumpRouter.swift        click → activate/select/focus/collapse sequence
├── NotchShape.swift              SwiftUI Shape (port from farouqaldori/claude-island, Apache 2.0)
├── NotchPanel.swift              NSPanel subclass (adapted from claude-island, Apache 2.0)
├── IslandRootView.swift          SwiftUI closed pill + expanded row list
└── IslandWindowController.swift  NSWindowController owning the NotchPanel

AppDelegate observes UserDefaults.didChangeNotification for IslandSettings.enabledKey and creates/destroys the controller reactively.

References

  • Spec: docs/superpowers/specs/2026-04-09-cmux-island-design.md — 11 sections covering module layout, data model, projection rules, UI geometry, opt-in, testing, smoke-test checklist, Phase 2 extension points.
  • Plan: docs/superpowers/plans/2026-04-09-cmux-island.md — 20 TDD tasks.
  • Reference port: farouqaldori/claude-island (Apache 2.0). Only NotchShape.swift, NotchPanel configuration, and the NSPanel mechanics were ported. ClaudeSessionMonitor, HookSocketServer, and HookInstaller from upstream were not ported — those implement a hook-driven session model that is out of scope for the cmux Island MVP. Attribution added to THIRD_PARTY_LICENSES.md.

Tests

New unit tests under cmuxTests/ (run via cmux-unit scheme in CI, never locally per CLAUDE.md):

  • IslandSessionPhaseTests — 5 tests covering the phase-normalization synonym table (running_tool, waiting_for_input, case-insensitive + trim-tolerant lookup).
  • IslandSessionSortTests — 3 tests covering phase-rank priority + recency tie-break.
  • IslandStateStoreTests — 5 tests covering empty source, single-session emission (debounced), sort on init-snapshot path, sort on tick path, and clearing.
  • IslandJumpRouterTests — 3 tests covering happy path, workspace-gone short-circuit, panel-gone continuation.
  • IslandSettingsFileStoreTests — 4 tests covering settings.json round-trip through the CmuxSettingsFileStore parser + whitelist membership.

No snapshot tests of NotchShape per CLAUDE.md test quality policy (those would be source-shape tests). Visual verification is manual via the Debug menu.

Known MVP limitations (documented for Phase 2)

  • Workspace-scoped session projection. A workspace with two panels running different agents produces one island row (the highest-priority set-status entry), bound to the workspace's focused panel. Panel-specific set-status --tab resolution is Phase 2.
  • Unread count is workspace-scoped, gated by a per-panel unread flag. TerminalNotificationStore has no public per-panel count API today; the implementation gates the workspace count by hasUnreadNotification(forTabId:surfaceId:) and falls back to 0. Documented in an in-code TODO.
  • island.enabled = true via settings.json before launch. There is a narrow launch-time race where AppDelegate.tabManager (a weak var) may not yet be assigned when the island enable observer fires synchronously on .prepend(...). For users who flip the toggle at runtime (expected primary path), this is a non-issue. For pre-enabled users, a toggle round-trip in Settings will reliably create the controller.
  • Sort stability at identical priority + timestamp. Dictionary.values.sorted is not stable-by-insertion-order; if two status entries in the same workspace collide on both fields, the winner can flap. Acceptable for MVP (display-only effect).
  • Relative time units in expanded rows use hardcoded English s/m/h/d suffixes. Localization keys for these can be added in Phase 2.

Phase 2+ extension points (not built)

  • Pending permission approvals (approve/deny buttons) — additive to the row model, requires a new cmux approve/cmux deny CLI or blocking socket RPC.
  • Extraction to a separate cmuxIsland.app target — swap IslandStateStore for a SocketIslandStateProvider; the SwiftUI layer already depends only on the protocol.
  • Multi-display support, hover-to-expand, mark-read from island, auto-dismiss idle rows.

Test plan

  • CI unit tests: xcodebuild -scheme cmux-unit via CI. Expect all Island*Tests classes green.
  • Tagged Debug build smoke test: ./scripts/reload.sh --tag island-mvp then walk the spec §9 12-step checklist on a notch-equipped Mac:
    • Settings → Island toggle on while no sessions exist → island stays hidden
    • cmux set-status claude_code Running → pill appears with green dot, count 1
    • Click pill → expands; row shows workspace + panel, phase RUNNING, elapsed counts up
    • Click outside → collapses
    • cmux set-status codex Idle in a second workspace → count 2, dot still green
    • cmux set-status claude_code Error → dot turns red
    • Click pill → expand → click Claude Code row → cmux comes to front, workspace selected, panel focused
    • cmux clear-status claude_code → row disappears
    • cmux clear-status codex → island orderOuts with no artifacts
    • Space / full-screen switching → island follows (.canJoinAllSpaces, .fullScreenAuxiliary)
    • Rapidly toggle Settings → Island off/on 10 times → no leftover panels or observers
    • On a non-notch Mac: island renders as a floating pill at top-center
  • Debug window (DEBUG builds only): Debug > Debug Windows > Island Controller… → inject synthetic sessions → verify notch shape + row layout render correctly
  • Review the commit history (28 commits, each ≤1 file group, TDD where applicable)

🤖 Generated with Claude Code


Summary by cubic

Adds the opt-in cmux Island: a notch-anchored overlay that shows active agent sessions from cmux set-status and lets you jump to the right workspace and terminal split in one click. Off by default; enable in Settings → Island or via ~/.config/cmux/settings.json.

  • New Features

    • Detects sessions from known keys (e.g., claude_code, codex, copilot_cli) with zero new CLI surface.
    • Click a row → activate app, select workspace, focus panel, then collapse.
    • New module: IslandStateStore, IslandStateProvider, IslandJumpRouter, IslandWindowController, IslandRootView.
    • Debug menu (DEBUG): “Island Controller…” to inject and clear test sessions.
    • Ports NotchShape and NotchPanel from farouqaldori/claude-island (Apache 2.0); licenses updated.
    • Tests added for phase normalization, sort order, state store, jump routing, and settings parser.
  • Migration

    • No CLI changes.
    • To enable: Settings → Island, or set "island": { "enabled": true } in ~/.config/cmux/settings.json.

Written for commit f5b0f8b. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Added cmux Island: an opt-in always-on-top overlay displaying active AI agent sessions with status indicators and unread counts, enabling click-to-jump navigation to relevant workspaces and panels. Configurable in Settings.

jukben and others added 28 commits April 9, 2026 22:54
Brainstormed design for a notch-anchored Dynamic Island overlay that
surfaces active AI agent sessions (Monitor + Jump scope). Captures
module boundary, data model derived from existing set-status entries,
NotchPanel/NotchShape port from farouqaldori/claude-island, opt-in
configuration, testing plan, and explicit Phase 2 extension points
(approvals, companion-app extraction).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
20 TDD tasks covering the full MVP: types, state projection, focus
routing, NotchPanel + NotchShape port, IslandRootView, settings.json
integration, Debug menu, AppDelegate lifecycle, Xcode project
registration, smoke test, changelog, and PR.

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

Introduces IslandSettings.enabledKey as the single source of truth for
the cmux Island opt-in toggle, and teaches CmuxSettingsFileStore to
read an "island" section from ~/.config/cmux/settings.json. Tests cover
the round-trip parser path and the path whitelist.

First commit of the cmux Island MVP. Spec:
docs/superpowers/specs/2026-04-09-cmux-island-design.md

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

Code review flagged that the inlined "island.enabled" string literal in
parseIslandSection could drift from IslandSettings.enabledKey if either
side is renamed. The literal is still required — IslandSettings.swift
is not yet registered in project.pbxproj and the symbol is not visible
to this file until Task 17 — so this commit adds an explicit
TODO(Task 17) marker above the line so the cleanup is searchable.

The review also recommended reverting two visibility widenings from
e248d7e (ManagedStringOverride and ManagedCustomSettings from private
to internal). That revert is not applicable: both types are reachable
from ResolvedSettingsSnapshot (which must stay internal for the
@testable test helper's return type), and Swift requires property
types to be at least as visible as their containing type, so the whole
chain is forced to be internal. Documenting here for the record; no
code change for that item.

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

Core data types for the cmux Island MVP. Phase normalization is
table-driven and table-tested; sort comparator + rank prepared for the
upcoming store.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pins the sort order (running → waiting → error → idle → unknown,
tie-break by most recent activity) against regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Defines the two protocols that bound the island's read path:
IslandStateProvider is what the view observes; IslandStateSource is
what the store subscribes to. Includes InMemoryIslandStateSource for
tests and the debug-menu inject path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Four fixes from the code review of tasks 2-4:

- IslandAgentKind.color: return NSColor instead of SwiftUI.Color so
  Sources/Island/IslandSession.swift stays free of SwiftUI imports.
  Spec §5.1 specified NSColor; the view layer can bridge with
  Color(nsColor:) at the SwiftUI call site.
- Comparator: inline comment on the lastActivity > rhs comparison to
  spell out the intentional inversion ("descending: newer activity
  ranks first"), since reading `>` inside a `<` operator is a trap
  for the casual reader.
- IslandSession: add explicit Comparable conformance — the `<`
  operator already satisfies the protocol; declaring it makes the
  type usable in Comparable-constrained generics.
- InMemoryIslandStateSource: declare @unchecked Sendable explicitly.
  All mutable state is @MainActor-pinned so the class is already
  isolation-safe; the explicit conformance makes the contract
  audit-visible rather than relying on compiler inference.

The reviewer's suggestion to add "stopped"/"cancelled"/"aborted"
synonyms to the phase normalization table is deliberately deferred —
it is not in the MVP spec (§5.3) and adding it would be scope creep.
Flagged for Phase 2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Debounced Combine pipeline that resorts and republishes the session
list whenever the upstream IslandStateSource ticks. Tests exercise
projection, sort order, and empty-state transitions via
InMemoryIslandStateSource.

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

Code review on Task 5 flagged that testSortOrderRunningBeforeIdle only
exercises the init-snapshot sort path. A regression in the debounced
sink that drops sorted(by: <) would not be caught. The new test
constructs an empty store, subscribes first, then populates the source
so the sort runs inside the debounced sink closure.

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

Production IslandStateSource that subscribes to @published tab list,
per-workspace statusEntries/panelTitles/panelCustomTitles/panels/title/
customTitle, and TerminalNotificationStore.objectWillChange, then emits
a debounce-friendly change stream the store can snapshot.

Unread count is workspace-scoped for MVP: TerminalNotificationStore
exposes unreadCount(forTabId:) and hasUnreadNotification(forTabId:
surfaceId:), but no per-panel count. We report the workspace count
only when the panel holds an unread indicator; otherwise 0. A TODO
marks the cleanup for when a per-panel count API lands in Phase 2.

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

Address two comment-accuracy findings from code review:

1. The priming resubscribe(to:) call before the $tabs sink is logically
   redundant when init runs on @mainactor (Combine delivers the initial
   $tabs value synchronously). The previous comment overstated its
   necessity. New comment documents the invariant ("existing workspaces
   always have observers before the first snapshot") and explains why
   we keep the defensive call.

2. The unread count helper has two edge cases that weren't documented:
   workspace-level notifications stored with surfaceId: nil will return
   false negatives, and two unread panels in the same workspace will
   both report the full workspace count. Both now called out next to
   the Phase 2 TODO.

Code behavior is unchanged; only comments updated.

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

Single write-back surface from the Island module into cmux core. The
router depends only on the protocol; tests use a spy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The router guarantees collapseIsland() runs exactly once and that
activateApp() is skipped when the target workspace no longer exists.
Verified against a SpyFocusSink.

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

Code review caught two gaps in Batch B:

1. Spec §6.6 required a dlog("island.jump failed…") call in DEBUG
   builds when either selectWorkspace or focusPanel returns false.
   The router was silently ignoring both failure cases. Added
   conditional dlog emissions for workspace-gone and panel-gone
   paths, guarded by #if DEBUG and importing Bonsplit (the module
   that defines dlog).
2. Spec §6.6 described the happy-path sequence as
   [activate → select → focus → collapse], but the plan (and the
   Batch B implementation) correctly re-ordered it to
   [select → activate → focus → collapse] so that a jump to a
   torn-down workspace does not steal focus. Updated §6.6 to
   reflect the actual shipping order and added a "Sequence note"
   block explaining why the earlier ordering was abandoned.

The router docstring is also updated to point at the intent (§6.6)
and to spell out the probe-before-activate ordering in prose.

No test changes — existing tests already cover both failure paths
and expect the sequence that the router now emits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Apache 2.0 port of the inward-top / outward-bottom quadratic notch shape
used as the mask for the island overlay. Attribution added to
THIRD_PARTY_LICENSES.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Non-activating, all-spaces floating panel configured to sit above the
menu bar. Adapted from claude-island's NotchWindow (Apache 2.0).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
15 new keys covering the Settings section, phase pills, header,
Debug Windows menu entry, and the debug-window labels. English +
Japanese matching cmux's minimum-supported convention (52 existing
entries in the file already use the same 2-language-only shape;
SwiftUI falls back to the source language for the other 17 locales
until translations are added).

Keys added:
- island.header.title
- island.settings.title, .enable.label, .enable.help, .known_kinds.help
- island.phase.running/idle/waiting/error/unknown
- island.debug.window.title, .injectTestSession, .clearTestSessions
- menu.debug.islandController
- settings.section.island

Preserves the file's existing insertion order (no re-sort), so the
diff is +255/-0 rather than the full-file churn that a naive rewrite
would produce.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SwiftUI root for the cmux Island. Closed state shows a dot + count on
the left extension of the notch; expanded state shows a vertical list
of session rows that route clicks through IslandJumpRouter. Uses only
localized strings. View model exposes layout geometry derived from
the notch size and an aggregate-color indicator matching spec §5.5.

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

Spec reviewer flagged a bare middot literal in the unread badge
Text("·\(count)"). Fixed the flagged case plus four analogous ones
that a strict reading of CLAUDE.md's localization rule would also
reject: the collapsed pill session count, the agent-kind monogram,
and the two composite row label strings ("workspace · panel" and
"agentKind · elapsed"). All of these carry structural separators
and/or data that is not translated as a whole — `Text(verbatim:)`
is the idiomatic SwiftUI signal that the string is intentionally
not localizable.

The only `Text(...)` entries that still use `String(localized:)`
are the ones that correspond to phrases a translator would want
to edit: the header title and the five phase pill labels.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wires provider -> view model -> NotchPanel, toggles visibility based on
the sessions list, and flips ignoresMouseEvents when the view opens
or closes. Provides a shutdown() path AppDelegate calls when the
island.enabled setting turns off, and a close() convenience used by
the router's collapse callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Users can now opt in to the cmux Island overlay from Settings. Toggle
is bound to the "island.enabled" UserDefaults key (inlined as a string
literal until Task 17 registers Sources/Island/IslandSettings.swift in
project.pbxproj — marked with TODO(Task 17)). The setting is also
reachable through ~/.config/cmux/settings.json via the parser added
in Task 1.

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

IslandControllerDebugWindowController hosts an enable toggle, an
inject-test-session button, a clear button, and a live synthetic
session count for visual iteration during development. Wrapped in
#if DEBUG per CLAUDE.md. Sessions stored in local @State (pending
Task 17 pbxproj registration); will migrate to InMemoryIslandStateSource
once Island files are in the build target.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds 10 Sources/Island/*.swift files to the GhosttyTabs (cmux) target and
5 cmuxTests/Island*Tests.swift files to the cmuxTests target so the
module and its tests compile.

Also adds the missing `override` keyword on
`IslandWindowController.close()` — this is a latent Task 13 bug that
only surfaces now that the Island sources are actually built.

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

Now that project.pbxproj registers Sources/Island/*.swift, call sites
that previously inlined "island.enabled" or used a placeholder debug
type can reference the real symbols:

- CmuxSettingsFileStore.parseIslandSection: "island.enabled" literal
  → IslandSettings.enabledKey
- SettingsView @AppStorage: "island.enabled" literal →
  IslandSettings.enabledKey, default → IslandSettings.defaultEnabled
- IslandControllerDebugWindowController / IslandDebugView: the
  IslandDebugSession stand-in is replaced with a real IslandSession
  held by an InMemoryIslandStateSource, so the debug window now
  exercises the production provider protocol.

`grep TODO(Task 17)` returns zero matches after this commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AppDelegate now observes UserDefaults.didChangeNotification for
island.enabled changes and creates or tears down an
IslandWindowController reactively. The store reads from
TabManagerIslandStateSource, the focus sink writes back through
TabManagerIslandFocusSink, and the jump router collapses via the
injected closure. Disabling the toggle releases the panel, cancels
subscriptions, and leaves no leftover observer.

This completes the MVP integration for the cmux Island feature.

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

vercel Bot commented Apr 9, 2026

Copy link
Copy Markdown

@jukben 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 Apr 9, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Introduces "cmux Island," an opt-in macOS Dynamic Island–style overlay that displays active AI agent sessions sourced from existing cmux set-status status entries. The feature includes session state management, navigation routing to focus relevant workspaces/panels, comprehensive test coverage, and Settings integration.

Changes

Cohort / File(s) Summary
Island Core Module
Sources/Island/IslandSession.swift, Sources/Island/IslandSettings.swift, Sources/Island/IslandStateProvider.swift, Sources/Island/IslandStateStore.swift, Sources/Island/IslandFocusSink.swift, Sources/Island/IslandJumpRouter.swift, Sources/Island/IslandRootView.swift, Sources/Island/IslandWindowController.swift, Sources/Island/NotchPanel.swift, Sources/Island/NotchShape.swift
Comprehensive Island subsystem: defines session/phase data models, settings keys, state providers and stores for projecting sessions from TabManager, focus/routing protocol and implementations for workspace/panel navigation, SwiftUI root view and view model, window/panel management, and notch shape rendering.
Project & Localization
GhosttyTabs.xcodeproj/project.pbxproj, Resources/Localizable.xcstrings
Added 10 new Island source files and 5 test files to build phases; added 15 new localization string keys (titles, settings, phase labels, debug UI, menu items) in English and Japanese.
AppDelegate Integration
Sources/AppDelegate.swift
Added opt-in Island overlay lifecycle: observes UserDefaults.didChangeNotification for IslandSettings.enabledKey, creates/destroys IslandWindowController reactively with main-queue dispatch, and guards against duplicate creation.
Settings & Debugging
Sources/KeyboardShortcutSettingsFileStore.swift, Sources/cmuxApp.swift
Extended settings file parser to recognize island.enabled JSON boolean; added debug-only "Island Controller" menu window with test session injection/clearing UI and Settings section toggle, both bound to @AppStorage(IslandSettings.enabledKey).
Test Suite
cmuxTests/IslandSessionPhaseTests.swift, cmuxTests/IslandSessionSortTests.swift, cmuxTests/IslandJumpRouterTests.swift, cmuxTests/IslandStateStoreTests.swift, cmuxTests/IslandSettingsFileStoreTests.swift
Comprehensive test coverage for phase normalization, session sorting, router navigation flow, state store subscription/debounce, and settings file round-trip parsing.
Documentation & Configuration
.gitignore, CHANGELOG.md, THIRD_PARTY_LICENSES.md, docs/superpowers/specs/2026-04-09-cmux-island-design.md
Added ignore patterns for brainstorm/worktree directories; added "Unreleased" changelog entry; added third-party attribution for ported Claude Island files; added comprehensive MVP design spec detailing architecture, state flow, UI behavior, integration requirements, and testing expectations.

Sequence Diagram(s)

sequenceDiagram
    participant AppDelegate
    participant UserDefaults
    participant IslandWindowController
    participant IslandStateStore
    participant TabManager
    participant IslandRootView

    AppDelegate->>AppDelegate: applicationDidFinishLaunching
    AppDelegate->>UserDefaults: observe didChangeNotification
    UserDefaults->>AppDelegate: emit didChange (IslandSettings.enabledKey)
    AppDelegate->>AppDelegate: removeDuplicates, main queue dispatch
    
    alt enabled = true
        AppDelegate->>IslandWindowController: init(provider, router)
        IslandWindowController->>IslandStateStore: init(source: TabManagerIslandStateSource)
        IslandStateStore->>TabManager: observe tabs, statusEntries, notifications
        IslandWindowController->>IslandRootView: bind viewModel to provider
        IslandStateStore->>IslandRootView: sessionsPublisher emits [IslandSession]
        IslandRootView->>IslandRootView: render notch + sessions
    else enabled = false
        AppDelegate->>IslandWindowController: shutdown()
        IslandWindowController->>IslandWindowController: cancel subscriptions, order out, nil window
    end
Loading
sequenceDiagram
    participant User
    participant IslandRootView
    participant IslandRootViewModel
    participant IslandJumpRouter
    participant IslandFocusSink
    participant TabManager

    User->>IslandRootView: tap session row
    IslandRootView->>IslandRootViewModel: jump(to: session)
    IslandRootViewModel->>IslandJumpRouter: jump(to: session)
    
    IslandJumpRouter->>IslandFocusSink: selectWorkspace(id: workspaceId)
    alt workspace found
        IslandFocusSink->>TabManager: selectWorkspace
        IslandFocusSink->>IslandJumpRouter: return true
        IslandJumpRouter->>IslandFocusSink: activateApp()
        IslandFocusSink->>IslandFocusSink: NSApp.activate(ignoringOtherApps: true)
        IslandJumpRouter->>IslandFocusSink: focusPanel(id: panelId, inWorkspace: workspaceId)
        IslandFocusSink->>TabManager: focusPanel
        IslandJumpRouter->>IslandFocusSink: collapseIsland()
        IslandFocusSink->>IslandRootViewModel: close()
    else workspace not found
        IslandFocusSink->>IslandJumpRouter: return false
        IslandJumpRouter->>IslandFocusSink: collapseIsland()
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • #1204 — Modifies AppDelegate with stored properties and Combine setup in applicationDidFinishLaunching, overlapping with Island lifecycle initialization.
  • #2655 — Extends KeyboardShortcutSettingsFileStore to support new settings.json paths and add test helpers, directly related to Island settings file parsing.
  • #1717 — Modifies Xcode project build phases to add new source and test file entries, parallel approach to adding Island module files.

Poem

🐰 A notch blooms atop the screen so bright,
Sessions dancing in Dynamic Island light,
Click a row and workspace jumps to thee,
Routers guide us where we ought to be! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically summarizes the main feature—cmux Island MVP as a notch-anchored agent session overlay—matching the changeset's primary objective.
Description check ✅ Passed The PR description comprehensively covers the changeset with clear Summary, Architecture, References, Tests, Known Limitations, and Phase 2 extension points, meeting all key template sections.
Linked Issues check ✅ Passed The changeset fully addresses #2590: provides a global notch-anchored overlay for sessions, session-first anchored to workspace/surface/agent, supports jump-to-panel quick actions, is decoupled and opt-in via Settings, and exposes IslandStateProvider/IslandFocusSink seams for future companion app extraction.
Out of Scope Changes check ✅ Passed All changes are directly scoped to Island MVP implementation: new Island module, Settings integration, test suites, localized strings, documentation, and project registration—no unrelated refactoring or out-of-scope modifications present.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces the cmux Island MVP — a notch-anchored SwiftUI overlay (Dynamic Island–style) that surfaces active AI-agent sessions from existing cmux set-status entries and routes clicks to the corresponding workspace and terminal split. The feature is opt-in from Settings → Island, backed by a clean protocol seam (IslandStateProvider / IslandFocusSink) that enables future extraction to a companion app target.

  • P1 (launch-time silent failure): Users who set \"island\": { \"enabled\": true } in ~/.config/cmux/settings.json before launch will silently get no island. The .prepend(...) fires synchronously in applicationDidFinishLaunching while self.tabManager is still nil; the guard let tabManager silently returns without scheduling any retry. The workaround (toggle round-trip in Settings) is documented but users have no indication the feature failed to initialize.

Confidence Score: 4/5

Safe to merge once the P1 launch-time race is addressed; all remaining findings are P2 style/debug-tooling concerns.

One P1 remains: users who pre-enable the island via settings.json will silently get no island on launch, with no error or retry. All other findings are P2: a debug-only wiring gap, a deprecation warning, and widened internal types. Architecture, protocol seams, tests, and lifecycle management are all well-executed.

Sources/AppDelegate.swift (P1 launch-time race), Sources/cmuxApp.swift (debug source wiring)

Important Files Changed

Filename Overview
Sources/AppDelegate.swift Adds island controller lifecycle management; contains a P1 launch-time race where pre-enabled island silently fails to initialize when tabManager is nil at prepend-delivery time.
Sources/Island/IslandStateStore.swift Clean debounced Combine projection from source to publisher; TabManagerIslandStateSource correctly subscribes to all relevant workspace publishers and the notification store tick.
Sources/Island/IslandWindowController.swift Owns NotchPanel lifecycle, visibility reconciliation, and mouse-event pass-through toggling; shutdown() / close() semantics are intentionally split and correctly implemented.
Sources/Island/IslandJumpRouter.swift Correct probe-before-activate ordering per spec §6.6; collapse always fires exactly once in both happy-path and workspace-gone branches.
Sources/Island/IslandFocusSink.swift Protocol and production implementation are clean; one-directional dependency flow via injected collapse closure is well-designed.
Sources/cmuxApp.swift Adds Debug menu entry and IslandControllerDebugWindowController; the injected in-memory source in IslandDebugView is never wired to a visible island panel, making "Inject test session" a no-op in the smoke-test workflow.
Sources/KeyboardShortcutSettingsFileStore.swift Adds island.enabled to the settings parser and whitelist; previously-private types widened to internal to support the test helper, which unnecessarily expands the module's internal API surface.

Sequence Diagram

sequenceDiagram
    participant AD as AppDelegate
    participant IWC as IslandWindowController
    participant ISS as IslandStateStore
    participant TMS as TabManagerIslandStateSource
    participant IRV as IslandRootView
    participant IJR as IslandJumpRouter
    participant IFS as TabManagerIslandFocusSink

    AD->>AD: observe UserDefaults didChange (island.enabled)
    AD->>ISS: IslandStateStore(source: TabManagerIslandStateSource)
    AD->>IWC: IslandWindowController(provider: store, router: router)
    IWC->>IRV: NSHostingView(IslandRootView(viewModel:))

    loop Workspace/Notification Changes
        TMS-->>ISS: changes publisher (debounce 50ms)
        ISS->>ISS: makeSnapshot().sorted(by: <)
        ISS-->>IWC: sessionsPublisher emission
        IWC->>IWC: reconcile (orderFront / orderOut)
        ISS-->>IRV: sessionsPublisher → viewModel.sessions update
    end

    Note over IRV: User clicks collapsed pill
    IRV->>IRV: viewModel.open() → isOpen = true
    IWC->>IWC: ignoresMouseEvents = false

    Note over IRV: User clicks a session row
    IRV->>IJR: jump(to: session)
    IJR->>IFS: selectWorkspace(id:) → Bool
    alt workspace found
        IJR->>IFS: activateApp()
        IJR->>IFS: focusPanel(id:inWorkspace:)
    end
    IJR->>IFS: collapseIsland()
    IFS-->>IWC: collapse closure → close() → viewModel.close()
    IWC->>IWC: ignoresMouseEvents = true

    Note over AD: island.enabled flipped off
    AD->>IWC: shutdown()
    IWC->>IWC: cancellables.removeAll(), orderOut, window = nil
Loading

Reviews (1): Last reviewed commit: "Changelog entry for cmux Island MVP (#25..." | Re-trigger Greptile

Comment thread Sources/AppDelegate.swift
Comment on lines +2688 to +2692

@MainActor
private func refreshIslandController(enabled: Bool) {
if enabled {
guard islandWindowController == nil else { return }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Launch-time silent failure for pre-enabled island

When island.enabled = true is set in settings.json before first launch, .prepend(...) fires synchronously inside applicationDidFinishLaunching — before tabManager is assigned. The guard let tabManager = self.tabManager else { return } silently exits with no retry or user-visible feedback, so the island is never initialized. The workaround (toggle round-trip in Settings) requires the user to know something went wrong.

A minimal fix is to schedule a one-shot retry after app finish:

guard let tabManager = self.tabManager else {
    // tabManager not yet assigned (pre-enabled via settings.json at launch).
    // Re-attempt after applicationDidFinishLaunching completes.
    DispatchQueue.main.async { [weak self] in
        self?.refreshIslandController(enabled: enabled)
    }
    return
}

Comment thread Sources/cmuxApp.swift
Comment on lines +2749 to 2793
Button(String(
localized: "island.debug.clearTestSessions",
defaultValue: "Clear test sessions"
)) {
source.clear()
sessionCount = 0
}
}

Divider()

Text(verbatim: "Injected sessions: \(sessionCount)")
.font(.system(size: 11))
.foregroundStyle(.secondary)

Spacer()
}
.padding(16)
.frame(minWidth: 380, minHeight: 480)
}

private func injectRandomSession() {
let kinds = IslandAgentKind.allCases
let kind = kinds[sessionCount % kinds.count]
let session = IslandSession(
id: UUID(),
workspaceId: UUID(),
panelId: UUID(),
agentKind: kind,
phase: .running,
workspaceTitle: "Debug workspace",
panelTitle: "Debug panel \(sessionCount + 1)",
lastActivity: Date(),
unreadCount: 0,
rawStatusValue: "running"
)
source.add(session)
sessionCount = source.makeSnapshot().count
}
}
#endif

private struct AboutPanelView: View {
@Environment(\.openURL) private var openURL

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 "Inject test session" is a no-op in the visible island

IslandDebugView creates its own @State private var source = InMemoryIslandStateSource() and injects sessions into it, but this source is never connected to a visible IslandWindowController. The toggle (islandEnabled) wires up the production controller via AppDelegate.refreshIslandController, which uses TabManagerIslandStateSource — not source. Injected sessions are counted in sessionCount but never appear in the island overlay.

The smoke-test checklist in the PR description expects "inject synthetic sessions → verify notch shape + row layout render correctly," which currently won't work. A fix would be to either create a dedicated IslandWindowController(provider: store, router: …) from the debug controller using this source, or expose a seam on the production controller to accept an override source.

self.tabManager = tabManager
self.collapse = collapse
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 NSApp.activate(ignoringOtherApps:) deprecated in macOS 14

NSApp.activate(ignoringOtherApps: true) was deprecated in macOS 14 in favour of NSApp.activate(). Since cmux targets Sequoia+, this generates a deprecation warning at compile time.

Suggested change
NSApp.activate()

Comment on lines 1496 to 1530
@@ -1493,7 +1524,7 @@ private struct ManagedCustomSettings: Equatable {
}
}

private enum ManagedSettingsValue: Equatable {
enum ManagedSettingsValue: Equatable {
case bool(Bool)
case int(Int)
case double(Double)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Previously-private types widened to internal to support the test helper

ResolvedSettingsSnapshot, ManagedSettingsValue, ManagedCustomSettings, and ManagedStringOverride were all private before this PR. They've been made internal (no access modifier) solely so IslandSettingsFileStoreTests can reference them via @testable import. This unnecessarily widens the module's internal API surface.

A narrower alternative: keep the types private and expose only the specific assertion path through the existing #if DEBUG testResolveSnapshot helper, having it return just the managedUserDefaults dictionary the tests actually check instead of the full snapshot.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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: 15

🧹 Nitpick comments (4)
cmuxTests/IslandSettingsFileStoreTests.swift (1)

52-55: Prefer using the shared key constant in the whitelist assertion.

Using IslandSettings.enabledKey here avoids drift if the key is renamed.

♻️ Suggested tweak
-        XCTAssertTrue(
-            CmuxSettingsFileStore.supportedSettingsJSONPaths.contains("island.enabled")
-        )
+        XCTAssertTrue(
+            CmuxSettingsFileStore.supportedSettingsJSONPaths.contains(IslandSettings.enabledKey)
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/IslandSettingsFileStoreTests.swift` around lines 52 - 55, Update
the testIslandEnabledPathIsWhitelisted assertion to use the shared key constant
instead of a hard-coded string: replace the literal "island.enabled" with
IslandSettings.enabledKey when checking
CmuxSettingsFileStore.supportedSettingsJSONPaths so the test follows renames of
the key.
cmuxTests/IslandSessionSortTests.swift (1)

17-21: Use id == panelId in the sort-test fixture.

Matching the session identity invariant in test data keeps comparator tests realistic and avoids introducing impossible model states in helpers.

Proposed fix
     ) -> IslandSession {
+        let panelId = UUID()
         IslandSession(
-            id: UUID(),
+            id: panelId,
             workspaceId: UUID(),
-            panelId: UUID(),
+            panelId: panelId,
             agentKind: .claudeCode,
             phase: phase,
             workspaceTitle: "w",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/IslandSessionSortTests.swift` around lines 17 - 21, The test
fixture for IslandSession used in the sort tests creates sessions with different
UUIDs for id and panelId, violating the session identity invariant; update the
fixture in IslandSessionSortTests (the IslandSession initializer used in the
sort test) so that id is assigned the same UUID as panelId (i.e., generate one
UUID and use it for both id and panelId) to ensure realistic, valid session
instances for the comparator tests.
Sources/Island/NotchShape.swift (1)

33-79: Clamp corner radii to rect bounds before path construction.

Clamping avoids invalid/overlapping geometry when the shape is animated into very small sizes.

Proposed fix
     func path(in rect: CGRect) -> Path {
+        let top = max(0, min(topCornerRadius, min(rect.width / 2, rect.height)))
+        let bottomMax = max(0, min(rect.width / 2 - top, rect.height - top))
+        let bottom = max(0, min(bottomCornerRadius, bottomMax))
+
         var path = Path()

         // Start at top-left
         path.move(to: CGPoint(x: rect.minX, y: rect.minY))

         // Top-left inward curve
         path.addQuadCurve(
-            to: CGPoint(x: rect.minX + topCornerRadius, y: rect.minY + topCornerRadius),
-            control: CGPoint(x: rect.minX + topCornerRadius, y: rect.minY)
+            to: CGPoint(x: rect.minX + top, y: rect.minY + top),
+            control: CGPoint(x: rect.minX + top, y: rect.minY)
         )

         // Left edge down
         path.addLine(
-            to: CGPoint(x: rect.minX + topCornerRadius, y: rect.maxY - bottomCornerRadius)
+            to: CGPoint(x: rect.minX + top, y: rect.maxY - bottom)
         )

         // Bottom-left outward curve
         path.addQuadCurve(
-            to: CGPoint(x: rect.minX + topCornerRadius + bottomCornerRadius, y: rect.maxY),
-            control: CGPoint(x: rect.minX + topCornerRadius, y: rect.maxY)
+            to: CGPoint(x: rect.minX + top + bottom, y: rect.maxY),
+            control: CGPoint(x: rect.minX + top, y: rect.maxY)
         )

         // Bottom edge
         path.addLine(
-            to: CGPoint(x: rect.maxX - topCornerRadius - bottomCornerRadius, y: rect.maxY)
+            to: CGPoint(x: rect.maxX - top - bottom, y: rect.maxY)
         )

         // Bottom-right outward curve
         path.addQuadCurve(
-            to: CGPoint(x: rect.maxX - topCornerRadius, y: rect.maxY - bottomCornerRadius),
-            control: CGPoint(x: rect.maxX - topCornerRadius, y: rect.maxY)
+            to: CGPoint(x: rect.maxX - top, y: rect.maxY - bottom),
+            control: CGPoint(x: rect.maxX - top, y: rect.maxY)
         )

         // Right edge up
         path.addLine(
-            to: CGPoint(x: rect.maxX - topCornerRadius, y: rect.minY + topCornerRadius)
+            to: CGPoint(x: rect.maxX - top, y: rect.minY + top)
         )

         // Top-right inward curve
         path.addQuadCurve(
             to: CGPoint(x: rect.maxX, y: rect.minY),
-            control: CGPoint(x: rect.maxX - topCornerRadius, y: rect.minY)
+            control: CGPoint(x: rect.maxX - top, y: rect.minY)
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Island/NotchShape.swift` around lines 33 - 79, In path(in rect:
CGRect) clamp topCornerRadius and bottomCornerRadius before building the Path:
compute safeTop = min(max(0, topCornerRadius), min(rect.width, rect.height) / 2)
and safeBottom = min(max(0, bottomCornerRadius), min(rect.width, rect.height) /
2) (or use half of the relevant edge length if you need independent width/height
limits), then use safeTop and safeBottom everywhere in the method instead of
topCornerRadius/bottomCornerRadius so the generated geometry cannot overlap or
become invalid when the rect is very small.
cmuxTests/IslandStateStoreTests.swift (1)

22-39: Align test fixture identity with the IslandSession model contract.

makeSession(...) currently creates unrelated id and panelId. Keeping id == panelId in fixtures makes tests closer to production semantics and avoids masking identity-based regressions.

Proposed fix
     private func makeSession(
         phase: IslandSessionPhase = .running,
         kind: IslandAgentKind = .claudeCode,
         lastActivity: Date = Date(timeIntervalSince1970: 100),
         unread: Int = 0
     ) -> IslandSession {
-        IslandSession(
-            id: UUID(),
+        let panelId = UUID()
+        return IslandSession(
+            id: panelId,
             workspaceId: UUID(),
-            panelId: UUID(),
+            panelId: panelId,
             agentKind: kind,
             phase: phase,
             workspaceTitle: "ws",
             panelTitle: "p",
             lastActivity: lastActivity,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/IslandStateStoreTests.swift` around lines 22 - 39, The test fixture
makeSession creates separate UUIDs for id and panelId which diverges from the
IslandSession identity contract; modify makeSession to generate a single UUID
(e.g., let uuid = UUID()) and use that same uuid for both the IslandSession id
and panelId fields so fixtures reflect production semantics (leave workspaceId
as-is unless intentionally tied).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/superpowers/specs/2026-04-09-cmux-island-design.md`:
- Around line 50-60: The fenced code blocks (e.g., the file-list block that
includes IslandStateProvider.swift, IslandStateStore.swift, IslandSession.swift,
IslandRootView.swift, NotchPanel.swift, etc., and the other examples mentioned)
are missing language identifiers; update each triple-backtick fence to include
an appropriate language token (text for plain directory/file listings, swift for
Swift code, json where applicable) so markdownlint stops flagging them and
rendering/tooling improves.
- Line 383: The test expectation in IslandJumpRouterTests is using the old
order; update the test that exercises jump(to: session) (which depends only on
IslandFocusSink spy) to assert the new routing contract: call sequence must be
selectWorkspace(id) → activateApp() → focusPanel(id) → collapse, and adjust the
"workspace deleted" edge-case test to assert collapse without calling
activateApp() (but still not calling selectWorkspace when appropriate); update
any spy assertions that reference activate → selectWorkspace to the new order
and ensure the spy records include activateApp() as the post-selectWorkspace
step.

In `@Sources/AppDelegate.swift`:
- Around line 2693-2706: The IslandWindowController is being bound to a single
TabManager instance (captured via self.tabManager) so it only represents the
active main window at creation; instead, make the state source and focus sink
aggregate across or rebind to the application's mainWindowContexts: replace
constructing TabManagerIslandStateSource(tabManager: tabManager) and
TabManagerIslandFocusSink(tabManager: tabManager, ...) with a source/router that
pulls from mainWindowContexts (or subscribe to mainWindow active-window changes
and recreate the store/focus sink when the active TabManager changes), and
update IslandJumpRouter/IslandWindowController to use that aggregated context
provider rather than a single TabManager instance (refer to
TabManagerIslandStateSource, TabManagerIslandFocusSink, IslandJumpRouter,
IslandWindowController, and mainWindowContexts).
- Around line 2678-2693: The prepended initial value from islandEnabledObserver
can be ignored on cold start because refreshIslandController(enabled:) returns
early when tabManager is nil; to fix, capture and persist the latest enabled
state (e.g., a property like lastIslandEnabled) when the sink receives values
and then, inside configure(...), after wiring tabManager call
refreshIslandController(enabled: lastIslandEnabled) to replay the stored true if
needed; reference islandEnabledObserver, refreshIslandController(enabled:),
tabManager, and configure(...) so you update the sink to store the latest value
and invoke refreshIslandController once configure completes.

In `@Sources/cmuxApp.swift`:
- Around line 5551-5572: The resetAllSettings() implementation does not reset
the new `@AppStorage-backed` islandEnabled property to its default; update
resetAllSettings() to explicitly set islandEnabled =
IslandSettings.defaultEnabled (or call a central IslandSettings.reset() helper)
so the toggle is restored after a full reset; locate references to
islandEnabled, resetAllSettings(), and IslandSettings.defaultEnabled in
Sources/cmuxApp.swift to make the change and ensure any persisted `@AppStorage`
key is overwritten when resetting.
- Around line 2699-2715: The Island debug panel is given the identifier
"cmux.islandDebug" but that identifier isn't included in the
cmuxAuxiliaryWindowIdentifiers collection, so Cmd+W falls through; add
"cmux.islandDebug" (use the same NSUserInterfaceItemIdentifier value) to the
cmuxAuxiliaryWindowIdentifiers constant/Set (where that collection is declared)
so the window is recognized as an auxiliary window and closes itself when Cmd+W
is pressed; ensure the identifier string exactly matches the one set on
window.identifier in the Island debug panel init.
- Around line 2760-2780: Replace the bare English UI strings with localized
variants: update the Text(verbatim: "Injected sessions: \(sessionCount)") to use
String(localized:..., defaultValue:...) (include formatting for sessionCount),
and in injectRandomSession() replace the workspaceTitle "Debug workspace" and
panelTitle "Debug panel \(sessionCount + 1)" when constructing IslandSession
with String(localized:..., defaultValue:...) values (use distinct localization
keys such as "island.debug.injected_sessions", "island.debug.workspace_title",
"island.debug.panel_title" and include the session index in the localized panel
title formatting). Ensure all three user-facing literals go through
String(localized:..., defaultValue:...) so no bare UI strings remain.

In `@Sources/Island/IslandFocusSink.swift`:
- Around line 37-65: The sink currently restricts lookups to the injected
TabManager (tabManager.tabs) causing selectWorkspace(id:) and
focusPanel(id:inWorkspace:) to fail for workspaces owned by other windows;
update the logic to resolve workspaces and panels from a global/app-wide
resolver instead of only TabManager—for example use the same cross-window lookup
used by sendPickedElementToTerminal (AppDelegate.shared?.workspaceFor(tabId:) or
an injected global WorkspaceResolver) to find the workspace and panel, then call
the per-workspace focus/select methods (workspace.focusPanel,
tabManager.selectWorkspace or the workspace-level select) and preserve
activateApp()/collapse behavior. Ensure selectWorkspace and focusPanel delegate
to the global resolver when TabManager.tabs does not contain the target so
islands work across all main windows.

In `@Sources/Island/IslandRootView.swift`:
- Around line 198-204: relativeTime(since:) computes against Date() once during
render so the label freezes; add a timer-driven tick state (e.g. `@State` private
var now: Date) that updates every second while the panel is expanded and stop it
when collapsed. In the view that calls relativeTime(since:) (the IslandRootView
body), start a Timer (Timer.publish / scheduledTimer) or a Combine publisher in
onAppear/onChange of the expansion state (e.g. isExpanded or expanded) to update
now = Date() every second only when expanded, and cancel/invalidate the timer
when collapsed or onDisappear, then pass now into relativeTime(since:) instead
of calling Date() inside that function so the view invalidates each second while
expanded.
- Around line 111-125: Replace the hard-coded composed strings that use
Text(verbatim:) in IslandRootView (the two Text lines combining
session.workspaceTitle · session.panelTitle and session.agentKind.displayName ·
relativeTime(since: session.lastActivity)) with localized strings/formatters:
keep the title and name values as arguments but build the full row text via
String(localized: "key.name", defaultValue: "...") with placeholders for
workspace/panel/agent and a localized elapsed-time formatter (or
RelativeDateTimeFormatter localized) instead of s/m/h/d suffixes; update both
the workspace/panel Text and the agent/time Text to use those localized keys and
pass session.workspaceTitle, session.panelTitle, session.agentKind.displayName
and the localized elapsed string into the localized format.

In `@Sources/Island/IslandStateStore.swift`:
- Around line 100-111: The compilation error comes from using
workspace.statusEntries.filter(...) and then .values (filter on a Dictionary
returns an Array of (key,value) tuples), so replace that pattern by filtering
the dictionary's values directly: compute matching as
workspace.statusEntries.values.filter { knownKeys.contains($0.key) } (or
reconstruct a Dictionary from the filtered tuples and then use .values) before
sorting; update the subsequent uses of matching, sorted and winner (in the block
around tabManager, matching, sorted, winner, and IslandAgentKind(rawValue: ...))
to work with the resulting [SidebarStatusEntry] so the sort and winner selection
compile.

In `@Sources/Island/IslandWindowController.swift`:
- Around line 98-103: In reconcile(sessions: [IslandSession]) ensure you reset
the view model when sessions become empty: when sessions.isEmpty call
viewModel.close() before calling window?.orderOut(nil) so the
IslandWindowController's viewModel state is collapsed on the empty transition
(keep the rest of the visibility logic intact).
- Around line 46-47: The panel currently sets panel.ignoresMouseEvents = true
when isOpen == false which makes the collapsed pill non-interactive; instead
keep panel.ignoresMouseEvents = false and implement hit-testing pass-through so
only the pill area receives events. Concretely: remove the ignoresMouseEvents
toggles in IslandWindowController, create/use a custom content view (e.g.,
ClickThroughContentView) or override hitTest(_:in:) on the existing contentView
to return nil for points outside the closed-pill frame and return the pill view
(or super.hitTest) for points inside that frame, and ensure the logic still
returns normal hit-testing when isOpen == true; reference
IslandWindowController, panel, isOpen, and the closed-pill hit area when making
the change.

In `@Sources/KeyboardShortcutSettingsFileStore.swift`:
- Line 80: defaultTemplateSections() in KeyboardShortcutSettingsFileStore
currently omits an "island" block so the generated settings.json template
doesn't include the new island.enabled setting; update defaultTemplateSections()
to emit an "island" section (matching the parser/whitelist key "island.enabled")
with the appropriate default value and explanatory comment so bootstrapped
config files advertise the feature, and make the same change in the other
template-generation location referenced around the 742-750 region to keep both
templates in sync.
- Around line 1475-1489: Extract the parsing logic out of the instance method
CmuxSettingsFileStore.parseSettingsFile into a new static helper (e.g.,
CmuxSettingsFileStore.parseSettingsRoot(root:sourcePath:) or a top-level static
helper) that takes the already-deserialized JSON root and returns
ResolvedSettingsSnapshot, then change testResolveSnapshot to preprocess the
input with JSONCParser.preprocess(jsonString:), deserialize the preprocessed
string into a root dictionary via JSONSerialization, and call the new static
parse helper to produce the snapshot—this avoids instantiating a live
CmuxSettingsFileStore (so no bootstrapping/reload/watchers) and ensures JSONC
preprocessing (comments/trailing commas/BOM) runs in tests.

---

Nitpick comments:
In `@cmuxTests/IslandSessionSortTests.swift`:
- Around line 17-21: The test fixture for IslandSession used in the sort tests
creates sessions with different UUIDs for id and panelId, violating the session
identity invariant; update the fixture in IslandSessionSortTests (the
IslandSession initializer used in the sort test) so that id is assigned the same
UUID as panelId (i.e., generate one UUID and use it for both id and panelId) to
ensure realistic, valid session instances for the comparator tests.

In `@cmuxTests/IslandSettingsFileStoreTests.swift`:
- Around line 52-55: Update the testIslandEnabledPathIsWhitelisted assertion to
use the shared key constant instead of a hard-coded string: replace the literal
"island.enabled" with IslandSettings.enabledKey when checking
CmuxSettingsFileStore.supportedSettingsJSONPaths so the test follows renames of
the key.

In `@cmuxTests/IslandStateStoreTests.swift`:
- Around line 22-39: The test fixture makeSession creates separate UUIDs for id
and panelId which diverges from the IslandSession identity contract; modify
makeSession to generate a single UUID (e.g., let uuid = UUID()) and use that
same uuid for both the IslandSession id and panelId fields so fixtures reflect
production semantics (leave workspaceId as-is unless intentionally tied).

In `@Sources/Island/NotchShape.swift`:
- Around line 33-79: In path(in rect: CGRect) clamp topCornerRadius and
bottomCornerRadius before building the Path: compute safeTop = min(max(0,
topCornerRadius), min(rect.width, rect.height) / 2) and safeBottom = min(max(0,
bottomCornerRadius), min(rect.width, rect.height) / 2) (or use half of the
relevant edge length if you need independent width/height limits), then use
safeTop and safeBottom everywhere in the method instead of
topCornerRadius/bottomCornerRadius so the generated geometry cannot overlap or
become invalid when the rect is very small.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 42ae1028-ead8-4189-8d2c-b54569c9728f

📥 Commits

Reviewing files that changed from the base of the PR and between 9155d09 and f5b0f8b.

📒 Files selected for processing (25)
  • .gitignore
  • CHANGELOG.md
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/Island/IslandFocusSink.swift
  • Sources/Island/IslandJumpRouter.swift
  • Sources/Island/IslandRootView.swift
  • Sources/Island/IslandSession.swift
  • Sources/Island/IslandSettings.swift
  • Sources/Island/IslandStateProvider.swift
  • Sources/Island/IslandStateStore.swift
  • Sources/Island/IslandWindowController.swift
  • Sources/Island/NotchPanel.swift
  • Sources/Island/NotchShape.swift
  • Sources/KeyboardShortcutSettingsFileStore.swift
  • Sources/cmuxApp.swift
  • THIRD_PARTY_LICENSES.md
  • cmuxTests/IslandJumpRouterTests.swift
  • cmuxTests/IslandSessionPhaseTests.swift
  • cmuxTests/IslandSessionSortTests.swift
  • cmuxTests/IslandSettingsFileStoreTests.swift
  • cmuxTests/IslandStateStoreTests.swift
  • docs/superpowers/plans/2026-04-09-cmux-island.md
  • docs/superpowers/specs/2026-04-09-cmux-island-design.md

Comment on lines +50 to +60
```
Sources/Island/
├── IslandStateProvider.swift — protocol; the only interface the view observes
├── IslandStateStore.swift — concrete provider; projects TabManager → IslandSession list
├── IslandSession.swift — value type + supporting enums
├── IslandFocusSink.swift — protocol the router calls to focus workspace/panel
├── IslandJumpRouter.swift — translates row taps into focus calls
├── IslandWindowController.swift — NSWindowController owning the NotchPanel
├── NotchPanel.swift — NSPanel subclass
├── NotchShape.swift — SwiftUI Shape (port from claude-island)
└── IslandRootView.swift — SwiftUI view for closed/expanded states

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

Add language identifiers to these fenced code blocks.

markdownlint is flagging these fences as missing a language, which hurts rendering and tooling. Please tag them as text, swift, json, etc. based on the content.

Also applies to: 73-79, 159-162, 180-182, 246-250, 261-272

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 50-50: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

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

In `@docs/superpowers/specs/2026-04-09-cmux-island-design.md` around lines 50 -
60, The fenced code blocks (e.g., the file-list block that includes
IslandStateProvider.swift, IslandStateStore.swift, IslandSession.swift,
IslandRootView.swift, NotchPanel.swift, etc., and the other examples mentioned)
are missing language identifiers; update each triple-backtick fence to include
an appropriate language token (text for plain directory/file listings, swift for
Swift code, json where applicable) so markdownlint stops flagging them and
rendering/tooling improves.


4. **`IslandVisibilityTests`** — given a `sessions: [IslandSession]` publisher stream, asserts the downstream `visible: Bool` stream matches §5.4.

5. **`IslandJumpRouterTests`** — router depends only on `IslandFocusSink`. Spy implementation records calls. Test verifies `jump(to: session)` produces the sequence `activate → selectWorkspace(id) → focusPanel(id) → collapse`. A test for the "workspace deleted" edge case verifies the router collapses without calling `activate`.

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

This test expectation still documents the old jump order.

Section 6.6 says the router must selectWorkspace first and only call activateApp() after that succeeds, but this line still says tests should assert activate → selectWorkspace → focusPanel → collapse. Please align the test plan with the current routing contract.

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

In `@docs/superpowers/specs/2026-04-09-cmux-island-design.md` at line 383, The
test expectation in IslandJumpRouterTests is using the old order; update the
test that exercises jump(to: session) (which depends only on IslandFocusSink
spy) to assert the new routing contract: call sequence must be
selectWorkspace(id) → activateApp() → focusPanel(id) → collapse, and adjust the
"workspace deleted" edge-case test to assert collapse without calling
activateApp() (but still not calling selectWorkspace when appropriate); update
any spy assertions that reference activate → selectWorkspace to the new order
and ensure the spy records include activateApp() as the post-selectWorkspace
step.

Comment thread Sources/AppDelegate.swift
Comment on lines +2678 to +2693
islandEnabledObserver = NotificationCenter.default
.publisher(for: UserDefaults.didChangeNotification)
.map { _ in UserDefaults.standard.bool(forKey: IslandSettings.enabledKey) }
.prepend(UserDefaults.standard.bool(forKey: IslandSettings.enabledKey))
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] enabled in
self?.refreshIslandController(enabled: enabled)
}
}

@MainActor
private func refreshIslandController(enabled: Bool) {
if enabled {
guard islandWindowController == nil else { return }
guard let tabManager = self.tabManager 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

Persisted Island enablement is dropped on cold start.

If island.enabled is already true, the prepended value is delivered during launch, but refreshIslandController immediately returns because tabManager has not been wired yet. Nothing replays that initial true after configure(...) runs, so the overlay never appears until the user toggles the setting again.

Suggested fix
 func configure(tabManager: TabManager, notificationStore: TerminalNotificationStore, sidebarState: SidebarState) {
     self.tabManager = tabManager
     self.notificationStore = notificationStore
     self.sidebarState = sidebarState
+    if UserDefaults.standard.bool(forKey: IslandSettings.enabledKey) {
+        refreshIslandController(enabled: true)
+    }
     disableSuddenTerminationIfNeeded()
     installLifecycleSnapshotObserversIfNeeded()
     prepareStartupSessionSnapshotIfNeeded()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 2678 - 2693, The prepended initial
value from islandEnabledObserver can be ignored on cold start because
refreshIslandController(enabled:) returns early when tabManager is nil; to fix,
capture and persist the latest enabled state (e.g., a property like
lastIslandEnabled) when the sink receives values and then, inside
configure(...), after wiring tabManager call refreshIslandController(enabled:
lastIslandEnabled) to replay the stored true if needed; reference
islandEnabledObserver, refreshIslandController(enabled:), tabManager, and
configure(...) so you update the sink to store the latest value and invoke
refreshIslandController once configure completes.

Comment thread Sources/AppDelegate.swift
Comment on lines +2693 to +2706
guard let tabManager = self.tabManager else { return }

let source = TabManagerIslandStateSource(tabManager: tabManager)
let store = IslandStateStore(source: source)

let focusSink = TabManagerIslandFocusSink(
tabManager: tabManager,
collapse: { [weak self] in
self?.islandWindowController?.close()
}
)
let router = IslandJumpRouter(focusSink: focusSink)

let controller = IslandWindowController(provider: store, router: router)

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

The Island controller is scoped to one active window, not the app.

This builds TabManagerIslandStateSource and TabManagerIslandFocusSink from the current self.tabManager, so the controller is permanently pinned to whichever main window was active when it was created. In multi-window sessions, the “global” island will miss sessions from other windows and won't reliably jump to them. Consider sourcing/routing through mainWindowContexts (or recreating/rebinding when the active main window changes) instead of capturing a single TabManager.

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

In `@Sources/AppDelegate.swift` around lines 2693 - 2706, The
IslandWindowController is being bound to a single TabManager instance (captured
via self.tabManager) so it only represents the active main window at creation;
instead, make the state source and focus sink aggregate across or rebind to the
application's mainWindowContexts: replace constructing
TabManagerIslandStateSource(tabManager: tabManager) and
TabManagerIslandFocusSink(tabManager: tabManager, ...) with a source/router that
pulls from mainWindowContexts (or subscribe to mainWindow active-window changes
and recreate the store/focus sink when the active TabManager changes), and
update IslandJumpRouter/IslandWindowController to use that aggregated context
provider rather than a single TabManager instance (refer to
TabManagerIslandStateSource, TabManagerIslandFocusSink, IslandJumpRouter,
IslandWindowController, and mainWindowContexts).

Comment thread Sources/cmuxApp.swift
Comment on lines +2699 to +2715
window.identifier = NSUserInterfaceItemIdentifier("cmux.islandDebug")
window.center()
window.contentView = NSHostingView(rootView: IslandDebugView())
AppDelegate.shared?.applyWindowDecorations(to: window)
super.init(window: window)
window.delegate = self
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func show() {
window?.center()
window?.makeKeyAndOrderFront(nil)
}

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

Register this panel as an auxiliary window.

Line 2699 adds a new auxiliary window identifier, but cmuxAuxiliaryWindowIdentifiers is not updated. Cmd+W on the Island debug panel will currently fall through to workspace/panel close behavior instead of closing the panel itself.

Suggested fix
 private let cmuxAuxiliaryWindowIdentifiers: Set<String> = [
     "cmux.settings",
     "cmux.about",
     "cmux.licenses",
     "cmux.browser-popup",
     "cmux.settingsAboutTitlebarDebug",
     "cmux.debugWindowControls",
     "cmux.browserImportHintDebug",
     "cmux.sidebarDebug",
     "cmux.menubarDebug",
     "cmux.backgroundDebug",
+    "cmux.islandDebug",
 ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/cmuxApp.swift` around lines 2699 - 2715, The Island debug panel is
given the identifier "cmux.islandDebug" but that identifier isn't included in
the cmuxAuxiliaryWindowIdentifiers collection, so Cmd+W falls through; add
"cmux.islandDebug" (use the same NSUserInterfaceItemIdentifier value) to the
cmuxAuxiliaryWindowIdentifiers constant/Set (where that collection is declared)
so the window is recognized as an auxiliary window and closes itself when Cmd+W
is pressed; ensure the identifier string exactly matches the one set on
window.identifier in the Island debug panel init.

Comment on lines +100 to +111
for workspace in tabManager.tabs {
// Find the highest-priority status entry whose key is a known
// agent kind. Ties broken by most recent timestamp (spec §5.2).
let matching = workspace.statusEntries.filter { knownKeys.contains($0.key) }
guard !matching.isEmpty else { continue }

let sorted = matching.values.sorted { lhs, rhs in
if lhs.priority != rhs.priority { return lhs.priority > rhs.priority }
return lhs.timestamp > rhs.timestamp
}
guard let winner = sorted.first else { continue }
guard let kind = IslandAgentKind(rawValue: winner.key) else { continue }

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the type of statusEntries and how filter works on it
ast-grep --pattern 'statusEntries.filter { $_ }'
rg -n "statusEntries" Sources/Workspace.swift | head -30

Repository: manaflow-ai/cmux

Length of output: 1164


🏁 Script executed:

sed -n '95,115p' Sources/Island/IslandStateStore.swift

Repository: manaflow-ai/cmux

Length of output: 1157


🏁 Script executed:

rg -n "extension.*Array|extension.*Sequence" Sources/Island/IslandStateStore.swift
rg -n "\.values" Sources/Island/IslandStateStore.swift | head -20

Repository: manaflow-ai/cmux

Length of output: 125


🏁 Script executed:

rg -n "extension (Array|Sequence)" --type swift | xargs -I {} grep -A 5 "var values"
rg -n "var values" Sources/ --type swift

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

cat > /tmp/test_dict.swift << 'EOF'
import Foundation

let dict: [String: String] = ["a": "1", "b": "2"]
let filtered = dict.filter { $0.key == "a" }

// Check the type
print(type(of: filtered))

// Try to access .values
// This should fail: filtered.values
EOF

# Check Swift docs or look for similar patterns in the codebase
rg "\.filter.*\{.*\.key" Sources/ --type swift | head -10

Repository: manaflow-ai/cmux

Length of output: 1215


🏁 Script executed:

# Check if the project can be found and look for build evidence
find . -name "*.xcodeproj" -o -name "build" -o -name ".build" | head -5

# Also check if there are any compiler directives or alternate implementations
rg -B3 -A3 "matching.values" Sources/Island/IslandStateStore.swift

Repository: manaflow-ai/cmux

Length of output: 443


🏁 Script executed:

# Look for how filter is used on dictionaries elsewhere in the codebase
rg -B2 -A4 "\.filter.*\{.*\.key.*\}.*\.map|\.filter.*\{.*\.key.*\}.*\.values" Sources/ --type swift | head -30

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Let's check what methods are actually available
python3 << 'EOF'
# In Swift, Dictionary.filter returns an array of tuples [(key, value)]
# But there's also Dictionary(uniqueKeysWithValues:) and other initializers
# Let's search for patterns that might construct a dictionary from filter

import subprocess
result = subprocess.run([
    'rg', 
    r'Dictionary\s*\(\s*uniqueKeysWithValues|filter.*uniqueKeysWithValues', 
    'Sources/', 
    '--type', 'swift'
], capture_output=True, text=True)
print(result.stdout)
print(result.stderr)
EOF

Repository: manaflow-ai/cmux

Length of output: 1316


🏁 Script executed:

# Check if there's been any recent activity on this file
git log --oneline Sources/Island/IslandStateStore.swift | head -5

# Also check the exact line again to be sure
sed -n '100,112p' Sources/Island/IslandStateStore.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 905


🏁 Script executed:

# Let me verify what the actual type would be by looking at how Dictionary.filter works
# Check the Swift documentation pattern or look for .map(\.value) patterns in similar code
rg -B2 -A2 "\.filter.*\.map" Sources/Island/ --type swift

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check the exact type signature by looking at related dictionary operations
rg -A10 "statusEntries.filter" Sources/Island/IslandStateStore.swift

# Also check how similar patterns work in Workspace.swift
rg -B2 -A5 "\.filter.*validSurfaceIds.contains" Sources/Workspace.swift | head -20

Repository: manaflow-ai/cmux

Length of output: 1905


🏁 Script executed:

# Let me check the git status to see if this is on main or a branch
git rev-parse --abbrev-ref HEAD
git status

# Also verify the current state of the code matches what we're seeing
git show HEAD:Sources/Island/IslandStateStore.swift | sed -n '100,112p'

Repository: manaflow-ai/cmux

Length of output: 1050


🏁 Script executed:

# Search for any extensions that might add .values to Array or Sequence
rg "extension.*Array|extension.*Sequence" Sources/ --type swift -A 10 | grep -A 10 "values"

# Also check if there's a typealias or something
rg "typealias.*values" Sources/ --type swift

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Let me check if this code has actually been tested/compiled by looking at recent test results
# or checking if there are any build errors reported
git log --all --oneline --grep="Island" | head -10

# Also, let me verify the exact Swift behavior with a simple test
# by checking if Dictionary.filter returns something with .values
python3 << 'EOF'
# In Swift:
# Dictionary.filter returns Array<(Key, Value)>
# which is [String: SidebarStatusEntry] filtered to [(String, SidebarStatusEntry)]
# This array does NOT have a .values property

# The code should be either:
# 1. workspace.statusEntries.values.filter { ... } - filter the values directly
# 2. OR Dictionary(uniqueKeysWithValues: workspace.statusEntries.filter { ... }) - reconstruct dict

print("Dictionary.filter in Swift returns an Array of tuples")
print("Array does not have a .values property")
print("This code would fail to compile")
EOF

Repository: manaflow-ai/cmux

Length of output: 236


Fix compilation error: matching.values is invalid because .filter() on a Dictionary returns an Array, not a Dictionary.

When you call .filter() on workspace.statusEntries (a [String: SidebarStatusEntry] dictionary), it returns an Array<(key: String, value: SidebarStatusEntry)>, which doesn't have a .values property. This code will not compile.

Use one of these approaches instead:

  • workspace.statusEntries.values.filter { ... }.sorted { ... } — filter the values directly
  • Dictionary(uniqueKeysWithValues: workspace.statusEntries.filter { ... }).values.sorted { ... } — reconstruct the dictionary after filtering
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Island/IslandStateStore.swift` around lines 100 - 111, The
compilation error comes from using workspace.statusEntries.filter(...) and then
.values (filter on a Dictionary returns an Array of (key,value) tuples), so
replace that pattern by filtering the dictionary's values directly: compute
matching as workspace.statusEntries.values.filter { knownKeys.contains($0.key) }
(or reconstruct a Dictionary from the filtered tuples and then use .values)
before sorting; update the subsequent uses of matching, sorted and winner (in
the block around tabManager, matching, sorted, winner, and
IslandAgentKind(rawValue: ...)) to work with the resulting [SidebarStatusEntry]
so the sort and winner selection compile.

Comment on lines +46 to +47
panel.ignoresMouseEvents = true

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

The collapsed island can't be clicked open.

ignoresMouseEvents is true for the entire panel whenever isOpen == false, but the only open path is the SwiftUI tap gesture on the closed pill. That makes the collapsed island non-interactive as soon as it appears. Keep the panel interactive for the pill hit area and implement pass-through via hit-testing, not whole-window mouse ignoring.

Also applies to: 63-67

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

In `@Sources/Island/IslandWindowController.swift` around lines 46 - 47, The panel
currently sets panel.ignoresMouseEvents = true when isOpen == false which makes
the collapsed pill non-interactive; instead keep panel.ignoresMouseEvents =
false and implement hit-testing pass-through so only the pill area receives
events. Concretely: remove the ignoresMouseEvents toggles in
IslandWindowController, create/use a custom content view (e.g.,
ClickThroughContentView) or override hitTest(_:in:) on the existing contentView
to return nil for points outside the closed-pill frame and return the pill view
(or super.hitTest) for points inside that frame, and ensure the logic still
returns normal hit-testing when isOpen == true; reference
IslandWindowController, panel, isOpen, and the closed-pill hit area when making
the change.

Comment on lines +98 to +103
private func reconcile(sessions: [IslandSession]) {
if sessions.isEmpty {
window?.orderOut(nil)
} else if window?.isVisible != true {
window?.orderFront(nil)
}

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

Reset the open state when the last session disappears.

orderOut(nil) hides the window, but it leaves viewModel.isOpen unchanged. If the island was expanded when sessions went empty, the next non-empty emission comes back already open instead of restarting in the collapsed state. Call viewModel.close() on the empty transition before ordering the panel out.

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

In `@Sources/Island/IslandWindowController.swift` around lines 98 - 103, In
reconcile(sessions: [IslandSession]) ensure you reset the view model when
sessions become empty: when sessions.isEmpty call viewModel.close() before
calling window?.orderOut(nil) so the IslandWindowController's viewModel state is
collapsed on the empty transition (keep the rest of the visibility logic
intact).

"automation.portBase",
"automation.portRange",
"customCommands.trustedDirectories",
"island.enabled",

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

Add the new setting to the generated settings.json template too.

The parser/whitelist now support island.enabled, but defaultTemplateSections() still doesn't emit an island block. Newly bootstrapped config files therefore won't advertise the feature or keep the template in sync with the managed settings surface.

Also applies to: 742-750

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

In `@Sources/KeyboardShortcutSettingsFileStore.swift` at line 80,
defaultTemplateSections() in KeyboardShortcutSettingsFileStore currently omits
an "island" block so the generated settings.json template doesn't include the
new island.enabled setting; update defaultTemplateSections() to emit an "island"
section (matching the parser/whitelist key "island.enabled") with the
appropriate default value and explanatory comment so bootstrapped config files
advertise the feature, and make the same change in the other template-generation
location referenced around the 742-750 region to keep both templates in sync.

Comment on lines +1475 to +1489
#if DEBUG
/// Test-only entry that parses a JSON string into a ResolvedSettingsSnapshot.
/// Used by IslandSettingsFileStoreTests to verify the settings.json pipeline
/// end-to-end without hitting disk.
static func testResolveSnapshot(jsonString: String, sourcePath: String = "test.json") throws -> ResolvedSettingsSnapshot {
guard let data = jsonString.data(using: .utf8),
let root = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw NSError(
domain: "CmuxSettingsFileStoreTests",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "Invalid JSON"]
)
}
return CmuxSettingsFileStore().parseSettingsFile(root: root, sourcePath: sourcePath)
}

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

Make testResolveSnapshot side-effect-free and run the real JSONC parse path.

This helper currently constructs a live CmuxSettingsFileStore, which bootstraps the default settings path, reloads, and starts watchers/observers before the test parse happens. It also skips JSONCParser.preprocess, so comments, trailing commas, and BOM handling are no longer covered by the test entry point. Please extract a static parse helper that preprocesses JSONC and parses the snapshot without creating a live store instance.

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

In `@Sources/KeyboardShortcutSettingsFileStore.swift` around lines 1475 - 1489,
Extract the parsing logic out of the instance method
CmuxSettingsFileStore.parseSettingsFile into a new static helper (e.g.,
CmuxSettingsFileStore.parseSettingsRoot(root:sourcePath:) or a top-level static
helper) that takes the already-deserialized JSON root and returns
ResolvedSettingsSnapshot, then change testResolveSnapshot to preprocess the
input with JSONCParser.preprocess(jsonString:), deserialize the preprocessed
string into a root dictionary via JSONSerialization, and call the new static
parse helper to produce the snapshot—this avoids instantiating a live
CmuxSettingsFileStore (so no bootstrapping/reload/watchers) and ensures JSONC
preprocessing (comments/trailing commas/BOM) runs in tests.

@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.

8 issues found across 25 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="docs/superpowers/specs/2026-04-09-cmux-island-design.md">

<violation number="1" location="docs/superpowers/specs/2026-04-09-cmux-island-design.md:220">
P2: Target screen selection prefers any notched display over the main display, which conflicts with the MVP goal and non-goal of main-display-only behavior. In multi-display setups with a non-notched main display, this would place the island off the main display.</violation>

<violation number="2" location="docs/superpowers/specs/2026-04-09-cmux-island-design.md:383">
P2: Jump routing test sequence contradicts the spec’s select-first order, creating a spec/test inconsistency that can drive incorrect implementation or noisy tests.</violation>
</file>

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

<violation number="1" location="Sources/AppDelegate.swift:2693">
P2: The initial `.prepend` emission can fire before `tabManager` is set, and `refreshIslandController` bails out when `tabManager` is nil. There’s no follow‑up call after `configure(tabManager:...)`, so if Island is already enabled at launch the controller may never be created until the setting is toggled again.</violation>
</file>

<file name="Sources/Island/IslandWindowController.swift">

<violation number="1" location="Sources/Island/IslandWindowController.swift:66">
P2: Collapsed island cannot be clicked open because the panel ignores all mouse events when `isOpen` is false, and `isOpen` starts false with the only open action being a tap gesture.</violation>
</file>

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

<violation number="1" location="Sources/KeyboardShortcutSettingsFileStore.swift:1479">
P2: DEBUG test helper instantiates a full CmuxSettingsFileStore, triggering bootstrap/reload and UserDefaults/file watchers, so it can create/read the real settings.json and mutate global defaults despite claiming to avoid disk. This makes tests environment‑dependent and can pollute state.</violation>
</file>

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

<violation number="1" location="Sources/cmuxApp.swift:2726">
P2: The debug view's `InMemoryIslandStateSource` is never connected to a visible `IslandWindowController`. Injected test sessions update `sessionCount` locally but never appear in the island overlay, making the "Inject test session" button a no-op for visual verification. Either create a dedicated `IslandWindowController` backed by this `source`, or expose a seam on the production controller to accept an override source.</violation>
</file>

<file name="Sources/Island/IslandStateStore.swift">

<violation number="1" location="Sources/Island/IslandStateStore.swift:117">
P2: Island sessions are derived from workspace.focusedPanelId, but the source never emits changes when focus changes (focusedPanelId isn’t @Published). This can leave IslandStateStore with stale panelId/panelTitle until another unrelated update happens.</violation>
</file>

<file name="Sources/Island/IslandFocusSink.swift">

<violation number="1" location="Sources/Island/IslandFocusSink.swift:46">
P3: `NSApp.activate(ignoringOtherApps:)` is deprecated in macOS 14+. Since cmux targets Sequoia, replace with `NSApp.activate()`.</violation>
</file>

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


`ignoresMouseEvents` is toggled based on open/closed status so that, when collapsed, clicks outside the hit region pass through to the menu bar and apps underneath.

Target screen is `NSScreen.screens.first(where: { $0.hasPhysicalNotch }) ?? NSScreen.main`.

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 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: Target screen selection prefers any notched display over the main display, which conflicts with the MVP goal and non-goal of main-display-only behavior. In multi-display setups with a non-notched main display, this would place the island off the main display.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/superpowers/specs/2026-04-09-cmux-island-design.md, line 220:

<comment>Target screen selection prefers any notched display over the main display, which conflicts with the MVP goal and non-goal of main-display-only behavior. In multi-display setups with a non-notched main display, this would place the island off the main display.</comment>

<file context>
@@ -0,0 +1,435 @@
+
+`ignoresMouseEvents` is toggled based on open/closed status so that, when collapsed, clicks outside the hit region pass through to the menu bar and apps underneath.
+
+Target screen is `NSScreen.screens.first(where: { $0.hasPhysicalNotch }) ?? NSScreen.main`.
+
+### 6.2 Shape: `NotchShape`
</file context>
Suggested change
Target screen is `NSScreen.screens.first(where: { $0.hasPhysicalNotch }) ?? NSScreen.main`.
Target screen is `NSScreen.main ?? NSScreen.screens.first(where: { $0.hasPhysicalNotch })`.
Fix with Cubic


4. **`IslandVisibilityTests`** — given a `sessions: [IslandSession]` publisher stream, asserts the downstream `visible: Bool` stream matches §5.4.

5. **`IslandJumpRouterTests`** — router depends only on `IslandFocusSink`. Spy implementation records calls. Test verifies `jump(to: session)` produces the sequence `activate → selectWorkspace(id) → focusPanel(id) → collapse`. A test for the "workspace deleted" edge case verifies the router collapses without calling `activate`.

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 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: Jump routing test sequence contradicts the spec’s select-first order, creating a spec/test inconsistency that can drive incorrect implementation or noisy tests.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/superpowers/specs/2026-04-09-cmux-island-design.md, line 383:

<comment>Jump routing test sequence contradicts the spec’s select-first order, creating a spec/test inconsistency that can drive incorrect implementation or noisy tests.</comment>

<file context>
@@ -0,0 +1,435 @@
+
+4. **`IslandVisibilityTests`** — given a `sessions: [IslandSession]` publisher stream, asserts the downstream `visible: Bool` stream matches §5.4.
+
+5. **`IslandJumpRouterTests`** — router depends only on `IslandFocusSink`. Spy implementation records calls. Test verifies `jump(to: session)` produces the sequence `activate → selectWorkspace(id) → focusPanel(id) → collapse`. A test for the "workspace deleted" edge case verifies the router collapses without calling `activate`.
+
+6. **`IslandConfigRoundTripTests`** — `island.enabled` written via `@AppStorage` round-trips through `CmuxConfig` → `settings.json` → `CmuxConfig`, so editing the file is equivalent to the Settings toggle.
</file context>
Fix with Cubic

Comment thread Sources/AppDelegate.swift
private func refreshIslandController(enabled: Bool) {
if enabled {
guard islandWindowController == nil else { return }
guard let tabManager = self.tabManager else { return }

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 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: The initial .prepend emission can fire before tabManager is set, and refreshIslandController bails out when tabManager is nil. There’s no follow‑up call after configure(tabManager:...), so if Island is already enabled at launch the controller may never be created until the setting is toggled again.

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

<comment>The initial `.prepend` emission can fire before `tabManager` is set, and `refreshIslandController` bails out when `tabManager` is nil. There’s no follow‑up call after `configure(tabManager:...)`, so if Island is already enabled at launch the controller may never be created until the setting is toggled again.</comment>

<file context>
@@ -2669,6 +2671,44 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
+    private func refreshIslandController(enabled: Bool) {
+        if enabled {
+            guard islandWindowController == nil else { return }
+            guard let tabManager = self.tabManager else { return }
+
+            let source = TabManagerIslandStateSource(tabManager: tabManager)
</file context>
Fix with Cubic

viewModel.$isOpen
.receive(on: DispatchQueue.main)
.sink { [weak panel] isOpen in
panel?.ignoresMouseEvents = !isOpen

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 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: Collapsed island cannot be clicked open because the panel ignores all mouse events when isOpen is false, and isOpen starts false with the only open action being a tap gesture.

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

<comment>Collapsed island cannot be clicked open because the panel ignores all mouse events when `isOpen` is false, and `isOpen` starts false with the only open action being a tap gesture.</comment>

<file context>
@@ -0,0 +1,132 @@
+        viewModel.$isOpen
+            .receive(on: DispatchQueue.main)
+            .sink { [weak panel] isOpen in
+                panel?.ignoresMouseEvents = !isOpen
+            }
+            .store(in: &cancellables)
</file context>
Fix with Cubic

/// Test-only entry that parses a JSON string into a ResolvedSettingsSnapshot.
/// Used by IslandSettingsFileStoreTests to verify the settings.json pipeline
/// end-to-end without hitting disk.
static func testResolveSnapshot(jsonString: String, sourcePath: String = "test.json") throws -> ResolvedSettingsSnapshot {

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 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: DEBUG test helper instantiates a full CmuxSettingsFileStore, triggering bootstrap/reload and UserDefaults/file watchers, so it can create/read the real settings.json and mutate global defaults despite claiming to avoid disk. This makes tests environment‑dependent and can pollute state.

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

<comment>DEBUG test helper instantiates a full CmuxSettingsFileStore, triggering bootstrap/reload and UserDefaults/file watchers, so it can create/read the real settings.json and mutate global defaults despite claiming to avoid disk. This makes tests environment‑dependent and can pollute state.</comment>

<file context>
@@ -1457,23 +1471,40 @@ final class CmuxSettingsFileStore {
+    /// Test-only entry that parses a JSON string into a ResolvedSettingsSnapshot.
+    /// Used by IslandSettingsFileStoreTests to verify the settings.json pipeline
+    /// end-to-end without hitting disk.
+    static func testResolveSnapshot(jsonString: String, sourcePath: String = "test.json") throws -> ResolvedSettingsSnapshot {
+        guard let data = jsonString.data(using: .utf8),
+              let root = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
</file context>
Fix with Cubic

Comment thread Sources/cmuxApp.swift
@MainActor
private struct IslandDebugView: View {
@AppStorage(IslandSettings.enabledKey) private var islandEnabled = IslandSettings.defaultEnabled
@State private var source = InMemoryIslandStateSource()

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 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: The debug view's InMemoryIslandStateSource is never connected to a visible IslandWindowController. Injected test sessions update sessionCount locally but never appear in the island overlay, making the "Inject test session" button a no-op for visual verification. Either create a dedicated IslandWindowController backed by this source, or expose a seam on the production controller to accept an override source.

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

<comment>The debug view's `InMemoryIslandStateSource` is never connected to a visible `IslandWindowController`. Injected test sessions update `sessionCount` locally but never appear in the island overlay, making the "Inject test session" button a no-op for visual verification. Either create a dedicated `IslandWindowController` backed by this `source`, or expose a seam on the production controller to accept an override source.</comment>

<file context>
@@ -2669,6 +2677,117 @@ private final class SidebarDebugWindowController: NSWindowController, NSWindowDe
+@MainActor
+private struct IslandDebugView: View {
+    @AppStorage(IslandSettings.enabledKey) private var islandEnabled = IslandSettings.defaultEnabled
+    @State private var source = InMemoryIslandStateSource()
+    @State private var sessionCount: Int = 0
+
</file context>
Fix with Cubic

// the first panel in the workspace if nothing is focused. This
// is good enough for MVP — panel-specific `set-status --tab`
// resolution is Phase 2.
guard let panelId = workspace.focusedPanelId

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 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: Island sessions are derived from workspace.focusedPanelId, but the source never emits changes when focus changes (focusedPanelId isn’t @published). This can leave IslandStateStore with stale panelId/panelTitle until another unrelated update happens.

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

<comment>Island sessions are derived from workspace.focusedPanelId, but the source never emits changes when focus changes (focusedPanelId isn’t @Published). This can leave IslandStateStore with stale panelId/panelTitle until another unrelated update happens.</comment>

<file context>
@@ -0,0 +1,217 @@
+            // the first panel in the workspace if nothing is focused. This
+            // is good enough for MVP — panel-specific `set-status --tab`
+            // resolution is Phase 2.
+            guard let panelId = workspace.focusedPanelId
+                ?? workspace.panels.keys.first else { continue }
+
</file context>
Fix with Cubic

}

func activateApp() {
NSApp.activate(ignoringOtherApps: true)

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 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.

P3: NSApp.activate(ignoringOtherApps:) is deprecated in macOS 14+. Since cmux targets Sequoia, replace with NSApp.activate().

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

<comment>`NSApp.activate(ignoringOtherApps:)` is deprecated in macOS 14+. Since cmux targets Sequoia, replace with `NSApp.activate()`.</comment>

<file context>
@@ -0,0 +1,71 @@
+    }
+
+    func activateApp() {
+        NSApp.activate(ignoringOtherApps: true)
+    }
+
</file context>
Suggested change
NSApp.activate(ignoringOtherApps: true)
NSApp.activate()
Fix with Cubic

@jukben jukben closed this Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pluggable macOS Dynamic Island Overlay for cmux

1 participant