Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
103 changes: 91 additions & 12 deletions Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
addSubview(backgroundView)

pinImageView.imageScaling = .scaleProportionallyDown
pinImageView.contentTintColor = NSColor.secondaryLabelColor.withAlphaComponent(0.8)
// Dynamic color + alphaValue, not withAlphaComponent: see applyModel.
pinImageView.contentTintColor = .secondaryLabelColor
pinImageView.alphaValue = 0.8
addSubview(pinImageView)

chevronButton.onClick = { [weak self] in self?.actions?.onToggleCollapsed() }
Expand Down Expand Up @@ -139,7 +141,12 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
// Legacy parity: no implicit layer actions on content/color changes.
CATransaction.begin()
CATransaction.setDisableActions(true)
defer { CATransaction.commit() }
defer {
CATransaction.commit()
#if DEBUG
logTitlePaintProbe(model)
#endif
}
let metrics = SidebarWorkspaceGroupHeaderMetrics(fontScale: model.fontScale)
let percent = model.globalFontMagnificationPercent

Expand Down Expand Up @@ -178,7 +185,13 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
ofSize: GlobalFontMagnification.scaledSize(metrics.nameFontSize, percent: percent),
weight: .semibold
)
nameField.textColor = model.isAnchorActive ? .labelColor : NSColor.labelColor.withAlphaComponent(0.9)
// Dim via alphaValue, never withAlphaComponent: resolving a dynamic
// color at configure time snapshots the ambient drawing appearance,
// and the synchronous selection commit can configure this cell inside
// the dark terminal's display pass — the title then rendered
// white-on-white in a light sidebar and vanished.
nameField.textColor = .labelColor
nameField.alphaValue = model.isAnchorActive ? 1 : 0.9

let showsBadge = model.anchorUnreadCount > 0
unreadBadgeView.isHidden = !showsBadge
Expand Down Expand Up @@ -213,10 +226,10 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
backgroundView.layer?.cornerRadius = model.isMultiSelected && !model.isAnchorActive
? 6
: 4
backgroundView.layer?.backgroundColor = headerBackgroundColor(for: model).cgColor
backgroundView.layer?.backgroundColor = resolvedCGColor { self.headerBackgroundColor(for: model) }

topDropIndicator.layer?.backgroundColor = cmuxAccentNSColor().cgColor
bottomDropIndicator.layer?.backgroundColor = cmuxAccentNSColor().cgColor
topDropIndicator.layer?.backgroundColor = resolvedCGColor { cmuxAccentNSColor() }
bottomDropIndicator.layer?.backgroundColor = resolvedCGColor { cmuxAccentNSColor() }
topDropIndicator.isHidden = !model.topDropIndicatorVisible
bottomDropIndicator.isHidden = !model.bottomDropIndicatorVisible

Expand All @@ -236,16 +249,57 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
/// Live drop-line painting during native reorder drags; see
/// `SidebarWorkspaceRowTableCellView.paintControllerDropIndicator`.
func paintControllerDropIndicator(top: Bool, bottom: Bool) {
topDropIndicator.layer?.backgroundColor = cmuxAccentNSColor().cgColor
bottomDropIndicator.layer?.backgroundColor = cmuxAccentNSColor().cgColor
topDropIndicator.layer?.backgroundColor = resolvedCGColor { cmuxAccentNSColor() }
bottomDropIndicator.layer?.backgroundColor = resolvedCGColor { cmuxAccentNSColor() }
topDropIndicator.isHidden = !top
bottomDropIndicator.isHidden = !bottom
}

