From 5307c7226c348cc0ae959839e87b5f8f5e93acb4 Mon Sep 17 00:00:00 2001 From: Leo Date: Tue, 20 Oct 2026 18:42:29 -0700 Subject: [PATCH 1/8] test(cmux-next): the session hover card is the compact one, crisp and never scrolls (failing) Co-Authored-By: Claude Opus 5.5 --- .../WorkspaceHoverCardSummaryTests.swift | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardSummaryTests.swift diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardSummaryTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardSummaryTests.swift new file mode 100644 index 000000000000..6beca7c6e5d7 --- /dev/null +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardSummaryTests.swift @@ -0,0 +1,57 @@ +import AppKit +@testable import CmuxNextDesign +@testable import CmuxNextSidebar +import Foundation +import Testing + +/// Leo 2026-10-20: the session hover card was a scrolling "History" of truncated prompts and +/// "claude-sr2 finished" lines, mushy, with an awkward inner scroll. "Make it more useful, no +/// scroll, crisp" (ChatGPT's session card is the floor). The default card is the compact one: the +/// title with its machine icon, age and unread dot; folder, branch, machine and pull request; the +/// status with how long it has held; the agent's last message rendered, not raw markdown; then +/// the last three moments, one line each. It never scrolls. +@MainActor @Suite(.serialized) struct WorkspaceHoverCardSummaryTests { + static let now = WorkspaceHoverCardTimelineTests.now + + static func workspace() -> SidebarWorkspace { + var workspace = WorkspaceHoverCardTimelineTests.workspace() + workspace.title = "GUI push · coordinator (this chat)" + workspace.unread = .dot + workspace.lastActivity = now.addingTimeInterval(-2 * 3600) + workspace.activity = .working + workspace.detail.activitySince = now.addingTimeInterval(-4 * 60) + workspace.lastMessage = "**Merged** #22670. Next: `fix` the [hover card](https://example.com/pr).\n\n- one\n- two" + return workspace + } + + @Test func theDefaultCardIsTheCompactOne() { + #expect(WorkspaceHoverCardOptions().layout == .compact) + } + + @Test func theCompactCardSaysWhatMatters() throws { + let card = WorkspaceHoverCardContent.make(Self.workspace(), machine: nil, now: Self.now, options: WorkspaceHoverCardOptions(), + timeline: WorkspaceHoverCardTimelineTests.history(10)) + #expect(card.unread, "the unread workspace shows no unread dot") + #expect(card.age == "2h") + let kinds = card.facts.map(\.kind) + for kind in ["folder", "branch", "pullRequest"] { #expect(kinds.contains(kind), "no \(kind) in \(kinds)") } + let reply = try #require(card.facts.first { $0.kind == WorkspaceHoverCardContent.Fact.lastReplyKind }) + #expect(reply.text == "Merged #22670. Next: fix the hover card. one two", "the last message reads raw: \(reply.text)") + #expect(reply.lines == 3) + #expect(card.timeline.count == 3) + #expect(card.stats == nil) + } + + @Test func theCompactCardNeverScrolls() throws { + let view = WorkspaceHoverCardView() + let card = WorkspaceHoverCardContent.make(Self.workspace(), machine: nil, now: Self.now, options: WorkspaceHoverCardOptions(), + timeline: WorkspaceHoverCardTimelineTests.history(40)) + view.configure(card, now: Self.now) + view.layoutSubtreeIfNeeded() + #expect(!view.timeline.scrollView.hasVerticalScroller, "the history scrolls") + let document = try #require(view.timeline.scrollView.documentView) + #expect(document.frame.height <= view.timeline.scrollView.frame.height + 0.5, "the history is taller than its view") + #expect(view.fittingSize.height < 360, "the card is \(view.fittingSize.height) pt tall") + #expect(!view.lines.contains { $0.contains("**") || $0.contains("`") }, "lines: \(view.lines)") + } +} From d9941aebf93bf3fbd43db2d4336d2efa969d5db2 Mon Sep 17 00:00:00 2001 From: Leo Date: Tue, 20 Oct 2026 19:01:22 -0700 Subject: [PATCH 2/8] fix(cmux-next): the session hover card says what matters and never scrolls Leo 2026-10-20: the workspace hover card was a scrolling "History" of truncated prompts and "finished" lines, mushy, with an awkward inner scroll. "Make it more useful, no scroll, crisp." The compact layout is now the default and reworked: - The title (two lines at most) with its kind or machine icon, the age and an unread dot in the accent color. - The status with how long it has held. - Why it waits or its task, folder, branch, pull request, ports and machine; the noisier facts stay on the other layouts. - The agent's last message, up to three lines, rendered from markdown: bold, italic and code in the card's fonts, block marks dropped, never the raw marks. - The last three moments, one line each, with no header and no scroller; the list is as tall as its lines. The timeline, log, classic and minimal layouts stay in Settings (sidebar.hoverCards.layout). Regenerated the settings sources. Co-Authored-By: Claude Opus 5.5 --- .../WorkspaceHoverCardOptions.swift | 10 +-- .../SettingsSchema+HoverCards.generated.swift | 4 +- ...gsSchema+HoverCardsChoices.generated.swift | 2 +- .../Logic/WorkspaceHoverCardContent.swift | 48 +++++++++++++- .../Views/WorkspaceHoverCard.swift | 64 ++++++++++++++++++- .../WorkspaceHoverCardTimelineView.swift | 21 ++++-- .../WorkspaceHoverCardTimelineTests.swift | 6 +- plans/cmux-next/hover-card-history/DESIGN.md | 2 +- schemas/settings/source/hover-cards.json | 4 +- 9 files changed, 139 insertions(+), 22 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/HoverCards/WorkspaceHoverCardOptions.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/HoverCards/WorkspaceHoverCardOptions.swift index c0b37025e1f1..d5f046793793 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/HoverCards/WorkspaceHoverCardOptions.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/HoverCards/WorkspaceHoverCardOptions.swift @@ -7,10 +7,12 @@ import Foundation public nonisolated struct WorkspaceHoverCardOptions: Sendable, Hashable { /// The card's design. public enum Layout: String, Sendable, Hashable, CaseIterable { - /// The default: the title, status and a stats line, the workspace's facts, then the - /// scrollable history (prompts, starts, waits, finished turns), newest first. + /// The title, status and a stats line, the workspace's facts, then the scrollable history + /// (prompts, starts, waits, finished turns), newest first (u3320). case timeline - /// The facts card with the last three moments under it, no scrolling. + /// The default (Leo 2026-10-20: "more useful, no scroll, crisp"): the title with its age + /// and unread dot, the status, where it runs and its pull request, the agent's last + /// message rendered, then the last three moments, one line each. It never scrolls. case compact /// The history alone, taller and wider, each prompt up to three lines. case log @@ -20,7 +22,7 @@ public nonisolated struct WorkspaceHoverCardOptions: Sendable, Hashable { case minimal } - public var layout: Layout = .timeline + public var layout: Layout = .compact /// 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. diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+HoverCards.generated.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+HoverCards.generated.swift index 4f261944ae73..c42c88cf0dda 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+HoverCards.generated.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+HoverCards.generated.swift @@ -9,9 +9,9 @@ extension HoverCardSettings { public nonisolated static let delayPath = ["sidebar", "hoverCards", "delay"] public nonisolated static let delayDefault: Double = 250 public nonisolated static let delayRange: ClosedRange = 0...5000 - /// `sidebar.hoverCards.layout`: u3320 (Lawrence 2026-10-19): the workspace card's design (plans/cmux-next/hover-card-history/DESIGN.md). timeline (default): stats and a scrollable history of prompts, waits and finished turns; compact: the facts and the last three moments; log: the history alone; classic: the card before the history; minimal: title, status, last prompt. + /// `sidebar.hoverCards.layout`: u3320 (Lawrence 2026-10-19): the workspace card's design (plans/cmux-next/hover-card-history/DESIGN.md). compact (default since Leo 2026-10-20, "no scroll, crisp"): the facts, the last message rendered and the last three moments, never scrolling; timeline: stats and a scrollable history of prompts, waits and finished turns; log: the history alone; classic: the card before the history; minimal: title, status, last prompt. public nonisolated static let layoutPath = ["sidebar", "hoverCards", "layout"] - public nonisolated static let layoutDefault: HoverCardLayoutSetting = .timeline + public nonisolated static let layoutDefault: HoverCardLayoutSetting = .compact /// `sidebar.hoverCards.sections.timings`: u3320 (Lawrence 2026-10-19): which parts a workspace hover card shows. On by default. public nonisolated static let timingsPath = ["sidebar", "hoverCards", "sections", "timings"] public nonisolated static let timingsDefault = true diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+HoverCardsChoices.generated.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+HoverCardsChoices.generated.swift index fdd51d11c117..15ed961a3b8d 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+HoverCardsChoices.generated.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+HoverCardsChoices.generated.swift @@ -1,7 +1,7 @@ // GENERATED by scripts/cmux-next/gen-settings-sources.py from // schemas/settings/source/hover-cards.json. Do not edit; edit the JSON and rerun. -/// u3320 (Lawrence 2026-10-19): the workspace card's design (plans/cmux-next/hover-card-history/DESIGN.md). timeline (default): stats and a scrollable history of prompts, waits and finished turns; compact: the facts and the last three moments; log: the history alone; classic: the card before the history; minimal: title, status, last prompt. +/// u3320 (Lawrence 2026-10-19): the workspace card's design (plans/cmux-next/hover-card-history/DESIGN.md). compact (default since Leo 2026-10-20, "no scroll, crisp"): the facts, the last message rendered and the last three moments, never scrolling; timeline: stats and a scrollable history of prompts, waits and finished turns; log: the history alone; classic: the card before the history; minimal: title, status, last prompt. public nonisolated enum HoverCardLayoutSetting: String, Sendable, Hashable, CaseIterable { case timeline case compact diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Logic/WorkspaceHoverCardContent.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Logic/WorkspaceHoverCardContent.swift index c825247b262a..e4ad49b3b915 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Logic/WorkspaceHoverCardContent.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Logic/WorkspaceHoverCardContent.swift @@ -33,6 +33,9 @@ struct WorkspaceHoverCardContent: Equatable { var accessory: HoverCardAccessory? /// The text's own ink, over the message or metadata ink (an offline machine's warning). var ink: HoverCardTone? + /// The markdown `text` was rendered from (an agent's last message): the card draws its + /// bold, italic and code, never the raw marks (`renderedMarkdown`). + var markdown: String? /// The row the shared hover card row draws (`HoverCardRowView`). var row: HoverCardRowContent { @@ -53,6 +56,8 @@ struct WorkspaceHoverCardContent: Equatable { var icon: IconName /// How long ago the workspace was last active ("4w"); nil without activity. var age: String? + /// The workspace has something unread: a dot after the age, as on ChatGPT's session card. + var unread = false var facts: [Fact] /// The workspace's merged status and its style hint (the row's indicator). var activity: StatusIndicatorState = .idle @@ -106,6 +111,10 @@ struct WorkspaceHoverCardContent: Equatable { case .classic: break case .compact: + // The default (Leo 2026-10-20: "more useful, no scroll, crisp"): what the workspace is, + // where it runs and what it said last, then the last three moments, one line each. + card.facts.removeAll { !Self.compactKinds.contains($0.kind) } + for index in card.facts.indices where card.facts[index].kind == Fact.lastReplyKind { card.facts[index].lines = 3 } card.timeline = Array(shown.prefix(options.historyRows)) case .timeline: card.timeline = Array(shown.prefix(options.historyRows)) @@ -125,6 +134,37 @@ struct WorkspaceHoverCardContent: Equatable { return card } + /// The facts the compact card keeps: why it waits or what it works on, where it runs, its + /// pull request, and the agent's last message. + static let compactKinds: Set = ["needsInput", "task", "folder", "branch", "pullRequest", "ports", "machine", + "machineStatus", Fact.lastReplyKind] + + /// `markdown` as one paragraph of inline markdown: code fences, heading, quote and list marks + /// dropped, its lines joined by a space, so a few lines of the card read as prose. + static func inlineMarkdown(_ markdown: String) -> String { + markdown.split(whereSeparator: \.isNewline).compactMap { line -> String? in + var text = line.trimmingCharacters(in: .whitespaces) + if text.hasPrefix("```") { return nil } + let heading = text.drop { $0 == "#" } + if heading.count < text.count, heading.first == " " { text = heading.trimmingCharacters(in: .whitespaces) } + while text.hasPrefix(">") { text = text.dropFirst().trimmingCharacters(in: .whitespaces) } + if let mark = ["- ", "* ", "+ "].first(where: { text.hasPrefix($0) }) { text = String(text.dropFirst(mark.count)) } + let number = text.prefix { $0.isNumber } + if !number.isEmpty, text.dropFirst(number.count).hasPrefix(". ") { text = String(text.dropFirst(number.count + 2)) } + return text.isEmpty ? nil : text + }.joined(separator: " ") + } + + /// `markdown`'s inline styles (bold, italic, code, links) parsed; the raw text when it is not markdown. + static func renderedMarkdown(_ markdown: String) -> AttributedString { + let inline = inlineMarkdown(markdown) + return (try? AttributedString(markdown: inline, options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace))) + ?? AttributedString(inline) + } + + /// `markdown` as the words it reads as, without its marks. + static func plainMarkdown(_ markdown: String) -> String { String(renderedMarkdown(markdown).characters) } + /// "Prompts: 4 · Worked 12m" over `timeline` (newest first): the time from each prompt or /// start to the next moment after it that is a wait, finish, failure or later prompt, a running /// turn until `now`. Nil with neither prompts nor worked time. @@ -223,11 +263,15 @@ struct WorkspaceHoverCardContent: Equatable { // The agent chat's last reply, when no task says more: the card's last line, under the // metadata, two lines at most (Leo 2026-10-19, Claude desktop's session card). if detail.task == nil, let message = workspace.lastMessage, !message.isEmpty { - facts.append(Fact(icon: .agentChatList, text: message, lines: 2, kind: Fact.lastReplyKind)) + let text = Self.plainMarkdown(message) + if !text.isEmpty { + facts.append(Fact(icon: .agentChatList, text: text, lines: 2, kind: Fact.lastReplyKind, markdown: message)) + } } return WorkspaceHoverCardContent( title: workspace.title, icon: remote.map(hostIcon) ?? workspace.kind.iconName, - age: workspace.lastActivity.flatMap { age(since: $0, now: now) }, facts: facts, + age: workspace.lastActivity.flatMap { age(since: $0, now: now) }, unread: workspace.unread.isUnread, + facts: facts, activity: workspace.activity, activityStyle: workspace.activityStyle, activitySince: detail.activitySince) } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift index 7ceae37a791d..8cbcd865def7 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift @@ -260,6 +260,8 @@ final class WorkspaceHoverCardView: NSView { private let activityLabel = ChromeLabel(labelWithString: "") private let activityRow = NSStackView() private let ageLabel = ChromeLabel(labelWithString: "") + /// After the age while the workspace has something unread (ChatGPT's session card). + private let unreadDot = UnreadDotView() private let factsStack = NSStackView() /// CPU and memory with the last seconds of CPU as bars (`WorkspaceHoverCardContent.resourceRow`). private let resourceRow = HoverCardRowView() @@ -307,6 +309,8 @@ final class WorkspaceHoverCardView: NSView { kindIcon.setContentHuggingPriority(.required, for: .horizontal) kindIcon.line = titleLabel kindIcon.side = Self.iconSize + unreadDot.line = ageLabel + unreadDot.isHidden = true // Digits of one width: a running time that ticks keeps its width ("1m 11s", "1m 18s"). activityLabel.font = Self.liveFont activityLabel.lineBreakMode = .byTruncatingTail @@ -318,7 +322,7 @@ final class WorkspaceHoverCardView: NSView { // Every icon sits on one column at the leading inset, its text beside it (the fact rows' // grid), so the kind icon leads the name instead of floating after it. - let titleRow = NSStackView(views: [kindIcon, titleLabel, ageLabel]) + let titleRow = NSStackView(views: [kindIcon, titleLabel, ageLabel, unreadDot]) titleRow.orientation = .horizontal titleRow.alignment = .firstBaseline titleRow.spacing = Metrics.space2 @@ -363,6 +367,8 @@ final class WorkspaceHoverCardView: NSView { resourceRow.widthAnchor.constraint(equalTo: stack.widthAnchor), kindIcon.widthAnchor.constraint(equalToConstant: Self.iconSize), kindIcon.heightAnchor.constraint(equalToConstant: Self.iconSize), + unreadDot.widthAnchor.constraint(equalToConstant: UnreadDotView.side), + unreadDot.heightAnchor.constraint(equalToConstant: UnreadDotView.side), activitySlot.widthAnchor.constraint(equalToConstant: Self.iconSize), activitySlot.heightAnchor.constraint(equalToConstant: Self.iconSize), activity.widthAnchor.constraint(equalToConstant: Self.iconSize), @@ -424,6 +430,9 @@ final class WorkspaceHoverCardView: NSView { kindIcon.image = NSImage.icon(content.icon, size: Self.iconSize) ageLabel.stringValue = content.age ?? "" ageLabel.isHidden = content.age == nil + unreadDot.isHidden = !content.unread + // The compact card keeps a long name to two lines; the others show it whole. + titleLabel.maximumNumberOfLines = content.options.layout == .compact ? 2 : 6 activity.configure(content.activity, style: content.activityStyle) let word = Strings.activity(content.activity) activityWord = word @@ -431,7 +440,8 @@ final class WorkspaceHoverCardView: NSView { activityRow.isHidden = word == nil // The width the title wraps at (the panel sizes the card in one // pass): the row less its icon and the age. - let age = ageLabel.isHidden ? 0 : ageLabel.intrinsicContentSize.width + Metrics.space4 + let age = (ageLabel.isHidden ? 0 : ageLabel.intrinsicContentSize.width + Metrics.space4) + + (unreadDot.isHidden ? 0 : UnreadDotView.side + Metrics.space2) titleLabel.preferredMaxLayoutWidth = width - 2 * Self.padding - Self.iconSize - Metrics.space2 - age while factViews.count < content.facts.count { addFactRow() } let wrap = width - 2 * Self.padding - Self.iconSize - Metrics.space2 @@ -455,8 +465,10 @@ final class WorkspaceHoverCardView: NSView { let log = content.options.layout == .log timeline.isHidden = content.timeline.isEmpty if !content.timeline.isEmpty { + // The compact card's last moments never scroll (Leo 2026-10-20: "no scroll, crisp"). timeline.configure(content.timeline, width: width - 2 * Self.padding, - maxHeight: log ? Self.logMaxHeight : Self.timelineMaxHeight, textLines: log ? 3 : 2, now: now) + maxHeight: log ? Self.logMaxHeight : Self.timelineMaxHeight, textLines: log ? 3 : 2, now: now, + scrolls: content.options.layout != .compact) } applyColors() } @@ -585,6 +597,52 @@ extension WorkspaceHoverCardView { // the shared overlay glass shows. for row in factViews { row.applyColors() } resourceRow.applyColors() + performWithTheme { + unreadDot.layer?.backgroundColor = Palette.accent.cgColor + // The last message in its own styles (bold, italic, code), never its raw marks. + for (index, row) in factViews.enumerated() where index < (content?.facts.count ?? 0) && !row.isHidden { + guard let markdown = content?.facts[index].markdown else { continue } + row.textLabel.attributedStringValue = Self.styled(markdown, color: Palette.textPrimary) + } + } + } + + /// `markdown`'s inline styles in the card's fonts: bold in the emphasized body, italic, + /// code in the monospaced body, all in `color`. + static func styled(_ markdown: String, color: NSColor) -> NSAttributedString { + let rendered = WorkspaceHoverCardContent.renderedMarkdown(markdown) + let body = Typography.body + let out = NSMutableAttributedString() + for run in rendered.runs { + let intent = run.inlinePresentationIntent ?? [] + var font = intent.contains(.code) ? Typography.system(.monospaced, size: body.pointSize, weight: .regular) : body + if intent.contains(.stronglyEmphasized) { font = Typography.bodyEmphasized } + if intent.contains(.emphasized) { font = NSFontManager.shared.convert(font, toHaveTrait: .italicFontMask) } + out.append(NSAttributedString(string: String(rendered[run.range].characters), attributes: [.font: font, .foregroundColor: color])) + } + return out + } +} + +/// The unread dot on the title row, centered on the middle of `line`'s text like the kind icon. +private final class UnreadDotView: NSView { + static var side: CGFloat { 6 } + weak var line: ChromeLabel? + + init() { + super.init(frame: .zero) + wantsLayer = true + layer?.cornerRadius = Self.side / 2 + setContentHuggingPriority(.required, for: .horizontal) + setAccessibilityElement(false) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } + + override var firstBaselineOffsetFromTop: CGFloat { + guard let font = line?.font else { return super.firstBaselineOffsetFromTop } + return Self.side / 2 + (font.ascender + font.descender) / 2 } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCardTimelineView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCardTimelineView.swift index 4bb444d81a9f..438863632062 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCardTimelineView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCardTimelineView.swift @@ -5,7 +5,8 @@ import CmuxNextIcons /// The workspace hover card's history (u3320): one line per moment, newest first, a time column /// and an icon column, the prompt or what happened beside them. It scrolls past `maxHeight`; the /// card window ignores the mouse, so the hover card coordinator hands it the wheel -/// (`HoverCardPanel.scrollCard`). +/// (`HoverCardPanel.scrollCard`). The compact card's few lines never scroll: no header, one line +/// each, as tall as they are (Leo 2026-10-20: "no scroll, crisp"). final class WorkspaceHoverCardTimelineView: NSView { private static var iconSize: CGFloat { 12 } private static var timeColumn: CGFloat { 54 } @@ -15,6 +16,8 @@ final class WorkspaceHoverCardTimelineView: NSView { private let rowsStack = NSStackView() private let document = FlippedDocumentView() private var heightConstraint: NSLayoutConstraint? + private var underHeader: NSLayoutConstraint? + private var atTop: NSLayoutConstraint? private var rows: [(time: ChromeLabel, icon: NSImageView, text: ChromeLabel)] = [] private var entries: [SidebarTimelineEntry] = [] private var textWidth: CGFloat = 0 @@ -41,11 +44,14 @@ final class WorkspaceHoverCardTimelineView: NSView { addSubview(scrollView) let height = scrollView.heightAnchor.constraint(equalToConstant: 0) heightConstraint = height + let under = scrollView.topAnchor.constraint(equalTo: header.bottomAnchor, constant: Metrics.space2) + underHeader = under + atTop = scrollView.topAnchor.constraint(equalTo: topAnchor) NSLayoutConstraint.activate([ header.topAnchor.constraint(equalTo: topAnchor), header.leadingAnchor.constraint(equalTo: leadingAnchor), header.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor), - scrollView.topAnchor.constraint(equalTo: header.bottomAnchor, constant: Metrics.space2), + under, scrollView.leadingAnchor.constraint(equalTo: leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), @@ -70,9 +76,16 @@ final class WorkspaceHoverCardTimelineView: NSView { /// Shows `entries` at `width`; the list is at most `maxHeight` tall and scrolls past it. /// `textLines` is how many lines a prompt may wrap to. A new set of entries scrolls to the top. - func configure(_ entries: [SidebarTimelineEntry], width: CGFloat, maxHeight: CGFloat, textLines: Int, now: Date) { + /// Without `scrolls`, the list has no header and no scroller, one line per entry, all shown. + func configure(_ entries: [SidebarTimelineEntry], width: CGFloat, maxHeight: CGFloat, textLines: Int, now: Date, + scrolls: Bool = true) { let changed = entries != self.entries self.entries = entries + header.isHidden = !scrolls + underHeader?.isActive = scrolls + atTop?.isActive = !scrolls + scrollView.hasVerticalScroller = scrolls + let textLines = scrolls ? textLines : 1 textWidth = width - Self.timeColumn - Self.iconSize - 2 * Metrics.space2 while rows.count < entries.count { addRow() } for (index, row) in rows.enumerated() { @@ -87,7 +100,7 @@ final class WorkspaceHoverCardTimelineView: NSView { } let content = rowsStack.fittingSize.height document.frame = CGRect(x: 0, y: 0, width: width, height: content) - heightConstraint?.constant = min(content, maxHeight) + heightConstraint?.constant = scrolls ? min(content, maxHeight) : content if changed { scrollView.contentView.scroll(to: .zero) } applyColors() } diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardTimelineTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardTimelineTests.swift index 69706ca5dbb1..c5924e28f145 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardTimelineTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardTimelineTests.swift @@ -37,8 +37,8 @@ import Testing return options } - @Test func theDefaultCardListsTheHistoryNewestFirstWithStats() { - let card = WorkspaceHoverCardContent.make(Self.workspace(), machine: nil, now: Self.now, options: WorkspaceHoverCardOptions(), timeline: Self.history()) + @Test func theTimelineCardListsTheHistoryNewestFirstWithStats() { + let card = WorkspaceHoverCardContent.make(Self.workspace(), machine: nil, now: Self.now, options: Self.options(.timeline), timeline: Self.history()) #expect(card.options.layout == .timeline) #expect(card.timeline.map(\.kind) == [.finished, .prompt, .finished, .prompt, .finished, .prompt]) #expect(card.timeline.first?.date == Self.now) @@ -92,7 +92,7 @@ import Testing @Test func theCardViewListsTheHistoryAndCapsItsHeight() throws { let view = WorkspaceHoverCardView() - let card = WorkspaceHoverCardContent.make(Self.workspace(), machine: nil, now: Self.now, options: WorkspaceHoverCardOptions(), timeline: Self.history(40)) + let card = WorkspaceHoverCardContent.make(Self.workspace(), machine: nil, now: Self.now, options: Self.options(.timeline), timeline: Self.history(40)) view.configure(card, now: Self.now) view.layoutSubtreeIfNeeded() let lines = view.lines diff --git a/plans/cmux-next/hover-card-history/DESIGN.md b/plans/cmux-next/hover-card-history/DESIGN.md index 3115f1ad3817..fbba834ea49b 100644 --- a/plans/cmux-next/hover-card-history/DESIGN.md +++ b/plans/cmux-next/hover-card-history/DESIGN.md @@ -82,7 +82,7 @@ cmux.json, the palette's settings rows (Cmd+Shift+P, the `,` scope) and `cmux se | Key | Default | | | --- | --- | --- | -| `sidebar.hoverCards.layout` | `timeline` | `timeline`, `compact`, `log`, `classic`, `minimal` | +| `sidebar.hoverCards.layout` | `compact` | `timeline`, `compact`, `log`, `classic`, `minimal` | | `sidebar.hoverCards.sections.timings` | `true` | running times and the stats line | | `sidebar.hoverCards.sections.prompts` | `true` | prompts and starts in the history | | `sidebar.hoverCards.sections.waiting` | `true` | waits, finishes, failures in the history | diff --git a/schemas/settings/source/hover-cards.json b/schemas/settings/source/hover-cards.json index 76aed578327d..89a9a48bbb4f 100644 --- a/schemas/settings/source/hover-cards.json +++ b/schemas/settings/source/hover-cards.json @@ -61,7 +61,7 @@ }, { "key": "sidebar.hoverCards.layout", - "note": "u3320 (Lawrence 2026-10-19): the workspace card's design (plans/cmux-next/hover-card-history/DESIGN.md). timeline (default): stats and a scrollable history of prompts, waits and finished turns; compact: the facts and the last three moments; log: the history alone; classic: the card before the history; minimal: title, status, last prompt.", + "note": "u3320 (Lawrence 2026-10-19): the workspace card's design (plans/cmux-next/hover-card-history/DESIGN.md). compact (default since Leo 2026-10-20, \"no scroll, crisp\"): the facts, the last message rendered and the last three moments, never scrolling; timeline: stats and a scrollable history of prompts, waits and finished turns; log: the history alone; classic: the card before the history; minimal: title, status, last prompt.", "title": { "key": "settings.sidebar.hoverCards.layout", "text": "Workspace Card Layout" @@ -108,7 +108,7 @@ } } ], - "default": "timeline", + "default": "compact", "keywords": [ "hover", "card", From a457bce41163bbbbad988b4cf254c84dda0c7588 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 22 Oct 2026 02:40:00 -0700 Subject: [PATCH 3/8] cmux-next sidebar: the compact hover card keeps "Since you looked"; its unread dot is the notification blue Base's #23510 puts what a workspace's agents did since you looked at the top of the card, and the log and minimal layouts keep it. The compact card's fact filter dropped it, so making compact the default would have switched that line off (SidebarSinceLookedTests reads it from the default card). The unread dot took the theme accent, which is a grey in the default theme; the sidebar's own unread dots are Palette.notificationBlue because a grey dot read as missing (2026-10-21). Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-663f-7650-b9f7-64d198c6b88b&host=Air-Blue --- .../CmuxNextSidebar/Logic/WorkspaceHoverCardContent.swift | 8 ++++---- .../CmuxNextSidebar/Views/WorkspaceHoverCard.swift | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Logic/WorkspaceHoverCardContent.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Logic/WorkspaceHoverCardContent.swift index 25de7336a339..0d4d1e7598d5 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Logic/WorkspaceHoverCardContent.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Logic/WorkspaceHoverCardContent.swift @@ -139,10 +139,10 @@ struct WorkspaceHoverCardContent: Equatable { return card } - /// The facts the compact card keeps: why it waits or what it works on, where it runs, its - /// pull request, and the agent's last message. - static let compactKinds: Set = ["needsInput", "task", "folder", "branch", "pullRequest", "ports", "machine", - "machineStatus", Fact.lastReplyKind] + /// The facts the compact card keeps: what its agents did since you looked, why it waits or what + /// it works on, where it runs, its pull request, and the agent's last message. + static let compactKinds: Set = [WorkspaceHoverCardContent.sinceLookedKind, "needsInput", "task", "folder", "branch", + "pullRequest", "ports", "machine", "machineStatus", Fact.lastReplyKind] /// `markdown` as one paragraph of inline markdown: code fences, heading, quote and list marks /// dropped, its lines joined by a space, so a few lines of the card read as prose. diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift index 37ccac9b7ada..3e17324f16bb 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift @@ -613,7 +613,9 @@ extension WorkspaceHoverCardView { for row in factViews { row.applyColors() } resourceRow.applyColors() performWithTheme { - unreadDot.layer?.backgroundColor = Palette.accent.cgColor + // The notification blue, as on the sidebar's own unread dots: the default theme's + // accent is a grey, and a grey dot read as missing (2026-10-21). + unreadDot.layer?.backgroundColor = Palette.notificationBlue.cgColor // The last message in its own styles (bold, italic, code), never its raw marks. for (index, row) in factViews.enumerated() where index < (content?.facts.count ?? 0) && !row.isHidden { guard let markdown = content?.facts[index].markdown else { continue } From 87abca5355a7009e13e88b6ccc4f66810d273a3f Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 22 Oct 2026 02:45:44 -0700 Subject: [PATCH 4/8] cmux-next sidebar: the hover card's unread dot needs no super baseline call crash-class rules (check-objc-selectors.py) failed on super.firstBaselineOffsetFromTop in UnreadDotView: NSView's getter is not in the probed selector table. With no line to follow, the dot now takes its own height as its baseline (an image's), so there is no super call to probe. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-663f-7650-b9f7-64d198c6b88b&host=Air-Blue --- .../Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift index 3e17324f16bb..e1df409adfde 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift @@ -657,8 +657,10 @@ private final class UnreadDotView: NSView { @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } + /// With no line to follow, the dot sits on the baseline like an image. No `super` call: NSView's + /// getter is not in the probed selector table (scripts/cmux-next/check-objc-selectors.py). override var firstBaselineOffsetFromTop: CGFloat { - guard let font = line?.font else { return super.firstBaselineOffsetFromTop } + guard let font = line?.font else { return Self.side } return Self.side / 2 + (font.ascender + font.descender) / 2 } } From 6eefb23a3c7f09f267754aa484f94fb5f2505c75 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 22 Oct 2026 07:21:03 -0700 Subject: [PATCH 5/8] cmux-next sidebar: the hover card's unread dot sits on the title's capitals, like its icons Base's 1231b9d3 puts every hover card icon on its line's capitals (NSFont.iconBaselineOffsetFromTop); the dot used the old midpoint of ascender and descender. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-663f-7650-b9f7-64d198c6b88b&host=Air-Blue --- .../Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift index c3cff9e374f1..f9fffd01469c 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift @@ -649,7 +649,8 @@ extension WorkspaceHoverCardView { } } -/// The unread dot on the title row, centered on the middle of `line`'s text like the kind icon. +/// The unread dot on the title row, centered on `line`'s capitals like the card's icons +/// (`HoverCardIconView`, `NSFont.iconBaselineOffsetFromTop`). private final class UnreadDotView: NSView { static var side: CGFloat { 6 } weak var line: ChromeLabel? @@ -669,6 +670,6 @@ private final class UnreadDotView: NSView { /// getter is not in the probed selector table (scripts/cmux-next/check-objc-selectors.py). override var firstBaselineOffsetFromTop: CGFloat { guard let font = line?.font else { return Self.side } - return Self.side / 2 + (font.ascender + font.descender) / 2 + return font.iconBaselineOffsetFromTop(iconHeight: Self.side) } } From 4ffa79af40c95d021a942df193cedd30372fbfaf Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 22 Oct 2026 20:25:59 -0700 Subject: [PATCH 6/8] cmux-next sidebar: the compact default carries through tests, settings and the history view Review of 6eefb23a (Codex, BLOCK): - HoverCardContentSettingsTests still expected the timeline default; it now expects compact, and a bad layout keeps compact. - WorkspaceHoverCardScrollTests' long-history case asks for the timeline layout: the compact default never scrolls, by design. - A list that does not scroll resets its clip view to the top, also when the same entries scrolled before under the timeline layout (the laid-out cache skipped the reset). - HoverCardSettings' layout fallback is WorkspaceHoverCardOptions' default, not a second hard-coded timeline. - Bold code in the last message stays monospaced. The settings exports (schema, MDM docs, source stamp, defaults snapshot) come from the PR autofix's Mac regeneration. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-663f-7650-b9f7-64d198c6b88b&host=Air-Blue --- .../CmuxNextSettings/HoverCardSettings+Options.swift | 2 +- .../CmuxNextSidebar/Views/WorkspaceHoverCard.swift | 7 +++++-- .../Views/WorkspaceHoverCardTimelineView.swift | 6 ++++++ .../CmuxNextAppTests/WorkspaceHoverCardScrollTests.swift | 4 ++++ .../HoverCardContentSettingsTests.swift | 9 +++++---- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HoverCardSettings+Options.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HoverCardSettings+Options.swift index b3b01fda456f..b30dede20d28 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HoverCardSettings+Options.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HoverCardSettings+Options.swift @@ -4,7 +4,7 @@ extension HoverCardSettings { /// The workspace card's options as the sidebar reads them (`DesignSettings.workspaceHoverCard`). public var workspaceCardOptions: WorkspaceHoverCardOptions { var options = WorkspaceHoverCardOptions() - options.layout = WorkspaceHoverCardOptions.Layout(rawValue: layout.rawValue) ?? .timeline + options.layout = WorkspaceHoverCardOptions.Layout(rawValue: layout.rawValue) ?? WorkspaceHoverCardOptions().layout options.showsTimings = showsTimings options.showsPrompts = showsPrompts options.showsWaiting = showsWaiting diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift index f9fffd01469c..ca3db16ee4d2 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift @@ -640,8 +640,11 @@ extension WorkspaceHoverCardView { let out = NSMutableAttributedString() for run in rendered.runs { let intent = run.inlinePresentationIntent ?? [] - var font = intent.contains(.code) ? Typography.system(.monospaced, size: body.pointSize, weight: .regular) : body - if intent.contains(.stronglyEmphasized) { font = Typography.bodyEmphasized } + let strong = intent.contains(.stronglyEmphasized) + // Bold code stays monospaced, in the emphasized body's weight. + var font = intent.contains(.code) + ? Typography.system(.monospaced, size: body.pointSize, weight: strong ? .medium : .regular) + : strong ? Typography.bodyEmphasized : body if intent.contains(.emphasized) { font = NSFontManager.shared.convert(font, toHaveTrait: .italicFontMask) } out.append(NSAttributedString(string: String(rendered[run.range].characters), attributes: [.font: font, .foregroundColor: color])) } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCardTimelineView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCardTimelineView.swift index 9f02883da300..55afbe671b76 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCardTimelineView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCardTimelineView.swift @@ -90,6 +90,12 @@ final class WorkspaceHoverCardTimelineView: NSView { underHeader?.isActive = scrolls atTop?.isActive = !scrolls scrollView.hasVerticalScroller = scrolls + // A list that does not scroll starts at its top, also when the same entries scrolled + // before under another layout (the laid-out cache below keeps the clip view as it was). + if !scrolls, scrollView.contentView.bounds.origin != .zero { + scrollView.contentView.scroll(to: .zero) + scrollView.reflectScrolledClipView(scrollView.contentView) + } let textLines = scrolls ? textLines : 1 let maxHeight = scrolls ? maxHeight : .greatestFiniteMagnitude while rows.count < entries.count { addRow() } diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardScrollTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardScrollTests.swift index e0c1c6467b32..9db91a1938fc 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardScrollTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardScrollTests.swift @@ -82,7 +82,11 @@ import Testing } /// The history is longer than the card: the wheel on it scrolls the history, and the card stays. + /// The timeline layout scrolls; the default compact card never does (Leo 2026-10-20). @Test func aScrollOnALongHistoryScrollsTheHistory() async throws { + let layout = DesignSettings.shared.workspaceHoverCard.layout + DesignSettings.shared.workspaceHoverCard.layout = .timeline + defer { DesignSettings.shared.workspaceHoverCard.layout = layout } let clock = ManualClock() let (list, window, coordinator, rows) = Self.sidebar(clock: clock) let now = Date() diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextSettingsTests/HoverCardContentSettingsTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextSettingsTests/HoverCardContentSettingsTests.swift index d8aecb79e69d..7abee9e21f55 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextSettingsTests/HoverCardContentSettingsTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextSettingsTests/HoverCardContentSettingsTests.swift @@ -6,14 +6,15 @@ import Testing /// (`sidebar.hoverCards.layout`, `.sections.*`, `.maxRows`): Settings rows, cmux.json, the /// palette's settings rows and `cmux settings set` all read the same schema rows. struct HoverCardContentSettingsTests { - @Test func defaultsAreTheStatsAndHistoryCardWithEveryPart() throws { + /// Leo 2026-10-20 ("more useful, no scroll, crisp"): the compact card is the default. + @Test func defaultsAreTheCompactCardWithEveryPart() throws { let snapshot = CmuxConfigSnapshot.parse(.object([:]), validDensities: [], validMetrics: []) let options = snapshot.hoverCards.workspaceCardOptions #expect(options == WorkspaceHoverCardOptions()) - #expect(options.layout == .timeline) + #expect(options.layout == .compact) #expect(options.maxRows == 20) let layout = try #require(SettingsSchema.descriptor(for: HoverCardSettings.layoutPath)) - #expect(layout.defaultValue == .string("timeline")) + #expect(layout.defaultValue == .string("compact")) for path in [HoverCardSettings.timingsPath, HoverCardSettings.promptsPath, HoverCardSettings.waitingPath, HoverCardSettings.gitPath, HoverCardSettings.portsPath] { let row = try #require(SettingsSchema.descriptor(for: path), "no Settings row for \(path)") @@ -41,7 +42,7 @@ struct HoverCardContentSettingsTests { @Test func aBadLayoutKeepsTheDefaultWithADiagnostic() { let root = JSONValue.object(["sidebar": .object(["hoverCards": .object(["layout": .string("huge"), "maxRows": .number(500)])])]) let snapshot = CmuxConfigSnapshot.parse(root, validDensities: [], validMetrics: []) - #expect(snapshot.hoverCards.workspaceCardOptions.layout == .timeline) + #expect(snapshot.hoverCards.workspaceCardOptions.layout == .compact) #expect(snapshot.hoverCards.workspaceCardOptions.maxRows == 100) #expect(snapshot.diagnostics.contains { $0.path == "sidebar.hoverCards.layout" }) } From 4a0c6246b96cb3aebf2ce52393717a8a9a3a422d Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 22 Oct 2026 20:52:22 -0700 Subject: [PATCH 7/8] chore(next): settings exports for the compact hover card default The PR autofix's repair (run 39895024003, artifact pr-autofix-patch) regenerated them on a Mac at 4ffa79af; its push job is main-only, so the lander applies the patch as it came: the MDM docs, settings-schema.json, the defaults snapshot and the source stamp now say compact. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-663f-7650-b9f7-64d198c6b88b&host=Air-Blue --- docs/mdm/com.manaflow.cmux.json | 2 +- docs/mdm/com.manaflow.cmux.plist | 2 +- docs/mdm/managed-preferences.md | 2 +- schemas/defaults/cmux-next.json | 2 +- schemas/settings-export/settings-schema.sources.sha256 | 6 +++--- schemas/settings/settings-schema.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/mdm/com.manaflow.cmux.json b/docs/mdm/com.manaflow.cmux.json index 1a6e05e0eb5e..5e6d32d07d22 100644 --- a/docs/mdm/com.manaflow.cmux.json +++ b/docs/mdm/com.manaflow.cmux.json @@ -3345,7 +3345,7 @@ "type" : "boolean" }, "sidebar.hoverCards.layout" : { - "default" : "timeline", + "default" : "compact", "description" : "What a workspace's hover card shows: its history of prompts and waits, its details, or both.", "enum" : [ "timeline", diff --git a/docs/mdm/com.manaflow.cmux.plist b/docs/mdm/com.manaflow.cmux.plist index 55abd653328e..a7b64437b0ea 100644 --- a/docs/mdm/com.manaflow.cmux.plist +++ b/docs/mdm/com.manaflow.cmux.plist @@ -4674,7 +4674,7 @@ pfm_default - timeline + compact pfm_description What a workspace's hover card shows: its history of prompts and waits, its details, or both. pfm_name diff --git a/docs/mdm/managed-preferences.md b/docs/mdm/managed-preferences.md index d5298452d9b0..271b5e25c4ce 100644 --- a/docs/mdm/managed-preferences.md +++ b/docs/mdm/managed-preferences.md @@ -323,7 +323,7 @@ Files: `com.manaflow.cmux.plist` (ProfileManifests: iMazing Profile Editor, Prof | `sidebar.steppingWraps` | boolean | `true` | | Wrap Around. Past the last item, the next item is the first again. | | `sidebar.hoverCards.enabled` | boolean | `true` | | Show Hover Cards. A card with details opens when the pointer rests on a workspace or a tab. | | `sidebar.hoverCards.delay` | real | `250` | 0 to 5000 | Hover Card Delay. How long the pointer rests on a workspace or a tab before its card opens. While a card shows, the next one opens at once. | -| `sidebar.hoverCards.layout` | string | `"timeline"` | `timeline`, `compact`, `log`, `classic`, `minimal` | Workspace Card Layout. What a workspace's hover card shows: its history of prompts and waits, its details, or both. | +| `sidebar.hoverCards.layout` | string | `"compact"` | `timeline`, `compact`, `log`, `classic`, `minimal` | Workspace Card Layout. What a workspace's hover card shows: its history of prompts and waits, its details, or both. | | `sidebar.hoverCards.sections.timings` | boolean | `true` | | Show Running Times in Workspace Cards. How long the workspace has worked or waited, and how long its command has run. | | `sidebar.hoverCards.sections.prompts` | boolean | `true` | | Show Prompts in Workspace Cards. Each prompt you sent and when, in the card's history. | | `sidebar.hoverCards.sections.waiting` | boolean | `true` | | Show Waits in Workspace Cards. When an agent waited for your input or finished its turn, in the card's history. | diff --git a/schemas/defaults/cmux-next.json b/schemas/defaults/cmux-next.json index 9aab92fa5cf8..6a8879144426 100644 --- a/schemas/defaults/cmux-next.json +++ b/schemas/defaults/cmux-next.json @@ -597,7 +597,7 @@ "setting.sidebar.hoverCards.groups.sections.status": true, "setting.sidebar.hoverCards.groups.tabGroups": true, "setting.sidebar.hoverCards.groups.workspaceGroups": true, - "setting.sidebar.hoverCards.layout": "timeline", + "setting.sidebar.hoverCards.layout": "compact", "setting.sidebar.hoverCards.maxRows": 20, "setting.sidebar.hoverCards.sections.git": true, "setting.sidebar.hoverCards.sections.ports": true, diff --git a/schemas/settings-export/settings-schema.sources.sha256 b/schemas/settings-export/settings-schema.sources.sha256 index 3f95d2851549..b4d768ac2356 100644 --- a/schemas/settings-export/settings-schema.sources.sha256 +++ b/schemas/settings-export/settings-schema.sources.sha256 @@ -178,7 +178,7 @@ ff3a5b0e5389f96e5bddebb79f1843820eac55f5e331dfba9fc0e0898543e1f1 Packages/macOS 54f4b23550b6c0ac0e22efcf4b524b1ae004eb834db83ba11ab82e1f209cf88e Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HomeServerSetting.swift -83f88735839c4190d9cebd97220398282f5899062e1b59c6f4b199d64d3f5c7d Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HoverCardSettings+Options.swift +cf37449aa85fb6ca698e4956248d7c827120870c6cb7098a76be369ead40f37a Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HoverCardSettings+Options.swift ae856418ce22482ef4262df8fb664e18e76cc91a98c72a19e6feb55f70ea4541 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HoverCardSettings.swift @@ -386,9 +386,9 @@ ca17a18722a553f6963edee26becc1d91c1027afdf2ad80bbe713286e0d0899f Packages/macOS 2e03c002892fb8c6e36c4cdf57c180909d104b4babf211f2fe189d59b629dca8 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Feed.generated.swift -e88319190f1a93f49e9da53a50e5e3399175e86e913a10fa4796e6e4f680cfad Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+HoverCards.generated.swift +d6d582fa578b09865055d4a27c009faa501f6fee71afb5b70dc703a182970044 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+HoverCards.generated.swift -04e7117b360aa6602c37c4de79fd04d4f2e48e81b0a588ef918ac01270837ed9 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+HoverCardsChoices.generated.swift +c5c0e356f8eed4d35ea606c6284851427e1c989eb3291d04bb2deceb0642206b Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+HoverCardsChoices.generated.swift 783ebcbcdc1c4435e2efb0e7e665fc4dcef37937dd1b367e60ca5b43ced483e9 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Labs.generated.swift diff --git a/schemas/settings/settings-schema.json b/schemas/settings/settings-schema.json index 3a201ae04181..1e6b475a0004 100644 --- a/schemas/settings/settings-schema.json +++ b/schemas/settings/settings-schema.json @@ -22896,7 +22896,7 @@ "consumers" : [ "cmux-next" ], - "default" : "timeline", + "default" : "compact", "default_label" : null, "group" : { "key" : "settings.group.sidebar", From de7eaa029847f02e7cc123c1d25489deef491a3f Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 22 Oct 2026 21:13:15 -0700 Subject: [PATCH 8/8] cmux-next sidebar: the hover card's unread dot is a dot, not a solid body (glass gate) The prepush gate's check-overlay-glass flagged the dot's layer fill as a hover-body-fill. It is a 6 pt unread dot on the title row, not a surface laid over the card's glass, so it carries the gate's reviewed escape. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-663f-7650-b9f7-64d198c6b88b&host=Air-Blue --- .../Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift index ca3db16ee4d2..eb2bc017c726 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/WorkspaceHoverCard.swift @@ -623,6 +623,7 @@ extension WorkspaceHoverCardView { performWithTheme { // The notification blue, as on the sidebar's own unread dots: the default theme's // accent is a grey, and a grey dot read as missing (2026-10-21). + // glass-opaque-allow: a 6 pt unread dot on the title row, not a body or surface over the glass. unreadDot.layer?.backgroundColor = Palette.notificationBlue.cgColor // The last message in its own styles (bold, italic, code), never its raw marks. for (index, row) in factViews.enumerated() where index < (content?.facts.count ?? 0) && !row.isHidden {