Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2db9bf0
test(sidebar): require workspace previews to touch their rows
teamleaderleo Oct 28, 2026
9b4b32c
fix(sidebar): place workspace previews against their drawn rows
teamleaderleo Oct 28, 2026
6287fdf
fix(sidebar): keep pinned preview hit geometry on its fill
teamleaderleo Oct 28, 2026
d3f0e07
Merge feat-cmux-next into workspace preview placement fix
teamleaderleo Oct 29, 2026
92b9a4b
feat(sidebar): show selectable inner tabs on workspace hover
teamleaderleo Oct 28, 2026
aa8ff4a
test(sidebar): adapt hover fixtures to selectable tab cards
teamleaderleo Oct 28, 2026
a01b718
test(media): find workspace hover target by row name
teamleaderleo Oct 28, 2026
2e78ac8
chore(settings): regenerate workspace hover exports on fleet
teamleaderleo Oct 28, 2026
54c9929
fix(settings): translate rich hover details for all Settings locales
teamleaderleo Oct 28, 2026
65ad52e
test(sidebar): capture switching between inner tabs
teamleaderleo Oct 28, 2026
2fa10cd
test(sidebar): capture both hover gallery appearances
teamleaderleo Oct 28, 2026
45fba27
test(sidebar): use a live hover source and focus the capture workspace
teamleaderleo Oct 28, 2026
9736b13
test(sidebar): explicitly select the second capture tab
teamleaderleo Oct 28, 2026
d1ee899
Merge commit 'c230bb58a50cf8b35ee7de9f1130bff840c8e459' into hover-fi…
teamleaderleo Oct 29, 2026
3b0b73b
Merge feat-cmux-next into selectable workspace hover
teamleaderleo Oct 29, 2026
4323f91
test(sidebar): wait for the workspace hover dwell before capture input
teamleaderleo Oct 29, 2026
1ad21a3
fix(sidebar): cancel stale workspace tab presses
teamleaderleo Oct 29, 2026
46fc445
Merge feat-cmux-next into selectable workspace hover
teamleaderleo Oct 29, 2026
464ea5a
Merge feat-cmux-next into selectable workspace hover
teamleaderleo Oct 30, 2026
4f0634d
fix(hover-cards): expose the interactive card to bench-hooks builds
teamleaderleo Oct 30, 2026
3f29885
fix(sidebar): keep status glyphs on workspace hover tab rows
teamleaderleo Oct 30, 2026
c5644a8
Merge remote-tracking branch 'origin/feat-cmux-next' into h9-24785
teamleaderleo Oct 30, 2026
5a720ae
docs(sidebar): log three squash merges for the Sidebar-Look sweep
teamleaderleo Oct 30, 2026
008f78e
test(sidebar): run the hover card key test only where windows reach t…
teamleaderleo Oct 30, 2026
9137768
Merge remote-tracking branch 'origin/feat-cmux-next' into HEAD
teamleaderleo Oct 30, 2026
ac2f881
Merge remote-tracking branch 'origin/feat-cmux-next' into h13/sidebar…
teamleaderleo Oct 30, 2026
8f04f2a
Hover card tests: keep the escape fixture's rows at the window top; c…
teamleaderleo Oct 30, 2026
22bd712
Merge branch 'feat-cmux-next' into codex/sidebar-workspace-hover-2026…
teamleaderleo Oct 30, 2026
f9554b1
Merge remote-tracking branch 'origin/codex/sidebar-workspace-hover-20…
teamleaderleo Oct 30, 2026
30eaf59
Merge remote-tracking branch 'origin/feat-cmux-next' into h13/sidebar…
teamleaderleo Oct 30, 2026
6551871
Merge remote-tracking branch 'origin/feat-cmux-next' into h13/sidebar…
teamleaderleo Oct 31, 2026
16632c4
Regenerate settings exports and the docs settings reference
teamleaderleo Oct 31, 2026
d24023e
Merge remote-tracking branch 'origin/feat-cmux-next' into h13/sidebar…
teamleaderleo Oct 31, 2026
8f481e6
Regenerate the settings schema after the base merge
teamleaderleo Oct 31, 2026
2d8a054
Merge remote-tracking branch 'origin/feat-cmux-next' into h13/sidebar…
teamleaderleo Oct 31, 2026
d17c5da
List WorkspaceTabHoverCardTests in the display suites
teamleaderleo Oct 31, 2026
e2a0a93
Pin the dark inner-tab hover card's text tokens; add a real hover cap…
teamleaderleo Oct 31, 2026
4df66d7
Draw the inner-tab hover card's paths in secondary text
teamleaderleo Oct 31, 2026
66a8297
Merge remote-tracking branch 'origin/feat-cmux-next' into h13/sidebar…
teamleaderleo Oct 31, 2026
ec60f85
Regenerate the defaults snapshot and settings docs after the base merge
teamleaderleo Oct 31, 2026
8174703
Merge remote-tracking branch 'origin/feat-cmux-next' into h13/sidebar…
teamleaderleo Oct 31, 2026
27da5cf
Refresh the defaults snapshot's source fingerprints after the base merge
teamleaderleo Oct 31, 2026
ffa9106
Regenerate settings exports for base's new sidebar.showNewWorkspaceCh…
teamleaderleo Oct 31, 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 @@ -38,6 +38,7 @@ enum DebugMouse {
if params["target"]?.stringValue == "page" {
return params["action"]?.stringValue == "scroll" ? scrollPage(params, services: services) : clickPage(params, services: services)
}
if params["target"]?.stringValue == "hover-card" { return hoverCard(params, services: services) }
let windowID = params["window"]?.stringValue
guard let controller = services.windows.controllers.first(where: { windowID == nil || $0.state.id == windowID }),
let window = controller.window else { return .object(["error": .string("no window")]) }
Expand Down Expand Up @@ -141,6 +142,32 @@ enum DebugMouse {
])
}