#if DEBUG
/// Ground truth for the disappearing-title investigation: records what
/// the title would actually draw as, resolved through the cell's own
/// appearance, plus the ambient drawing appearance at configure time
/// (the suspect when the title snapshots the wrong variant).
private func logTitlePaintProbe(_ model: SidebarGroupHeaderRowModel) {
var brightness: CGFloat = -1
var colorAlpha: CGFloat = -1
if let color = nameField.textColor {
effectiveAppearance.performAsCurrentDrawingAppearance {
if let rgb = color.usingColorSpace(.sRGB) {
brightness = rgb.brightnessComponent
colorAlpha = rgb.alphaComponent
}
}
}
let activeFlag: Int = model.isAnchorActive ? 1 : 0
// labelColor resolved WITHOUT an explicit appearance reports the
// ambient drawing appearance: ~1.0 means a dark appearance was
// current at configure time (the snapshot-resolution suspect).
let ambientProbe = NSColor.labelColor.usingColorSpace(.sRGB)?.brightnessComponent ?? -1
let effectiveName: String = effectiveAppearance.name.rawValue
let ambientText = String(format: "%.2f", ambientProbe)
let brightnessText = String(format: "%.2f", brightness)
let colorAlphaText = String(format: "%.2f", colorAlpha)
let fieldAlphaText = String(format: "%.2f", nameField.alphaValue)
let frameWidth = Int(nameField.frame.width)
var line = "sidebar.groupHeader.titlePaint name=\(model.name.prefix(12)) active=\(activeFlag)"
line += " ambientLabelBrightness=\(ambientText) effective=\(effectiveName)"
line += " brightness=\(brightnessText) colorAlpha=\(colorAlphaText)"
line += " fieldAlpha=\(fieldAlphaText) frameW=\(frameWidth)"
cmuxDebugLog(line)
Comment on lines +285 to +289

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove the raw group name from the debug event.

model.name is user-provided workspace content. Line 281 writes its prefix to the debug record. Keep only fixed diagnostic fields, or remove this probe.

As per coding guidelines, production Swift runtime logs must not expose customer content without explicit private redaction.

Proposed fix
-        var line = "sidebar.groupHeader.titlePaint name=\(model.name.prefix(12)) active=\(activeFlag)"
+        var line = "sidebar.groupHeader.titlePaint active=\(activeFlag)"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var line = "sidebar.groupHeader.titlePaint name=\(model.name.prefix(12)) active=\(activeFlag)"
line += " ambient=\(ambientName) effective=\(effectiveName)"
line += " brightness=\(brightnessText) colorAlpha=\(colorAlphaText)"
line += " fieldAlpha=\(fieldAlphaText) frameW=\(frameWidth)"
cmuxDebugLog(line)
var line = "sidebar.groupHeader.titlePaint active=\(activeFlag)"
line += " ambient=\(ambientName) effective=\(effectiveName)"
line += " brightness=\(brightnessText) colorAlpha=\(colorAlphaText)"
line += " fieldAlpha=\(fieldAlphaText) frameW=\(frameWidth)"
cmuxDebugLog(line)
🤖 Prompt for 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.

In `@Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift` around
lines 281 - 285, Remove the user-provided model.name prefix from the debug event
assembled in the line diagnostic block. Keep the remaining fixed diagnostic
fields and cmuxDebugLog call unchanged, or remove the probe if no safe fields
remain.

Source: Coding guidelines

}

var dropIndicatorPaintForTesting: (top: Bool, bottom: Bool) {
(!topDropIndicator.isHidden, !bottomDropIndicator.isHidden)
}

var titlePaintForTesting: (text: String, color: NSColor?, alpha: CGFloat) {
(nameField.stringValue, nameField.textColor, nameField.alphaValue)
}

