Limits move into the Claude/ChatGPT pills (header ran under the camera); balanced home margins
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,26 @@ enum DesktopApps {
|
|||||||
struct AppGridView: View {
|
struct AppGridView: View {
|
||||||
@ObservedObject var state: AppState
|
@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 {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
@@ -36,12 +56,12 @@ struct AppGridView: View {
|
|||||||
EmptyPill()
|
EmptyPill()
|
||||||
}
|
}
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
AppPill(app: DesktopApps.claude)
|
AppPill(app: DesktopApps.claude, limit: claudeLimit)
|
||||||
AppPill(app: DesktopApps.chatGPT)
|
AppPill(app: DesktopApps.chatGPT, limit: chatGPTLimit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.leading, 108)
|
.padding(.leading, 104)
|
||||||
.padding(.trailing, 14)
|
.padding(.trailing, 20)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
|
|
||||||
#if !APPSTORE
|
#if !APPSTORE
|
||||||
@@ -62,8 +82,14 @@ struct AppGridView: View {
|
|||||||
|
|
||||||
// MARK: - App pill
|
// MARK: - App pill
|
||||||
|
|
||||||
|
struct PillLimit {
|
||||||
|
let text: String
|
||||||
|
let color: String
|
||||||
|
}
|
||||||
|
|
||||||
struct AppPill: View {
|
struct AppPill: View {
|
||||||
let app: DesktopAppSlot
|
let app: DesktopAppSlot
|
||||||
|
var limit: PillLimit? = nil
|
||||||
@State private var appURL: URL?
|
@State private var appURL: URL?
|
||||||
@State private var isRunning = false
|
@State private var isRunning = false
|
||||||
@State private var isHovered = false
|
@State private var isHovered = false
|
||||||
@@ -78,23 +104,36 @@ struct AppPill: View {
|
|||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
icon
|
icon
|
||||||
.frame(width: 26, height: 26)
|
.frame(width: 26, height: 26)
|
||||||
VStack(alignment: .leading, spacing: 1) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(app.name)
|
HStack(spacing: 6) {
|
||||||
.font(.system(size: 13, weight: .semibold))
|
Text(app.name)
|
||||||
.foregroundColor(Color(hex: "#F5F6F8"))
|
.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) {
|
HStack(spacing: 5) {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(isRunning ? Color(hex: "#34D399") : Color(hex: "#5F646D"))
|
.fill(isRunning ? Color(hex: "#34D399") : Color(hex: "#5F646D"))
|
||||||
.frame(width: 5, height: 5)
|
.frame(width: 5, height: 5)
|
||||||
Text(status)
|
// With a limit shown, the green dot alone says "running".
|
||||||
.font(.system(size: 10.5))
|
if limit == nil || appURL == nil {
|
||||||
.foregroundColor(Color(hex: "#8E939C"))
|
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)
|
.padding(.horizontal, 12)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
|||||||
@@ -104,6 +104,6 @@ enum PlanTime {
|
|||||||
let f = DateFormatter()
|
let f = DateFormatter()
|
||||||
f.locale = Locale(identifier: "ru_RU")
|
f.locale = Locale(identifier: "ru_RU")
|
||||||
f.dateFormat = date.timeIntervalSince(now) < 6 * 86400 ? "EE HH:mm" : "d MMM HH:mm"
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -552,12 +552,8 @@ struct IslandHeader: View {
|
|||||||
// Right: plan pill (GitHub build, home view only) + action icons
|
// Right: plan pill (GitHub build, home view only) + action icons
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
#if !APPSTORE
|
#if !APPSTORE
|
||||||
if (state.view == .overview || state.view == .empty) && state.showPlanInNotch && state.planRelayInstalled {
|
// Plan limits live inside the Claude / ChatGPT pills (AppGridView):
|
||||||
ClaudePlanHeaderPill(state: state)
|
// here they ran under the camera.
|
||||||
}
|
|
||||||
if (state.view == .overview || state.view == .empty), let w = state.codexWeekly {
|
|
||||||
ChatGPTLimitPill(window: w)
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
HStack(spacing: 14) {
|
HStack(spacing: 14) {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
|||||||
@@ -176,9 +176,9 @@ enum IslandConst {
|
|||||||
|
|
||||||
static let viewLayouts: [IslandView: ViewLayout] = [
|
static let viewLayouts: [IslandView: ViewLayout] = [
|
||||||
// Home is the reference: height 150
|
// 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
|
// 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),
|
.approval: ViewLayout(height: 160, botX: 62, botY: nil, botDiameter: 56, agentMode: .column),
|
||||||
.question: 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),
|
.error: ViewLayout(height: 160, botX: 62, botY: nil, botDiameter: 58, agentMode: .column),
|
||||||
|
|||||||
Reference in New Issue
Block a user