diff --git a/Dayflow/Dayflow/Core/AI/ChatService.swift b/Dayflow/Dayflow/Core/AI/ChatService.swift index 67306c28..b285e711 100644 --- a/Dayflow/Dayflow/Core/AI/ChatService.swift +++ b/Dayflow/Dayflow/Core/AI/ChatService.swift @@ -435,7 +435,9 @@ final class ChatService: ObservableObject { // MARK: - Prompt Building - private func buildChatRequest(provider: DashboardChatProvider, isResume: Bool) -> DashboardChatRequest { + private func buildChatRequest(provider: DashboardChatProvider, isResume: Bool) + -> DashboardChatRequest + { switch provider { case .gemini: currentSessionId = nil @@ -899,43 +901,43 @@ final class ChatService: ObservableObject { private func metadataContractSection() -> String { """ - ## MEMORY CONTRACT (REQUIRED) - - You may receive an existing section called "## User Memory". - This memory is ONLY for durable assistant behavior, not a running life log. - Keep only these two fields: - - Profile: stable user context relevant to this app (very short) - - Style: response format/tone preferences (very short) - - DO NOT store: - - Contact names/relationships - - Travel plans or itineraries - - Investment/trading ideas - - One-off tasks, daily events, or temporary interests - - Secrets, passwords, tokens, API keys, or sensitive details - - ## RESPONSE FORMAT (REQUIRED) - - At the END of every response, include exactly these blocks in order: - - ```suggestions - ["Question 1", "Question 2", "Question 3"] - ``` - - ```memory - Profile: - Style: - ``` - - Rules: - - Include 3-4 suggestions. - - Frame each suggestion as a question the user could ask Dayflow. - - Every suggestion must be answerable using only the user's recorded Dayflow activity/data. - - Do not suggest anything that requires external information, browsing, recommendations, planning help, outreach, document creation, or any other action outside analyzing the existing data. - - Keep suggestion text short (<50 chars). - - Do not add any other metadata blocks. - - Do not mention the memory block in normal prose. - """ + ## MEMORY CONTRACT (REQUIRED) + + You may receive an existing section called "## User Memory". + This memory is ONLY for durable assistant behavior, not a running life log. + Keep only these two fields: + - Profile: stable user context relevant to this app (very short) + - Style: response format/tone preferences (very short) + + DO NOT store: + - Contact names/relationships + - Travel plans or itineraries + - Investment/trading ideas + - One-off tasks, daily events, or temporary interests + - Secrets, passwords, tokens, API keys, or sensitive details + + ## RESPONSE FORMAT (REQUIRED) + + At the END of every response, include exactly these blocks in order: + + ```suggestions + ["Question 1", "Question 2", "Question 3"] + ``` + + ```memory + Profile: + Style: + ``` + + Rules: + - Include 3-4 suggestions. + - Frame each suggestion as a question the user could ask Dayflow. + - Every suggestion must be answerable using only the user's recorded Dayflow activity/data. + - Do not suggest anything that requires external information, browsing, recommendations, planning help, outreach, document creation, or any other action outside analyzing the existing data. + - Keep suggestion text short (<50 chars). + - Do not add any other metadata blocks. + - Do not mention the memory block in normal prose. + """ } // MARK: - Helpers @@ -972,7 +974,8 @@ final class ChatService: ObservableObject { private func toolSummary(command: String, output: String, exitCode: Int?) -> String { let lowercased = command.lowercased() let base = - lowercased.contains("sqlite3") ? "Database query" + lowercased.contains("sqlite3") + ? "Database query" : (lowercased.contains("fetchtimeline") || lowercased.contains("fetchobservations") ? "Data fetch" : "Tool") if let exitCode, exitCode != 0 { @@ -1017,7 +1020,9 @@ final class ChatService: ObservableObject { var suggestions: [String] = [] var memoryBlob: String? - if let (stripped, rawSuggestions) = extractTaggedBlock(from: workingText, pattern: suggestionPattern) { + if let (stripped, rawSuggestions) = extractTaggedBlock( + from: workingText, pattern: suggestionPattern) + { workingText = stripped let jsonString = rawSuggestions.trimmingCharacters(in: .whitespacesAndNewlines) if let data = jsonString.data(using: .utf8), @@ -1161,7 +1166,8 @@ final class ChatService: ObservableObject { } private func normalizeAutoMemoryBlob(_ raw: String) -> String? { - let lines = raw + let lines = + raw .replacingOccurrences(of: "\r\n", with: "\n") .replacingOccurrences(of: "\r", with: "\n") .split(separator: "\n") @@ -1197,7 +1203,8 @@ final class ChatService: ObservableObject { extension ChatService { /// Check if an LLM provider is configured static var isProviderConfigured: Bool { - let geminiKey = KeychainManager.shared.retrieve(for: "gemini")? + let geminiKey = + KeychainManager.shared.retrieve(for: "gemini")? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return !geminiKey.isEmpty || CLIDetector.isInstalled(.codex) || CLIDetector.isInstalled(.claude) } diff --git a/Dayflow/Dayflow/Core/AI/DashboardChatMemoryStore.swift b/Dayflow/Dayflow/Core/AI/DashboardChatMemoryStore.swift index a003e8e4..6fb1b828 100644 --- a/Dayflow/Dayflow/Core/AI/DashboardChatMemoryStore.swift +++ b/Dayflow/Dayflow/Core/AI/DashboardChatMemoryStore.swift @@ -38,7 +38,8 @@ enum DashboardChatMemoryStore { } private static func normalize(_ input: String) -> String { - var text = input + var text = + input .replacingOccurrences(of: "\r\n", with: "\n") .replacingOccurrences(of: "\r", with: "\n") .trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/Dayflow/Dayflow/Core/Analysis/AnalysisManager.swift b/Dayflow/Dayflow/Core/Analysis/AnalysisManager.swift index d7e7e354..4254b48d 100644 --- a/Dayflow/Dayflow/Core/Analysis/AnalysisManager.swift +++ b/Dayflow/Dayflow/Core/Analysis/AnalysisManager.swift @@ -790,9 +790,10 @@ final class AnalysisManager: AnalysisManaging { let mergeCandidate = mergeCandidateForIdleBatch(startingAt: batchStart) let mergeGapSeconds = mergeCandidate.map { max(0, first.capturedAt - $0.endTs) } - let replacementStart = mergeCandidate.map { - Date(timeIntervalSince1970: TimeInterval($0.startTs)) - } ?? batchStart + let replacementStart = + mergeCandidate.map { + Date(timeIntervalSince1970: TimeInterval($0.startTs)) + } ?? batchStart let idleMetadata = IdleCardMetadata( classifierVersion: assessment.classifierVersion, inputCoverageRatio: assessment.coverageRatio, @@ -883,7 +884,9 @@ final class AnalysisManager: AnalysisManaging { return true } - private func mergeCandidateForIdleBatch(startingAt batchStart: Date) -> TimelineCardWithTimestamps? { + private func mergeCandidateForIdleBatch(startingAt batchStart: Date) + -> TimelineCardWithTimestamps? + { guard let previousCard = store.fetchLastTimelineCard(endingBefore: batchStart) else { return nil } diff --git a/Dayflow/Dayflow/Core/Recording/StorageManager.swift b/Dayflow/Dayflow/Core/Recording/StorageManager.swift index 77ea73fb..c21dda0a 100644 --- a/Dayflow/Dayflow/Core/Recording/StorageManager.swift +++ b/Dayflow/Dayflow/Core/Recording/StorageManager.swift @@ -517,7 +517,9 @@ final class StorageManager: StorageManaging, @unchecked Sendable { func markExecutionStarted(id: Int64) { lock.lock() defer { lock.unlock() } - guard var operation = activeOperations[id], operation.executionStartedAt == nil else { return } + guard var operation = activeOperations[id], operation.executionStartedAt == nil else { + return + } operation.executionStartedAt = CFAbsoluteTimeGetCurrent() activeOperations[id] = operation } @@ -559,10 +561,12 @@ final class StorageManager: StorageManaging, @unchecked Sendable { .sorted { $0.startedAt < $1.startedAt } let cutoff = now - recentWindowSeconds - let recentReads = recentOperations + let recentReads = + recentOperations .filter { $0.kind == .read && $0.completedAt >= cutoff } .sorted { $0.completedAt > $1.completedAt } - let recentWrites = recentOperations + let recentWrites = + recentOperations .filter { $0.kind == .write && $0.completedAt >= cutoff } .sorted { $0.completedAt > $1.completedAt } diff --git a/Dayflow/Dayflow/Views/UI/ChatView.swift b/Dayflow/Dayflow/Views/UI/ChatView.swift index 86daabe0..11988dce 100644 --- a/Dayflow/Dayflow/Views/UI/ChatView.swift +++ b/Dayflow/Dayflow/Views/UI/ChatView.swift @@ -571,11 +571,13 @@ struct ChatView: View { VStack(spacing: 16) { // Runtime requirement section VStack(spacing: 12) { - Image(systemName: anyRuntimeAvailable ? "checkmark.circle.fill" : "bolt.horizontal.circle") - .font(.system(size: 32)) - .foregroundColor(anyRuntimeAvailable ? Color(hex: "34C759") : Color(hex: "F98D3D")) - .contentTransition(.symbolEffect(.replace)) - .animation(.easeOut(duration: 0.2), value: anyRuntimeAvailable) + Image( + systemName: anyRuntimeAvailable ? "checkmark.circle.fill" : "bolt.horizontal.circle" + ) + .font(.system(size: 32)) + .foregroundColor(anyRuntimeAvailable ? Color(hex: "34C759") : Color(hex: "F98D3D")) + .contentTransition(.symbolEffect(.replace)) + .animation(.easeOut(duration: 0.2), value: anyRuntimeAvailable) if anyRuntimeAvailable { Text("Gemini key or CLI runtime detected") @@ -934,7 +936,7 @@ struct ChatView: View { AnalyticsService.shared.capture( "chat_memory_manual_saved", [ - "chars": storedMemoryBlob.count, + "chars": storedMemoryBlob.count ]) } @@ -1025,7 +1027,8 @@ struct ChatView: View { } private func isGeminiConfigured() -> Bool { - let key = KeychainManager.shared.retrieve(for: "gemini")? + let key = + KeychainManager.shared.retrieve(for: "gemini")? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return !key.isEmpty } diff --git a/Dayflow/DayflowTests/TimeParsingTests.swift b/Dayflow/DayflowTests/TimeParsingTests.swift index f9ad74ea..aab420f6 100644 --- a/Dayflow/DayflowTests/TimeParsingTests.swift +++ b/Dayflow/DayflowTests/TimeParsingTests.swift @@ -1,14 +1,15 @@ import XCTest + @testable import Dayflow final class TimeParsingTests: XCTestCase { - func testValidTimes() { - XCTAssertEqual(parseTimeHMMA(timeString: "9:30 AM"), 9 * 60 + 30) - XCTAssertEqual(parseTimeHMMA(timeString: "11:59 PM"), 23 * 60 + 59) - } + func testValidTimes() { + XCTAssertEqual(parseTimeHMMA(timeString: "9:30 AM"), 9 * 60 + 30) + XCTAssertEqual(parseTimeHMMA(timeString: "11:59 PM"), 23 * 60 + 59) + } - func testInvalidTimes() { - XCTAssertNil(parseTimeHMMA(timeString: "")) - XCTAssertNil(parseTimeHMMA(timeString: "invalid")) - } + func testInvalidTimes() { + XCTAssertNil(parseTimeHMMA(timeString: "")) + XCTAssertNil(parseTimeHMMA(timeString: "invalid")) + } } diff --git a/Dayflow/DayflowUITests/DayflowUITests.swift b/Dayflow/DayflowUITests/DayflowUITests.swift index 78ae952b..bd18c2a9 100644 --- a/Dayflow/DayflowUITests/DayflowUITests.swift +++ b/Dayflow/DayflowUITests/DayflowUITests.swift @@ -7,35 +7,35 @@ import XCTest final class DayflowUITests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it's important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - @MainActor - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - @MainActor - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it's important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } } + } } diff --git a/Dayflow/DayflowUITests/DayflowUITestsLaunchTests.swift b/Dayflow/DayflowUITests/DayflowUITestsLaunchTests.swift index 6303753d..05446a59 100644 --- a/Dayflow/DayflowUITests/DayflowUITestsLaunchTests.swift +++ b/Dayflow/DayflowUITests/DayflowUITestsLaunchTests.swift @@ -7,25 +7,25 @@ import XCTest final class DayflowUITestsLaunchTests: XCTestCase { - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } - override func setUpWithError() throws { - continueAfterFailure = false - } + override func setUpWithError() throws { + continueAfterFailure = false + } - @MainActor - func testLaunch() throws { - let app = XCUIApplication() - app.launch() + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } }