Skip to content

Commit 090653b

Browse files
authored
v1.8.4: collapsible insights, star glow system, and menu bar polish (#103)
Redesigned Insights as a collapsible section with 5 rows (Today, All Time, Longest, Tools, Period). Menu bar icon now uses severity-based glow: none below 80%, static star glow at 80-95%, breathing at 95%+, starburst rays when throttled. Text-first layout with 12pt font matches macOS battery. Added two-tap logout confirmation, "Updated Xm ago" footer timestamp, time-proximity boost for auto mode urgency, and shared CollapsibleSectionHeader/ FooterLink/RefreshButton components. CI skips doc-only and draft PR builds. Co-authored-by: KyleNesium <22541778+KyleNesium@users.noreply.github.com>
1 parent 9dce604 commit 090653b

32 files changed

+695
-239
lines changed

.github/workflows/ci.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,20 @@ name: CI
33
on:
44
push:
55
branches: [main]
6+
paths-ignore:
7+
- '**.md'
8+
- 'spec/**'
9+
- 'LICENSE'
10+
- '.github/*.md'
611
pull_request:
712
branches: [main]
13+
types: [opened, synchronize, ready_for_review]
14+
paths-ignore:
15+
- '**.md'
16+
- 'spec/**'
17+
- 'LICENSE'
18+
- '.github/*.md'
19+
workflow_dispatch:
820

921
concurrency:
1022
group: ${{ github.workflow }}-${{ github.ref }}
@@ -13,6 +25,7 @@ concurrency:
1325
jobs:
1426
build-and-test:
1527
name: Build, Test & Verify
28+
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.draft != true
1629
runs-on: macos-15
1730
timeout-minutes: 10
1831
steps:

AIBattery/Info.plist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
<key>CFBundlePackageType</key>
1818
<string>APPL</string>
1919
<key>CFBundleShortVersionString</key>
20-
<string>1.8.3</string>
20+
<string>1.8.4</string>
2121
<key>CFBundleVersion</key>
22-
<string>1.8.3</string>
22+
<string>1.8.4</string>
2323
<key>LSMinimumSystemVersion</key>
2424
<string>13.0</string>
2525
<key>CFBundleIconFile</key>

AIBattery/Models/UsageSnapshot.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,34 @@ struct UsageSnapshot {
6767
}
6868

6969
// Tier 3: Urgency-normalized — each mode's thresholds map to a shared 0–1 scale.
70+
// Rate limit modes get a time-proximity boost when estimated time to limit is short:
71+
// a window hitting its cap in minutes is more urgent than one with hours left.
7072
// Ties broken by actionability: context > 5h > 7d.
7173
let scored: [(MetricMode, Double)] = MetricMode.allCases.map { mode in
72-
(mode, Self.urgencyScore(percent: percent(for: mode), mode: mode))
74+
var score = Self.urgencyScore(percent: percent(for: mode), mode: mode)
75+
if let rl = rateLimits {
76+
let window = mode == .sevenDay ? RateLimitUsage.sevenDayWindow : RateLimitUsage.fiveHourWindow
77+
if mode == .fiveHour || mode == .sevenDay,
78+
let ttl = rl.estimatedTimeToLimit(for: window) {
79+
// Boost: <30min → +0.20, <2h → +0.10, <6h → +0.05
80+
let boost: Double
81+
switch ttl {
82+
case ..<(30 * 60): boost = 0.20
83+
case ..<(2 * 3600): boost = 0.10
84+
case ..<(6 * 3600): boost = 0.05
85+
default: boost = 0.0
86+
}
87+
score += boost
88+
}
89+
}
90+
return (mode, score)
7391
}
7492
let maxUrgency = scored.map(\.1).max() ?? 0
7593
// Among tied modes, prefer context > 5h > 7d (most actionable first)
7694
let tiePriority: [MetricMode] = [.contextHealth, .fiveHour, .sevenDay]
7795
return scored
7896
.filter { $0.1 == maxUrgency }
79-
.min { tiePriority.firstIndex(of: $0.0)! < tiePriority.firstIndex(of: $1.0)! }!
97+
.min { (tiePriority.firstIndex(of: $0.0) ?? .max) < (tiePriority.firstIndex(of: $1.0) ?? .max) }!
8098
.0
8199
}
82100