var headerBackgroundLayerColorForTesting: CGColor? {
backgroundView.layer?.backgroundColor
Comment on lines 292 to +301

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove the shared test-only paint-state seam.

The DEBUG-only properties in production source exist only to support these tests. Production source must not export paint state solely for cmuxTests.

  • Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift#L288-L297: remove dropIndicatorPaintForTesting, titlePaintForTesting, and headerBackgroundLayerColorForTesting.
  • cmuxTests/SidebarAppKitRowCellTests.swift#L957-L980: inspect supported view behavior after removing the production-only accessors.

As per coding guidelines, production Swift source must not add test-only members. As per path instructions, production Sources code must not add test-only or debug-only seams.

📍 Affects 2 files
  • Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift#L288-L297 (this comment)
  • cmuxTests/SidebarAppKitRowCellTests.swift#L957-L980
🤖 Prompt for 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.

In `@Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift` around
lines 288 - 297, Remove the test-only properties dropIndicatorPaintForTesting,
titlePaintForTesting, and headerBackgroundLayerColorForTesting from
Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift. Update
cmuxTests/SidebarAppKitRowCellTests.swift lines 957-980 to inspect the supported
view behavior without these production accessors.

Sources: Coding guidelines, Path instructions

}
#endif

private func updatePlusVisibility() {
Expand All @@ -270,9 +324,9 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
CATransaction.begin()
CATransaction.setDisableActions(true)
backgroundView.layer?.cornerRadius = 4
backgroundView.layer?.backgroundColor = NSColor.labelColor.withAlphaComponent(0.08).cgColor
backgroundView.layer?.backgroundColor = resolvedCGColor { NSColor.labelColor.withAlphaComponent(0.08) }
CATransaction.commit()
nameField.textColor = .labelColor
nameField.alphaValue = 1
}

/// Modifier-click preview: paints the same dim membership tint as an
Expand All @@ -282,7 +336,7 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
CATransaction.begin()
CATransaction.setDisableActions(true)
backgroundView.layer?.cornerRadius = 6
backgroundView.layer?.backgroundColor = headerMultiSelectionBackgroundColor(for: model).cgColor
backgroundView.layer?.backgroundColor = resolvedCGColor { self.headerMultiSelectionBackgroundColor(for: model) }
CATransaction.commit()
}

Expand All @@ -295,7 +349,12 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
backgroundView.layer?.cornerRadius = 4
backgroundView.layer?.backgroundColor = NSColor.clear.cgColor
CATransaction.commit()
nameField.textColor = NSColor.labelColor.withAlphaComponent(0.9)
// The title color stays the dynamic label color; only the settled
// dim is previewed, so no appearance resolution happens mid-click.
nameField.alphaValue = 0.9
#if DEBUG
logTitlePaintProbe(model)
#endif
}

/// Inverse of the press treatment: previewing a different row must peel a
Expand All @@ -308,6 +367,26 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
applyModel(model)
}

/// Resolves a dynamic color against this cell's own effective appearance.
/// Layer colors are CGColors and resolve at conversion time; converting
/// under the ambient drawing appearance (which can belong to another
/// view mid-click — see the title comment in `applyModel`) snapshots the
/// wrong variant.
private func resolvedCGColor(_ make: () -> NSColor) -> CGColor {
var resolved = CGColor(gray: 0, alpha: 0)
effectiveAppearance.performAsCurrentDrawingAppearance {
resolved = make().cgColor
}
return resolved
}

/// Layer colors were resolved through the appearance current at the last
/// configure; a theme flip must re-resolve them.
override func viewDidChangeEffectiveAppearance() {
super.viewDidChangeEffectiveAppearance()
if let model { applyModel(model) }
}

