From ae394370bfae4d9ebea6427b598cbbc7d98734e2 Mon Sep 17 00:00:00 2001 From: maksarsanjeev Date: Tue, 6 Oct 2026 22:27:26 +0300 Subject: [PATCH] Limits move into the Claude/ChatGPT pills (header ran under the camera); balanced home margins Co-Authored-By: Claude Opus 5.5 --- NotchBuddy/Sources/App/AppGridView.swift | 69 +++++++++++++++---- NotchBuddy/Sources/App/CodexUsage.swift | 2 +- NotchBuddy/Sources/App/IslandRootView.swift | 8 +-- .../Sources/CoucouKit/IslandTypes.swift | 4 +- 4 files changed, 59 insertions(+), 24 deletions(-) diff --git a/NotchBuddy/Sources/App/AppGridView.swift b/NotchBuddy/Sources/App/AppGridView.swift index 0b56b3c..d264173 100644 --- a/NotchBuddy/Sources/App/AppGridView.swift +++ b/NotchBuddy/Sources/App/AppGridView.swift @@ -28,6 +28,26 @@ enum DesktopApps { struct AppGridView: View { @ObservedObject var state: AppState + /// "9% · сброс через 1 д 18 ч" — the Claude window closest to its limit. + private var claudeLimit: PillLimit? { + guard state.planRelayInstalled, let usage = state.claudePlanUsage, + let pct = ClaudePlanGauge.dominantPct(usage) else { return nil } + var text = "\(Int(pct.rounded()))%" + if let w = ClaudePlanGauge.dominantWindow(usage), w.resetsAt > Date() { + text += " · сброс через \(PlanTime.untilReset(w.resetsAt))" + } + return PillLimit(text: text, color: ClaudePlanGauge.color(for: pct)) + } + + /// "23% · сброс сб 15:22" — ChatGPT weekly limit from the Codex logs. + private var chatGPTLimit: PillLimit? { + guard let w = state.codexWeekly else { return nil } + let pct = ClaudePlanGauge.effectivePct(w) + var text = "\(Int(pct.rounded()))%" + if w.resetsAt > Date() { text += " · сброс \(PlanTime.resetMoment(w.resetsAt))" } + return PillLimit(text: text, color: ClaudePlanGauge.color(for: pct)) + } + var body: some View { ZStack { VStack(spacing: 8) { @@ -36,12 +56,12 @@ struct AppGridView: View { EmptyPill() } HStack(spacing: 8) { - AppPill(app: DesktopApps.claude) - AppPill(app: DesktopApps.chatGPT) + AppPill(app: DesktopApps.claude, limit: claudeLimit) + AppPill(app: DesktopApps.chatGPT, limit: chatGPTLimit) } } - .padding(.leading, 108) - .padding(.trailing, 14) + .padding(.leading, 104) + .padding(.trailing, 20) .padding(.vertical, 6) #if !APPSTORE @@ -62,8 +82,14 @@ struct AppGridView: View { // MARK: - App pill +struct PillLimit { + let text: String + let color: String +} + struct AppPill: View { let app: DesktopAppSlot + var limit: PillLimit? = nil @State private var appURL: URL? @State private var isRunning = false @State private var isHovered = false @@ -78,23 +104,36 @@ struct AppPill: View { HStack(spacing: 10) { icon .frame(width: 26, height: 26) - VStack(alignment: .leading, spacing: 1) { - Text(app.name) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(Color(hex: "#F5F6F8")) + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 6) { + Text(app.name) + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(Color(hex: "#F5F6F8")) + Spacer(minLength: 4) + Image(systemName: "arrow.up.right") + .font(.system(size: 9, weight: .semibold)) + .foregroundColor(isHovered ? Color(hex: app.color) : Color(hex: "#5F646D")) + } HStack(spacing: 5) { Circle() .fill(isRunning ? Color(hex: "#34D399") : Color(hex: "#5F646D")) .frame(width: 5, height: 5) - Text(status) - .font(.system(size: 10.5)) - .foregroundColor(Color(hex: "#8E939C")) + // With a limit shown, the green dot alone says "running". + if limit == nil || appURL == nil { + Text(status) + .font(.system(size: 10.5)) + .foregroundColor(Color(hex: "#8E939C")) + .lineLimit(1) + } + if let limit, appURL != nil { + Text(limit.text) + .font(.system(size: 10.5, weight: .semibold)) + .foregroundColor(Color(hex: limit.color)) + .lineLimit(1) + .minimumScaleFactor(0.8) + } } } - Spacer(minLength: 4) - Image(systemName: "arrow.up.right") - .font(.system(size: 9, weight: .semibold)) - .foregroundColor(isHovered ? Color(hex: app.color) : Color(hex: "#5F646D")) } .padding(.horizontal, 12) .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/NotchBuddy/Sources/App/CodexUsage.swift b/NotchBuddy/Sources/App/CodexUsage.swift index 8c915dc..0fda927 100644 --- a/NotchBuddy/Sources/App/CodexUsage.swift +++ b/NotchBuddy/Sources/App/CodexUsage.swift @@ -104,6 +104,6 @@ enum PlanTime { let f = DateFormatter() f.locale = Locale(identifier: "ru_RU") f.dateFormat = date.timeIntervalSince(now) < 6 * 86400 ? "EE HH:mm" : "d MMM HH:mm" - return f.string(from: date).replacingOccurrences(of: ".", with: "") + return f.string(from: date).replacingOccurrences(of: ".", with: "").lowercased() } } diff --git a/NotchBuddy/Sources/App/IslandRootView.swift b/NotchBuddy/Sources/App/IslandRootView.swift index f47da83..0141852 100644 --- a/NotchBuddy/Sources/App/IslandRootView.swift +++ b/NotchBuddy/Sources/App/IslandRootView.swift @@ -552,12 +552,8 @@ struct IslandHeader: View { // Right: plan pill (GitHub build, home view only) + action icons HStack(spacing: 8) { #if !APPSTORE - if (state.view == .overview || state.view == .empty) && state.showPlanInNotch && state.planRelayInstalled { - ClaudePlanHeaderPill(state: state) - } - if (state.view == .overview || state.view == .empty), let w = state.codexWeekly { - ChatGPTLimitPill(window: w) - } + // Plan limits live inside the Claude / ChatGPT pills (AppGridView): + // here they ran under the camera. #endif HStack(spacing: 14) { Button(action: { diff --git a/NotchBuddy/Sources/CoucouKit/IslandTypes.swift b/NotchBuddy/Sources/CoucouKit/IslandTypes.swift index 95f1889..477b97b 100644 --- a/NotchBuddy/Sources/CoucouKit/IslandTypes.swift +++ b/NotchBuddy/Sources/CoucouKit/IslandTypes.swift @@ -176,9 +176,9 @@ enum IslandConst { static let viewLayouts: [IslandView: ViewLayout] = [ // Home is the reference: height 150 - .overview: ViewLayout(height: 160, botX: 68, botY: nil, botDiameter: 58, agentMode: .pills), + .overview: ViewLayout(height: 160, botX: 58, botY: nil, botDiameter: 58, agentMode: .pills), // All non-chat views match home height (150) — law - .empty: ViewLayout(height: 160, botX: 70, botY: nil, botDiameter: 62, agentMode: .none), + .empty: ViewLayout(height: 160, botX: 58, botY: nil, botDiameter: 58, agentMode: .none), .approval: ViewLayout(height: 160, botX: 62, botY: nil, botDiameter: 56, agentMode: .column), .question: ViewLayout(height: 160, botX: 62, botY: nil, botDiameter: 56, agentMode: .column), .error: ViewLayout(height: 160, botX: 62, botY: nil, botDiameter: 58, agentMode: .column),