AIBattery/Utilities/DateFormatters.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,30 @@ enum DateFormatters {
3333
f.dateFormat = "MMM"
3434
return f
3535
}()
36+
37+
/// "Nov 6" — month + day, no year. Pinned to en_US_POSIX for deterministic output.
38+
static let rangeShort: DateFormatter = {
39+
let f = DateFormatter()
40+
f.locale = Locale(identifier: "en_US_POSIX")
41+
f.dateFormat = "MMM d"
42+
return f
43+
}()
44+
45+
/// "Nov 6, 2025" — month + day + year. Pinned to en_US_POSIX for deterministic output.
46+
static let rangeWithYear: DateFormatter = {
47+
let f = DateFormatter()
48+
f.locale = Locale(identifier: "en_US_POSIX")
49+
f.dateFormat = "MMM d, yyyy"
50+
return f
51+
}()
52+
53+
/// Formats a date range: same year → "Nov 6 – Mar 10, 2026", cross-year → "Dec 15, 2025 – Mar 10, 2026".
54+
static func formatDateRange(from start: Date, to end: Date) -> String {
55+
let cal = Calendar.current
56+
let sameYear = cal.component(.year, from: start) == cal.component(.year, from: end)
57+
if sameYear {
58+
return "\(rangeShort.string(from: start))\(rangeWithYear.string(from: end))"
59+
}
60+
return "\(rangeWithYear.string(from: start))\(rangeWithYear.string(from: end))"
61+
}
3662
}

