Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
9bffba2
cloud sidebar: hide link tabs while a machine connects, line its conn…
lucasr1b Oct 2, 2026
29e9610
test: clicking a ports status row's text should not refresh or wake
lucasr1b Oct 2, 2026
ec601a1
cloud sidebar: only the ports status button acts, clicking the row's …
lucasr1b Oct 2, 2026
687e3f9
test: machines under the cloud machines section reorder by drag
lucasr1b Oct 3, 2026
49bf865
cloud sidebar: continuous machine and tab reorder, refresh icons in t…
lucasr1b Oct 3, 2026
974de84
move the header refresh icons and tab switch highlight to the follow-…
lucasr1b Oct 3, 2026
712b24e
cloud sidebar: refresh icons in the section headers, sliding tab high…
lucasr1b Oct 3, 2026
fba5a8d
machine lift: closing machines above the held one is not a move
lucasr1b Oct 3, 2026
9cc463c
Merge branch 'cloud-machine-reorder' into cloud-sidebar-polish
lucasr1b Oct 3, 2026
bf080de
cloud sidebar polish: ports alignment, empty states, pane button, ful…
lucasr1b Oct 3, 2026
856abd3
right sidebar width fits the selected tab's name with the others as i…
lucasr1b Oct 3, 2026
e0ba72b
cloud sidebar: refresh icons sit after the section counts; a failed c…
lucasr1b Oct 3, 2026
0f1853f
Resolve PR 17070 review feedback and merge main
austinywang Oct 3, 2026
b9c46ed
Unify machine drag release cleanup
austinywang Oct 3, 2026
e03ae8c
cloud sidebar: refresh icon hover fill, a quieter tab switch, "Discov…
lucasr1b Oct 3, 2026
9cb17b6
Fix Swift 6 drag cleanup build errors
austinywang Oct 3, 2026
b094c04
Merge branch 'cloud-sidebar-connecting' (#16812) into cloud-sidebar-p…
lucasr1b Oct 3, 2026
bf9dea6
cloud sidebar: a failed create's row is just its notice
lucasr1b Oct 3, 2026
becd460
cloud sidebar: clicking a header's refresh icon refreshes instead of …
lucasr1b Oct 3, 2026
d5f0b94
Merge branch 'cloud-machine-reorder' into cloud-sidebar-polish
lucasr1b Oct 3, 2026
c099968
cloud sidebar polish: fit the merge under the line budgets
lucasr1b Oct 3, 2026
ef643c7
refactor cloud tree coordinator for line budget
austinywang Oct 3, 2026
ddb9c92
Merge main (db43cdb42777) into repair-pr-17070
austinywang Oct 3, 2026
2b86ac4
Merge remote-tracking branch 'origin/main' into cloud-machine-reorder
austinywang Oct 3, 2026
abb83ce
Merge remote-tracking branch 'origin/cloud-machine-reorder' into clou…
austinywang Oct 3, 2026
31ca1c9
Merge main (d2adb1b734b6) into repair-pr-17070
austinywang Oct 3, 2026
1dd8bd3
Fix namespace type convention lint
austinywang Oct 3, 2026
cced903
Merge remote-tracking branch 'origin/cloud-machine-reorder' into repa…
austinywang Oct 3, 2026
253d057
Document stateless namespace lint exceptions
austinywang Oct 3, 2026
5530189
Retry CI after transient macOS runner failures
austinywang Oct 3, 2026
d849aff
fix cloud tree machine lookup imports
austinywang Oct 3, 2026
3db7160
fix right sidebar drag test module import
austinywang Oct 3, 2026
327c3fe
Merge main (34348eafcd9e) into cloud-sidebar-polish
austinywang Oct 3, 2026
824577f
ci: isolate owned macOS canonical roots by runner
austinywang Oct 3, 2026
bd6be7b
Merge remote-tracking branch 'origin/cloud-machine-reorder' into clou…
austinywang Oct 3, 2026
1c326ce
Merge commit 'f296dc6f09e6ec14d247f9997e8634aa48dd2476' into cloud-si…
austinywang Oct 3, 2026
77b545d
resolve current main cloud sidebar merge
austinywang Oct 3, 2026
aa6191c
Merge main (3ebb0f255714) into cloud-sidebar-polish
austinywang Oct 3, 2026
0b9d070
fix review findings and hold canonical roots
austinywang Oct 3, 2026
63bae4c
Merge main (68bb2f15cd08) into cloud-sidebar-polish
austinywang Oct 3, 2026
ac19025
ci: keep owned pool picks on the pinned Xcode
austinywang Oct 3, 2026
b9af469
fix: remove deleted cloud refresh source from project
austinywang Oct 3, 2026
8e4d1f0
fix: label machine create failure test outputs
austinywang Oct 3, 2026
c506417
fix: split throwing ordering assertions
austinywang Oct 3, 2026
5298e3d
fix: keep scoped cloud usage visible during refresh
austinywang Oct 3, 2026
1de722b
fix(sidebar): soften tab reorder animation
austinywang Oct 3, 2026
417d9e3
fix(sidebar): keep reorder motion responsive
austinywang Oct 3, 2026
0e922af
fix(sidebar): hand off tabs dragged into main panes
austinywang Oct 3, 2026
58566c7
fix(sidebar): render drag preview icons from bitmaps
austinywang Oct 3, 2026
7c8f195
fix(sidebar): import drag preview font scaling
austinywang Oct 3, 2026
2a6a24a
Merge origin/main into sidebar animation PR
austinywang Oct 4, 2026
1ec30a9
Merge origin/main into sidebar animation PR
austinywang Oct 4, 2026
27fd4d7
Use mainline sidebar drag boundary API
austinywang Oct 4, 2026
167a8c7
Merge remote-tracking branch 'origin/main' into fix/tab-reorder-anima…
austinywang Oct 4, 2026
809b536
Restore mainline cloud refresh button
austinywang Oct 4, 2026
b9139de
Keep cloud machine lookup in the app target
austinywang Oct 4, 2026
01df838
Keep cloud refresh header in the app target
austinywang Oct 4, 2026
ff467ac
Wire cloud sidebar polish tests
austinywang Oct 4, 2026
58905e9
Wire sidebar source files into cmux target
austinywang Oct 4, 2026
9558553
Use current sidebar drag boundary signature
austinywang Oct 4, 2026
6c482d4
Keep full sidebar tab label in drag preview
austinywang Oct 4, 2026
e72fe91
Give dragged unfocused tabs full label width
austinywang Oct 5, 2026
d820cc7
Merge origin/main into sidebar animation PR
austinywang Oct 5, 2026
dcb72ad
Preserve full drag frame for unfocused tabs
austinywang Oct 5, 2026
bb4f37d
Remove duplicate cloud refresh header after main merge
austinywang Oct 5, 2026
5719aeb
Anchor icon while dragged tab label expands
austinywang Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,14 @@ public struct RightSidebarModeBarDragLayout: Equatable {
return result
}

/// True once the pointer is far enough above or below the bar that the
/// tab is being carried out of it, toward a pane.
public static func leavesBar(pointerY: CGFloat, tabFrame: CGRect, barHeight: CGFloat) -> Bool {
/// True once the pointer is outside the mode bar, toward a pane. The main
/// workspace is to the left of the right sidebar, so crossing x = 0 must
/// hand off just like leaving vertically does.
public static func leavesBar(pointer: CGPoint, tabFrame: CGRect, barHeight: CGFloat) -> Bool {
let margin = max(barHeight, tabFrame.height) / 2
return pointerY < tabFrame.minY - margin || pointerY > tabFrame.maxY + margin
return pointer.x < 0
|| pointer.y < tabFrame.minY - margin
|| pointer.y > tabFrame.maxY + margin
}

private func heldOffset(_ translation: CGFloat) -> CGFloat {
Expand Down
11 changes: 11 additions & 0 deletions Sources/Cloud/CloudTreeMachineDetailLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ struct CloudTreeMachineDetailLayout {
}
}
node.children.forEach(collect)
// While the machine connects, Ports, Terminals and Displays have
// nothing current to show, so only Resources (fleet telemetry, which
// does not need the link) keeps its tab.
if rows.contains(where: Self.isConnectingPlaceholder) {
for tab in [CloudTreeMachineDetailTab.ports, .terminals, .displays] { pools[tab] = nil }
}
let tabs = CloudTreeMachineDetailTab.allCases.filter { pools[$0] != nil }
if !tabs.isEmpty {
let row = tabRow(machine: machine, tabs: tabs, pools: pools)
Expand Down Expand Up @@ -162,5 +168,10 @@ struct CloudTreeMachineDetailLayout {
return CloudTreeNode(id: node.id, kind: .terminal(row), children: node.children, isPinned: node.isPinned)
}

private static func isConnectingPlaceholder(_ node: CloudTreeNode) -> Bool {
if case .placeholder(_, let placeholder) = node.kind { return placeholder.style == .connecting }
return false
}

private static func baseID(_ machine: SurfaceMachineID) -> String { "machine:\(machine.rawValue)" }
}
10 changes: 8 additions & 2 deletions Sources/RightSidebarChromeStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,9 @@ struct RightSidebarModeBarItem: Identifiable, Equatable, Sendable {
struct ModeBarButton: View {
let item: RightSidebarModeBarItem
let isSelected: Bool
/// The tab is actively being dragged. Its icon stays anchored while the
/// full label slot opens around it.
var isDragged = false
var badgeCount: Int = 0
let shortcutHint: StoredShortcut
let showsShortcutHint: Bool
Expand All @@ -383,7 +386,10 @@ struct ModeBarButton: View {
/// With its label hidden, the icon (and badge) moves into the middle of
/// the label's empty slot, so it sits centered in the tab's highlight.
private var hiddenLabelShift: CGFloat {
labelFits ? 0 : (labelWidth + Self.contentSpacing) / 2
// A dragged tab is promoted to the full-label slot by the parent
// layout. Keep the glyph at its resting x position while the text
// reveals, instead of animating it from the icon-only center.
isDragged || labelFits ? 0 : (labelWidth + Self.contentSpacing) / 2
}
private static let contentSpacing: CGFloat = 4

Expand Down Expand Up @@ -417,7 +423,7 @@ struct ModeBarButton: View {
.frame(minWidth: 0, alignment: .leading)
.clipped()
.mask { ModeBarLabelEdgeFade(naturalWidth: naturalLabelWidth) }
.opacity(labelFits ? 1 : 0)
.opacity(isDragged ? 1 : (labelFits ? 1 : 0))
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.width
} action: { width in
Expand Down
44 changes: 33 additions & 11 deletions Sources/RightSidebarModeBarDrag.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AppKit
import CmuxFoundation
import Observation
import CmuxSidebar
import SwiftUI
Expand All @@ -12,8 +13,9 @@ import SwiftUI
@Observable
final class RightSidebarModeBarDragController {
nonisolated static let coordinateSpace = "RightSidebarModeBarTabs"
/// Gentle and nearly critically damped: the tabs part without bouncing.
static let spring = Animation.spring(response: 0.28, dampingFraction: 0.86)
/// A longer, nearly critically damped spring keeps lateral tab movement
/// gradual while staying responsive when the order changes.
static let spring = Animation.spring(response: 0.34, dampingFraction: 0.9)

struct Session {
let mode: RightSidebarMode
Expand Down Expand Up @@ -69,7 +71,7 @@ final class RightSidebarModeBarDragController {
guard var current = session, !current.isCarriedOut else { return }
let tabFrame = current.layout.frames[current.layout.source]
if mode.canOpenAsPane,
RightSidebarModeBarDragLayout.leavesBar(pointerY: location.y, tabFrame: tabFrame, barHeight: barHeight),
RightSidebarModeBarDragLayout.leavesBar(pointer: location, tabFrame: tabFrame, barHeight: barHeight),
carryOut(current, tabFrame: tabFrame, travel: travel, animation: animation, dragImage: dragImage) {
return
}
Expand Down Expand Up @@ -147,7 +149,17 @@ final class RightSidebarModeBarDragController {
guard let view = anchor.view,
let event = NSApp.currentEvent, event.type == .leftMouseDragged,
let image = dragImage(tabFrame.size) else { return false }
let frame = tabFrame.offsetBy(dx: current.layout.draggedOffset(translation: translation), dy: travel.height)
let lifted = tabFrame.offsetBy(dx: current.layout.draggedOffset(translation: translation), dy: travel.height)
// AppKit scales the drag contents to the dragging frame. Unfocused
// tabs can have an icon-only resting frame, so use the preview's full
// intrinsic size for the frame while keeping the press point anchored.
let grabX = current.startLocation.x - tabFrame.minX
let frame = NSRect(
x: lifted.minX + max(0, grabX - image.size.width + image.size.height / 2),
y: lifted.midY - image.size.height / 2,
width: image.size.width,
height: image.size.height
)
guard let source = RightSidebarModeDragPayload.beginPaneDrag(
mode: current.mode, from: view, event: event, frame: frame, image: image,
onEnd: { [weak self] in self?.paneDragEnded() }
Expand Down Expand Up @@ -257,7 +269,10 @@ struct RightSidebarModeBarTabDrag: ViewModifier {
private func dragImage(size: CGSize) -> NSImage? {
let renderer = ImageRenderer(
content: RightSidebarModeBarDragPreview(mode: mode)
.frame(width: size.width, height: size.height)
// Keep the preview at its intrinsic width. The resting tab
// can be narrower while the drag image must retain the full label.
.fixedSize(horizontal: true, vertical: false)
.frame(minWidth: size.width, minHeight: size.height)
.environment(\.colorScheme, colorScheme)
)
renderer.scale = controller.anchor.view?.window?.backingScaleFactor ?? 2
Expand All @@ -268,16 +283,23 @@ struct RightSidebarModeBarTabDrag: ViewModifier {
/// The tab as it looks selected, for the image a carried-out drag shows.
private struct RightSidebarModeBarDragPreview: View {
let mode: RightSidebarMode
@Environment(\.cmuxGlobalFontMagnificationPercent) private var globalFontPercent

var body: some View {
HStack(spacing: 4) {
CmuxSystemSymbolImage(
// ImageRenderer cannot capture the NSViewRepresentable used by
// CmuxSystemSymbolImage. Supply its materialized bitmap instead.
if let image = RenderableSystemSymbol.configuredAppKitImage(
systemName: mode.symbolName,
pointSize: RightSidebarChromeControlStyle.modeIconSize,
weight: RightSidebarChromeControlStyle.iconWeight,
tint: RightSidebarChromeControlStyle.pillForegroundColor(isSelected: true, isHovered: true),
appliesGlobalFontMagnification: true
)
pointSize: GlobalFontMagnification.scaledSize(
RightSidebarChromeControlStyle.modeIconSize, percent: globalFontPercent
),
weight: RightSidebarChromeControlStyle.iconWeight
) {
Image(nsImage: image)
.renderingMode(.template)
.foregroundStyle(RightSidebarChromeControlStyle.pillForegroundColor(isSelected: true, isHovered: true))
}
Text(mode.label)
.cmuxFont(size: RightSidebarChromeControlStyle.labelSize, weight: RightSidebarChromeControlStyle.labelWeight)
.lineLimit(1)
Expand Down
8 changes: 7 additions & 1 deletion Sources/RightSidebarPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ struct RightSidebarPanelView: View {
isSelected: item.isSelected(
mode: fileExplorerState.mode
),
isDragged: modeBarDrag.isLifted(item.mode),
badgeCount: item.mode == .feed ? feedPendingCount : 0,
shortcutHint: shortcut,
showsShortcutHint: ShortcutHintTitlebarPolicy.shouldShow(
Expand All @@ -254,7 +255,12 @@ struct RightSidebarPanelView: View {
mode: item.mode, displayedModes: displayedModes,
barHeight: titlebarHeight, controller: modeBarDrag
))
.layoutValue(key: RightSidebarModeBarTabSelectedKey.self, value: item.isSelected(mode: fileExplorerState.mode))
.layoutValue(
key: RightSidebarModeBarTabSelectedKey.self,
// Give the dragged tab the same full-label slot as
// the focused tab, even when it started unfocused.
value: item.isSelected(mode: fileExplorerState.mode) || modeBarDrag.isLifted(item.mode)
)
}
}
.animation(reduceMotion ? nil : ModeBarButton.switchAnimation, value: fileExplorerState.mode)
Expand Down
7 changes: 4 additions & 3 deletions cmuxTests/RightSidebarTabCustomizationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,10 @@ final class RightSidebarTabCustomizationTests: XCTestCase {

func testModeBarDragLayoutLeavesTheBarOnlyWellOutsideIt() {
let tab = CGRect(x: 0, y: 7, width: 40, height: 24)
XCTAssertFalse(RightSidebarModeBarDragLayout.leavesBar(pointerY: 40, tabFrame: tab, barHeight: 38))
XCTAssertTrue(RightSidebarModeBarDragLayout.leavesBar(pointerY: 51, tabFrame: tab, barHeight: 38))
XCTAssertTrue(RightSidebarModeBarDragLayout.leavesBar(pointerY: -13, tabFrame: tab, barHeight: 38))
XCTAssertFalse(RightSidebarModeBarDragLayout.leavesBar(pointer: CGPoint(x: 1, y: 40), tabFrame: tab, barHeight: 38))
XCTAssertTrue(RightSidebarModeBarDragLayout.leavesBar(pointer: CGPoint(x: 1, y: 51), tabFrame: tab, barHeight: 38))
XCTAssertTrue(RightSidebarModeBarDragLayout.leavesBar(pointer: CGPoint(x: 1, y: -13), tabFrame: tab, barHeight: 38))
XCTAssertTrue(RightSidebarModeBarDragLayout.leavesBar(pointer: CGPoint(x: -1, y: 40), tabFrame: tab, barHeight: 38))
XCTAssertNil(RightSidebarModeBarDragLayout(frames: [], source: 0))
}

Expand Down
Loading