Merge remote-tracking branch 'origin/main' into wt1234

This commit is contained in:
iamtoruk 2026-09-02 07:06:47 -07:00
commit 6fa5dfbeb7
20 changed files with 1516 additions and 143 deletions

View file

@ -28,8 +28,8 @@ There is no separate build step required to run the dev CLI. `npm run dev` runs
| `npm run test:locks` | Runs the three parallelism-sensitive `cache-refresh-lock` suites serially. |
| `npm run test:watch` | Same scope as `npm test`, in watch mode. |
| `npm run dev -- status` | Runs the CLI in dev mode against your real data. |
| `npm run build` | Bundles the litellm pricing snapshot, then runs `tsup` to produce `dist/cli.js`. |
| `npm run bundle-litellm` | Refreshes `src/data/litellm-snapshot.json` from the upstream litellm repo. |
| `npm run build` | Builds the CLI and dashboard from the checked-in pricing catalogs without mutating tracked source files. |
| `npm run bundle-litellm` | Explicitly refreshes the checked-in pricing catalogs from their upstream sources. Review and commit the resulting data changes separately. |
To test a specific suite, run vitest directly with a path:

View file

@ -1685,6 +1685,23 @@ final class AppStore {
}
}
/// Sessions the CLI reported as live under this provider, newest first. Nil
/// when the payload carries no live-session block at all (a CLI that predates
/// it), which the popover renders as an absent section rather than as
/// "none running".
func capacityDockLiveSessions(for provider: CapacityDockProvider) -> [LiveSession]? {
guard let block = menubarPayload?.liveSessions else { return nil }
return block.sessions.filter { $0.provider == provider.id }
}
/// Today's totals for the dock popover. The background loop keeps the
/// menu-bar status payload fresh, so it wins whenever the badge is already
/// scoped to today; otherwise fall back to the popover's own today cache.
var capacityDockToday: CurrentBlock? {
if menubarPeriod == .today, let payload = menubarPayload { return payload.current }
return todayPayload?.current
}
func capacityDockQuotaSummary(for provider: CapacityDockProvider) -> QuotaSummary? {
if let filter = provider.legacyFilter {
return quotaSummary(for: filter)

View file

@ -132,14 +132,23 @@ final class CapacityDockController {
func refreshQuotaPresentation() {
guard model.preferences.isEnabled else { return }
if let provider = model.hoveredProvider {
model.detailHeight = CapacityDockMetrics.detailHeight(
quota: store.capacityDockQuotaSummary(for: provider),
scale: model.detailScale
)
model.detailHeight = glanceDetailHeight(for: provider)
layoutDetail(for: provider, transaction: .immediate)
}
}
/// Panel height for the glance popover, measured from the same store content
/// `CapacityDockDetailView` renders.
private func glanceDetailHeight(for provider: CapacityDockProvider) -> CGFloat {
return CapacityDockMetrics.detailHeight(
quota: store.capacityDockQuotaSummary(for: provider),
sessionCount: store.capacityDockLiveSessions(for: provider)?.count,
hasToday: store.capacityDockToday != nil,
tailEdge: model.detailTailEdge,
scale: model.detailScale
)
}
func reposition() {
guard model.preferences.isEnabled, !model.interaction.isDragging else { return }
layoutRail(preserveCurrentTop: false, animate: false)
@ -160,10 +169,7 @@ final class CapacityDockController {
!snapshot.selectedProviders.contains(hovered) {
hideDetail(animated: false)
} else if let hovered = model.hoveredProvider {
model.detailHeight = CapacityDockMetrics.detailHeight(
quota: store.capacityDockQuotaSummary(for: hovered),
scale: model.detailScale
)
model.detailHeight = glanceDetailHeight(for: hovered)
}
guard snapshot.isEnabled else {
@ -550,10 +556,7 @@ final class CapacityDockController {
let wasShowingDetail = detailPanel?.isVisible == true && model.hoveredProvider != nil
detailIsDismissing = false
model.hoveredProvider = provider
model.detailHeight = CapacityDockMetrics.detailHeight(
quota: store.capacityDockQuotaSummary(for: provider),
scale: model.detailScale
)
model.detailHeight = glanceDetailHeight(for: provider)
ensureDetailPanel()
layoutDetail(for: provider, transaction: wasShowingDetail ? .detailFollow : .detailPresent)
detailPanel?.orderFrontRegardless()
@ -959,6 +962,9 @@ final class CapacityDockController {
preferredEdge: model.attachmentEdge
)
model.detailTailEdge = side.opposite
// The tail's own allowance is part of the content insets, so the height
// is only exact once the side is known.
model.detailHeight = glanceDetailHeight(for: provider)
let target = CapacityDockPlacement.detailFrame(
size: CGSize(width: model.detailWidth, height: model.detailHeight),
railFrame: railPanel.frame,

View file

@ -14,6 +14,10 @@ struct MenubarPayload: Codable, Sendable {
let history: HistoryBlock
let combined: CombinedUsage?
let claudeConfigs: ClaudeConfigSelector?
/// Sessions whose transcript was appended inside the CLI's liveness window.
/// Absent on payloads from a CLI that predates the block, so absence means
/// "unknown", not "nothing running": the popover hides the section either way.
let liveSessions: LiveSessionsBlock?
init(generated: String,
current: CurrentBlock,
@ -21,7 +25,9 @@ struct MenubarPayload: Codable, Sendable {
history: HistoryBlock,
combined: CombinedUsage?,
claudeConfigs: ClaudeConfigSelector? = nil,
stale: Bool? = nil) {
stale: Bool? = nil,
liveSessions: LiveSessionsBlock? = nil) {
self.liveSessions = liveSessions
self.generated = generated
self.stale = stale
self.current = current
@ -32,7 +38,7 @@ struct MenubarPayload: Codable, Sendable {
}
enum CodingKeys: String, CodingKey {
case generated, stale, current, optimize, history, combined, claudeConfigs
case generated, stale, current, optimize, history, combined, claudeConfigs, liveSessions
}
init(from decoder: Decoder) throws {
@ -44,6 +50,72 @@ struct MenubarPayload: Codable, Sendable {
history = try c.decode(HistoryBlock.self, forKey: .history)
combined = try c.decodeIfPresent(CombinedUsage.self, forKey: .combined)
claudeConfigs = try c.decodeIfPresent(ClaudeConfigSelector.self, forKey: .claudeConfigs)
liveSessions = try c.decodeIfPresent(LiveSessionsBlock.self, forKey: .liveSessions)
}
}
struct LiveSessionsBlock: Codable, Sendable {
let windowSeconds: Int
let sessions: [LiveSession]
}
struct LiveSession: Codable, Sendable, Identifiable {
let id: String
/// Provider catalog id, matching the dock ring the session runs under.
let provider: String
let project: String
let branch: String?
let model: String?
let contextTokens: Int?
let contextWindow: Int?
let startedAt: String
let lastActivityAt: String
/// Seconds since this session last wrote, as of the payload's build. Absent
/// on payloads from a CLI that predates it, which reads as "not idle".
var idleSeconds: Int? = nil
/// A live session that is waiting on the user rather than generating. The
/// panel dims these so the two states are told apart at a glance.
var isIdle: Bool { (idleSeconds ?? 0) > Self.idleThresholdSeconds }
static let idleThresholdSeconds = 120
/// Row title: the folder in flight, plus the branch when the transcript
/// named one.
var title: String {
guard let branch, !branch.isEmpty else { return project }
return "\(project) · \(branch)"
}
/// Fraction of the context window in use, nil when the CLI could not read
/// a usage record so the row renders without a ring.
var contextFraction: Double? {
guard let contextTokens, let contextWindow, contextWindow > 0 else { return nil }
return min(max(Double(contextTokens) / Double(contextWindow), 0), 1)
}
var contextRemaining: Int? {
guard let contextTokens, let contextWindow else { return nil }
return max(0, contextWindow - contextTokens)
}
/// How long this session has been open, in the same shape the quota rows use
/// for resets. Empty when the timestamp is unparseable.
func elapsedLabel(now: Date = Date()) -> String {
guard let started = Self.parseISO8601(startedAt) else { return "" }
let minutes = Int(max(0, now.timeIntervalSince(started)) / 60)
let hours = minutes / 60
if hours > 0 { return "\(hours)h \(minutes % 60)m" }
return "\(minutes)m"
}
/// The CLI stamps milliseconds; the plain formatter rejects those, so try the
/// fractional variant first.
static func parseISO8601(_ value: String) -> Date? {
let withFraction = ISO8601DateFormatter()
withFraction.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = withFraction.date(from: value) { return date }
return ISO8601DateFormatter().date(from: value)
}
}

View file

@ -2,6 +2,25 @@ import AppKit
import Observation
import SwiftUI
private extension String {
/// `asCompactTokens` prints an uppercase K; the glance popover uses the
/// lowercase form (182k) beside the uppercase M and B.
func lowercasedThousands() -> String { replacingOccurrences(of: "K", with: "k") }
}
private extension View {
/// Hairline between glance sections, drawn as an overlay so it never adds a
/// point the computed panel height did not reserve.
func dividerBelow() -> some View {
overlay(alignment: .bottom) {
Rectangle()
.fill(Color.white.opacity(0.08))
.frame(height: 0.5)
.padding(.horizontal, CapacityDockGlance.contentInset)
}
}
}
private extension Color {
/// Warm off-white for Capacity Dock text: a very mild orange tint so bright
/// labels on the dark card read softer than pure white and do not stress the eyes.
@ -59,26 +78,146 @@ enum CapacityDockMetrics {
+ alongPad
}
static func detailHeight(quota: QuotaSummary?, scale: CGFloat) -> CGFloat {
/// The glance popover's height is the sum of its blocks, not a fitted size:
/// the hosting view has intrinsic sizing off, so every block below carries
/// the same explicit frame height in `CapacityDockDetailView`. Keep the two
/// in step, and keep every term a whole number of points (see `points`).
static func detailHeight(
quota: QuotaSummary?,
sessionCount: Int?,
hasToday: Bool,
tailEdge: CapacityDockEdge,
scale: CGFloat
) -> CGFloat {
guard let quota else { return 186 * scale }
let rows = min(max(quota.details.count, quota.primary == nil ? 0 : 1), 5)
let visibleFooter = CapacityDockQuotaPresentation.visibleFooterLines(
quota.footerLines,
connection: quota.connection
)
let footer = visibleFooter.isEmpty ? 0 : min(visibleFooter.count, 2) * 16 + 4
let actionExtra: CGFloat = CapacityDockConnectionAction.resolve(quota: quota) == nil ? 0 : 38
// Each section carries its own padding, so the panel adds none.
var height = CapacityDockGlance.headerHeight
// The tail only eats vertical room when it points up or down.
if !tailEdge.isVertical { height += CapacityDockGlance.tailAllowance }
if let sessionCount { height += CapacityDockGlance.sessionsHeight(count: sessionCount) }
if hasToday { height += CapacityDockGlance.todayHeight }
if CapacityDockGlance.drawsWindows(quota) {
height += CapacityDockGlance.windows(quota).isEmpty
? CapacityDockGlance.windowsEmptyHeight
: CapacityDockGlance.windowsHeight
}
height += CapacityDockConnectionAction.resolve(quota: quota) == nil ? 0 : 38
let connectionExtra: CGFloat = switch quota.connection {
case .terminalFailure: 90
case .disconnected: 18
case .loading, .stale, .transientFailure: 16
case .connected: 0
}
let base = min(
470,
max(132, 88 + CGFloat(rows) * 50 + CGFloat(footer) + actionExtra + connectionExtra)
)
return base * scale
height += connectionExtra
return (height * scale).rounded()
}
}
/// Fixed block heights shared by the glance popover's layout and its panel
/// size, plus the ramps its numbers and rings use. Every section owns its own
/// padding, so the panel itself only insets horizontally.
enum CapacityDockGlance {
/// The panel's padding on all four sides. Horizontally it is applied by each
/// section rather than the panel, so group fills can still run edge to edge.
static let contentInset: CGFloat = 16
static let tailAllowance: CGFloat = 18
/// 16 top + 20 title + 8 bottom.
static let headerHeight: CGFloat = 44
static let sectionPadTop: CGFloat = 8
static let sectionPadBottom: CGFloat = 10
/// A 10.5pt caption's line box, shared by every section header.
static let captionLine: CGFloat = 13
/// Content driven: 8 padding + a 12pt line + 2 + a 10.5pt line + 8 padding.
/// Held as a constant because the panel frame is computed, not fitted.
static let pillHeight: CGFloat = 46
static let pillGap: CGFloat = 6
/// Three stacked lines, 13 + 13 + 12, with two 3pt gaps. Taller than the
/// 17pt burned figure beside it, so it sets the row.
static let todayContentHeight: CGFloat = 44
/// 8 top + 24 percent + 2 + 13 label + 2 + 12 reset + 16 bottom.
static let windowsHeight: CGFloat = 77
/// 8 top + one secondary line + 16 bottom.
static let windowsEmptyHeight: CGFloat = 37
/// The list scrolls past this many pills rather than truncating.
static let maxVisibleSessionRows = 4
static let maxWindowColumns = 4
/// Height of the scrolling pill list: every pill up to the visible cap, then
/// the list scrolls inside that.
static func sessionListHeight(count: Int) -> CGFloat {
let visible = min(count, maxVisibleSessionRows)
guard visible > 0 else { return 0 }
return CGFloat(visible) * pillHeight + CGFloat(visible - 1) * pillGap
}
static func sessionsHeight(count: Int) -> CGFloat {
var height = sectionPadTop + captionLine + sectionPadBottom
if count > 0 { height += pillGap + sessionListHeight(count: count) }
return height
}
static let todayHeight: CGFloat = sectionPadTop + captionLine + pillGap
+ todayContentHeight + sectionPadBottom
/// Four bands, matching the rail's own sense of escalation: comfortable,
/// watch it, nearly out, over. Rings start green because a ring with no
/// colour reads as broken.
static func severityColor(_ fraction: Double) -> Color {
if fraction >= 0.9 { return .red }
if fraction >= 0.8 { return .orange }
if fraction >= 0.7 { return .yellow }
return .green
}
/// Share of the pill the tint covers. Anything outside 0...1 is a bad
/// context reading, not a reason to paint past the pill.
static func gaugeFillFraction(_ fraction: Double) -> Double {
min(max(fraction, 0), 1)
}
/// Where the tint starts fading, as a share of the filled band. The fade is
/// a fixed 12pt tail, so a short band fades over its whole length instead of
/// inverting.
static func pillFadeStart(filledWidth: CGFloat, scale: CGFloat) -> CGFloat {
let fade = 12 * scale
guard filledWidth > fade else { return 0 }
return (filledWidth - fade) / filledWidth
}
/// How far the reveal edge leans, as a share of the text's height.
static let gaugeSlantRatio: CGFloat = 0.35
/// Where the slanted reveal edge crosses the top and bottom of the text box.
/// The edge starts fully off the left and ends fully past the right, so 0
/// reveals nothing and 1 fills every glyph. Both values are clamped into the
/// box, which turns the parallelogram into a triangle at the two extremes
/// rather than letting it fold over itself.
static func gaugeRevealEdge(
fraction: Double,
width: CGFloat,
height: CGFloat
) -> (top: CGFloat, bottom: CGFloat) {
let clamped = min(max(fraction, 0), 1)
let slant = height * gaugeSlantRatio
let bottom = (width + slant) * clamped
let clamp = { (value: CGFloat) in min(max(value, 0), width) }
return (clamp(bottom - slant), clamp(bottom))
}
/// A provider that is not connected has no quota and no spend to stand in for
/// one, so the windows row gives way to the reconnect guidance entirely.
static func drawsWindows(_ quota: QuotaSummary) -> Bool {
switch quota.connection {
case .disconnected, .terminalFailure: return false
case .connected, .loading, .stale, .transientFailure: return true
}
}
/// The windows the row draws, in the order the provider service reported them.
static func windows(_ quota: QuotaSummary) -> [QuotaSummary.Window] {
let ordered = quota.details.isEmpty ? [quota.primary].compactMap { $0 } : quota.details
return Array(ordered.prefix(maxWindowColumns))
}
}
@ -485,6 +624,7 @@ struct CapacityDockDetailView: View {
let model: CapacityDockViewModel
let quota: (CapacityDockProvider) -> QuotaSummary?
let onConnect: (CapacityDockProvider) -> Void
@Environment(AppStore.self) private var store
var body: some View {
let bubbleShape = CapacityDockBubbleShape(
@ -514,83 +654,355 @@ struct CapacityDockDetailView: View {
}
private var detailInsets: EdgeInsets {
let horizontal = 22 * model.detailScale
let vertical = 16 * model.detailScale
let tailAllowance = 18 * model.detailScale
let tailAllowance = CapacityDockGlance.tailAllowance * model.detailScale
// Every section carries its own padding, so the panel adds only the tail's
// allowance on whichever side the tail points.
return EdgeInsets(
top: vertical + (model.detailTailEdge == .top ? tailAllowance : 0),
leading: horizontal + (model.detailTailEdge == .left ? tailAllowance : 0),
bottom: vertical + (model.detailTailEdge == .bottom ? tailAllowance : 0),
trailing: horizontal + (model.detailTailEdge == .right ? tailAllowance : 0)
top: model.detailTailEdge == .top ? tailAllowance : 0,
leading: model.detailTailEdge == .left ? tailAllowance : 0,
bottom: model.detailTailEdge == .bottom ? tailAllowance : 0,
trailing: model.detailTailEdge == .right ? tailAllowance : 0
)
}
@ViewBuilder
private func detail(for provider: CapacityDockProvider, quota: QuotaSummary?) -> some View {
VStack(alignment: .leading, spacing: 11 * model.detailScale) {
HStack(spacing: 8 * model.detailScale) {
if let image = ProviderIconCache.image(named: provider.iconName) {
Image(nsImage: image)
.resizable()
.scaledToFit()
.foregroundStyle(Color.capacityDockText)
.frame(width: 24 * model.detailScale, height: 24 * model.detailScale)
}
Text("\(provider.displayName) Usage")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(Color.capacityDockText)
Spacer(minLength: 8)
if let plan = quota?.planLabel, !plan.isEmpty {
Text(plan)
.font(.system(size: 10, weight: .medium))
.foregroundStyle(Color.capacityDockText.opacity(0.62))
.lineLimit(1)
}
}
if let quota {
connectionLabel(quota.connection, provider: provider)
if quota.details.isEmpty, let primary = quota.primary {
CapacityDockQuotaRow(
window: primary,
scale: model.detailScale
)
} else {
ForEach(Array(quota.details.prefix(5).enumerated()), id: \.offset) { _, window in
CapacityDockQuotaRow(
window: window,
scale: model.detailScale
)
}
}
let footerLines = CapacityDockQuotaPresentation.visibleFooterLines(
quota.footerLines,
connection: quota.connection
)
if !footerLines.isEmpty {
Divider().overlay(Color.capacityDockText.opacity(0.12))
ForEach(Array(footerLines.prefix(2).enumerated()), id: \.offset) { _, line in
Text(line)
.font(.system(size: 10))
.foregroundStyle(Color.capacityDockText.opacity(0.58))
}
}
} else {
if let quota {
glance(for: provider, quota: quota)
} else {
VStack(alignment: .leading, spacing: 11 * model.detailScale) {
header(provider, plan: nil)
Text(ProviderConnectionGuidance.dockInstruction(for: provider))
.font(.system(size: 12))
.foregroundStyle(Color.capacityDockText.opacity(0.62))
.fixedSize(horizontal: false, vertical: true)
connectButton(provider, quota: nil)
}
}
}
if provider.catalogEntry.hasLiveCodeBurnQuotaAdapter,
let action = CapacityDockConnectionAction.resolve(quota: quota) {
let title = action.title(for: provider)
Button(title) { onConnect(provider) }
.buttonStyle(.borderedProminent)
.tint(provider.ringColor)
.controlSize(.small)
.accessibilityLabel("\(title) \(provider.displayName)")
/// Sections stack at fixed heights that mirror `CapacityDockMetrics.detailHeight`,
/// because the panel frame is computed rather than fitted. Separators are
/// overlays so a hairline never adds a point the height did not reserve.
@ViewBuilder
private func glance(for provider: CapacityDockProvider, quota: QuotaSummary) -> some View {
VStack(alignment: .leading, spacing: 0) {
headerSection(provider, plan: quota.planLabel).dividerBelow()
connectionLabel(quota.connection, provider: provider)
if let sessions = store.capacityDockLiveSessions(for: provider) {
sessionsSection(sessions).dividerBelow()
}
if let today = store.capacityDockToday {
todaySection(today).dividerBelow()
}
if CapacityDockGlance.drawsWindows(quota) { windowsSection(quota) }
Spacer(minLength: 0)
connectButton(provider, quota: quota)
}
}
@ViewBuilder
private func headerSection(_ provider: CapacityDockProvider, plan: String?) -> some View {
let s = model.detailScale
header(provider, plan: plan)
.padding(.top, CapacityDockGlance.contentInset * s)
.padding(.bottom, 8 * s)
.padding(.horizontal, CapacityDockGlance.contentInset * s)
}
@ViewBuilder
private func header(_ provider: CapacityDockProvider, plan: String?) -> some View {
let s = model.detailScale
HStack(spacing: 8 * s) {
if let image = ProviderIconCache.image(named: provider.iconName) {
Image(nsImage: image)
.resizable()
.scaledToFit()
// Matches the 17pt title's cap height plus a little, so the
// mark reads as its equal rather than as a bullet.
.frame(width: 18 * s, height: 18 * s)
}
Text(provider.displayName)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(Color.capacityDockText)
Spacer(minLength: 6)
if let plan, !plan.isEmpty {
Text(plan)
.font(.system(size: 10, weight: .medium))
.foregroundStyle(Color.capacityDockText.opacity(0.6))
.lineLimit(1)
}
}
.frame(height: 20 * s)
}
@ViewBuilder
private func sectionCaption(_ title: String, trailing: String?) -> some View {
let s = model.detailScale
HStack(spacing: 8 * s) {
Text(title)
.font(.system(size: 10.5, weight: .semibold))
if let trailing {
Spacer(minLength: 8)
Text(trailing)
.font(.system(size: 10.5))
.monospacedDigit()
}
}
.foregroundStyle(Color.capacityDockText.opacity(0.6))
.frame(height: CapacityDockGlance.captionLine * s)
}
@ViewBuilder
private func sessionsSection(_ sessions: [LiveSession]) -> some View {
let s = model.detailScale
VStack(alignment: .leading, spacing: 0) {
sectionCaption("Sessions", trailing: sessionsTrailing(sessions.count))
if !sessions.isEmpty {
ScrollView(.vertical) {
VStack(spacing: CapacityDockGlance.pillGap * s) {
ForEach(sessions) { session in
sessionPill(session)
}
}
}
.scrollIndicators(.automatic)
.frame(height: CapacityDockGlance.sessionListHeight(count: sessions.count) * s)
.padding(.top, CapacityDockGlance.pillGap * s)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, CapacityDockGlance.sectionPadTop * s)
.padding(.bottom, CapacityDockGlance.sectionPadBottom * s)
.padding(.horizontal, CapacityDockGlance.contentInset * s)
}
private func sessionsTrailing(_ count: Int) -> String {
switch count {
case 0: return "none running"
case 1: return "1 running"
default: return "\(count) running"
}
}
@ViewBuilder
private func sessionPill(_ session: LiveSession) -> some View {
let s = model.detailScale
let fraction = session.contextFraction
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 2 * s) {
Text(session.title)
.font(.system(size: 12))
.foregroundStyle(Color.capacityDockText)
.lineLimit(1)
.truncationMode(.tail)
.frame(height: 15 * s)
Text(sessionSubtitle(session))
.font(.system(size: 10.5))
.monospacedDigit()
.foregroundStyle(Color.capacityDockText.opacity(0.6))
.lineLimit(1)
.frame(height: 13 * s)
// The model name identifies the row, so the project above it
// gives up width first.
.layoutPriority(1)
}
Spacer(minLength: 12)
if let fraction {
VStack(alignment: .trailing, spacing: 2 * s) {
PercentGaugeText(
label: "\(Int((fraction * 100).rounded()))%",
fraction: fraction,
font: .system(size: 12)
)
.frame(height: 15 * s)
if let remaining = session.contextRemaining {
Text("\(Double(remaining).asCompactTokens().lowercasedThousands()) left")
.font(.system(size: 10))
.monospacedDigit()
.foregroundStyle(Color.capacityDockText.opacity(0.6))
.frame(height: 13 * s)
}
}
.layoutPriority(1)
}
}
.padding(.vertical, 8 * s)
.padding(.horizontal, 12 * s)
.frame(height: CapacityDockGlance.pillHeight * s)
.background(
ZStack(alignment: .leading) {
Color.white.opacity(0.06)
if let fraction {
GeometryReader { geometry in
pillFill(fraction: fraction, width: geometry.size.width, scale: s)
}
}
}
.clipShape(RoundedRectangle(cornerRadius: 8 * s, style: .continuous))
)
// A session waiting on the user recedes; one that is generating does not.
.opacity(session.isIdle ? 0.55 : 1)
}
/// The pill itself is the context gauge: a tinted band over the first N% of
/// its width, faded out at the leading edge so the boundary reads as a
/// gradient rather than a rule.
@ViewBuilder
private func pillFill(fraction: Double, width: CGFloat, scale: CGFloat) -> some View {
let filled = width * CapacityDockGlance.gaugeFillFraction(fraction)
let colour = CapacityDockGlance.severityColor(fraction)
LinearGradient(
stops: [
.init(color: colour.opacity(0.14), location: 0),
.init(
color: colour.opacity(0.14),
location: CapacityDockGlance.pillFadeStart(filledWidth: filled, scale: scale)
),
.init(color: colour.opacity(0), location: 1),
],
startPoint: .leading,
endPoint: .trailing
)
.frame(width: filled)
}
private func sessionSubtitle(_ session: LiveSession) -> String {
[session.model, session.elapsedLabel().isEmpty ? nil : session.elapsedLabel()]
.compactMap { $0 }
.joined(separator: " · ")
}
@ViewBuilder
private func todaySection(_ today: CurrentBlock) -> some View {
let s = model.detailScale
VStack(alignment: .leading, spacing: 0) {
sectionCaption("Today", trailing: nil)
HStack(alignment: .center, spacing: 8 * s) {
HStack(alignment: .firstTextBaseline, spacing: 5 * s) {
Text(today.cost.asUSD())
.font(.system(size: 17, weight: .semibold))
.monospacedDigit()
.foregroundStyle(Color.capacityDockText)
Text("burned")
.font(.system(size: 11.5))
.foregroundStyle(Color.capacityDockText.opacity(0.6))
}
Spacer(minLength: 8)
VStack(alignment: .trailing, spacing: 3 * s) {
tokenLine("arrow.down", Double(today.inputTokens))
tokenLine("arrow.up", Double(today.outputTokens))
Text("\(today.calls.asThousandsSeparated()) calls")
.font(.system(size: 10))
.monospacedDigit()
.foregroundStyle(Color.capacityDockText.opacity(0.6))
.frame(height: 12 * s)
}
}
.frame(height: CapacityDockGlance.todayContentHeight * s)
.padding(.top, CapacityDockGlance.pillGap * s)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, CapacityDockGlance.sectionPadTop * s)
.padding(.bottom, CapacityDockGlance.sectionPadBottom * s)
.padding(.horizontal, CapacityDockGlance.contentInset * s)
}
@ViewBuilder
private func tokenLine(_ symbol: String, _ value: Double) -> some View {
let s = model.detailScale
HStack(spacing: 4 * s) {
Image(systemName: symbol)
.font(.system(size: 9, weight: .semibold))
.foregroundStyle(Color.capacityDockText.opacity(0.6))
Text(value.asCompactTokens().lowercasedThousands())
.font(.system(size: 10.5))
.monospacedDigit()
.foregroundStyle(Color.capacityDockText)
}
.frame(height: 13 * s)
}
/// One column per quota window, in the order the provider reported them.
@ViewBuilder
private func windowsSection(_ quota: QuotaSummary) -> some View {
let s = model.detailScale
let windows = CapacityDockGlance.windows(quota)
Group {
if windows.isEmpty {
budgetLine()
.frame(height: CapacityDockGlance.captionLine * s)
} else {
// A single window has no siblings to line up with, so it reads as
// a left-aligned figure rather than a lone centred digit.
let alignment: HorizontalAlignment = windows.count == 1 ? .leading : .center
HStack(spacing: 0) {
ForEach(Array(windows.enumerated()), id: \.offset) { _, window in
windowColumn(window, alignment: alignment)
}
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, CapacityDockGlance.sectionPadTop * s)
.padding(.bottom, CapacityDockGlance.contentInset * s)
.padding(.horizontal, CapacityDockGlance.contentInset * s)
}
@ViewBuilder
private func windowColumn(
_ window: QuotaSummary.Window,
alignment: HorizontalAlignment
) -> some View {
let s = model.detailScale
VStack(alignment: alignment, spacing: 0) {
PercentGaugeText(
label: window.percentLabel,
fraction: window.percent,
font: .system(size: 20, weight: .semibold)
)
.frame(height: 24 * s)
Text(CapacityDockQuotaPresentation.displayLabel(window.label))
.font(.system(size: 11))
.foregroundStyle(Color.capacityDockText.opacity(0.6))
.lineLimit(1)
.frame(height: CapacityDockGlance.captionLine * s)
.padding(.top, 2 * s)
Text(window.resetsInLabel)
.font(.system(size: 10))
.monospacedDigit()
.foregroundStyle(Color.capacityDockText.opacity(0.3))
.lineLimit(1)
.frame(height: 12 * s)
.padding(.top, 2 * s)
}
.frame(
maxWidth: .infinity,
alignment: alignment == .leading ? .leading : .center
)
}
/// No quota window exists for this provider, so money is the capacity.
@ViewBuilder
private func budgetLine() -> some View {
let spend = store.capacityDockToday?.cost ?? 0
let budget = store.activeDailyBudget
Text(budget > 0 ? "today \(spend.asUSD()) of \(budget.asUSD())" : "no budget set")
.font(.system(size: 11))
.monospacedDigit()
.foregroundStyle(Color.capacityDockText.opacity(0.6))
}
@ViewBuilder
private func connectButton(_ provider: CapacityDockProvider, quota: QuotaSummary?) -> some View {
if provider.catalogEntry.hasLiveCodeBurnQuotaAdapter,
let action = CapacityDockConnectionAction.resolve(quota: quota) {
let title = action.title(for: provider)
Button(title) { onConnect(provider) }
.buttonStyle(.borderedProminent)
.tint(provider.ringColor)
.controlSize(.small)
.accessibilityLabel("\(title) \(provider.displayName)")
}
}
@ -639,50 +1051,56 @@ struct CapacityDockDetailView: View {
}
}
private struct CapacityDockQuotaRow: View {
let window: QuotaSummary.Window
let scale: CGFloat
/// A percentage drawn as its own gauge: the glyphs sit dim, and the value's own
/// severity colour fills them left to right up to the value, wiped off on a
/// slant. One band colour, so the fill says how bad it is and the wipe says how
/// far along it is.
private struct PercentGaugeText: View {
let label: String
let fraction: Double
let font: Font
var body: some View {
VStack(alignment: .leading, spacing: 6 * scale) {
HStack(alignment: .firstTextBaseline, spacing: 8 * scale) {
Text(CapacityDockQuotaPresentation.displayLabel(window.label))
.font(.system(size: 12, weight: .medium))
.foregroundStyle(Color.capacityDockText.opacity(0.82))
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 8)
Text(window.percentLabel)
.font(.system(size: 12, weight: .medium))
.monospacedDigit()
.foregroundStyle(Color.capacityDockText)
Text(label)
.font(font)
.monospacedDigit()
.foregroundStyle(Color.white.opacity(0.3))
.overlay {
CapacityDockGlance.severityColor(fraction)
.clipShape(PercentGaugeReveal(fraction: fraction))
.mask {
Text(label)
.font(font)
.monospacedDigit()
}
}
GeometryReader { geometry in
ZStack(alignment: .leading) {
Capsule().fill(Color.capacityDockText.opacity(0.14))
Capsule()
.fill(progressColor)
.frame(width: max(2, geometry.size.width * min(max(window.percent, 0), 1)))
}
}
.frame(height: 6 * scale)
if !window.resetsInLabel.isEmpty {
Text("Resets in \(window.resetsInLabel)")
.font(.system(size: 10))
.monospacedDigit()
.foregroundStyle(Color.capacityDockText.opacity(0.5))
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
.accessibilityLabel(label)
}
}
/// The slanted wipe. An overlay clip only, so it never changes layout size.
private struct PercentGaugeReveal: Shape {
var fraction: Double
var animatableData: Double {
get { fraction }
set { fraction = newValue }
}
private var progressColor: Color {
switch QuotaSummary.severity(for: window.percent) {
case .normal: return .green
case .warning: return .yellow
case .critical: return .orange
case .danger: return .red
}
func path(in rect: CGRect) -> Path {
let edge = CapacityDockGlance.gaugeRevealEdge(
fraction: fraction,
width: rect.width,
height: rect.height
)
guard edge.bottom > 0 else { return Path() }
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX + edge.top, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX + edge.bottom, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.closeSubpath()
return path
}
}

View file

@ -0,0 +1,284 @@
import Foundation
import Testing
@testable import CodeBurnMenubar
private func window(_ label: String, _ percent: Double, resetsAt: Date? = nil) -> QuotaSummary.Window {
QuotaSummary.Window(label: label, percent: percent, resetsAt: resetsAt)
}
private func quota(_ details: [QuotaSummary.Window], primary: QuotaSummary.Window? = nil) -> QuotaSummary {
QuotaSummary(
providerFilter: .all,
connection: .connected,
primary: primary,
details: details,
planLabel: "Max 20x",
footerLines: []
)
}
@Suite("Capacity Dock glance popover")
struct CapacityDockGlanceTests {
@Test("Window columns keep the provider's own order and cap at four")
func windowOrderAndCap() {
let five = window("5-hour", 0.16)
let weekly = window("Weekly", 0.14)
let fable = window("Fable", 0.25)
#expect(CapacityDockGlance.windows(quota([five, weekly, fable])) == [five, weekly, fable])
let many = (0..<6).map { window("w\($0)", Double($0) / 10) }
#expect(CapacityDockGlance.windows(quota(many)).count == CapacityDockGlance.maxWindowColumns)
}
@Test("A provider with only a primary window still draws one column")
func windowsFallBackToPrimary() {
let primary = window("30-day", 0.21)
#expect(CapacityDockGlance.windows(quota([], primary: primary)) == [primary])
}
@Test("No windows at all leaves the row empty, so the budget line takes over")
func windowsAbsent() {
#expect(CapacityDockGlance.windows(quota([])).isEmpty)
}
@Test("The pill tint ramps green, yellow, orange, red at 70, 80 and 90 percent")
func severityRamp() {
#expect(CapacityDockGlance.severityColor(0.0) == .green)
#expect(CapacityDockGlance.severityColor(0.69) == .green)
#expect(CapacityDockGlance.severityColor(0.70) == .yellow)
#expect(CapacityDockGlance.severityColor(0.79) == .yellow)
#expect(CapacityDockGlance.severityColor(0.80) == .orange)
#expect(CapacityDockGlance.severityColor(0.89) == .orange)
#expect(CapacityDockGlance.severityColor(0.90) == .red)
#expect(CapacityDockGlance.severityColor(1.5) == .red)
}
@Test("The pill fill fraction clamps into 0...1 so the tint cannot overrun")
func pillFillFractionClamps() {
#expect(CapacityDockGlance.gaugeFillFraction(0) == 0)
#expect(CapacityDockGlance.gaugeFillFraction(0.73) == 0.73)
#expect(CapacityDockGlance.gaugeFillFraction(1) == 1)
// A bad context reading must not paint past the pill, or short of it.
#expect(CapacityDockGlance.gaugeFillFraction(-0.4) == 0)
#expect(CapacityDockGlance.gaugeFillFraction(2.5) == 1)
}
@Test("The tint's fade tail never inverts on a short fill")
func pillFadeStart() {
// A long band keeps a 12pt tail, so the fade starts near its end.
#expect(abs(CapacityDockGlance.pillFadeStart(filledWidth: 112, scale: 1) - 100.0 / 112.0) < 1e-9)
// Shorter than the tail, the whole band is the fade.
#expect(CapacityDockGlance.pillFadeStart(filledWidth: 8, scale: 1) == 0)
#expect(CapacityDockGlance.pillFadeStart(filledWidth: 0, scale: 1) == 0)
// The tail scales with the panel.
#expect(CapacityDockGlance.pillFadeStart(filledWidth: 48, scale: 2) == 0.5)
}
@Test("The reveal is empty at zero, full at one, and slanted between")
func gaugeRevealEdges() {
let width: CGFloat = 40
let height: CGFloat = 20
func edge(_ fraction: Double) -> (top: CGFloat, bottom: CGFloat) {
CapacityDockGlance.gaugeRevealEdge(fraction: fraction, width: width, height: height)
}
// Nothing coloured at 0, every glyph coloured at 1.
#expect(edge(0) == (top: 0, bottom: 0))
#expect(edge(1) == (top: width, bottom: width))
// Mid-run the bottom leads the top, which is what makes the wipe diagonal.
let mid = edge(0.5)
#expect(mid.bottom > mid.top)
#expect(mid.bottom - mid.top == height * CapacityDockGlance.gaugeSlantRatio)
#expect(mid.bottom > 0 && mid.bottom < width)
}
@Test("The reveal never goes backwards or escapes the text box")
func gaugeRevealIsMonotonicAndClamped() {
let width: CGFloat = 40
let height: CGFloat = 20
var previous: (top: CGFloat, bottom: CGFloat) = (0, 0)
for step in 0...20 {
let edge = CapacityDockGlance.gaugeRevealEdge(
fraction: Double(step) / 20,
width: width,
height: height
)
#expect(edge.top >= previous.top)
#expect(edge.bottom >= previous.bottom)
#expect(edge.top >= 0 && edge.top <= width)
#expect(edge.bottom >= 0 && edge.bottom <= width)
#expect(edge.bottom >= edge.top)
previous = edge
}
}
@Test("Out-of-range percentages clamp rather than overdraw")
func gaugeRevealClampsOutOfRange() {
let width: CGFloat = 40
#expect(CapacityDockGlance.gaugeRevealEdge(fraction: -0.5, width: width, height: 20).bottom == 0)
#expect(CapacityDockGlance.gaugeRevealEdge(fraction: 1.8, width: width, height: 20).top == width)
}
@Test("A low percentage colours only the start of the run")
func gaugeRevealAtLowPercent() {
// 16% of a two-glyph run reaches into the second glyph and no further.
let edge = CapacityDockGlance.gaugeRevealEdge(fraction: 0.16, width: 40, height: 20)
#expect(edge.bottom > 0)
#expect(edge.bottom < 40 * 0.3)
}
@Test("Sessions grow by a pill plus its gap up to the visible cap, then scroll")
func sessionsSectionHeight() {
let empty = CapacityDockGlance.sessionsHeight(count: 0)
#expect(
empty == CapacityDockGlance.sectionPadTop
+ CapacityDockGlance.captionLine
+ CapacityDockGlance.sectionPadBottom
)
#expect(CapacityDockGlance.sessionListHeight(count: 0) == 0)
let step = CapacityDockGlance.pillGap + CapacityDockGlance.pillHeight
for count in 1...CapacityDockGlance.maxVisibleSessionRows {
let grown = CapacityDockGlance.sessionsHeight(count: count)
- CapacityDockGlance.sessionsHeight(count: count - 1)
#expect(grown == step)
}
// Four pills and their three gaps, then the list scrolls inside that.
let fourPills: CGFloat = 4 * CapacityDockGlance.pillHeight + 3 * CapacityDockGlance.pillGap
#expect(CapacityDockGlance.sessionListHeight(count: 4) == fourPills)
#expect(fourPills == 202)
let capped = CapacityDockGlance.sessionsHeight(count: 4)
#expect(CapacityDockGlance.sessionsHeight(count: 5) == capped)
#expect(CapacityDockGlance.sessionsHeight(count: 40) == capped)
}
@Test("Height is the exact sum of the sections the view draws")
func heightIsTheSumOfItsSections() {
func height(_ count: Int?, hasToday: Bool, windows: [QuotaSummary.Window]) -> CGFloat {
CapacityDockMetrics.detailHeight(
quota: quota(windows),
sessionCount: count,
hasToday: hasToday,
tailEdge: .right,
scale: 1
)
}
let three = [window("Weekly", 0.24), window("5-hour", 0.26), window("Fable", 0.4)]
let full = height(1, hasToday: true, windows: three)
#expect(
full == CapacityDockGlance.headerHeight
+ CapacityDockGlance.sessionsHeight(count: 1)
+ CapacityDockGlance.todayHeight
+ CapacityDockGlance.windowsHeight
)
// 44 header + 83 sessions + 81 today + 77 windows
#expect(full == 285)
// The panel opens and closes on the same 16pt inset it uses sideways.
let headerParts: CGFloat = CapacityDockGlance.contentInset + 20 + 8
#expect(CapacityDockGlance.headerHeight == headerParts)
#expect(
CapacityDockGlance.windowsHeight
== CapacityDockGlance.sectionPadTop + 53 + CapacityDockGlance.contentInset
)
// Today is three stacked lines (13 + 3 + 13 + 3 + 12) inside its padding.
#expect(CapacityDockGlance.todayContentHeight == 44)
#expect(CapacityDockGlance.todayHeight == 81)
// Past four sessions the list scrolls, so the panel stops growing.
let capped = height(4, hasToday: true, windows: three)
#expect(height(12, hasToday: true, windows: three) == capped)
#expect(capped == 44 + CapacityDockGlance.sessionsHeight(count: 4) + 81 + 77)
// Each section is independently droppable.
#expect(full - height(nil, hasToday: true, windows: three) == CapacityDockGlance.sessionsHeight(count: 1))
#expect(full - height(1, hasToday: false, windows: three) == CapacityDockGlance.todayHeight)
#expect(
height(1, hasToday: true, windows: []) - full
== CapacityDockGlance.windowsEmptyHeight - CapacityDockGlance.windowsHeight
)
// Column count does not change the row's height.
#expect(height(1, hasToday: true, windows: [three[0]]) == full)
}
@Test("A vertical tail adds its allowance to the panel height")
func tailAllowance() {
func height(_ tailEdge: CapacityDockEdge) -> CGFloat {
CapacityDockMetrics.detailHeight(
quota: quota([window("5-hour", 0.2)]),
sessionCount: 2,
hasToday: true,
tailEdge: tailEdge,
scale: 1
)
}
#expect(height(.top) - height(.right) == CapacityDockGlance.tailAllowance)
#expect(height(.bottom) == height(.top))
#expect(height(.left) == height(.right))
}
@Test("Every scaled height stays a whole point so the panel cannot thrash")
func heightIsAlwaysWhole() {
for scale in [0.9, 1.0, 1.15, 1.25, 1.4] {
let height = CapacityDockMetrics.detailHeight(
quota: quota([window("5-hour", 0.2), window("Weekly", 0.5)]),
sessionCount: 4,
hasToday: true,
tailEdge: .bottom,
scale: CGFloat(scale)
)
#expect(height == height.rounded())
}
}
@Test("A session reports its context fraction and what is left")
func sessionContextMath() {
let session = LiveSession(
id: "s1",
provider: "claude",
project: "codeburn",
branch: "feat/dock",
model: "Opus 4.8",
contextTokens: 160_000,
contextWindow: 200_000,
startedAt: "2026-09-01T10:00:00.000Z",
lastActivityAt: "2026-09-01T12:00:00.000Z"
)
#expect(session.title == "codeburn · feat/dock")
#expect(session.contextFraction == 0.8)
#expect(session.contextRemaining == 40_000)
let now = LiveSession.parseISO8601("2026-09-01T12:04:00.000Z")!
#expect(session.elapsedLabel(now: now) == "2h 4m")
}
@Test("A session with no usage record renders unfilled rather than at zero")
func sessionWithoutContext() {
let session = LiveSession(
id: "s2",
provider: "codex",
project: "rakazo",
branch: nil,
model: nil,
contextTokens: nil,
contextWindow: nil,
startedAt: "not-a-date",
lastActivityAt: "2026-09-01T12:00:00.000Z"
)
#expect(session.title == "rakazo")
#expect(session.contextFraction == nil)
#expect(session.contextRemaining == nil)
#expect(session.elapsedLabel() == "")
}
@Test("A payload without the live-session block decodes as unknown, not as empty")
func liveSessionsAreOptional() throws {
let json = """
{"windowSeconds":120,"sessions":[
{"id":"a","provider":"claude","project":"codeburn","branch":"main","model":"Opus 4.8",
"contextTokens":100,"contextWindow":200000,
"startedAt":"2026-09-01T10:00:00.000Z","lastActivityAt":"2026-09-01T12:00:00.000Z"},
{"id":"b","provider":"codex","project":"rakazo","branch":null,"model":null,
"contextTokens":null,"contextWindow":null,
"startedAt":"2026-09-01T10:00:00.000Z","lastActivityAt":"2026-09-01T12:00:00.000Z"}
]}
"""
let block = try JSONDecoder().decode(LiveSessionsBlock.self, from: Data(json.utf8))
#expect(block.sessions.count == 2)
#expect(block.sessions.filter { $0.provider == "claude" }.count == 1)
#expect(block.sessions[1].contextFraction == nil)
}
}

View file

@ -284,7 +284,21 @@ struct CapacityDockPresentationTests {
footerLines: [reason]
)
#expect(CapacityDockMetrics.detailHeight(quota: quota, scale: 1) >= 216)
let height = CapacityDockMetrics.detailHeight(
quota: quota,
sessionCount: nil,
hasToday: false,
tailEdge: .right,
scale: 1
)
// Worst case the card must not clip: the provider header, the guidance
// block at full wrap ("Reconnect required", a 2-line reason and a 3-line
// instruction at ~13pt each, plus their spacing), and the action button.
// The windows row is absent, since a disconnected provider has no quota.
let guidance: CGFloat = 84
let actionButton: CGFloat = 38
let floor: CGFloat = CapacityDockGlance.headerHeight + guidance + actionButton
#expect(height >= floor)
}
@MainActor

View file

@ -14,9 +14,9 @@
],
"scripts": {
"bundle-litellm": "node scripts/bundle-litellm.mjs",
"build": "node scripts/bundle-litellm.mjs && tsup && node -e \"const fs=require('fs'); fs.copyFileSync('src/cli.ts','dist/cli.js'); fs.chmodSync('dist/cli.js',0o755)\" && npm run build:dash",
"build": "tsup && node -e \"const fs=require('fs'); fs.copyFileSync('src/cli.ts','dist/cli.js'); fs.chmodSync('dist/cli.js',0o755)\" && npm run build:dash",
"build:cli": "tsup && node -e \"const fs=require('fs'); fs.copyFileSync('src/cli.ts','dist/cli.js'); fs.chmodSync('dist/cli.js',0o755)\"",
"build:dash": "cd dash && npm install --no-audit --no-fund --silent && npm run build",
"build:dash": "cd dash && npm ci --no-audit --no-fund --silent && npm run build",
"dev": "NODE_OPTIONS=--no-deprecation tsx src/cli.ts",
"test": "vitest run tests --exclude \"tests/cache-refresh-lock*\"",
"test:locks": "vitest run tests/cache-refresh-lock.test.ts tests/cache-refresh-lock-corrupt-body.test.ts tests/cache-refresh-lock-process.test.ts tests/cache-refresh-lock-status-snapshot.test.ts --poolOptions.forks.singleFork=true",

View file

@ -132,7 +132,9 @@ function capture(bin, cacheDir, outDir, extra = {}) {
}
// The one field that moves between two runs of the same payload.
const stripGenerated = obj => JSON.parse(JSON.stringify(obj, (k, v) => (k.startsWith('generated') ? undefined : v)))
// liveSessions is a wall-clock snapshot (idleSeconds, lastActivityAt), not a parse
// product, so two invocations legitimately differ there.
const stripGenerated = obj => JSON.parse(JSON.stringify(obj, (k, v) => (k.startsWith('generated') || k === 'liveSessions' ? undefined : v)))
// Shards carry real stat data and are published under a random filename, so
// "identical" means identical after normalizing both away.

View file

@ -410,6 +410,10 @@ async function findLastBoundary(filePath: string): Promise<{
// 200K unless the session itself proves bigger.
const MILLION_WINDOW_RE = /opus-4-8|\[1m\]/
export function reportedContextWindow(model: string, maxSeenTokens: number): number {
return MILLION_WINDOW_RE.test(model) || maxSeenTokens > 220_000 ? 1_000_000 : 200_000
}
export async function buildContextTree(session: SessionRef): Promise<ContextTreeResult> {
const boundary = await findLastBoundary(session.filePath)
const builder = new TreeBuilder()
@ -445,8 +449,7 @@ export async function buildContextTree(session: SessionRef): Promise<ContextTree
(builder.lastUsage.cache_creation_input_tokens ?? 0) +
(builder.lastUsage.output_tokens ?? 0)
builder.maxSeenTokens = Math.max(builder.maxSeenTokens, context)
const million = MILLION_WINDOW_RE.test(builder.model) || builder.maxSeenTokens > 220_000
reported = { context, window: million ? 1_000_000 : 200_000 }
reported = { context, window: reportedContextWindow(builder.model, builder.maxSeenTokens) }
}
return {

304
src/live-sessions.ts Normal file
View file

@ -0,0 +1,304 @@
/// "What is using the quota right now": sessions whose transcript was appended
/// inside a short liveness window, with the context each one is holding. Feeds
/// the optional `liveSessions` block of the menubar payload; the app renders
/// only what it finds here.
import { open, readdir, stat } from 'node:fs/promises'
import { basename, join } from 'node:path'
import { getClaudeConfigDirs, getDesktopSessionsDirs } from './providers/claude.js'
import { reportedContextWindow } from './context-tree.js'
import { getShortModelName } from './models.js'
import type { ApiUsage, AssistantMessageContent, JournalEntry } from './types.js'
export const LIVE_WINDOW_SECONDS = 600
/// Only the tail of a live transcript is read, and it is scanned backwards only
/// as far as the last assistant turn. A long-running session's file reaches tens
/// of MB and this runs on every payload build; the fields we want (last model,
/// last usage, current branch) all sit at the end.
export const TAIL_BYTES = 256 * 1024
export type LiveSession = {
id: string
/// Provider catalog id, matching the dock ring the session runs under.
provider: string
project: string
/// Git branch of the last turn, the second half of the row's title. Null when
/// the transcript never named one (non-Claude tools, or a non-repo cwd).
branch: string | null
/// Short display name of the model that answered last.
model: string | null
/// Tokens the next turn would resend, and the window they are measured against.
/// Both null when the tail held no assistant usage, so the app omits the ring.
contextTokens: number | null
contextWindow: number | null
startedAt: string
lastActivityAt: string
/// Seconds since this session last wrote, at the moment the payload was
/// built. A session can be live but waiting on the user, which reads very
/// differently from one that is generating.
idleSeconds: number
}
export type LiveSessionsBlock = {
windowSeconds: number
sessions: LiveSession[]
}
export type LiveSessionInput = {
id: string
provider: string
project: string
branch: string | null
model: string | null
contextTokens: number | null
contextWindow: number | null
startedMs: number
lastActivityMs: number
/// Last-activity time of each sub-agent run belonging to this session.
subagentActivityMs: number[]
}
/// Pure core: liveness and parent/sub-agent folding, newest first.
export function buildLiveSessions(
inputs: LiveSessionInput[],
nowMs: number,
windowSeconds: number = LIVE_WINDOW_SECONDS,
): LiveSessionsBlock {
const windowMs = windowSeconds * 1000
const isLive = (ms: number) => ms > 0 && nowMs - ms <= windowMs
// A session working through a sub-agent leaves its own transcript untouched
// for as long as that agent runs, so its sub-agents count as its activity.
const activityOf = (input: LiveSessionInput) => Math.max(input.lastActivityMs, ...input.subagentActivityMs)
const sessions = inputs
.filter(input => isLive(activityOf(input)))
.map(input => ({
id: input.id,
provider: input.provider,
project: input.project,
branch: input.branch,
model: input.model,
contextTokens: input.contextTokens,
contextWindow: input.contextWindow,
startedAt: new Date(input.startedMs).toISOString(),
lastActivityAt: new Date(activityOf(input)).toISOString(),
idleSeconds: Math.max(0, Math.round((nowMs - activityOf(input)) / 1000)),
}))
.sort((a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt))
return { windowSeconds, sessions }
}
async function readTail(filePath: string, maxBytes: number): Promise<string> {
const handle = await open(filePath, 'r')
try {
const { size } = await handle.stat()
const start = size > maxBytes ? size - maxBytes : 0
const buffer = Buffer.alloc(size - start)
if (buffer.length === 0) return ''
await handle.read(buffer, 0, buffer.length, start)
const text = buffer.toString('utf8')
// A mid-file start almost certainly lands inside a line; drop that fragment.
return start > 0 ? text.slice(text.indexOf('\n') + 1) : text
} finally {
await handle.close()
}
}
/// Tokens the next request would carry: everything the model reads back plus
/// what it just wrote. Mirrors the reported-context sum in context-tree.ts.
function contextOf(usage: ApiUsage): number {
return (usage.input_tokens ?? 0)
+ (usage.cache_read_input_tokens ?? 0)
+ (usage.cache_creation_input_tokens ?? 0)
+ (usage.output_tokens ?? 0)
}
function assistantMessage(entry: JournalEntry): AssistantMessageContent | null {
const message = entry.message
if (!message || typeof message !== 'object') return null
const candidate = message as Partial<AssistantMessageContent>
if (candidate.role !== 'assistant' || !candidate.usage) return null
return candidate as AssistantMessageContent
}
type ScannedFile = {
sessionId: string
cwd: string
isSidechain: boolean
branch: string | null
model: string | null
contextTokens: number | null
contextWindow: number | null
}
/// Reads one live transcript tail and walks it backwards, stopping at the last
/// assistant turn. Never throws: an unreadable or unexpected file yields a bare
/// record rather than taking the payload down.
export async function scanTranscript(filePath: string): Promise<ScannedFile> {
const result: ScannedFile = {
sessionId: basename(filePath).replace(/\.jsonl$/, ''),
cwd: '',
isSidechain: false,
branch: null,
model: null,
contextTokens: null,
contextWindow: null,
}
let text = ''
try {
text = await readTail(filePath, TAIL_BYTES)
} catch {
return result
}
const lines = text.split('\n')
let needsIdentity = true
let needsUsage = true
// Backwards: the newest turn wins for every field, so the first hit going up
// is the answer and there is no reason to parse the rest of the tail.
for (let index = lines.length - 1; index >= 0; index -= 1) {
if (!needsIdentity && !needsUsage) break
const line = lines[index]
if (!line || !line.trim()) continue
let entry: JournalEntry
try {
entry = JSON.parse(line) as JournalEntry
} catch {
continue
}
if (needsIdentity) {
if (typeof entry.sessionId === 'string' && entry.sessionId) result.sessionId = entry.sessionId
if (typeof entry.cwd === 'string' && entry.cwd) result.cwd = entry.cwd
if (entry.isSidechain === true) result.isSidechain = true
if (typeof entry.gitBranch === 'string' && entry.gitBranch) result.branch = entry.gitBranch
// cwd and branch travel together on every user/assistant entry, so one
// entry carrying them settles identity.
if (result.cwd && result.branch) needsIdentity = false
}
if (!needsUsage) continue
const assistant = assistantMessage(entry)
if (!assistant) continue
const context = contextOf(assistant.usage)
if (context <= 0) continue
result.contextTokens = context
const rawModel = typeof assistant.model === 'string' ? assistant.model : ''
if (rawModel) result.model = getShortModelName(rawModel)
// ponytail: the window comes from this one turn, so a session that compacted
// below 220k on a 1M model reads as 200k until its next big turn. Track a
// running max again if that misreport ever matters.
result.contextWindow = reportedContextWindow(rawModel, context)
needsUsage = false
}
return result
}
/// Claude nests sub-agent transcripts under their parent's own directory
/// (`<parent-session-id>/subagents/...`), so the parent file is that directory
/// plus `.jsonl`. Returns null for any other shape.
function parentTranscriptPath(sidechainPath: string): string | null {
const marker = sidechainPath.lastIndexOf('/subagents/')
if (marker <= 0) return null
return `${sidechainPath.slice(0, marker)}.jsonl`
}
type FileTimes = { path: string; mtimeMs: number; birthtimeMs: number }
/// Transcript roots, straight from the Claude config rather than through the
/// provider registry. `discoverAllSessions` walks every provider's tree and
/// costs about half a second on a large history; this block only ever reads
/// Claude transcripts, and it runs on every payload build.
async function transcriptRoots(): Promise<string[]> {
const configured = await getClaudeConfigDirs().catch(() => [])
return [...configured.map(dir => join(dir, 'projects')), ...getDesktopSessionsDirs()]
}
/// Every transcript touched inside the window. One recursive listing per root,
/// then a stat per file: nothing is opened until it is known to be live.
async function liveTranscripts(nowMs: number, windowMs: number): Promise<FileTimes[]> {
const paths: string[] = []
for (const root of await transcriptRoots()) {
const entries = await readdir(root, { recursive: true, withFileTypes: true }).catch(() => [])
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue
paths.push(join(entry.parentPath, entry.name))
}
}
const stats = await Promise.all(paths.map(async path => {
const info = await stat(path).catch(() => null)
if (!info?.isFile()) return null
if (nowMs - info.mtimeMs > windowMs) return null
return { path, mtimeMs: info.mtimeMs, birthtimeMs: info.birthtimeMs || info.mtimeMs }
}))
return stats.filter((entry): entry is FileTimes => entry !== null)
}
/// Walks the live transcripts and builds the pure-core inputs. Sidechain
/// (sub-agent) transcripts fold into their parent session: they keep it alive
/// while it waits on them.
export async function collectLiveSessionInputs(
nowMs: number,
windowSeconds: number,
): Promise<LiveSessionInput[]> {
const windowMs = windowSeconds * 1000
const inputs = new Map<string, LiveSessionInput>()
type PendingSidechain = { sessionId: string; mtimeMs: number; path: string }
const pendingSidechains: PendingSidechain[] = []
const toInput = (scan: ScannedFile, times: Omit<FileTimes, 'path'>, fallbackProject: string): LiveSessionInput => ({
id: scan.sessionId,
provider: 'claude',
project: scan.cwd ? basename(scan.cwd) : fallbackProject,
branch: scan.branch,
model: scan.model,
contextTokens: scan.contextTokens,
contextWindow: scan.contextWindow,
startedMs: times.birthtimeMs,
lastActivityMs: times.mtimeMs,
subagentActivityMs: [],
})
/// A parent whose own transcript is idle because its sub-agent is doing the
/// work. Its file is not live, so read it here to recover its details.
const loadIdleParent = async (sidechain: PendingSidechain): Promise<LiveSessionInput | null> => {
const parentPath = parentTranscriptPath(sidechain.path)
if (!parentPath) return null
const info = await stat(parentPath).catch(() => null)
if (!info?.isFile()) return null
const scan = await scanTranscript(parentPath)
return toInput(
scan,
{ mtimeMs: info.mtimeMs, birthtimeMs: info.birthtimeMs || info.mtimeMs },
basename(parentPath).replace(/\.jsonl$/, ''),
)
}
for (const file of await liveTranscripts(nowMs, windowMs)) {
const scan = await scanTranscript(file.path)
if (scan.isSidechain) {
pendingSidechains.push({ sessionId: scan.sessionId, mtimeMs: file.mtimeMs, path: file.path })
continue
}
inputs.set(scan.sessionId, toInput(scan, file, basename(file.path).replace(/\.jsonl$/, '')))
}
// Claude writes the PARENT session id into every sidechain line, so the scan's
// session id is the parent to attach to.
for (const sidechain of pendingSidechains) {
let parent = inputs.get(sidechain.sessionId)
if (!parent) {
const recovered = await loadIdleParent(sidechain)
if (!recovered) continue
parent = recovered
inputs.set(parent.id, parent)
}
parent.subagentActivityMs.push(sidechain.mtimeMs)
}
return [...inputs.values()]
}
export async function collectLiveSessions(
nowMs: number = Date.now(),
windowSeconds: number = LIVE_WINDOW_SECONDS,
): Promise<LiveSessionsBlock> {
const inputs = await collectLiveSessionInputs(nowMs, windowSeconds)
return buildLiveSessions(inputs, nowMs, windowSeconds)
}

View file

@ -31,10 +31,12 @@ function redactSessionDetails(details: Array<{ cost: number; savingsUSD: number;
}
export function redactProjectNames(payload: MenubarPayload, includeNames: boolean): MenubarPayload {
if (includeNames) return payload
const timeline = payload.history?.timeline
// Live sessions name projects and branches; MCP consumers never need them.
const { liveSessions: _liveSessions, ...rest } = payload
if (includeNames) return rest
const timeline = rest.history?.timeline
return {
...payload,
...rest,
current: {
...payload.current,
topProjects: payload.current.topProjects.map(p => ({

View file

@ -91,6 +91,7 @@ import type { GranularHistory } from './granular-history.js'
import { getShortModelName } from './models.js'
import type { ReworkedFile } from './workflow-insights.js'
import type { PrRow, BranchRow } from './sessions-report.js'
import type { LiveSessionsBlock } from './live-sessions.js'
const TOP_ACTIVITIES_LIMIT = 20
const TOP_MODELS_LIMIT = 20
@ -212,6 +213,10 @@ export type MenubarPayload = {
/// section AND its command wrote it. Surfaces render what they recognize
/// and ignore the rest; absence always means "no plugin output today".
plugins?: Record<string, unknown>
/// Add-only. Sessions whose transcript was appended inside the liveness
/// window, with the context each is holding. Omitted when the producer did
/// not compute it, so absence means "unknown", never "nothing is running".
liveSessions?: LiveSessionsBlock
current: {
label: string
cost: number

View file

@ -10,8 +10,11 @@ export function sanitizeForSharing(payload: MenubarPayload): MenubarPayload {
// Older peers may predate the history field even though current producers
// always include it, so keep the boundary tolerant while sanitizing.
const timeline = payload.history?.timeline
// Live sessions name the project and branch in flight, the most identifying
// block of all.
const { liveSessions: _liveSessions, ...rest } = payload
return {
...payload,
...rest,
current: {
...payload.current,
topProjects: [],

View file

@ -6,6 +6,7 @@ import { parseAllSessions, filterProjectsByName, filterProjectsByDays, filterPro
import { findUnpricedModels, getFlatRateModelsConfigHash, getLocalModelSavingsConfigHash, getPriceOverridesConfigHash, getShortModelName, isExpectedFreeModel } from './models.js'
import { getAllProviders, safeDiscoverSessions } from './providers/index.js'
import { loadPlugins, pluginPayloadSections } from './plugins/loader.js'
import { collectLiveSessions } from './live-sessions.js'
import { claude, getClaudeConfigDirs, getDesktopSessionsDirs } from './providers/claude.js'
import { stat } from 'node:fs/promises'
import { aggregateProjectsIntoDays, buildPeriodDataFromDays, dateKeyInTz } from './day-aggregator.js'
@ -1180,5 +1181,10 @@ export async function buildMenubarPayloadForRange(periodInfo: PeriodInfo, opts:
// default, so the payload is byte-identical without plugins installed).
const pluginSections = await pluginPayloadSections(await loadPlugins())
if (Object.keys(pluginSections).length > 0) payload.plugins = pluginSections
// Add-only live-session block. Its own disk pass is independent of the
// aggregation above, so a failure here must leave the rest of the payload
// intact rather than blank the menubar.
const liveSessions = await collectLiveSessions().catch(() => null)
if (liveSessions) payload.liveSessions = liveSessions
return payload
}

182
tests/live-sessions.test.ts Normal file
View file

@ -0,0 +1,182 @@
import { describe, expect, it } from 'vitest'
import { mkdtemp, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { LIVE_WINDOW_SECONDS, TAIL_BYTES, buildLiveSessions, scanTranscript, type LiveSessionInput } from '../src/live-sessions.js'
const NOW = Date.parse('2026-09-01T12:00:00.000Z')
function input(over: Partial<LiveSessionInput> = {}): LiveSessionInput {
return {
id: 'session-a',
provider: 'claude',
project: 'codeburn',
branch: 'main',
model: 'Opus 4.8',
contextTokens: 100_000,
contextWindow: 200_000,
startedMs: NOW - 3_600_000,
lastActivityMs: NOW - 10_000,
subagentActivityMs: [],
...over,
}
}
describe('buildLiveSessions', () => {
it('keeps sessions touched inside the window and drops older ones', () => {
const block = buildLiveSessions([
input({ id: 'fresh', lastActivityMs: NOW - 10_000 }),
input({ id: 'stale', lastActivityMs: NOW - 3_600_000 }),
], NOW, LIVE_WINDOW_SECONDS)
expect(block.sessions.map(s => s.id)).toEqual(['fresh'])
expect(block.windowSeconds).toBe(LIVE_WINDOW_SECONDS)
})
it('counts a session idle for minutes as still live, at ten minutes wide', () => {
expect(LIVE_WINDOW_SECONDS).toBe(600)
// A session waiting on the user for five minutes is still open, not gone.
const block = buildLiveSessions([
input({ id: 'thinking', lastActivityMs: NOW - 5_000 }),
input({ id: 'waiting', lastActivityMs: NOW - 300_000 }),
], NOW, LIVE_WINDOW_SECONDS)
expect(block.sessions.map(s => s.id)).toEqual(['thinking', 'waiting'])
expect(block.sessions.map(s => s.idleSeconds)).toEqual([5, 300])
})
it('reports idle from the sub-agent that kept the session alive', () => {
const block = buildLiveSessions([
input({ lastActivityMs: NOW - 400_000, subagentActivityMs: [NOW - 20_000] }),
], NOW, LIVE_WINDOW_SECONDS)
expect(block.sessions[0]!.idleSeconds).toBe(20)
})
it('keeps a parent alive while only its sub-agent is writing', () => {
const block = buildLiveSessions([
input({ id: 'parent', lastActivityMs: NOW - 3_600_000, subagentActivityMs: [NOW - 5_000] }),
], NOW, LIVE_WINDOW_SECONDS)
expect(block.sessions).toHaveLength(1)
// The sub-agent's write is the session's last activity.
expect(block.sessions[0]!.lastActivityAt).toBe(new Date(NOW - 5_000).toISOString())
})
it('ignores a sub-agent that is itself stale', () => {
const block = buildLiveSessions([
input({ lastActivityMs: NOW - 3_600_000, subagentActivityMs: [NOW - 3_700_000] }),
], NOW, LIVE_WINDOW_SECONDS)
expect(block.sessions).toEqual([])
})
it('sorts newest first and carries the context fields through', () => {
const block = buildLiveSessions([
input({ id: 'older', lastActivityMs: NOW - 60_000 }),
input({ id: 'newer', lastActivityMs: NOW - 1_000, contextTokens: 42, contextWindow: 1_000_000 }),
], NOW, LIVE_WINDOW_SECONDS)
expect(block.sessions.map(s => s.id)).toEqual(['newer', 'older'])
expect(block.sessions[0]).toMatchObject({ contextTokens: 42, contextWindow: 1_000_000, branch: 'main' })
})
it('treats a missing timestamp as not live rather than as the epoch', () => {
expect(buildLiveSessions([input({ lastActivityMs: 0 })], NOW, LIVE_WINDOW_SECONDS).sessions).toEqual([])
})
})
describe('scanTranscript', () => {
async function transcript(lines: unknown[]): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'live-sessions-'))
const path = join(dir, 'abc123.jsonl')
await writeFile(path, lines.map(l => JSON.stringify(l)).join('\n'))
return path
}
it('reads branch, model and the last assistant context', async () => {
const path = await transcript([
{ type: 'user', sessionId: 's1', cwd: '/Users/x/codeburn', gitBranch: 'feat/dock' },
{
type: 'assistant',
message: {
role: 'assistant',
model: 'claude-opus-4-5-20260101',
usage: { input_tokens: 10, cache_read_input_tokens: 90_000, cache_creation_input_tokens: 1_000, output_tokens: 500 },
},
},
])
const scan = await scanTranscript(path)
expect(scan.sessionId).toBe('s1')
expect(scan.cwd).toBe('/Users/x/codeburn')
expect(scan.branch).toBe('feat/dock')
expect(scan.contextTokens).toBe(91_510)
expect(scan.contextWindow).toBe(200_000)
expect(scan.model).toBeTruthy()
})
it('takes the last assistant turn, not the largest', async () => {
const usage = (input: number) => ({
type: 'assistant',
message: { role: 'assistant', model: 'claude-sonnet-4-5', usage: { input_tokens: input } },
})
const scan = await scanTranscript(await transcript([usage(150_000), usage(20_000)]))
expect(scan.contextTokens).toBe(20_000)
})
it('widens the window when the session outgrew 200k', async () => {
const scan = await scanTranscript(await transcript([
{ type: 'assistant', message: { role: 'assistant', model: 'claude-sonnet-4-5', usage: { input_tokens: 300_000 } } },
{ type: 'assistant', message: { role: 'assistant', model: 'claude-sonnet-4-5', usage: { input_tokens: 250_000 } } },
]))
expect(scan.contextWindow).toBe(1_000_000)
})
it('flags a sidechain and degrades with no usage at all', async () => {
const scan = await scanTranscript(await transcript([
{ type: 'user', sessionId: 'parent-1', isSidechain: true, cwd: '/Users/x/eywa' },
]))
expect(scan.isSidechain).toBe(true)
expect(scan.sessionId).toBe('parent-1')
expect(scan.contextTokens).toBeNull()
expect(scan.contextWindow).toBeNull()
expect(scan.model).toBeNull()
})
it('reads only the last 256 KB, so anything older than the tail is not seen', async () => {
expect(TAIL_BYTES).toBe(256 * 1024)
const usage = {
type: 'assistant',
message: { role: 'assistant', model: 'claude-sonnet-4-5', usage: { input_tokens: 4242 } },
}
// One real turn, then enough filler to push it clear out of the tail window.
const filler = { type: 'user', cwd: '/Users/x/codeburn', gitBranch: 'main', pad: 'x'.repeat(2000) }
const fillerCount = Math.ceil(TAIL_BYTES / JSON.stringify(filler).length) + 20
const dir = await mkdtemp(join(tmpdir(), 'live-sessions-'))
const path = join(dir, 'long.jsonl')
await writeFile(path, [usage, ...Array(fillerCount).fill(filler)].map(l => JSON.stringify(l)).join('\n'))
const scan = await scanTranscript(path)
// The identity fields still resolve, because they repeat on every entry.
expect(scan.branch).toBe('main')
// The assistant turn is beyond the tail, so no context is reported at all
// rather than a stale figure read from the head of a huge file.
expect(scan.contextTokens).toBeNull()
expect(scan.contextWindow).toBeNull()
})
it('still finds a turn that sits just inside the tail', async () => {
const filler = { type: 'user', cwd: '/Users/x/codeburn', gitBranch: 'main', pad: 'x'.repeat(2000) }
const fillerCount = Math.floor(TAIL_BYTES / JSON.stringify(filler).length / 2)
const usage = {
type: 'assistant',
message: { role: 'assistant', model: 'claude-sonnet-4-5', usage: { input_tokens: 4242 } },
}
const dir = await mkdtemp(join(tmpdir(), 'live-sessions-'))
const path = join(dir, 'short.jsonl')
await writeFile(path, [usage, ...Array(fillerCount).fill(filler)].map(l => JSON.stringify(l)).join('\n'))
expect((await scanTranscript(path)).contextTokens).toBe(4242)
})
it('survives malformed lines and an unreadable file', async () => {
const dir = await mkdtemp(join(tmpdir(), 'live-sessions-'))
const path = join(dir, 'broken.jsonl')
await writeFile(path, '{not json\n{"type":"user","gitBranch":"main"}\n')
expect((await scanTranscript(path)).branch).toBe('main')
expect((await scanTranscript(join(dir, 'missing.jsonl'))).contextTokens).toBeNull()
})
})

View file

@ -72,4 +72,21 @@ describe('redact', () => {
expect(out.current.topProjects[0]!.sessionDetails![0]!.date).toBe('2026-06-01')
expect(out.history.timeline?.sessionSeries[0]!.label).toContain('secret-client-repo')
})
it('drops the live-session block even when names are included', () => {
const withSessions: MenubarPayload = {
...payload(),
liveSessions: {
windowSeconds: 120,
sessions: [{
id: 'a', provider: 'claude', project: 'secret-client-repo', branch: 'secret-branch',
model: 'Opus 4.8', contextTokens: 1, contextWindow: 200_000,
startedAt: '2026-09-01T10:00:00.000Z', lastActivityAt: '2026-09-01T12:00:00.000Z',
}],
},
}
expect(redactProjectNames(withSessions, false).liveSessions).toBeUndefined()
const included = redactProjectNames(withSessions, true)
expect(included.liveSessions).toBeUndefined()
expect(JSON.stringify(included)).not.toContain('secret-branch')
})
})

View file

@ -262,7 +262,10 @@ function stripVolatile(payload: unknown): unknown {
if (payload && typeof payload === 'object') {
return Object.fromEntries(
Object.entries(payload as Record<string, unknown>)
.filter(([k]) => !k.toLowerCase().startsWith('generated'))
// `generated` is a timestamp, and `liveSessions` is a wall-clock
// snapshot of what is running rather than anything the parse produced,
// so neither can be compared across two separate CLI invocations.
.filter(([k]) => !k.toLowerCase().startsWith('generated') && k !== 'liveSessions')
.map(([k, v]) => [k, stripVolatile(v)]),
)
}

View file

@ -0,0 +1,18 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {
scripts: Record<string, string>
}
describe('release build contract', () => {
it('builds from checked-in pricing catalogs instead of mutating them from the network', () => {
expect(packageJson.scripts['bundle-litellm']).toBe('node scripts/bundle-litellm.mjs')
expect(packageJson.scripts.build).not.toContain('bundle-litellm')
})
it('installs dashboard dependencies without rewriting the lockfile', () => {
expect(packageJson.scripts['build:dash']).toContain('npm ci')
expect(packageJson.scripts['build:dash']).not.toContain('npm install')
})
})

View file

@ -61,4 +61,21 @@ describe('sanitizeForSharing', () => {
const clean = sanitizeForSharing(fixture())
expect(JSON.stringify(clean)).not.toContain('secret-project')
})
it('drops the live-session block, which names the project and branch in flight', () => {
const withSessions = {
...fixture(),
liveSessions: {
windowSeconds: 120,
sessions: [{
id: 'a', provider: 'claude', project: 'secret-project', branch: 'secret-branch',
model: 'Opus 4.8', contextTokens: 1, contextWindow: 200_000,
startedAt: '2026-09-01T10:00:00.000Z', lastActivityAt: '2026-09-01T12:00:00.000Z',
}],
},
}
const clean = sanitizeForSharing(withSessions)
expect(clean.liveSessions).toBeUndefined()
expect(JSON.stringify(clean)).not.toContain('secret-branch')
})
})