Repository navigation
Conversation
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>
|
@jukben is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughIntroduces "cmux Island," an opt-in macOS Dynamic Island–style overlay that displays active AI agent sessions sourced from existing Changes
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR introduces the cmux Island MVP — a notch-anchored SwiftUI overlay (Dynamic Island–style) that surfaces active AI-agent sessions from existing
Confidence Score: 4/5Safe 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
Sequence DiagramsequenceDiagram
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
Reviews (1): Last reviewed commit: "Changelog entry for cmux Island MVP (#25..." | Re-trigger Greptile |
|
|
||
| @MainActor | ||
| private func refreshIslandController(enabled: Bool) { | ||
| if enabled { | ||
| guard islandWindowController == nil else { return } |
There was a problem hiding this comment.
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
}| 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 | ||
|
|
There was a problem hiding this comment.
"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 | ||
| } | ||
|
|
There was a problem hiding this comment.
| @@ -1493,7 +1524,7 @@ private struct ManagedCustomSettings: Equatable { | |||
| } | |||
| } | |||
|
|
|||
| private enum ManagedSettingsValue: Equatable { | |||
| enum ManagedSettingsValue: Equatable { | |||
| case bool(Bool) | |||
| case int(Int) | |||
| case double(Double) | |||
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.enabledKeyhere 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: Useid == panelIdin 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 theIslandSessionmodel contract.
makeSession(...)currently creates unrelatedidandpanelId. Keepingid == panelIdin 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
📒 Files selected for processing (25)
.gitignoreCHANGELOG.mdGhosttyTabs.xcodeproj/project.pbxprojResources/Localizable.xcstringsSources/AppDelegate.swiftSources/Island/IslandFocusSink.swiftSources/Island/IslandJumpRouter.swiftSources/Island/IslandRootView.swiftSources/Island/IslandSession.swiftSources/Island/IslandSettings.swiftSources/Island/IslandStateProvider.swiftSources/Island/IslandStateStore.swiftSources/Island/IslandWindowController.swiftSources/Island/NotchPanel.swiftSources/Island/NotchShape.swiftSources/KeyboardShortcutSettingsFileStore.swiftSources/cmuxApp.swiftTHIRD_PARTY_LICENSES.mdcmuxTests/IslandJumpRouterTests.swiftcmuxTests/IslandSessionPhaseTests.swiftcmuxTests/IslandSessionSortTests.swiftcmuxTests/IslandSettingsFileStoreTests.swiftcmuxTests/IslandStateStoreTests.swiftdocs/superpowers/plans/2026-04-09-cmux-island.mddocs/superpowers/specs/2026-04-09-cmux-island-design.md
| ``` | ||
| 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 |
There was a problem hiding this comment.
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`. |
There was a problem hiding this comment.
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.
| 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 } |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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).
| 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) | ||
| } |
There was a problem hiding this comment.
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.
| 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 } |
There was a problem hiding this comment.
🧩 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 -30Repository: manaflow-ai/cmux
Length of output: 1164
🏁 Script executed:
sed -n '95,115p' Sources/Island/IslandStateStore.swiftRepository: 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 -20Repository: 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 swiftRepository: 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 -10Repository: 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.swiftRepository: 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 -30Repository: 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)
EOFRepository: 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 -nRepository: 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 swiftRepository: 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 -20Repository: 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 swiftRepository: 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")
EOFRepository: 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 directlyDictionary(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.
| panel.ignoresMouseEvents = true | ||
|
|
There was a problem hiding this comment.
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.
| private func reconcile(sessions: [IslandSession]) { | ||
| if sessions.isEmpty { | ||
| window?.orderOut(nil) | ||
| } else if window?.isVisible != true { | ||
| window?.orderFront(nil) | ||
| } |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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.
| #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) | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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`. |
There was a problem hiding this comment.
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>
| Target screen is `NSScreen.screens.first(where: { $0.hasPhysicalNotch }) ?? NSScreen.main`. | |
| Target screen is `NSScreen.main ?? NSScreen.screens.first(where: { $0.hasPhysicalNotch })`. |
|
|
||
| 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`. |
There was a problem hiding this comment.
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>
| private func refreshIslandController(enabled: Bool) { | ||
| if enabled { | ||
| guard islandWindowController == nil else { return } | ||
| guard let tabManager = self.tabManager else { return } |
There was a problem hiding this comment.
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>
| viewModel.$isOpen | ||
| .receive(on: DispatchQueue.main) | ||
| .sink { [weak panel] isOpen in | ||
| panel?.ignoresMouseEvents = !isOpen |
There was a problem hiding this comment.
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>
| /// 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 { |
There was a problem hiding this comment.
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>
| @MainActor | ||
| private struct IslandDebugView: View { | ||
| @AppStorage(IslandSettings.enabledKey) private var islandEnabled = IslandSettings.defaultEnabled | ||
| @State private var source = InMemoryIslandStateSource() |
There was a problem hiding this comment.
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>
| // 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 |
There was a problem hiding this comment.
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>
| } | ||
|
|
||
| func activateApp() { | ||
| NSApp.activate(ignoringOtherApps: true) |
There was a problem hiding this comment.
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>
| NSApp.activate(ignoringOtherApps: true) | |
| NSApp.activate() |
Sorry, I didn't want to open the PR yet, I'm just playing around at this moment 🙏
Summary
cmux set-statusentries with known agent keys (claude_code,codex,copilot_cli,opencode,gemini_cli,cursor,amp,droid). Zero new CLI surface — existing hook integrations indocs/notifications.mdlight up automatically.~/.config/cmux/settings.jsonunder"island": { "enabled": true }via the existingCmuxSettingsFileStorepipeline.Sources/Island/with a narrowIslandStateProvider/IslandFocusSinkseam so the feature can be extracted to a companioncmuxIsland.apptarget 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
AppDelegateobservesUserDefaults.didChangeNotificationforIslandSettings.enabledKeyand creates/destroys the controller reactively.References
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.docs/superpowers/plans/2026-04-09-cmux-island.md— 20 TDD tasks.farouqaldori/claude-island(Apache 2.0). OnlyNotchShape.swift,NotchPanelconfiguration, and the NSPanel mechanics were ported.ClaudeSessionMonitor,HookSocketServer, andHookInstallerfrom upstream were not ported — those implement a hook-driven session model that is out of scope for the cmux Island MVP. Attribution added toTHIRD_PARTY_LICENSES.md.Tests
New unit tests under
cmuxTests/(run viacmux-unitscheme 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 coveringsettings.jsonround-trip through theCmuxSettingsFileStoreparser + whitelist membership.No snapshot tests of
NotchShapeper 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)
set-statusentry), bound to the workspace's focused panel. Panel-specificset-status --tabresolution is Phase 2.TerminalNotificationStorehas no public per-panel count API today; the implementation gates the workspace count byhasUnreadNotification(forTabId:surfaceId:)and falls back to 0. Documented in an in-code TODO.island.enabled = trueviasettings.jsonbefore launch. There is a narrow launch-time race whereAppDelegate.tabManager(aweak 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.Dictionary.values.sortedis 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).s/m/h/dsuffixes. Localization keys for these can be added in Phase 2.Phase 2+ extension points (not built)
cmux approve/cmux denyCLI or blocking socket RPC.cmuxIsland.apptarget — swapIslandStateStorefor aSocketIslandStateProvider; the SwiftUI layer already depends only on the protocol.Test plan
xcodebuild -scheme cmux-unitvia CI. Expect allIsland*Testsclasses green../scripts/reload.sh --tag island-mvpthen walk the spec §9 12-step checklist on a notch-equipped Mac:cmux set-status claude_code Running→ pill appears with green dot, count 1cmux set-status codex Idlein a second workspace → count 2, dot still greencmux set-status claude_code Error→ dot turns redcmux clear-status claude_code→ row disappearscmux clear-status codex→ island orderOuts with no artifacts.canJoinAllSpaces,.fullScreenAuxiliary)Debug > Debug Windows > Island Controller…→ inject synthetic sessions → verify notch shape + row layout render correctly🤖 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-statusand 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
claude_code,codex,copilot_cli) with zero new CLI surface.IslandStateStore,IslandStateProvider,IslandJumpRouter,IslandWindowController,IslandRootView.NotchShapeandNotchPanelfromfarouqaldori/claude-island(Apache 2.0); licenses updated.Migration
"island": { "enabled": true }in~/.config/cmux/settings.json.Written for commit f5b0f8b. Summary will update on new commits.
Summary by CodeRabbit