AIBattery/Utilities/ThemeColors.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,28 @@ enum ThemeColors {
4949
/// the opaque light-mode background provides enough contrast.
5050
private static let deepOrange = Color(nsColor: .systemOrange)
5151

52+
/// System colors wrapped as SwiftUI Color — matches barNSColor exactly.
53+
private static let sysGreen = Color(nsColor: .systemGreen)
54+
private static let sysRed = Color(nsColor: .systemRed)
55+
private static let sysBlue = Color(nsColor: .systemBlue)
56+
private static let sysCyan = Color(nsColor: .systemTeal)
57+
private static let sysPurple = Color(nsColor: .systemPurple)
58+
5259
/// Color for a usage percentage (0–100).
5360
static func barColor(percent: Double) -> Color {
5461
if isColorblind {
5562
switch percent {
56-
case 0..<50: return .blue
57-
case 50..<80: return .cyan
63+
case 0..<50: return sysBlue
64+
case 50..<80: return sysCyan
5865
case 80..<95: return amber
59-
default: return .purple
66+
default: return sysPurple
6067
}
6168
}
6269
switch percent {
63-
case 0..<50: return .green
70+
case 0..<50: return sysGreen
6471
case 50..<80: return gold
6572
case 80..<95: return deepOrange
66-
default: return .red
73+
default: return sysRed
6774
}
6875
}
6976

AIBattery/Utilities/UserDefaultsKeys.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ enum UserDefaultsKeys {
2424
static let contextCollapsed = "aibattery_contextCollapsed"
2525
static let tokensCollapsed = "aibattery_tokensCollapsed"
2626
static let activityCollapsed = "aibattery_activityCollapsed"
27+
static let insightsCollapsed = "aibattery_insightsCollapsed"
2728
/// Prefix for per-account token expiry timestamps (append account ID).
2829
static let tokenExpiresAtPrefix = "aibattery_expiresAt_"
2930
}

AIBattery/Views/ActivityChartView.swift

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,24 +65,11 @@ struct ActivityChartView: View {
6565
VStack(alignment: .leading, spacing: 6) {
6666
// Header with toggle
6767
HStack {
68-
Button {
69-
withAnimation(.easeInOut(duration: 0.2)) { collapsed.toggle() }
70-
} label: {
71-
HStack(spacing: 4) {
72-
Image(systemName: "chevron.right")
73-
.font(.system(size: 8, weight: .bold))
74-
.rotationEffect(.degrees(collapsed ? 0 : 90))
75-
.foregroundStyle(ThemeColors.tertiaryLabel)
76-
Text("Activity")
77-
.font(.subheadline.bold())
78-
.foregroundStyle(.primary)
79-
}
80-
.contentShape(Rectangle())
81-
}
82-
.buttonStyle(.plain)
83-
.accessibilityLabel("Activity, \(collapsed ? "collapsed" : "expanded")")
84-
.accessibilityHint(collapsed ? "Double-tap to expand" : "Double-tap to collapse")
85-
.help("Message activity over time")
68+
CollapsibleSectionHeader(
69+
title: "Activity",
70+
collapsed: $collapsed,
71+
tooltip: "Message activity over time"
72+
)
8673
Spacer()
8774
if collapsed, let snapshot, let change = changeVsYesterday(snapshot) {
8875
Text(change.symbol)
@@ -114,7 +101,7 @@ struct ActivityChartView: View {
114101
Image(systemName: "chart.line.flattrend.xyaxis")
115102
.font(.system(size: 14))
116103
.foregroundStyle(ThemeColors.tertiaryLabel)
117-
Text("No activity data")
104+
Text("No activity in \(mode.rawValue) window")
118105
.font(.caption2)
119106
.foregroundStyle(ThemeColors.tertiaryLabel)
120107
}
@@ -134,6 +121,7 @@ struct ActivityChartView: View {
134121
// Trend summary
135122
if !collapsed, let snapshot {
136123
trendSummary(snapshot)
124+
.accessibilityElement(children: .combine)
137125
}
138126
}
139127
.padding(.horizontal, 16)
@@ -438,7 +426,7 @@ struct ActivityChartView: View {
438426
.font(.system(.caption, design: .monospaced))
439427
.foregroundStyle(ThemeColors.caution)
440428
} else {
441-
Text("Throttled: 0")
429+
Text("Throttled: 0×")
442430
.font(.system(.caption, design: .monospaced))
443431
.foregroundStyle(ThemeColors.secondaryLabel)
444432
}

AIBattery/Views/AuthView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,10 @@ public struct AuthView: View {
9797
}
9898
.frame(maxWidth: .infinity, alignment: .leading)
9999

100-
TextField("Paste authorization code...", text: $authCode)
100+
TextField("Paste code...", text: $authCode)
101101
.textFieldStyle(.roundedBorder)
102102
.font(.system(.caption, design: .monospaced))
103+
.onSubmit(submitCode)
103104
.accessibilityLabel("Authorization code")
104105
.accessibilityHint("Paste the code from the browser")
105106

@@ -160,6 +161,7 @@ public struct AuthView: View {
160161
.buttonStyle(.plain)
161162
.font(.caption2)
162163
.foregroundStyle(.secondary)
164+
.keyboardShortcut("q", modifiers: .command)
163165
}
164166
Spacer()
165167
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import SwiftUI
2+
3+
/// Shared collapsible section header with rotating chevron and bold title.
4+
/// Used by Context Health, Tokens, Activity, and Insights sections.
5+
struct CollapsibleSectionHeader: View {
6+
let title: String
7+
@Binding var collapsed: Bool
8+
var tooltip: String = ""
9+
10+
var body: some View {
11+
Button {
12+
withAnimation(.easeInOut(duration: 0.2)) { collapsed.toggle() }
13+
} label: {
14+
HStack(spacing: 4) {
15+
Image(systemName: "chevron.right")
16+
.font(.system(size: 8, weight: .bold))
17+
.rotationEffect(.degrees(collapsed ? 0 : 90))
18+
.foregroundStyle(ThemeColors.tertiaryLabel)
19+
Text(title)
20+
.font(.subheadline.bold())
21+
.foregroundStyle(.primary)
22+
}
23+
.contentShape(Rectangle())
24+
}
25+
.buttonStyle(.plain)
26+
.help(tooltip)
27+
.accessibilityAddTraits(.isHeader)
28+
.accessibilityLabel("\(title), \(collapsed ? "collapsed" : "expanded")")
29+
.accessibilityHint(collapsed ? "Double-tap to expand" : "Double-tap to collapse")
30+
}
31+
}

AIBattery/Views/CopyableText.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@ struct CopyableModifier: ViewModifier {
5858
// Cancel any previous feedback timer
5959
feedbackTask?.cancel()
6060

61-
withAnimation(.easeOut(duration: 0.12)) {
61+
withAnimation(.easeOut(duration: 0.15)) {
6262
copied = true
6363
}
6464
feedbackTask = Task {
65-
try? await Task.sleep(nanoseconds: 1_200_000_000)
65+
try? await Task.sleep(nanoseconds: 1_500_000_000)
6666
guard !Task.isCancelled else { return }
67-
withAnimation(.easeIn(duration: 0.2)) {
67+
withAnimation(.easeIn(duration: 0.15)) {
6868
copied = false
6969
}
7070
}

0 commit comments

Comments
 (0)