/// A click/move on the actual non-activating card window, through the app's
/// event queue and local monitors. x/y start at the card's top-left, without
/// the transparent shadow band. This proves its hit test, rather than
/// invoking a tab-selection callback directly.
private static func hoverCard(_ params: [String: JSONValue], services: AppServices) -> JSONValue {
guard let card = services.hoverCards.debugInteractiveCard,
let x = params["x"]?.doubleValue, let y = params["y"]?.doubleValue else {
return .object(["error": .string("no interactive hover card, or missing x/y")])
}
let screen = CGPoint(x: card.frame.minX + x, y: card.frame.maxY - y)
let base = card.window.convertPoint(fromScreen: screen)
let topLeft = InputJournal.topLeftPoint(base, in: card.window, relativeTo: card.window)
let action = params["action"]?.stringValue ?? "click"
guard ["move", "click"].contains(action) else { return .object(["error": .string("pass action move or click")]) }
let types: [NSEvent.EventType] = action == "move" ? [.mouseMoved] : [.leftMouseDown, .leftMouseUp]
let events = types.compactMap { mouse($0, at: topLeft, in: card.window, flags: [], clicks: action == "move" ? 0 : 1) }
guard events.count == types.count else { return .object(["error": .string("could not synthesize card events")]) }
// Keep the coordinator's synthetic screen point authoritative. Walking
// tracking areas here would send a row exit before the non-activating
// card receives the move, which is unlike the real panel crossing.
services.hoverCards.pointerMoved(to: screen)
SyntheticInput.register(events)
for event in events { NSApp.postEvent(event, atStart: false) }
return .object(["posted": .number(Double(events.count)), "target": .string("hover-card")])
}

