Skip to content
Merged
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
79 changes: 69 additions & 10 deletions Sources/CodeCaps/GlanceViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,20 @@ struct GlanceHeaderBar: View {
}

var body: some View {
// Spacing stays ZERO: this row separates its groups with explicit
// Spacers, and a non-zero spacing silently added itself to EVERY
// implicit gap in a header with eight or more children — about 32pt of
// a 570pt popover, which is what pushed the cluster into crowding.
// The mark's own gap is on the mark instead.
HStack(spacing: 0) {
// Owner 2026-10-10: "in top left should have the black on
// transparent codecaps logo". The owner's own artwork, drawn as a
// template so one file is correct on both appearances. This is the
// Glance popover, not the PiP: the PiP header is gone by the
// owner's own request (#193), and the provider logos identify the
// HUD there.
GlanceBrandMark()
.padding(.trailing, Metrics.glanceBrandMarkGap)
Text("CodeCaps")
.font(.system(size: 13, weight: .semibold))
.lineLimit(1)
Expand Down Expand Up @@ -969,7 +982,45 @@ struct GlanceHeaderBar: View {
}
}

/// The two-box switch between From Mac and From Fleet, in Title Case. The two
/// The owner's cropped black-on-transparent mark, resolved through the same
/// bundle the menu bar and PiP use and drawn as a template. Never re-cut,
/// re-exported or substituted.
struct GlanceBrandMark: View {
@Environment(\.colorScheme) private var colorScheme
static let asset: NSImage? = {
guard let url = ResourceBundle.resolved?.url(forResource: "CodeCapsMenuBarIcon", withExtension: "png"),
let image = NSImage(contentsOf: url) else { return nil }
image.isTemplate = true
return image
}()

/// The frame is applied to EACH branch, not to the Group around them.
/// A `.resizable()` image proposes its natural size, and the menu-bar
/// artwork is 850pt square: under a `.fixedSize` header that leaked ~40pt
/// of the 570pt popover's width into a 12pt mark. Clamping the Group from
/// the outside is too late — the ideal width has already been spent.
var body: some View {
Group {
if let image = Self.asset {
Image(nsImage: image)
.resizable()
.renderingMode(.template)
.interpolation(.high)
.foregroundStyle(colorScheme == .dark ? Color.white : Theme.ink)
.frame(width: Metrics.glanceBrandMarkSize, height: Metrics.glanceBrandMarkSize)
} else {
// Only if the owner's artwork is ever missing from the bundle.
Image(systemName: "gauge.with.dots.needle.50percent")
.font(.system(size: Metrics.glanceBrandMarkSize))
.foregroundStyle(Theme.accent)
.frame(width: Metrics.glanceBrandMarkSize, height: Metrics.glanceBrandMarkSize)
}
}
.accessibilityHidden(true)
}
}

/// The two-box switch between From Mac and All Sources, in Title Case. The two
/// boxes are the same width, so the switch does not change shape as it flips.
struct GlanceViewToggle: View {
@Binding var selection: GlanceViewMode
Expand All @@ -980,17 +1031,22 @@ struct GlanceViewToggle: View {
private static let widest = GlanceViewMode.allCases.map(\.title).max { $0.count < $1.count } ?? ""

var body: some View {
// Owner 2026-10-10: "the from mac and from fleet should look more like
// a toggle than just outline basic shapes and should match in style
// more with the open codecaps in bottom right".
//
// It had a full-height 1pt rule between the boxes and an outline around
// the whole pair, which reads as two adjacent rectangles rather than one
// control. It now reads as the footer's prominent button did: a solid
// filled selected segment sitting on a track, with the hairline around
// the whole thing instead of down the middle. Same corner radius, same
// accent fill, same on-accent ink — one control vocabulary in the window.
HStack(spacing: 0) {
ForEach(Array(GlanceViewMode.allCases.enumerated()), id: \.element) { index, mode in
if index > 0 {
Rectangle()
.fill(Theme.controlBorder)
.frame(width: 1, height: Metrics.glanceHeaderControlHeight)
}
ForEach(Array(GlanceViewMode.allCases.enumerated()), id: \.element) { _, mode in
segment(mode)
}
}
.background(Theme.surface)
.background(Theme.controlTrack)
.clipShape(RoundedRectangle(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(Theme.controlBorder))
.fixedSize()
Expand All @@ -1008,7 +1064,7 @@ struct GlanceViewToggle: View {
.font(.system(size: 11, weight: .semibold))
.lineLimit(1)
.fixedSize()
.foregroundStyle(selected ? Theme.onAccent : .secondary)
.foregroundStyle(selected ? Theme.onAccent : Theme.ink.opacity(0.7))
.padding(.horizontal, Metrics.glanceHeaderSegmentPadding)
.frame(height: Metrics.glanceHeaderControlHeight)
.background(selected ? model.accentColor : Color.clear)
Expand Down Expand Up @@ -1051,7 +1107,10 @@ struct GlanceAlarmAllToggle: View {
Text("All")
.font(.system(size: 13, weight: .semibold))
}
.foregroundStyle(tint)
// Owner 2026-10-10: "the All next to alarm should be black or normal
// dark text not accent color". Ink follows the appearance and the
// high-contrast setting; the accent stays where it carries meaning.
.foregroundStyle(Theme.ink)
Comment on lines 1107 to +1113

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Bug low

Orphaned computed property in GlanceAlarmAllToggle (Sources/CodeCaps/GlanceViews.swift): private var tint: AnyShapeStyle has no remaining call sites after .foregroundStyle(tint) inside label was changed to .foregroundStyle(Theme.ink). Delete the property — it still resolves isOn and model.accentColor, so any future caller expecting the label's color would silently receive the accent instead of Theme.ink.

Prompt for LLM

File Sources/CodeCaps/GlanceViews.swift:

Line 1107 to 1113:

Orphaned computed property in `GlanceAlarmAllToggle` (Sources/CodeCaps/GlanceViews.swift): `private var tint: AnyShapeStyle` has no remaining call sites after `.foregroundStyle(tint)` inside `label` was changed to `.foregroundStyle(Theme.ink)`. Delete the property — it still resolves `isOn` and `model.accentColor`, so any future caller expecting the label's color would silently receive the accent instead of `Theme.ink`.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

.frame(height: Metrics.glanceHeaderControlHeight)
.contentShape(Rectangle())
.fixedSize()
Expand Down
53 changes: 44 additions & 9 deletions Sources/CodeCaps/PipWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,18 +168,30 @@ enum PipMetrics {
static let horizontalPeek: CGFloat = 64

/// The width one row needs at a given detail level.
static func rowWidth(for detail: Detail) -> CGFloat {
///
/// `hasFlame` widens the row when a usage spike puts the flame beside the
/// logo. It has to be part of this and not just an extra padding on the
/// row: the same number drives `fitSize` and `applyFit`, so leaving it out
/// both clipped the icon in the vertical list and let it overflow the
/// fixed frame in the horizontal one.
static func rowWidth(for detail: Detail, hasFlame: Bool = false) -> CGFloat {
let base: CGFloat
switch detail {
case .full: return fullMinWidth
case .noCountdown: return noCountdownMinWidth
case .singleMeter: return singleMeterMinWidth
case .logoOnly: return logoOnlyMinWidth
case .minimal: return minimalMinWidth
case .barsOnly: return barsOnlyMinWidth
case .singleBar: return singleMeterMinWidth
case .full: base = fullMinWidth
case .noCountdown: base = noCountdownMinWidth
case .singleMeter: base = singleMeterMinWidth
case .logoOnly: base = logoOnlyMinWidth
case .minimal: base = minimalMinWidth
case .barsOnly: base = barsOnlyMinWidth
case .singleBar: base = singleMeterMinWidth
}
return hasFlame && detail.showsProviderLogo ? base + flameWidth : base
}

/// The flame's own width plus its gap from the logo. Fixed so the row
/// arithmetic above can be exact rather than approximate.
static let flameWidth: CGFloat = rowSpacing + 10

/// Whether the panel is wide enough to scroll its rows sideways.
///
/// Owner 2026-10-10: "once it gets to where it is in the screenshot or
Expand Down Expand Up @@ -463,7 +475,9 @@ struct PipWidgetView: View {
/// edge, so scrolling never hides which platform you are looking at —
/// which was the whole point of the owner's request.
private func horizontalRows(detail: PipMetrics.Detail) -> some View {
let rowWidth = PipMetrics.rowWidth(for: detail)
// Wide enough for the widest pinned row, including a flame if any row
// has one, so no row is clipped by the frame below.
let rowWidth = PipMetrics.rowWidth(for: detail, hasFlame: anyRowSpiking)
return ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: PipMetrics.rowSpacing * 2) {
ForEach(targetRows) { row in
Expand All @@ -485,6 +499,13 @@ struct PipWidgetView: View {
/// button to appear when the mouse is anywhere over the window.
@State private var isHovering = false

/// Whether any pinned row currently shows a usage spike. One answer for
/// the whole strip: rows are a uniform width by design, so one spiking row
/// widens them all rather than each row being a different size.
private var anyRowSpiking: Bool {
targetRows.contains { model.hasActiveRunawayAnomaly(for: $0) }
}

private var targetRows: [DisplaySection] {
PipWidgetController.targetRows(for: model)
}
Expand Down Expand Up @@ -703,12 +724,26 @@ struct PipWidgetView: View {
.prefix(detail.maxMeters)
)

let spiking = model.hasActiveRunawayAnomaly(for: row)
Comment thread
kody-ai[bot] marked this conversation as resolved.

return HStack(spacing: PipMetrics.rowSpacing) {
if detail.showsProviderLogo {
PlatformLogo(providerKey: row.providerKey,
size: PipMetrics.providerLogoSize,
style: markStyle(for: row),
iconHint: row.poolKey == nil ? row.section.iconHint : nil)
// Owner 2026-10-10: "after the platform name (or logo if no
// name shown) there should be a little flame for usage spike".
// Beside the LOGO rather than after the name, because the name
// is the first thing the width ladder drops — at narrow widths
// this is the only place the signal can live.
if spiking {
Image(systemName: "flame.fill")
.font(.system(size: PipMetrics.providerLogoSize * 0.7))
.foregroundStyle(Theme.warning)
.help("Usage spike detected on this platform")
.accessibilityLabel("Usage spike detected on this platform")
}
}

if detail.showsTitle {
Expand Down
12 changes: 12 additions & 0 deletions Sources/CodeCaps/QuotaComponents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ enum Theme {
? dyn(NSColor.black.withAlphaComponent(0.92), NSColor.white.withAlphaComponent(0.95))
: dyn(NSColor.black.withAlphaComponent(0.16), NSColor.white.withAlphaComponent(0.22))

/// The filled track a segmented control sits on, so an unselected segment
/// reads as "not chosen" rather than as an outlined box. Owner 2026-10-10.
static let controlTrack: Color = highContrast
? dyn(NSColor.black.withAlphaComponent(0.10), NSColor.white.withAlphaComponent(0.16))
: dyn(NSColor.black.withAlphaComponent(0.06), NSColor.white.withAlphaComponent(0.10))

/// A control that is present but off, such as an unchecked row bell:
/// visible enough to find, quiet enough not to read as a setting.
static let faint: Color = highContrast
Expand Down Expand Up @@ -391,6 +397,12 @@ enum Metrics {
/// was the 10pt every header gap used, which read as cramped (owner delta,
/// 2026-09-30); twice that sets the title apart from the controls.
static let glanceHeaderTitleGap: CGFloat = 20

/// The owner's mark at the top-left of the Glance header, 2026-10-10.
/// Deliberately small: the popover is 570pt wide and the header already
/// carries the title, the scope switch, the bell and the two buttons.
static let glanceBrandMarkSize: CGFloat = 12
static let glanceBrandMarkGap: CGFloat = 4
/// The least space between the switch and the right-hand cluster. The
/// popover is wide, so the two only get this close if the cluster grows.
static let glanceHeaderClusterGap: CGFloat = 16
Expand Down
6 changes: 5 additions & 1 deletion Tests/CodeCapsTests/GlanceToggleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,11 @@ final class GlanceToggleTests: XCTestCase {
// the gutters, title, switch, bell, enlarged refresh button, and settings button.
let toggle = NSHostingView(rootView: GlanceViewToggle(selection: .constant(.fromMac), model: model)).fittingSize.width
let bell = NSHostingView(rootView: GlanceAlarmAllToggle(isOn: .constant(true), model: model)).fittingSize.width
let expected = Metrics.glanceGutter * 2 + width("CodeCaps", size: 13, weight: .semibold)
// The header also carries the owner's mark at the top-left (owner
// 2026-10-10), so its size and gap are part of the budget here too.
let expected = Metrics.glanceGutter * 2
+ Metrics.glanceBrandMarkSize + Metrics.glanceBrandMarkGap
+ width("CodeCaps", size: 13, weight: .semibold)
+ 8 + toggle + 12 + bell + 12 + 22 + 6 + 22
XCTAssertEqual(headerIdealWidth(parts: [], model: model), expected, accuracy: 3)
}
Expand Down
13 changes: 13 additions & 0 deletions Tests/CodeCapsTests/PipMetricsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ final class PipMetricsTests: XCTestCase {
}
}

/// Kody caught this on #208: the flame beside the logo added width that
/// `rowWidth(for:)` did not know about, so it was clipped in the vertical
/// list and overflowed the fixed frame in the horizontal strip.
func testARowCarryingAFlameIsWiderThanOneWithout() {
for level in PipMetrics.Detail.allCases {
XCTAssertEqual(
PipMetrics.rowWidth(for: level, hasFlame: true) - PipMetrics.rowWidth(for: level),
level.showsProviderLogo ? PipMetrics.flameWidth : 0,
"\(level) must reserve exactly the flame's width when it draws one")
}
XCTAssertGreaterThan(PipMetrics.flameWidth, 0, "the flame is not free")
}

func testNarrowWindowStillShowsBarAndPercent() {
// The owner's floor: only the bars and the percentages remain.
let detail = PipMetrics.detail(forWidth: PipMetrics.minWidth)
Expand Down
Loading