private func headerBackgroundColor(for model: SidebarGroupHeaderRowModel) -> NSColor {
if model.isAnchorActive {
return NSColor.labelColor.withAlphaComponent(0.08)
Expand Down
43 changes: 36 additions & 7 deletions Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,31 @@ final class SidebarWorkspaceRowTableCellView: NSTableCellView {
}

private func applyBackgroundStyle(_ style: SidebarWorkspaceRowBackgroundStyle) {
backgroundView.layer?.backgroundColor = (style.color ?? .clear)
.withAlphaComponent((style.color == nil ? 0 : style.opacity) * ((style.color?.alphaComponent) ?? 1)).cgColor
backgroundView.layer?.backgroundColor = resolvedCGColor {
(style.color ?? .clear)
.withAlphaComponent((style.color == nil ? 0 : style.opacity) * ((style.color?.alphaComponent) ?? 1))
}
}

/// Resolves a dynamic color against this cell's own effective appearance.
/// Configure can run inside another view's display pass (the synchronous
/// selection commit swaps the dark terminal view mid-click), where
/// `withAlphaComponent`/`cgColor` snapshot the ambient appearance's
/// variant — dynamic text then rendered white-on-white and vanished.
private func resolvedColor(_ make: () -> NSColor) -> NSColor {
var resolved = NSColor.clear
effectiveAppearance.performAsCurrentDrawingAppearance {
resolved = make()
}
return resolved
}

private func resolvedCGColor(_ make: () -> NSColor) -> CGColor {
var resolved = CGColor(gray: 0, alpha: 0)
effectiveAppearance.performAsCurrentDrawingAppearance {
resolved = make().cgColor
}
return resolved
}

override var isFlipped: Bool { true }
Expand Down Expand Up @@ -363,7 +386,7 @@ final class SidebarWorkspaceRowTableCellView: NSTableCellView {
applyBackgroundStyle(style)
if settings.activeTabIndicatorStyle == .solidFill, model.isActive {
backgroundView.layer?.borderWidth = 1.5
backgroundView.layer?.borderColor = NSColor.labelColor.withAlphaComponent(0.5).cgColor
backgroundView.layer?.borderColor = resolvedCGColor { NSColor.labelColor.withAlphaComponent(0.5) }
} else {
backgroundView.layer?.borderWidth = 0
}
Expand Down Expand Up @@ -476,12 +499,16 @@ final class SidebarWorkspaceRowTableCellView: NSTableCellView {
descriptionView.attributedStringValue = SidebarRowPalette.attributed(
rendered,
font: .systemFont(ofSize: model.scaled(10.5)),
color: model.isActive ? palette.secondary(0.84) : NSColor.secondaryLabelColor.withAlphaComponent(0.95)
color: model.isActive
? palette.secondary(0.84)
: resolvedColor { NSColor.secondaryLabelColor.withAlphaComponent(0.95) }
)
} else {
descriptionView.stringValue = display
descriptionView.font = .systemFont(ofSize: model.scaled(10.5))
descriptionView.textColor = model.isActive ? palette.secondary(0.84) : NSColor.secondaryLabelColor.withAlphaComponent(0.95)
descriptionView.textColor = model.isActive
? palette.secondary(0.84)
: resolvedColor { NSColor.secondaryLabelColor.withAlphaComponent(0.95) }
}
}

Expand Down Expand Up @@ -728,7 +755,7 @@ final class SidebarWorkspaceRowTableCellView: NSTableCellView {
let toggleFont = NSFont.systemFont(ofSize: model.scaled(10), weight: .semibold)
let toggleColor = model.isActive
? palette.secondary(0.9)
: NSColor.secondaryLabelColor.withAlphaComponent(0.9)
: resolvedColor { NSColor.secondaryLabelColor.withAlphaComponent(0.9) }
metadataToggleButton.isHidden = allEntries.count <= 3
if !metadataToggleButton.isHidden {
metadataToggleButton.configure(
Expand Down Expand Up @@ -783,7 +810,9 @@ final class SidebarWorkspaceRowTableCellView: NSTableCellView {
progressView.configure(
fraction: CGFloat(progress.value),
barHeight: max(3, 3 * model.fontScale),
trackColor: model.isActive ? palette.selectedForeground(0.15) : NSColor.secondaryLabelColor.withAlphaComponent(0.2),
trackColor: model.isActive
? palette.selectedForeground(0.15)
: resolvedColor { NSColor.secondaryLabelColor.withAlphaComponent(0.2) },
fillColor: model.isActive ? palette.selectedForeground(0.8) : cmuxAccentNSColor(),
labelText: progress.label,
labelFont: labelFont,
Expand Down
81 changes: 0 additions & 81 deletions Sources/Sidebar/AppKitList/SidebarSelectionCoalescer.swift

This file was deleted.

Loading
Loading