/// Top-left window-local point from `x`,`y` or the center of `pane`.
private static func point(_ params: [String: JSONValue], controller: WindowController, window: NSWindow) -> NSPoint? {
if let x = params["x"]?.doubleValue, let y = params["y"]?.doubleValue { return NSPoint(x: x, y: y) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,8 @@ public final class HoverCardCoordinator {
let panel = panel ?? HoverCardPanel()
self.panel = panel
panel.present(body: body.view, anchor: anchor, placement: body.placement, parent: parent,
themeAnchor: body.themeAnchor, sliding: sliding, cornerRadius: body.cornerRadius, applyTheme: body.applyTheme)
themeAnchor: body.themeAnchor, sliding: sliding, interactive: body.interactive,
cornerRadius: body.cornerRadius, applyTheme: body.applyTheme)
// A hover card whose row's escape slot already holds the pointer (it rested in the
// sidebar's margin beside the row) goes at once: the slot never shows its row's card.
if case .shown(let shown) = machine.phase, shown.id == target.id, escapeTarget(at: currentPointer()) == target.id {
Expand Down Expand Up @@ -373,6 +374,26 @@ public final class HoverCardCoordinator {
/// disappears"): the card ignores the mouse, so the window under it would scroll. The card
/// scrolls its own scroll view (a long timeline) instead, stays, and the event goes no further.
func handleMonitoredEvent(_ event: NSEvent) -> NSEvent? {
if let panel, panel.isInteractive, panel.isShowingCard {
let belongsToParent = event.window.map { window in
sequence(first: window, next: \.parent).contains { $0 === panel.parent }
} ?? false
let pointerOnTarget = machine.shownTarget.flatMap { owners[$0.id]?.source?.hoverCardTargetFrame(for: $0.id) }?.contains(currentPointer()) == true
let pointerOnCard = panel.cardFrame.contains(currentPointer())
if event.type == .keyDown, belongsToParent, (pointerOnTarget || pointerOnCard),
let body = panel.body as? any HoverCardInteractiveBody,
body.handleHoverCardKeyDown(event) {
return nil
}
let point = event.window.map { $0.convertPoint(toScreen: event.locationInWindow) } ?? currentPointer()
if panel.cardFrame.contains(point) {
if event.type == .leftMouseDown {
// Let the interactive card's row receive the click. Its
// action dismisses after it focuses the selected tab.
return event
}
}
}
if event.type == .scrollWheel, let panel, machine.shownTarget != nil, panel.isShowingCard {
// A real event names the window under the pointer; a synthesized one may name none.
let point = event.window.map { $0.convertPoint(toScreen: event.locationInWindow) } ?? currentPointer()
Expand Down Expand Up @@ -520,6 +541,15 @@ public final class HoverCardCoordinator {
"card_text": panel.map { $0.isShowingCard ? $0.bodyLines.joined(separator: "\n") : "" } ?? "",
]
}

#if DEBUG || CMUX_BENCH_HOOKS
/// The currently interactive card, for synthetic native input proofs.
/// Coordinates are relative to the card, excluding the window's shadow band.
public var debugInteractiveCard: (window: NSWindow, frame: CGRect)? {
guard let panel, panel.isInteractive, panel.isShowingCard else { return nil }
return (panel, panel.cardFrame)
}
#endif
}

struct WeakSource {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,33 @@ public struct HoverCardBody {
public var placement: HoverCardPlacement
public weak var themeAnchor: NSView?
public var applyTheme: () -> Void
/// An interactive body receives mouse events inside the card and may consume
/// keyboard navigation before the coordinator dismisses the card.
public var interactive: Bool
/// The card's corner radius; nil is the floating panels' (`HoverCardPanel.cornerRadius`). A card
/// that hangs from its target with no gap takes the target's shape (a tab's card, the tab's
/// `Metrics.tabCornerRadius`).
public var cornerRadius: CGFloat?
public init(view: NSView, placement: HoverCardPlacement, themeAnchor: NSView?, cornerRadius: CGFloat? = nil,
public init(view: NSView, placement: HoverCardPlacement, themeAnchor: NSView?, interactive: Bool = false,
cornerRadius: CGFloat? = nil,
applyTheme: @escaping () -> Void) {
self.view = view
self.placement = placement
self.themeAnchor = themeAnchor
self.interactive = interactive
self.cornerRadius = cornerRadius
self.applyTheme = applyTheme
}
}

/// A card body that handles keyboard navigation while the card window remains
/// non-activating. The coordinator calls this from its local event monitor.
@MainActor
public protocol HoverCardInteractiveBody: AnyObject {
/// Returns true when the body consumed `event`.
func handleHoverCardKeyDown(_ event: NSEvent) -> Bool
}

/// A card body that can say what it shows, for `debug.hover_cards` (GUI proofs read it).
@MainActor
public protocol HoverCardTextReporting {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public nonisolated struct WorkspaceHoverCardOptions: Sendable, Hashable {
}

public var layout: Layout = .timeline
/// When enabled, keep the richer workspace card with prompts, needs-input state and facts.
/// The sidebar defaults to the inner-tab preview; this switch preserves the previous card.
public var showsRichDetails = false
/// Running times: the status's and a command's elapsed time, and the stats line.
public var showsTimings = true
/// Prompts the person sent, and starts of work, in the history.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ extension HoverCardSettings {
public var workspaceCardOptions: WorkspaceHoverCardOptions {
var options = WorkspaceHoverCardOptions()
options.layout = WorkspaceHoverCardOptions.Layout(rawValue: layout.rawValue) ?? .timeline
options.showsRichDetails = showsRichDetails
options.showsTimings = showsTimings
options.showsPrompts = showsPrompts
options.showsWaiting = showsWaiting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public nonisolated struct HoverCardSettings: Sendable, Equatable {
public var delayMilliseconds = HoverCardSettings.delayDefault
/// The workspace card's design (`sidebar.hoverCards.layout`).
public var layout = HoverCardSettings.layoutDefault
/// Whether to preserve the richer workspace card instead of listing inner tabs.
public var showsRichDetails = HoverCardSettings.richDetailsDefault
/// `sidebar.hoverCards.sections.*`: which parts the workspace card shows.
public var showsTimings = HoverCardSettings.timingsDefault
public var showsPrompts = HoverCardSettings.promptsDefault
Expand Down Expand Up @@ -40,6 +42,7 @@ public nonisolated struct HoverCardSettings: Sendable, Equatable {
if let value = reader.bool("enabled") { settings.enabled = value }
if let value = reader.number("delay", range: delayRange) { settings.delayMilliseconds = value }
if let value = reader.choice("layout", HoverCardLayoutSetting.self) { settings.layout = value }
if let value = reader.bool("richDetails") { settings.showsRichDetails = value }
if let value = reader.number("maxRows", range: maxRowsRange) { settings.maxRows = value.rounded() }
diagnostics += reader.diagnostics
if var sections = ConfigFieldReader(root, at: ["sidebar", "hoverCards", "sections"], diagnostics: &diagnostics) {
Expand Down
Loading
Loading