Repository navigation
Redesign notifications popover: bigger, minimal, swipe to dismiss - #4778
Conversation
- Larger frame (min 460x480, ideal 560x620, max 760x760) so more notifications are visible at once. - Compact rows with a thin accent bar for unread, tighter typography, hover-revealed clear button. No more boxed cards per row, just dividers between rows. - Drag a row left or right to dismiss; passing the threshold slides it out and removes the notification. Tap to open as before. - Empty/loading/populated states share the same outer frame so the popover stops resizing when notifications come and go.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds resizable/persisted NotificationsPopover sizing, header unread-count badge and restyled Jump/Clear controls, unified empty/populated body using ScrollView+LazyVStack, rewrites NotificationPopoverRow for hover-only clear and NSTrackingArea-based hover detection with accessibility actions, and adds markUnread(id:) plus localization entries. ChangesNotifications Popover Redesign
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
Caution Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional.
❌ Failed checks (4 errors, 1 warning, 1 inconclusive)
✅ Passed checks (11 passed)
✨ 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 redesigns the notifications popover with a fixed-frame resizable layout (560×760 default, persisted via
Confidence Score: 4/5Safe to merge for functionality; the missing locale translations should be addressed before shipping to non-en/ja users. All seven new notifications.* string keys are translated only for en and ja, leaving 17 other supported locales showing raw English labels in the popover, context menu, and accessibility strings. The notification logic and resize mechanics look correct. Resources/Localizable.xcstrings — all 7 new keys need translations for the remaining 17 locales before the popover UI is fully localized. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[NotificationsPopoverView] --> B[header]
A --> C[content ViewBuilder]
A --> D[resizeHandle ResizeGripperRepresentable]
C --> E{snapshot state}
E -->|no notifications| F[emptyState bell.slash]
E -->|list empty| G[emptyState bell.badge]
E -->|populated| H[ScrollView / LazyVStack]
H --> I[NotificationPopoverRow x N]
I --> J[ZStack]
J --> K[Button primary action]
J --> L[clearButton hover-only sibling]
I --> M[HoverTrackingRepresentable NSTrackingArea]
I --> N[contextMenu Open / Mark / Dismiss]
K -->|onOpen| O[open notification]
L -->|onClear| P[withAnimation store.remove]
N -->|onToggleRead| Q{isRead?}
Q -->|true| R[markUnread - clears workspace flags]
Q -->|false| S[markRead + clearFocusedReadIndicator]
D --> T[ResizeGripperNSView screenCoord drag]
T -->|mouseUp| U[savedWidth and Height to UserDefaults once]
T -->|mouseDragged| V[liveWidth and Height in State only]
Reviews (22): Last reviewed commit: "Notifications popover: snapshot list bef..." | Re-trigger Greptile |
| .onEnded { value in | ||
| if abs(value.translation.width) > Self.dismissThreshold { | ||
| let direction: CGFloat = value.translation.width > 0 ? 1 : -1 | ||
| withAnimation(.easeOut(duration: 0.18)) { | ||
| dragOffset = direction * 600 | ||
| } | ||
| onClear() | ||
| } else { | ||
| withAnimation(.interactiveSpring(response: 0.28, dampingFraction: 0.85)) { | ||
| dragOffset = 0 | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Swipe animation and store removal race
onClear() is called immediately after starting the dragOffset animation, not after it completes. The parent's onClear closure wraps notificationStore.remove in its own withAnimation, so SwiftUI removes the row from the ForEach data source while the slide-out is still in progress — the row snaps away before it finishes sliding off screen. The same problem exists in the hover clear button at line 2429–2433. The slide animation needs to finish before the store is mutated, but Task.sleep would violate the repo's no-hacky-sleeps policy; the right fix is to drive the removal from an animationCompletion or by keeping the item in the list in a "dismissing" state until the animation transaction ends.
There was a problem hiding this comment.
Not applicable — swipe-to-dismiss was removed; rows are dismissed via the hover X button or right-click → Dismiss, both of which call onClear() directly and let SwiftUI's withAnimation drive the ForEach removal.
— Claude Code
| private var unreadCount: Int { | ||
| notificationStore.notifications.reduce(0) { $0 + ($1.isRead ? 0 : 1) } | ||
| } |
There was a problem hiding this comment.
Dual sources of truth for unread state
unreadCount iterates notificationStore.notifications directly (live list), while hasUnreadNotifications reads notificationStore.notificationMenuSnapshot.hasUnreadNotifications and the "Clear All" disabled state reads notificationMenuSnapshot.hasNotifications. If the snapshot is computed asynchronously or cached, the badge count and the button enabled states can diverge — e.g., the badge shows 0 while "Jump to Latest" is still enabled, or vice versa. Both the badge and the button guards should read from the same source.
There was a problem hiding this comment.
Fixed in a356eff — unreadCount now reads from notificationMenuSnapshot.unreadCount so the badge stays in sync with the hasUnreadNotifications / hasNotifications guards.
— Claude Code
There was a problem hiding this comment.
Acknowledged — already fixed in a356eff (unreadCount reads from notificationMenuSnapshot.unreadCount). Marking conversation resolved on my end.
— Claude Code
| .frame( | ||
| minWidth: NotificationsPopoverMetrics.minWidth, | ||
| idealWidth: NotificationsPopoverMetrics.idealWidth, | ||
| maxWidth: NotificationsPopoverMetrics.maxWidth, | ||
| minHeight: notificationStore.notifications.isEmpty | ||
| ? NotificationsPopoverMetrics.emptyMinHeight | ||
| : NotificationsPopoverMetrics.minHeight, | ||
| idealHeight: NotificationsPopoverMetrics.idealHeight, | ||
| maxHeight: NotificationsPopoverMetrics.maxHeight | ||
| ) |
There was a problem hiding this comment.
Popover still resizes between empty and populated states
The PR description says "all three popover states share the same outer frame so it stops resizing when notifications come and go," but minHeight is still conditional on notificationStore.notifications.isEmpty (320 vs 480 pt). Transitioning from the empty placeholder to the first notification will still trigger a height jump in the hosting NSPopover.
There was a problem hiding this comment.
Not applicable — the popover now uses a fixed .frame(width:height:) bound to user-controlled @AppStorage values across all three states (empty, hidden, populated), so it no longer resizes when content changes.
— Claude Code
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/Update/UpdateTitlebarAccessory.swift`:
- Around line 2288-2290: The local computed property unreadCount currently
recomputes unread by reducing over notificationStore.notifications, which can
diverge from notificationMenuSnapshot (used by
hasUnreadNotifications/stateHintTitle) and is O(n); change unreadCount to return
notificationStore.unreadCount instead (i.e., delegate to the store’s
unreadCount) so the popover badge uses the canonical, constant-time source and
stays in sync with notificationMenuSnapshot-based logic.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 6b04fc4b-fcaa-41ef-a46b-44d76dd11e99
📒 Files selected for processing (1)
Sources/Update/UpdateTitlebarAccessory.swift
…ayer .onHover and .onTapGesture on the same SwiftUI node share AppKit's mouse-tracking pipeline and arbitrate against each other, producing flaky enter/exit events right after the popover opens and when crossing rows fast. Move hover detection to a transparent background layer using .onContinuousHover, scope the opacity animation to the background only so it does not re-animate dragOffset, and bump the hover opacity from 0.07 to 0.11 so it is visible against .windowBackgroundColor in light mode.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/Update/UpdateTitlebarAccessory.swift`:
- Around line 2178-2188: The visual shortcut badge
Text(jumpToUnreadShortcut.displayString) is being read in addition to the
button's existing accessibilityValue, causing duplicate announcements; hide the
badge from accessibility by applying .accessibilityHidden(true) to that Text
view (or alternatively set an explicit accessibilityLabel/accessibilityValue on
the parent button to provide the shortcut once) so VoiceOver only announces the
shortcut once; locate the Text(jumpToUnreadShortcut.displayString) badge and add
the accessibility change, leaving the button's existing accessibilityValue
intact.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: fb12e499-cab5-4f4b-8b72-351de41bf5f3
📒 Files selected for processing (1)
Sources/Update/UpdateTitlebarAccessory.swift
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/Update/UpdateTitlebarAccessory.swift`:
- Around line 2458-2482: The clear affordance is currently only accessible via
hover because of .opacity(isHovering) and .allowsHitTesting(isHovering); add a
row-level accessibility action that calls onClear from NotificationPopoverRow so
assistive tech can dismiss without hovering (use accessibilityAction(named:
Text(LocalizedStringKey("notifications.row.clear"))) { onClear() } or
equivalent), and ensure the "notifications.row.clear" key is added to
Resources/Localizable.xcstrings for English and Japanese; keep clearButton
visuals the same but do not rely solely on allowsHitTesting(isHovering) for
accessibility activation (retain it for pointer behavior but provide the
explicit accessibility action on the row alongside the existing onOpen action).
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 85f91047-fd0d-43d7-8af0-df25a75ddea4
📒 Files selected for processing (1)
Sources/Update/UpdateTitlebarAccessory.swift
SwiftUI's .onHover and .onContinuousHover still arbitrate with the parent .onTapGesture in macOS popover content, leaving rows where hover never fires (notably on first popover open and when crossing between LazyVStack rows). Replace the SwiftUI hover modifier with an NSViewRepresentable that installs an NSTrackingArea (.mouseEnteredAndExited + .activeAlways + .inVisibleRect). The tracking NSView returns nil from hitTest so clicks pass through to the SwiftUI parent's tap gesture, and the view syncs current inside/outside state on tracking-area install for the case where the pointer is already inside the row when the popover opens.
…add right-click menu - Always show row timestamp (removed the hide-on-hover opacity). - Removed the hover background fade animation so it's an instant flip. - Right-click a row for Open / Mark as Read / Mark as Unread / Dismiss. - Added TerminalNotificationStore.markUnread(id:) sibling to markRead(id:). - Added en/ja localizations for the new menu strings.
Default size bumped to 560x760 (was 560x620 ideal). User can drag a bottom-right resize handle to set their own size; the chosen width and height persist via @AppStorage. Bounds are 420x320 .. 1000x1200.
NSPopover has no native resize chrome and there's no first-class SwiftUI resize API for it. The closest native-feeling thing is a hit-only corner region that drives a state-driven .frame: no visual button, just a resizeLeftRight cursor on hover (closest standard NSCursor to a diagonal resize). Also removed swipe-to-dismiss. The hover X button and the right-click Dismiss menu item are the dismiss surfaces; swiping rows in an NSPopover row list isn't a macOS convention.
SwiftUI's DragGesture reports translation in a local coordinate space that is literally being resized under the cursor as the popover grows. Each frame the gesture re-derives translation from a moving anchor, which produces dimension oscillation (the popover jumps between two sizes). Replace the SwiftUI gesture with an NSViewRepresentable that tracks NSEvent.mouseLocation in global screen coordinates. Screen coordinates are stable regardless of popover resize, so deltas are monotonic. Also disable implicit animation on the .frame width/height so the popover follows the cursor 1:1.
A user-initiated 'Mark as Read' should mirror the full dismissal intent, so the pane's focused-read indicator for that surface should clear alongside flipping the notification's isRead flag. Otherwise the unread count drops but the pane badge remains visible until the user later interacts with the terminal. Found by Codex review.
…sync race - Clamp popover size against the screen of NSApp.keyWindow (the popover's anchor window), not NSScreen.main. On multi-monitor setups the popover may appear on a different display than 'main'. - Remove the DispatchQueue.main.async hop in HoverTrackingNSView's updateTrackingAreas; the queued onChange(true) could land after mouseExited's synchronous onChange(false), leaving rows stuck in the hovered state. updateTrackingAreas already runs on the main thread, so the call is fine synchronously. Found by Cursor Bugbot.
…ications clearFocusedReadIndicator treats surfaceId == nil as 'clear any pane indicator for this tab', so passing nil for a workspace-level notification would wipe an unrelated pane's badge. Gate the call on notification.surfaceId being non-nil. Found by Codex review.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d2df95f. Configure here.
The localized notifications.resize key existed but was never wired to the actual control. Adds .accessibilityLabel/.accessibilityHint so VoiceOver and Full Keyboard Access users can discover the resize affordance. Found by Cursor Bugbot.
All items raised in this review have been addressed in subsequent commits (a356eff and later). Re-reviewing at HEAD.
🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Get PR details
gh pr view 4778 --repo manaflow-ai/cmux --json title,state,headRefName,baseRefName,reviews,statusCheckRollup 2>/dev/null || echo "PR details fetch failed"
# Get existing review comments (inline)
gh api repos/manaflow-ai/cmux/pulls/4778/comments --paginate 2>/dev/null | jq '[.[] | {id, path, line, body, user: .user.login, position}]' | head -300
# Get PR reviews
gh api repos/manaflow-ai/cmux/pulls/4778/reviews --paginate 2>/dev/null | jq '[.[] | {id, state, user: .user.login, submitted_at, body}]'Length of output: 195722 🏁 Script executed: #!/bin/bash
# Dismiss the three stale CHANGES_REQUESTED reviews from coderabbitai
REPO="manaflow-ai/cmux"
PR_NUMBER="4778"
REVIEW_IDS=("4361247049" "4361294309" "4361358743")
for review_id in "${REVIEW_IDS[@]}"; do
echo "Dismissing review $review_id..."
gh api --method PUT \
repos/$REPO/pulls/$PR_NUMBER/reviews/$review_id/dismissals \
-f message="All items raised in this review have been addressed in subsequent commits (a356eff and later). Re-reviewing at HEAD." \
2>&1
doneLength of output: 11708
Re-review requested by ✅ Actions performedFull review triggered. |
…w Button XCUITests query rows with app.buttons["NotificationPopoverRow.<id>"]. The previous pass put .accessibilityIdentifier on the combined outer ZStack, exposing the row as a container rather than a button, which breaks both XCUITest lookups and the button accessibility role for assistive tech. Move identifier + primary action + clear action back onto the inner Button. Found by Codex review.
…d unread on markUnread - ForEach now iterates an immutable snapshot captured outside the LazyVStack instead of reading notificationStore.notifications inside the row builder; the previous code reintroduced a store dependency below the list boundary, which CLAUDE.md flags as the same anti-pattern that caused the LazyLayoutViewCache spin-loop in the sessions panel (#2586). - markUnread(id:) now also clears the restored workspace unread indicator for the tab, so a session-restored unread hint plus a user-toggled unread notification no longer double-count in the header/dock/workspace badges. Found by Codex review.
* test: cover external dot path open * fix: open external path arguments without socket access * fix: preserve explicit socket path opens * fix: bound launchservices open helper * fix: avoid blocking primitive in open helper * fix: format localized path open errors * fix: complete path open localization * fix: scrub socket env for external path opens * fix: localize path open success output * Add cmux.xcworkspace with Packages visible alongside the project (manaflow-ai#4834) * Add cmux.xcworkspace with Packages visible alongside the project Top-level navigator shows cmux.xcodeproj plus every Packages/* SPM package as siblings, so local packages are editable in the same window without nesting under the project node. Additive only: existing -project build flows (reload.sh, CI) keep working unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Ignore xcshareddata under xcworkspaces Xcode auto-creates xcshareddata inside .xcworkspace bundles for WorkspaceSettings.xcsettings, IDEWorkspaceChecks.plist, and SwiftPM configuration. None of those carry intentional shared state for cmux today (build is driven by reload.sh and CI, no private SPM registry, no custom file headers). Ignoring them prevents Xcode bookkeeping churn in commits. Project-level xcshareddata (shared .xcscheme files) is unaffected because the pattern only matches inside .xcworkspace directories. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * fix: scrub legacy panel env for path opens * fix: limit path open locale entries * Redesign notifications popover: bigger, minimal, swipe to dismiss (manaflow-ai#4778) * Redesign notifications popover: bigger, minimal, swipe to dismiss - Larger frame (min 460x480, ideal 560x620, max 760x760) so more notifications are visible at once. - Compact rows with a thin accent bar for unread, tighter typography, hover-revealed clear button. No more boxed cards per row, just dividers between rows. - Drag a row left or right to dismiss; passing the threshold slides it out and removes the notification. Tap to open as before. - Empty/loading/populated states share the same outer frame so the popover stops resizing when notifications come and go. * Show jump-to-latest keyboard shortcut next to button label * Stabilize hover on notification rows: dedicated background tracking layer .onHover and .onTapGesture on the same SwiftUI node share AppKit's mouse-tracking pipeline and arbitrate against each other, producing flaky enter/exit events right after the popover opens and when crossing rows fast. Move hover detection to a transparent background layer using .onContinuousHover, scope the opacity animation to the background only so it does not re-animate dragOffset, and bump the hover opacity from 0.07 to 0.11 so it is visible against .windowBackgroundColor in light mode. * Use NSTrackingArea for notification row hover SwiftUI's .onHover and .onContinuousHover still arbitrate with the parent .onTapGesture in macOS popover content, leaving rows where hover never fires (notably on first popover open and when crossing between LazyVStack rows). Replace the SwiftUI hover modifier with an NSViewRepresentable that installs an NSTrackingArea (.mouseEnteredAndExited + .activeAlways + .inVisibleRect). The tracking NSView returns nil from hitTest so clicks pass through to the SwiftUI parent's tap gesture, and the view syncs current inside/outside state on tracking-area install for the case where the pointer is already inside the row when the popover opens. * Notifications popover: keep timestamp visible, drop hover animation, add right-click menu - Always show row timestamp (removed the hide-on-hover opacity). - Removed the hover background fade animation so it's an instant flip. - Right-click a row for Open / Mark as Read / Mark as Unread / Dismiss. - Added TerminalNotificationStore.markUnread(id:) sibling to markRead(id:). - Added en/ja localizations for the new menu strings. * Notifications popover: drop top margin, always semibold title * Make notifications popover resizable, taller default Default size bumped to 560x760 (was 560x620 ideal). User can drag a bottom-right resize handle to set their own size; the chosen width and height persist via @AppStorage. Bounds are 420x320 .. 1000x1200. * Notifications popover: invisible corner resize, drop swipe-to-dismiss NSPopover has no native resize chrome and there's no first-class SwiftUI resize API for it. The closest native-feeling thing is a hit-only corner region that drives a state-driven .frame: no visual button, just a resizeLeftRight cursor on hover (closest standard NSCursor to a diagonal resize). Also removed swipe-to-dismiss. The hover X button and the right-click Dismiss menu item are the dismiss surfaces; swiping rows in an NSPopover row list isn't a macOS convention. * Notifications popover: fix glitchy resize via AppKit screen coords SwiftUI's DragGesture reports translation in a local coordinate space that is literally being resized under the cursor as the popover grows. Each frame the gesture re-derives translation from a moving anchor, which produces dimension oscillation (the popover jumps between two sizes). Replace the SwiftUI gesture with an NSViewRepresentable that tracks NSEvent.mouseLocation in global screen coordinates. Screen coordinates are stable regardless of popover resize, so deltas are monotonic. Also disable implicit animation on the .frame width/height so the popover follows the cursor 1:1. * Notifications popover: use diagonal resize cursor on bottom-right corner * Notifications popover: address review feedback - unreadCount uses notificationMenuSnapshot.unreadCount instead of reducing the live list, so the badge stays consistent with the hasUnreadNotifications / hasNotifications guards that drive the Jump to Latest / Clear All disabled states. - Hide the visual keyboard-shortcut chip from accessibility; the button already exposes the shortcut via .accessibilityValue, so VoiceOver no longer reads it twice. - Add a row-level accessibility action to dismiss a notification, since the visible clear button is hover-only. Adds the localized string notifications.row.clear (en/ja). * markUnread(id:): clear manual workspace unread to avoid double-count When a user marks a read notification unread, the notification itself now provides the workspace unread indicator. If the workspace also had a manual unread flag set, the popover header, dock badge, and workspace badge would double-count that workspace because the count builders sum notification unread counts and workspace indicators. Mirrors what markLatestNotificationAsOldestUnread already does in the same file. Found by Codex review. * Restore Button wrapper on notification rows for keyboard access Replacing the original Button with .onTapGesture removed rows from the SwiftUI key-view loop, so keyboard-only users could tab to the header controls but not open a row. The original reason for dropping Button was that SwiftUI's .onHover arbitrated with the row's primary action — but hover is now driven entirely by an AppKit NSTrackingArea (HoverTrackingRepresentable), independent of the row's activation node. The Button wrapper is therefore safe again and restores space/return activation in the key-view loop. Found by Codex review. * Notifications popover: persist resize once on mouseUp, clamp drag baseline - Live resize uses @State (liveWidth/liveHeight); @AppStorage is written exactly once when the user releases the mouse, instead of on every mouseDragged event. Each @AppStorage write was hitting UserDefaults and broadcasting UserDefaults.didChangeNotification to every app-wide observer. - Drag baseline now comes from the clamped (currently displayed) size, not raw saved doubles, so a drag that starts with out-of-bounds stored values doesn't lose initial pointer travel re-entering the visible range. * Notifications popover: clear button as ZStack sibling, not nested in row Button Nesting clearButton inside the row's Button(action: onOpen) label created the classic SwiftUI nested-button hit-test problem on macOS: clicks on the inner X could be consumed by the outer row Button's tap area instead of clearing the notification. Move clearButton out of rowContent and into a ZStack(alignment: .trailing) at the body level so it's a sibling of the row Button with an independent hit target. Found by Codex review. * Notifications popover: clamp to screen, hide hover-only X from focus - Clamp the popover's frame against NSScreen.main?.visibleFrame minus an 80pt margin so the bottom-right resize handle stays reachable even when the saved size was captured on a larger display. - Mark the hover-only clear button .accessibilityHidden(!isHovering) so Full Keyboard Access / VoiceOver doesn't focus an invisible dismiss control. Dismissal is still exposed via the row's accessibility action and the right-click menu. Found by Codex. * CI: retry — GitHub Actions infra was returning 403/404 on codeload * CI: retrigger now that GitHub Actions infra is back * Mark-as-Read context menu: clear focused pane indicator too A user-initiated 'Mark as Read' should mirror the full dismissal intent, so the pane's focused-read indicator for that surface should clear alongside flipping the notification's isRead flag. Otherwise the unread count drops but the pane badge remains visible until the user later interacts with the terminal. Found by Codex review. * Notifications popover: clamp against host window's screen, fix hover sync race - Clamp popover size against the screen of NSApp.keyWindow (the popover's anchor window), not NSScreen.main. On multi-monitor setups the popover may appear on a different display than 'main'. - Remove the DispatchQueue.main.async hop in HoverTrackingNSView's updateTrackingAreas; the queued onChange(true) could land after mouseExited's synchronous onChange(false), leaving rows stuck in the hovered state. updateTrackingAreas already runs on the main thread, so the call is fine synchronously. Found by Cursor Bugbot. * Mark-as-Read: only clear focused pane indicator for pane-scoped notifications clearFocusedReadIndicator treats surfaceId == nil as 'clear any pane indicator for this tab', so passing nil for a workspace-level notification would wipe an unrelated pane's badge. Gate the call on notification.surfaceId being non-nil. Found by Codex review. * Notifications popover: add a11y label/hint to resize handle The localized notifications.resize key existed but was never wired to the actual control. Adds .accessibilityLabel/.accessibilityHint so VoiceOver and Full Keyboard Access users can discover the resize affordance. Found by Cursor Bugbot. * Notifications popover: move accessibility identifier back onto the row Button XCUITests query rows with app.buttons["NotificationPopoverRow.<id>"]. The previous pass put .accessibilityIdentifier on the combined outer ZStack, exposing the row as a container rather than a button, which breaks both XCUITest lookups and the button accessibility role for assistive tech. Move identifier + primary action + clear action back onto the inner Button. Found by Codex review. * Notifications popover: snapshot list before LazyVStack; clear restored unread on markUnread - ForEach now iterates an immutable snapshot captured outside the LazyVStack instead of reading notificationStore.notifications inside the row builder; the previous code reintroduced a store dependency below the list boundary, which CLAUDE.md flags as the same anti-pattern that caused the LazyLayoutViewCache spin-loop in the sessions panel (manaflow-ai#2586). - markUnread(id:) now also clears the restored workspace unread indicator for the tab, so a session-restored unread hint plus a user-toggled unread notification no longer double-count in the header/dock/workspace badges. Found by Codex review. --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Remove History from right sidebar (manaflow-ai#4785) * Remove History mode from right sidebar Drops the History tab from the right sidebar mode picker along with its keyboard shortcut, command palette entries, and the 'Open Full History' menu button. Recently Closed/Focused submenus and Reopen Last Closed still work from the menu bar. * Drop tests for deleted HistoryDayGrouping helper * Tolerate obsolete history mode and restore focus-history menu key - SessionRightSidebarToolPanelSnapshot now decodes an unknown mode (e.g. legacy 'history') as nil instead of failing the entire snapshot, so upgraded users can still restore prior sessions. - Restore the 'menu.history.showFullFocusHistory' key still referenced by AppDelegate+FocusHistoryContextMenu for non-sidebar focus history. * Remove duplicate @mainactor attribute left by prior edit --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Launch restored agent sessions via startup commands (manaflow-ai#4777) * Test agent restore startup command launch * Launch restored agents with startup commands * Tighten restored startup launch state * Preserve remote startup for agent restore * Relax remote startup restore assertion * Fix startup restore lifecycle * Remove unused restore launch flag * Run startup resumes in login shell * Tighten remote restore assertion * Handle csh agent restore launchers --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Wrap workspace titles in sidebar (manaflow-ai#4848) * Wrap workspace titles in sidebar * Add workspace title wrap toggle * Localize workspace title wrap setting * Align wrapped workspace title accessories * Reset workspace title wrap preference --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * test: cover bare relative external path open * fix: open bare relative path arguments externally * fix: preserve refresh surfaces command precedence * fix: keep bare path command matching case-sensitive * Add secure cmux navigation links (manaflow-ai#4857) * Add secure cmux navigation links * Localize cmux navigation link labels --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Fix matched sidebar terminal background (manaflow-ai#4780) * test: cover matched sidebar background tint * fix: match sidebar terminal background * fix: remove dead sidebar overlay path * test: cover matched sidebar boundary separators * fix: keep matched sidebar separators visible * test: cover matched sidebar chrome borders * test: sample sidebar boundary resizers --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Fix JSONC comment skipper for CRLF line endings (manaflow-ai#4869) * Fix JSONC // comment skipper for CRLF line endings Swift treats \r\n as a single extended grapheme cluster, so the existing source[index] != "\n" check inside the three // line-comment skippers never matches a CRLF line ending. On a file with CRLF throughout, the loop runs past the rest of the file looking for a standalone LF and strips everything after the first //. preprocess() then produces truncated JSON and loadCmuxSettingsRoot() / jsonObject() throw "Unexpected end of file". This was breaking the tmux corpus terminal-nightly job's testSurfaceResumeApprovalWritesRecordsIntoCmuxJSON, which explicitly exercises the CRLF path. Match any character whose first scalar is CR or LF so we stop at CR, LF, or CRLF correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Allow workflow_dispatch to run tmux corpus terminal-nightly So this workflow's macOS test job is reproducible on demand for branches under review, not only on the nightly schedule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix JSONC editor indent detection for CRLF line endings indentationBeforeLine walked back to find the start of the line containing a given index by stopping at "\n" or "\r" characters, but on CRLF input Swift treats "\r\n" as a single extended grapheme cluster that equals neither. The loop then walked past every line break to the start of the file and returned an empty indent, so newly inserted properties (e.g. resumeCommands) ended up at the wrong indentation level. Reuse the isLineTerminator helper that already handles CRLF correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Open forked conversations without palette action probe (manaflow-ai#4852) * Open forked conversations without palette action probe * Fix fork probe snapshot selection * Refresh fork probe cache per palette session * Preserve verified fork probe visibility * Preserve fork snapshot cache during live refresh * Allow verified fallback fork execution * Reprobe fork cache after fallback clears * Preserve focus after instant fork commands * Clear fork cache on execution failure * Avoid stale fork cache on palette reopen * Clear stale fallback fork cache before reprobe * Preserve verified fork cache on palette reopen * Require verified fork snapshot before fallback execution * Track actual fork fallback usage * Preserve verified fork cache fallback flag --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * test: cover bonsplit tab indicator drift * fix: update bonsplit tab indicator handling * Skip Cmd+Shift key forwarding test when Ghostty surface init fails (manaflow-ai#4871) The tmux corpus terminal-nightly runner cannot initialize a Metal-backed Ghostty surface; embedded_window logs "error initializing surface err=error.OutOfMemory" for every surface the suite creates, and ghostty_surface_new returns nil. The test then fails XCTAssertTrue on performKeyEquivalentAfterMenuMiss because ensureSurfaceReadyForInput cannot return a live surface, which looks like a key-forwarding regression rather than the environment issue that it is. Skip the test (via XCTSkipUnless on TerminalSurface.hasLiveSurface) when the surface fails to initialize, so CI surfaces the real problem (Metal unavailable on this runner) without masking it as a key-bind regression. The test still runs end-to-end on developer machines and on runners with a logged-in GUI session. Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add algorithmic complexity review rule (manaflow-ai#4866) * Add algorithmic complexity review rule * Align algorithmic complexity review hint --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Narrow AppDelegate CI quarantine (manaflow-ai#4874) Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Restore browser devtools config CI coverage * Add prompt and rules deeplinks (manaflow-ai#4839) * Add prompt and rules deeplinks * Preserve encoded punctuation in text deeplinks * Localize text deeplink dialogs * Support Freestyle SSH link users * Harden prompt and rules deeplinks * Suppress welcome for text deeplinks * Restore SSH deeplink trust gate * Preserve plus signs in text deeplinks * Respect no-focus for text deeplink target selection * Use deterministic text deeplink target * Start background text deeplink targets * Report text deeplink send failures * Handle mixed deeplink batches safely * Gate mixed external deeplink batches * Report failed external text deeplink sends * Stop after handling external cmux links * Allow multiline cmux text links * Reject control characters in cmux text links * Polish external cmux link handling * Label sendTextWhenReady callbacks --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Make session index backfill linear (manaflow-ai#4868) * Make session index backfill linear * Stabilize session index backfill tie breaks * Fix session index tie-break test setup --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Optimize batch workspace sidebar actions (manaflow-ai#4865) * Optimize batch workspace sidebar actions * Preserve workspace selection after drag reorder * Harden workspace batch action coverage --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * test: harden bonsplit scroll indicator check * Restore browser remote store CI coverage * Make browser search providers configurable (manaflow-ai#4849) * feat: make browser search providers configurable * fix: keep parsing browser settings after invalid custom search config * test: harden custom search config regression * fix: handle custom search edge cases * fix: validate custom search fallbacks * fix: localize search engine labels * fix: gate stale search suggestions * fix: allow blank custom search names * fix: add exa search provider --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Restore browser lifecycle CI coverage * docs: add cmux ssh deep links (manaflow-ai#4833) * docs: add cmux ssh deep links * docs: add cmux deeplink fallback pages * docs: add deeplink locale fallbacks * docs: tighten deeplink fallback validation * docs: mirror native ssh deeplink validation * docs: localize deeplink messages * docs: canonicalize rules deeplink alternates * Allow Freestyle SSH deeplink users * docs: validate text deeplink fallback params * docs: ignore blank optional deeplink params * docs: address deeplink review comments --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Restore file preview review CI coverage * Pin Xcode 26 (objectVersion 60) and add pbxproj normalizer + CI guard (manaflow-ai#4836) * Add deterministic normalizer for cmux.xcodeproj/project.pbxproj scripts/normalize-pbxproj.py sorts the high-churn sections (PBXBuildFile, PBXFileReference, and the files = (...) arrays inside Sources / Resources / Frameworks / CopyFiles build phases) into a deterministic order keyed on the entry comment plus UUID. The Xcode build does not care about the order of these flat dictionary sections; sorting them just kills the nondeterministic diff noise Xcode generates on every UI touch. Does not touch UUIDs, comments, or PBXGroup children = (...) arrays (navigator order is intentional). Idempotent: a second run produces zero diff. Standalone in this commit so the diff is just the script. The next commit applies the script and bumps objectVersion in one shot, so the resulting churn is contained and never repeated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Pin objectVersion = 60 and normalize pbxproj Bumps objectVersion from 56 to 60 (the format Xcode 16+ and Xcode 26 write by default) and runs scripts/normalize-pbxproj.py once to establish the deterministic baseline. After this commit, future diffs to project.pbxproj show only real changes, not Xcode's nondeterministic section reordering. One-time large diff. No semantic changes to targets, sources, build phases, or settings: pure sort + version pin. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add tracked pre-commit hook that normalizes pbxproj scripts/git-hooks/pre-commit calls scripts/normalize-pbxproj.py on cmux.xcodeproj/project.pbxproj when it is staged and re-stages the result. scripts/install-git-hooks.sh points the clone at this directory via `git config core.hooksPath scripts/git-hooks`, and scripts/setup.sh auto-runs it so devs get the hook without a separate manual step. After this, Xcode's nondeterministic reordering of build-file and file-reference sections is canceled out at commit time. The CI guard in the next commit enforces the rule for anyone who bypasses the hook with --no-verify or who never ran setup. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add CI guard for objectVersion pin and pbxproj normalization scripts/check-pbxproj.sh asserts cmux.xcodeproj/project.pbxproj has objectVersion = 60 (Xcode 26 default) and that the file is normalized per scripts/normalize-pbxproj.py. Wired as a step in the workflow-guard-tests job so every PR is gated. This catches anyone who bypasses the pre-commit hook with --no-verify or who never ran scripts/setup.sh. The error message points at the exact fix path. To bump the pin (e.g., when the team adopts a newer Xcode major), edit EXPECTED_OBJECT_VERSION in this script and the matching line in CLAUDE.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add .xcode-version and document Xcode 26 pin in CLAUDE.md .xcode-version records the major (26.0) for tooling that reads it (xcodes CLI, some CI helpers). CLAUDE.md gains an Xcode toolchain section explaining the pin, the normalizer + pre-commit hook + CI guard mechanics, and the procedure for bumping the pin in the future. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Read .xcode-version as the source of truth in check-pbxproj.sh scripts/check-pbxproj.sh now reads .xcode-version and maps the Xcode major to the expected objectVersion via a one-entry case statement. Bumping the team's Xcode pin becomes a one-file edit (.xcode-version), with a script update only required when Apple actually changes objectVersion in a new Xcode major. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Address CodeRabbit findings on check-pbxproj.sh and pre-commit hook scripts/check-pbxproj.sh now passes "$PBXPROJ" explicitly to normalize-pbxproj.py instead of letting it default to a path relative to the current working directory, so the guard works regardless of where CI invokes it. scripts/git-hooks/pre-commit refuses to run when the working-tree pbxproj has unstaged changes. Previously the hook would normalize the working-tree file and `git add` the result, which silently staged any unstaged hunks the user had deliberately left out of the commit. The hook now exits non-zero with a clear message telling the user to either stage the whole file or stash the unstaged hunks first. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Address Greptile findings: misleading comment + bump-step docs scripts/normalize-pbxproj.py: the comment said "preserve empty lines exactly where they are" but the implementation collapses blanks to a trailing group. Reworded the comment to match the actual behavior. CLAUDE.md: the bump procedure now mentions opening cmux.xcodeproj in the new Xcode so objectVersion gets rewritten automatically. Without that step a developer following the docs alone would update only the pin file and the script case, and the CI guard would fail on their next commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * Add scripts/cleanup-dev-builds.sh for safely reclaiming tagged DerivedData (manaflow-ai#4837) * Add scripts/cleanup-dev-builds.sh Removes tagged dev-build artifacts produced by scripts/reload.sh: DerivedData/cmux-<tag>/ (multi-GB each), /tmp/cmux-<tag>/, the per-tag debug socket and logs, the reload log, and the App Support cmuxd dev socket. Defaults to dry-run; pass --apply to delete. Safety rules always on: - Skip the tag of any running `cmux DEV <tag>` app - Skip the tag pointed at by /tmp/cmux-last-cli-path - Skip any tag tied to a live git worktree Filters: --older-than DAYS, --keep TAG (repeatable). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Drop "worktree exists" safety rule in cleanup-dev-builds.sh Existence of a git worktree with the same name is a weak signal of active use, and HQ tends to accumulate worktrees long after the work is done. The rule made cleanup over-protective for the typical case (worktree still around from a merged or abandoned PR). The remaining safety rules (skip running app, skip the tag pointed at by /tmp/cmux-last-cli-path) plus --keep TAG and --older-than DAYS cover what we actually want without false positives. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add /cleanup-builds slash command Wraps scripts/cleanup-dev-builds.sh with the standard preview -> confirm -> apply flow. Sits alongside the existing .claude/commands (pull, sync-branch, release, etc.) and enforces user confirmation before --apply. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Address review feedback on cleanup-dev-builds CodeRabbit + Greptile findings, all real: - Active-tag extraction from /tmp/cmux-last-cli-path now uses a regex match on /cmux-<tag>/ anywhere in the path, not just a strict $DERIVED_DATA_ROOT/cmux- prefix. Also avoids the unquoted parameter expansion that could be sensitive to glob metacharacters in DERIVED_DATA_ROOT. - discover_tags switched from find | xargs basename to a shell glob loop. Cleaner, works on macOS regardless of xargs flavor, handles the empty case naturally. - --older-than no longer skips tags whose DerivedData was already deleted (age == -1 sentinel). Orphan sockets/logs for those tags now get cleaned instead of being silently retained. - "freed" label reworded as "freed (estimated)" because the byte count is measured during planning, not after rm. - .claude/commands/cleanup-builds.md: blank lines around fenced blocks (MD031). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * Fix agent resume when saved cwd is deleted (manaflow-ai#4859) * test: cover agent resume with deleted cwd * fix: resume agents when saved cwd is gone * fix: harden restored cwd guard * fix: skip ghostty cwd for guarded restore commands * fix: drop duplicate cwd args on restore * fix: preserve shell args during cwd cleanup * fix: preserve custom resume cwd arguments * fix: preserve shell syntax during cwd cleanup * chore: remove unused shell word helper --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Restore file preview text saving CI coverage * Restore browser session history CI coverage * chore(rename): swap bundle id, product name, config dir to most Visual rename pass — upstream-merge friendly. Touches user-visible surfaces only; keeps Swift identifiers, package dirs, env vars, and on-disk xcodeproj/test target names so cmux upstream merges remain conflict-free. - pbxproj: PRODUCT_BUNDLE_IDENTIFIER com.cmuxterm.* → com.4etverg.most* - pbxproj: PRODUCT_NAME cmux → most ("cmux DEV" → "most DEV") - Entitlements: app group ids release/nightly → com.4etverg.most[.nightly] - CLI config path: ~/.config/cmux/cmux.json → ~/.config/most/most.json - One-shot legacy config migration in KeyboardShortcutSettingsFileStore - README.md (English) user-visible refs flipped; upstream URLs preserved Co-Authored-By: OpenAI Codex (gpt-5.4-mini) <noreply@openai.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(configuration): rename to most.json + document migration Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> Co-authored-by: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Co-authored-by: Konstantin <your@email.ar> Co-authored-by: OpenAI Codex (gpt-5.4-mini) <noreply@openai.com>
* test: cover external dot path open * fix: open external path arguments without socket access * fix: preserve explicit socket path opens * fix: bound launchservices open helper * fix: avoid blocking primitive in open helper * fix: format localized path open errors * fix: complete path open localization * fix: scrub socket env for external path opens * fix: localize path open success output * Add cmux.xcworkspace with Packages visible alongside the project (manaflow-ai#4834) * Add cmux.xcworkspace with Packages visible alongside the project Top-level navigator shows cmux.xcodeproj plus every Packages/* SPM package as siblings, so local packages are editable in the same window without nesting under the project node. Additive only: existing -project build flows (reload.sh, CI) keep working unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Ignore xcshareddata under xcworkspaces Xcode auto-creates xcshareddata inside .xcworkspace bundles for WorkspaceSettings.xcsettings, IDEWorkspaceChecks.plist, and SwiftPM configuration. None of those carry intentional shared state for cmux today (build is driven by reload.sh and CI, no private SPM registry, no custom file headers). Ignoring them prevents Xcode bookkeeping churn in commits. Project-level xcshareddata (shared .xcscheme files) is unaffected because the pattern only matches inside .xcworkspace directories. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * fix: scrub legacy panel env for path opens * fix: limit path open locale entries * Redesign notifications popover: bigger, minimal, swipe to dismiss (manaflow-ai#4778) * Redesign notifications popover: bigger, minimal, swipe to dismiss - Larger frame (min 460x480, ideal 560x620, max 760x760) so more notifications are visible at once. - Compact rows with a thin accent bar for unread, tighter typography, hover-revealed clear button. No more boxed cards per row, just dividers between rows. - Drag a row left or right to dismiss; passing the threshold slides it out and removes the notification. Tap to open as before. - Empty/loading/populated states share the same outer frame so the popover stops resizing when notifications come and go. * Show jump-to-latest keyboard shortcut next to button label * Stabilize hover on notification rows: dedicated background tracking layer .onHover and .onTapGesture on the same SwiftUI node share AppKit's mouse-tracking pipeline and arbitrate against each other, producing flaky enter/exit events right after the popover opens and when crossing rows fast. Move hover detection to a transparent background layer using .onContinuousHover, scope the opacity animation to the background only so it does not re-animate dragOffset, and bump the hover opacity from 0.07 to 0.11 so it is visible against .windowBackgroundColor in light mode. * Use NSTrackingArea for notification row hover SwiftUI's .onHover and .onContinuousHover still arbitrate with the parent .onTapGesture in macOS popover content, leaving rows where hover never fires (notably on first popover open and when crossing between LazyVStack rows). Replace the SwiftUI hover modifier with an NSViewRepresentable that installs an NSTrackingArea (.mouseEnteredAndExited + .activeAlways + .inVisibleRect). The tracking NSView returns nil from hitTest so clicks pass through to the SwiftUI parent's tap gesture, and the view syncs current inside/outside state on tracking-area install for the case where the pointer is already inside the row when the popover opens. * Notifications popover: keep timestamp visible, drop hover animation, add right-click menu - Always show row timestamp (removed the hide-on-hover opacity). - Removed the hover background fade animation so it's an instant flip. - Right-click a row for Open / Mark as Read / Mark as Unread / Dismiss. - Added TerminalNotificationStore.markUnread(id:) sibling to markRead(id:). - Added en/ja localizations for the new menu strings. * Notifications popover: drop top margin, always semibold title * Make notifications popover resizable, taller default Default size bumped to 560x760 (was 560x620 ideal). User can drag a bottom-right resize handle to set their own size; the chosen width and height persist via @AppStorage. Bounds are 420x320 .. 1000x1200. * Notifications popover: invisible corner resize, drop swipe-to-dismiss NSPopover has no native resize chrome and there's no first-class SwiftUI resize API for it. The closest native-feeling thing is a hit-only corner region that drives a state-driven .frame: no visual button, just a resizeLeftRight cursor on hover (closest standard NSCursor to a diagonal resize). Also removed swipe-to-dismiss. The hover X button and the right-click Dismiss menu item are the dismiss surfaces; swiping rows in an NSPopover row list isn't a macOS convention. * Notifications popover: fix glitchy resize via AppKit screen coords SwiftUI's DragGesture reports translation in a local coordinate space that is literally being resized under the cursor as the popover grows. Each frame the gesture re-derives translation from a moving anchor, which produces dimension oscillation (the popover jumps between two sizes). Replace the SwiftUI gesture with an NSViewRepresentable that tracks NSEvent.mouseLocation in global screen coordinates. Screen coordinates are stable regardless of popover resize, so deltas are monotonic. Also disable implicit animation on the .frame width/height so the popover follows the cursor 1:1. * Notifications popover: use diagonal resize cursor on bottom-right corner * Notifications popover: address review feedback - unreadCount uses notificationMenuSnapshot.unreadCount instead of reducing the live list, so the badge stays consistent with the hasUnreadNotifications / hasNotifications guards that drive the Jump to Latest / Clear All disabled states. - Hide the visual keyboard-shortcut chip from accessibility; the button already exposes the shortcut via .accessibilityValue, so VoiceOver no longer reads it twice. - Add a row-level accessibility action to dismiss a notification, since the visible clear button is hover-only. Adds the localized string notifications.row.clear (en/ja). * markUnread(id:): clear manual workspace unread to avoid double-count When a user marks a read notification unread, the notification itself now provides the workspace unread indicator. If the workspace also had a manual unread flag set, the popover header, dock badge, and workspace badge would double-count that workspace because the count builders sum notification unread counts and workspace indicators. Mirrors what markLatestNotificationAsOldestUnread already does in the same file. Found by Codex review. * Restore Button wrapper on notification rows for keyboard access Replacing the original Button with .onTapGesture removed rows from the SwiftUI key-view loop, so keyboard-only users could tab to the header controls but not open a row. The original reason for dropping Button was that SwiftUI's .onHover arbitrated with the row's primary action — but hover is now driven entirely by an AppKit NSTrackingArea (HoverTrackingRepresentable), independent of the row's activation node. The Button wrapper is therefore safe again and restores space/return activation in the key-view loop. Found by Codex review. * Notifications popover: persist resize once on mouseUp, clamp drag baseline - Live resize uses @State (liveWidth/liveHeight); @AppStorage is written exactly once when the user releases the mouse, instead of on every mouseDragged event. Each @AppStorage write was hitting UserDefaults and broadcasting UserDefaults.didChangeNotification to every app-wide observer. - Drag baseline now comes from the clamped (currently displayed) size, not raw saved doubles, so a drag that starts with out-of-bounds stored values doesn't lose initial pointer travel re-entering the visible range. * Notifications popover: clear button as ZStack sibling, not nested in row Button Nesting clearButton inside the row's Button(action: onOpen) label created the classic SwiftUI nested-button hit-test problem on macOS: clicks on the inner X could be consumed by the outer row Button's tap area instead of clearing the notification. Move clearButton out of rowContent and into a ZStack(alignment: .trailing) at the body level so it's a sibling of the row Button with an independent hit target. Found by Codex review. * Notifications popover: clamp to screen, hide hover-only X from focus - Clamp the popover's frame against NSScreen.main?.visibleFrame minus an 80pt margin so the bottom-right resize handle stays reachable even when the saved size was captured on a larger display. - Mark the hover-only clear button .accessibilityHidden(!isHovering) so Full Keyboard Access / VoiceOver doesn't focus an invisible dismiss control. Dismissal is still exposed via the row's accessibility action and the right-click menu. Found by Codex. * CI: retry — GitHub Actions infra was returning 403/404 on codeload * CI: retrigger now that GitHub Actions infra is back * Mark-as-Read context menu: clear focused pane indicator too A user-initiated 'Mark as Read' should mirror the full dismissal intent, so the pane's focused-read indicator for that surface should clear alongside flipping the notification's isRead flag. Otherwise the unread count drops but the pane badge remains visible until the user later interacts with the terminal. Found by Codex review. * Notifications popover: clamp against host window's screen, fix hover sync race - Clamp popover size against the screen of NSApp.keyWindow (the popover's anchor window), not NSScreen.main. On multi-monitor setups the popover may appear on a different display than 'main'. - Remove the DispatchQueue.main.async hop in HoverTrackingNSView's updateTrackingAreas; the queued onChange(true) could land after mouseExited's synchronous onChange(false), leaving rows stuck in the hovered state. updateTrackingAreas already runs on the main thread, so the call is fine synchronously. Found by Cursor Bugbot. * Mark-as-Read: only clear focused pane indicator for pane-scoped notifications clearFocusedReadIndicator treats surfaceId == nil as 'clear any pane indicator for this tab', so passing nil for a workspace-level notification would wipe an unrelated pane's badge. Gate the call on notification.surfaceId being non-nil. Found by Codex review. * Notifications popover: add a11y label/hint to resize handle The localized notifications.resize key existed but was never wired to the actual control. Adds .accessibilityLabel/.accessibilityHint so VoiceOver and Full Keyboard Access users can discover the resize affordance. Found by Cursor Bugbot. * Notifications popover: move accessibility identifier back onto the row Button XCUITests query rows with app.buttons["NotificationPopoverRow.<id>"]. The previous pass put .accessibilityIdentifier on the combined outer ZStack, exposing the row as a container rather than a button, which breaks both XCUITest lookups and the button accessibility role for assistive tech. Move identifier + primary action + clear action back onto the inner Button. Found by Codex review. * Notifications popover: snapshot list before LazyVStack; clear restored unread on markUnread - ForEach now iterates an immutable snapshot captured outside the LazyVStack instead of reading notificationStore.notifications inside the row builder; the previous code reintroduced a store dependency below the list boundary, which CLAUDE.md flags as the same anti-pattern that caused the LazyLayoutViewCache spin-loop in the sessions panel (manaflow-ai#2586). - markUnread(id:) now also clears the restored workspace unread indicator for the tab, so a session-restored unread hint plus a user-toggled unread notification no longer double-count in the header/dock/workspace badges. Found by Codex review. --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Remove History from right sidebar (manaflow-ai#4785) * Remove History mode from right sidebar Drops the History tab from the right sidebar mode picker along with its keyboard shortcut, command palette entries, and the 'Open Full History' menu button. Recently Closed/Focused submenus and Reopen Last Closed still work from the menu bar. * Drop tests for deleted HistoryDayGrouping helper * Tolerate obsolete history mode and restore focus-history menu key - SessionRightSidebarToolPanelSnapshot now decodes an unknown mode (e.g. legacy 'history') as nil instead of failing the entire snapshot, so upgraded users can still restore prior sessions. - Restore the 'menu.history.showFullFocusHistory' key still referenced by AppDelegate+FocusHistoryContextMenu for non-sidebar focus history. * Remove duplicate @mainactor attribute left by prior edit --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Launch restored agent sessions via startup commands (manaflow-ai#4777) * Test agent restore startup command launch * Launch restored agents with startup commands * Tighten restored startup launch state * Preserve remote startup for agent restore * Relax remote startup restore assertion * Fix startup restore lifecycle * Remove unused restore launch flag * Run startup resumes in login shell * Tighten remote restore assertion * Handle csh agent restore launchers --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Wrap workspace titles in sidebar (manaflow-ai#4848) * Wrap workspace titles in sidebar * Add workspace title wrap toggle * Localize workspace title wrap setting * Align wrapped workspace title accessories * Reset workspace title wrap preference --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * test: cover bare relative external path open * fix: open bare relative path arguments externally * fix: preserve refresh surfaces command precedence * fix: keep bare path command matching case-sensitive * Add secure cmux navigation links (manaflow-ai#4857) * Add secure cmux navigation links * Localize cmux navigation link labels --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Fix matched sidebar terminal background (manaflow-ai#4780) * test: cover matched sidebar background tint * fix: match sidebar terminal background * fix: remove dead sidebar overlay path * test: cover matched sidebar boundary separators * fix: keep matched sidebar separators visible * test: cover matched sidebar chrome borders * test: sample sidebar boundary resizers --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Fix JSONC comment skipper for CRLF line endings (manaflow-ai#4869) * Fix JSONC // comment skipper for CRLF line endings Swift treats \r\n as a single extended grapheme cluster, so the existing source[index] != "\n" check inside the three // line-comment skippers never matches a CRLF line ending. On a file with CRLF throughout, the loop runs past the rest of the file looking for a standalone LF and strips everything after the first //. preprocess() then produces truncated JSON and loadCmuxSettingsRoot() / jsonObject() throw "Unexpected end of file". This was breaking the tmux corpus terminal-nightly job's testSurfaceResumeApprovalWritesRecordsIntoCmuxJSON, which explicitly exercises the CRLF path. Match any character whose first scalar is CR or LF so we stop at CR, LF, or CRLF correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Allow workflow_dispatch to run tmux corpus terminal-nightly So this workflow's macOS test job is reproducible on demand for branches under review, not only on the nightly schedule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix JSONC editor indent detection for CRLF line endings indentationBeforeLine walked back to find the start of the line containing a given index by stopping at "\n" or "\r" characters, but on CRLF input Swift treats "\r\n" as a single extended grapheme cluster that equals neither. The loop then walked past every line break to the start of the file and returned an empty indent, so newly inserted properties (e.g. resumeCommands) ended up at the wrong indentation level. Reuse the isLineTerminator helper that already handles CRLF correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Open forked conversations without palette action probe (manaflow-ai#4852) * Open forked conversations without palette action probe * Fix fork probe snapshot selection * Refresh fork probe cache per palette session * Preserve verified fork probe visibility * Preserve fork snapshot cache during live refresh * Allow verified fallback fork execution * Reprobe fork cache after fallback clears * Preserve focus after instant fork commands * Clear fork cache on execution failure * Avoid stale fork cache on palette reopen * Clear stale fallback fork cache before reprobe * Preserve verified fork cache on palette reopen * Require verified fork snapshot before fallback execution * Track actual fork fallback usage * Preserve verified fork cache fallback flag --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * test: cover bonsplit tab indicator drift * fix: update bonsplit tab indicator handling * Skip Cmd+Shift key forwarding test when Ghostty surface init fails (manaflow-ai#4871) The tmux corpus terminal-nightly runner cannot initialize a Metal-backed Ghostty surface; embedded_window logs "error initializing surface err=error.OutOfMemory" for every surface the suite creates, and ghostty_surface_new returns nil. The test then fails XCTAssertTrue on performKeyEquivalentAfterMenuMiss because ensureSurfaceReadyForInput cannot return a live surface, which looks like a key-forwarding regression rather than the environment issue that it is. Skip the test (via XCTSkipUnless on TerminalSurface.hasLiveSurface) when the surface fails to initialize, so CI surfaces the real problem (Metal unavailable on this runner) without masking it as a key-bind regression. The test still runs end-to-end on developer machines and on runners with a logged-in GUI session. Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add algorithmic complexity review rule (manaflow-ai#4866) * Add algorithmic complexity review rule * Align algorithmic complexity review hint --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Narrow AppDelegate CI quarantine (manaflow-ai#4874) Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Restore browser devtools config CI coverage * Add prompt and rules deeplinks (manaflow-ai#4839) * Add prompt and rules deeplinks * Preserve encoded punctuation in text deeplinks * Localize text deeplink dialogs * Support Freestyle SSH link users * Harden prompt and rules deeplinks * Suppress welcome for text deeplinks * Restore SSH deeplink trust gate * Preserve plus signs in text deeplinks * Respect no-focus for text deeplink target selection * Use deterministic text deeplink target * Start background text deeplink targets * Report text deeplink send failures * Handle mixed deeplink batches safely * Gate mixed external deeplink batches * Report failed external text deeplink sends * Stop after handling external cmux links * Allow multiline cmux text links * Reject control characters in cmux text links * Polish external cmux link handling * Label sendTextWhenReady callbacks --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Make session index backfill linear (manaflow-ai#4868) * Make session index backfill linear * Stabilize session index backfill tie breaks * Fix session index tie-break test setup --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Optimize batch workspace sidebar actions (manaflow-ai#4865) * Optimize batch workspace sidebar actions * Preserve workspace selection after drag reorder * Harden workspace batch action coverage --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * test: harden bonsplit scroll indicator check * Restore browser remote store CI coverage * Make browser search providers configurable (manaflow-ai#4849) * feat: make browser search providers configurable * fix: keep parsing browser settings after invalid custom search config * test: harden custom search config regression * fix: handle custom search edge cases * fix: validate custom search fallbacks * fix: localize search engine labels * fix: gate stale search suggestions * fix: allow blank custom search names * fix: add exa search provider --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Restore browser lifecycle CI coverage * docs: add cmux ssh deep links (manaflow-ai#4833) * docs: add cmux ssh deep links * docs: add cmux deeplink fallback pages * docs: add deeplink locale fallbacks * docs: tighten deeplink fallback validation * docs: mirror native ssh deeplink validation * docs: localize deeplink messages * docs: canonicalize rules deeplink alternates * Allow Freestyle SSH deeplink users * docs: validate text deeplink fallback params * docs: ignore blank optional deeplink params * docs: address deeplink review comments --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Restore file preview review CI coverage * Pin Xcode 26 (objectVersion 60) and add pbxproj normalizer + CI guard (manaflow-ai#4836) * Add deterministic normalizer for cmux.xcodeproj/project.pbxproj scripts/normalize-pbxproj.py sorts the high-churn sections (PBXBuildFile, PBXFileReference, and the files = (...) arrays inside Sources / Resources / Frameworks / CopyFiles build phases) into a deterministic order keyed on the entry comment plus UUID. The Xcode build does not care about the order of these flat dictionary sections; sorting them just kills the nondeterministic diff noise Xcode generates on every UI touch. Does not touch UUIDs, comments, or PBXGroup children = (...) arrays (navigator order is intentional). Idempotent: a second run produces zero diff. Standalone in this commit so the diff is just the script. The next commit applies the script and bumps objectVersion in one shot, so the resulting churn is contained and never repeated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Pin objectVersion = 60 and normalize pbxproj Bumps objectVersion from 56 to 60 (the format Xcode 16+ and Xcode 26 write by default) and runs scripts/normalize-pbxproj.py once to establish the deterministic baseline. After this commit, future diffs to project.pbxproj show only real changes, not Xcode's nondeterministic section reordering. One-time large diff. No semantic changes to targets, sources, build phases, or settings: pure sort + version pin. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add tracked pre-commit hook that normalizes pbxproj scripts/git-hooks/pre-commit calls scripts/normalize-pbxproj.py on cmux.xcodeproj/project.pbxproj when it is staged and re-stages the result. scripts/install-git-hooks.sh points the clone at this directory via `git config core.hooksPath scripts/git-hooks`, and scripts/setup.sh auto-runs it so devs get the hook without a separate manual step. After this, Xcode's nondeterministic reordering of build-file and file-reference sections is canceled out at commit time. The CI guard in the next commit enforces the rule for anyone who bypasses the hook with --no-verify or who never ran setup. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add CI guard for objectVersion pin and pbxproj normalization scripts/check-pbxproj.sh asserts cmux.xcodeproj/project.pbxproj has objectVersion = 60 (Xcode 26 default) and that the file is normalized per scripts/normalize-pbxproj.py. Wired as a step in the workflow-guard-tests job so every PR is gated. This catches anyone who bypasses the pre-commit hook with --no-verify or who never ran scripts/setup.sh. The error message points at the exact fix path. To bump the pin (e.g., when the team adopts a newer Xcode major), edit EXPECTED_OBJECT_VERSION in this script and the matching line in CLAUDE.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add .xcode-version and document Xcode 26 pin in CLAUDE.md .xcode-version records the major (26.0) for tooling that reads it (xcodes CLI, some CI helpers). CLAUDE.md gains an Xcode toolchain section explaining the pin, the normalizer + pre-commit hook + CI guard mechanics, and the procedure for bumping the pin in the future. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Read .xcode-version as the source of truth in check-pbxproj.sh scripts/check-pbxproj.sh now reads .xcode-version and maps the Xcode major to the expected objectVersion via a one-entry case statement. Bumping the team's Xcode pin becomes a one-file edit (.xcode-version), with a script update only required when Apple actually changes objectVersion in a new Xcode major. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Address CodeRabbit findings on check-pbxproj.sh and pre-commit hook scripts/check-pbxproj.sh now passes "$PBXPROJ" explicitly to normalize-pbxproj.py instead of letting it default to a path relative to the current working directory, so the guard works regardless of where CI invokes it. scripts/git-hooks/pre-commit refuses to run when the working-tree pbxproj has unstaged changes. Previously the hook would normalize the working-tree file and `git add` the result, which silently staged any unstaged hunks the user had deliberately left out of the commit. The hook now exits non-zero with a clear message telling the user to either stage the whole file or stash the unstaged hunks first. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Address Greptile findings: misleading comment + bump-step docs scripts/normalize-pbxproj.py: the comment said "preserve empty lines exactly where they are" but the implementation collapses blanks to a trailing group. Reworded the comment to match the actual behavior. CLAUDE.md: the bump procedure now mentions opening cmux.xcodeproj in the new Xcode so objectVersion gets rewritten automatically. Without that step a developer following the docs alone would update only the pin file and the script case, and the CI guard would fail on their next commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * Add scripts/cleanup-dev-builds.sh for safely reclaiming tagged DerivedData (manaflow-ai#4837) * Add scripts/cleanup-dev-builds.sh Removes tagged dev-build artifacts produced by scripts/reload.sh: DerivedData/cmux-<tag>/ (multi-GB each), /tmp/cmux-<tag>/, the per-tag debug socket and logs, the reload log, and the App Support cmuxd dev socket. Defaults to dry-run; pass --apply to delete. Safety rules always on: - Skip the tag of any running `cmux DEV <tag>` app - Skip the tag pointed at by /tmp/cmux-last-cli-path - Skip any tag tied to a live git worktree Filters: --older-than DAYS, --keep TAG (repeatable). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Drop "worktree exists" safety rule in cleanup-dev-builds.sh Existence of a git worktree with the same name is a weak signal of active use, and HQ tends to accumulate worktrees long after the work is done. The rule made cleanup over-protective for the typical case (worktree still around from a merged or abandoned PR). The remaining safety rules (skip running app, skip the tag pointed at by /tmp/cmux-last-cli-path) plus --keep TAG and --older-than DAYS cover what we actually want without false positives. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add /cleanup-builds slash command Wraps scripts/cleanup-dev-builds.sh with the standard preview -> confirm -> apply flow. Sits alongside the existing .claude/commands (pull, sync-branch, release, etc.) and enforces user confirmation before --apply. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Address review feedback on cleanup-dev-builds CodeRabbit + Greptile findings, all real: - Active-tag extraction from /tmp/cmux-last-cli-path now uses a regex match on /cmux-<tag>/ anywhere in the path, not just a strict $DERIVED_DATA_ROOT/cmux- prefix. Also avoids the unquoted parameter expansion that could be sensitive to glob metacharacters in DERIVED_DATA_ROOT. - discover_tags switched from find | xargs basename to a shell glob loop. Cleaner, works on macOS regardless of xargs flavor, handles the empty case naturally. - --older-than no longer skips tags whose DerivedData was already deleted (age == -1 sentinel). Orphan sockets/logs for those tags now get cleaned instead of being silently retained. - "freed" label reworded as "freed (estimated)" because the byte count is measured during planning, not after rm. - .claude/commands/cleanup-builds.md: blank lines around fenced blocks (MD031). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * Fix agent resume when saved cwd is deleted (manaflow-ai#4859) * test: cover agent resume with deleted cwd * fix: resume agents when saved cwd is gone * fix: harden restored cwd guard * fix: skip ghostty cwd for guarded restore commands * fix: drop duplicate cwd args on restore * fix: preserve shell args during cwd cleanup * fix: preserve custom resume cwd arguments * fix: preserve shell syntax during cwd cleanup * chore: remove unused shell word helper --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> * Restore file preview text saving CI coverage * Restore browser session history CI coverage * chore(rename): swap bundle id, product name, config dir to most Visual rename pass — upstream-merge friendly. Touches user-visible surfaces only; keeps Swift identifiers, package dirs, env vars, and on-disk xcodeproj/test target names so cmux upstream merges remain conflict-free. - pbxproj: PRODUCT_BUNDLE_IDENTIFIER com.cmuxterm.* → com.4etverg.most* - pbxproj: PRODUCT_NAME cmux → most ("cmux DEV" → "most DEV") - Entitlements: app group ids release/nightly → com.4etverg.most[.nightly] - CLI config path: ~/.config/cmux/cmux.json → ~/.config/most/most.json - One-shot legacy config migration in KeyboardShortcutSettingsFileStore - README.md (English) user-visible refs flipped; upstream URLs preserved Co-Authored-By: OpenAI Codex (gpt-5.4-mini) <noreply@openai.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(configuration): rename to most.json + document migration Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> Co-authored-by: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Co-authored-by: Konstantin <your@email.ar> Co-authored-by: OpenAI Codex (gpt-5.4-mini) <noreply@openai.com>
Summary
Testing
./scripts/reload.sh --tag notifpopbuilds clean.notificationsPopover.jumpToLatest,notificationsPopover.clearAll,NotificationPopoverRow.<uuid>.Notes
accessibilityAction { onOpen() }for XCUITest reliability.notifications.*keys.Need help on this PR? Tag
@codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Changes notification unread/workspace-indicator coordination and pane focused-read clearing from the popover; mostly UI but incorrect logic could affect badge counts.
Overview
Redesigns the titlebar notifications popover with a larger default size, a user-resizable bottom-right grip (size persisted in
UserDefaults, clamped to the popover’s screen), and a stable outer frame across empty, hidden, and populated states.The header gains an unread count badge and tighter styling for Jump to Latest and Clear All. Rows are minimal (accent bar, dividers, hover-only clear) with AppKit-backed hover and resize so pointer behavior stays reliable in an
NSPopover. Context menu and toggle-read call new store APIs; marking read can clear pane focused-read indicators when the notification is pane-scoped.Adds
markUnread(id:)onTerminalNotificationStore, clearing manual/restored workspace unread for that tab so badges are not double-counted. Newnotifications.*strings (en/ja) for resize, read/unread, dismiss, and row actions.Reviewed by Cursor Bugbot for commit c00e8a8. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
Style
Accessibility
Documentation