Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Dayflow/Dayflow/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// Try to start recording, but handle permission failures gracefully
Task { [weak self] in
guard let self else { return }
guard ScreenRecordingPermissionNotice.isGranted else {
let hasScreenRecordingPermission =
ScreenRecordingPermissionNotice.isGranted || CGRequestScreenCaptureAccess()
guard hasScreenRecordingPermission else {
await MainActor.run {
AppState.shared.setRecording(
false,
Expand Down
3 changes: 3 additions & 0 deletions Dayflow/Dayflow/App/DayflowApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,9 @@ extension Notification.Name {
static let navigateToDaily = Notification.Name("navigateToDaily")
static let navigateToWeekly = Notification.Name("navigateToWeekly")
static let timelineDataUpdated = Notification.Name("timelineDataUpdated")
/// Posted when day-framing settings (boundary hour or start/end label mode)
/// change, so the timeline re-renders/reloads immediately without a restart.
static let dayFramingChanged = Notification.Name("dayFramingChanged")
static let showTimelineFailureToast = Notification.Name("showTimelineFailureToast")
static let showScreenRecordingPermissionNotice = Notification.Name(
"showScreenRecordingPermissionNotice")
Expand Down
36 changes: 31 additions & 5 deletions Dayflow/Dayflow/Core/AI/ChatPromptBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ private let chatServiceDayFormatter: DateFormatter = {
enum ChatPromptBuilder {
static func cliSystemPrompt() -> String {
let now = Date()
let currentDate = chatServiceLongDateFormatter.string(from: now)
let currentDate = chatServiceLongDateFormatter.string(from: currentTimelineLabelDate(now))
let currentTime = chatServiceTimeFormatter.string(from: now)
let dayFraming = dayFramingPrompt()

// Use full path (~ doesn't expand in sqlite3)
let dbPath = NSHomeDirectory() + "/Library/Application Support/Dayflow/chunks.sqlite"
Expand All @@ -44,7 +45,7 @@ enum ChatPromptBuilder {

Current date: \(currentDate)
Current time: \(currentTime)
Day boundary: Days start at 4:00 AM (not midnight)
\(dayFraming)

## DATA INTEGRITY (CRITICAL)

Expand Down Expand Up @@ -208,8 +209,9 @@ enum ChatPromptBuilder {

static func geminiSystemPrompt() -> String {
let now = Date()
let currentDate = chatServiceLongDateFormatter.string(from: now)
let currentDate = chatServiceLongDateFormatter.string(from: currentTimelineLabelDate(now))
let currentTime = chatServiceTimeFormatter.string(from: now)
let dayFraming = dayFramingPrompt()

let languageSection = languageOverrideSection()
let languageBlock: String
Expand All @@ -224,7 +226,7 @@ enum ChatPromptBuilder {

Current date: \(currentDate)
Current time: \(currentTime)
Day boundary: Days start at 4:00 AM (not midnight). "Yesterday" means the previous 4 AM–4 AM window.
\(dayFraming)

---

Expand Down Expand Up @@ -350,8 +352,32 @@ enum ChatPromptBuilder {
"""
}

private static func currentTimelineLabelDate(_ now: Date) -> Date {
timelineDisplayDate(from: now, now: now).timelineLabelDate()
}

private static func dayFramingPrompt() -> String {
let boundaryHour = DayBoundaryPreferences.boundaryHour
let boundaryTime = chatServiceTimeFormatter.string(
from: Calendar.current.date(
bySettingHour: boundaryHour, minute: 0, second: 0, of: Date()) ?? Date()
)
let window = "\(boundaryTime)-to-\(boundaryTime)"

if DayLabelPreferences.usesEndDate {
return """
Day framing: Logical days run \(window) and are labeled by their END date. The database `day` column remains keyed by the START date, so subtract one calendar day from a user-facing date before filtering `day`. "Today" and "yesterday" refer to consecutive labeled logical-day windows.
"""
}

return """
Day framing: Logical days run \(window) and are labeled by their START date. The database `day` column uses the same date label. "Today" and "yesterday" refer to consecutive logical-day windows.
"""
}

private static func todayDate() -> String {
chatServiceDayFormatter.string(from: Date())
let now = Date()
return chatServiceDayFormatter.string(from: timelineDisplayDate(from: now, now: now))
}

private static func categoryColorsSection() -> String {
Expand Down
15 changes: 12 additions & 3 deletions Dayflow/Dayflow/Core/AI/ChatToolExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ final class ChatToolExecutor {
return .failure(tool: .fetchTimeline, error: "Invalid date format. Use YYYY-MM-DD.")
}

let cards = StorageManager.shared.fetchTimelineCards(forDay: dateString)
guard let storageDayString = storageDayString(forLabel: dateString) else {
return .failure(tool: .fetchTimeline, error: "Could not parse date")
}
let cards = StorageManager.shared.fetchTimelineCards(forDay: storageDayString)

if cards.isEmpty {
return ChatToolResult(
Expand Down Expand Up @@ -263,11 +266,12 @@ final class ChatToolExecutor {

/// Convert "YYYY-MM-DD" to day boundaries (4 AM to 4 AM next day)
private func dayBoundaries(for dateString: String) -> (start: Date, end: Date)? {
guard let date = chatToolDayFormatter.date(from: dateString) else { return nil }
guard let labelDate = chatToolDayFormatter.date(from: dateString) else { return nil }
let date = labelDate.timelineDateFromLabel()

let calendar = Calendar.current
var startComponents = calendar.dateComponents([.year, .month, .day], from: date)
startComponents.hour = 4
startComponents.hour = DayBoundaryPreferences.boundaryHour
startComponents.minute = 0
startComponents.second = 0

Expand All @@ -280,6 +284,11 @@ final class ChatToolExecutor {
return (dayStart, dayEnd)
}

private func storageDayString(forLabel dateString: String) -> String? {
guard let labelDate = chatToolDayFormatter.date(from: dateString) else { return nil }
return chatToolDayFormatter.string(from: labelDate.timelineDateFromLabel())
}

/// Format date for human-readable display
private func formatDateForDisplay(_ dateString: String) -> String {
guard let date = chatToolDayFormatter.date(from: dateString) else { return dateString }
Expand Down
2 changes: 1 addition & 1 deletion Dayflow/Dayflow/Core/AI/DailyRecapScheduler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ final class DailyRecapScheduler: @unchecked Sendable {
let now = Date()
let hour = Calendar.current.component(.hour, from: now)

guard hour >= 4 else {
guard hour >= DayBoundaryPreferences.boundaryHour else {
return
}

Expand Down
22 changes: 15 additions & 7 deletions Dayflow/Dayflow/Core/AI/GeminiDirectProvider+DashboardTools.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ extension GeminiDirectProvider {
let dateRange = try parseDashboardDateRange(args: args)

let cards: [TimelineCard]
if dateRange.mode == "date", let date = dateRange.date {
cards = StorageManager.shared.fetchTimelineCards(forDay: date)
if dateRange.mode == "date" {
let storageDay = dashboardDateFormatter.string(from: dateRange.from)
cards = StorageManager.shared.fetchTimelineCards(forDay: storageDay)
} else {
cards = StorageManager.shared.fetchTimelineCardsByTimeRange(
from: dateRange.from, to: dateRange.to)
Expand All @@ -65,7 +66,7 @@ extension GeminiDirectProvider {

var items: [[String: Any]] = limitedCards.map { card in
var item: [String: Any] = [
"day": card.day,
"day": dashboardLabelDayString(forStorageDay: card.day),
"startTime": card.startTimestamp,
"endTime": card.endTimestamp,
"title": card.title,
Expand Down Expand Up @@ -187,7 +188,8 @@ extension GeminiDirectProvider {
for observation in observations {
let start = Date(timeIntervalSince1970: TimeInterval(observation.startTs))
let end = Date(timeIntervalSince1970: TimeInterval(observation.endTs))
let day = start.getDayInfoFor4AMBoundary().dayString
let storageDay = start.getDayInfoFor4AMBoundary().dayString
let day = dashboardLabelDayString(forStorageDay: storageDay)
let item: [String: Any] = [
"startTime": dashboardTimeFormatter.string(from: start),
"endTime": dashboardTimeFormatter.string(from: end),
Expand Down Expand Up @@ -265,13 +267,14 @@ extension GeminiDirectProvider {
}

func dashboardDayBounds(for dateString: String) throws -> (start: Date, end: Date) {
guard let dayDate = dashboardDateFormatter.date(from: dateString) else {
guard let labelDate = dashboardDateFormatter.date(from: dateString) else {
throw DashboardToolArgError.invalidDate(dateString)
}
let dayDate = labelDate.timelineDateFromLabel()

let calendar = Calendar.current
var startComponents = calendar.dateComponents([.year, .month, .day], from: dayDate)
startComponents.hour = 4
startComponents.hour = DayBoundaryPreferences.boundaryHour
startComponents.minute = 0
startComponents.second = 0
guard let dayStart = calendar.date(from: startComponents) else {
Expand All @@ -282,7 +285,7 @@ extension GeminiDirectProvider {
throw DashboardToolArgError.invalidDate(dateString)
}
var endComponents = calendar.dateComponents([.year, .month, .day], from: nextDay)
endComponents.hour = 4
endComponents.hour = DayBoundaryPreferences.boundaryHour
endComponents.minute = 0
endComponents.second = 0
guard let dayEnd = calendar.date(from: endComponents) else {
Expand All @@ -292,6 +295,11 @@ extension GeminiDirectProvider {
return (dayStart, dayEnd)
}

func dashboardLabelDayString(forStorageDay dayString: String) -> String {
guard let storageDate = dashboardDateFormatter.date(from: dayString) else { return dayString }
return dashboardDateFormatter.string(from: storageDate.timelineLabelDate())
}

func dashboardFetchDateDescription(for dateRange: DashboardDateRange) -> String {
if dateRange.mode == "date", let date = dateRange.date {
return formattedDashboardSingleDate(date) ?? date
Expand Down
5 changes: 3 additions & 2 deletions Dayflow/Dayflow/Core/Recording/JournalDayManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,15 @@ final class JournalDayManager: ObservableObject {

let formatter = DateFormatter()

let labelDate = date.timelineLabelDate()
if isToday {
// "Today, November 24" - no day of week needed
formatter.dateFormat = "MMMM d"
return "Today, \(formatter.string(from: date))"
return "Today, \(formatter.string(from: labelDate))"
} else {
// "Monday, November 24" - day of week helps orient for past days
formatter.dateFormat = "EEEE, MMMM d"
return formatter.string(from: date)
return formatter.string(from: labelDate)
}
}

Expand Down
Loading