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
91 changes: 49 additions & 42 deletions Dayflow/Dayflow/Core/AI/ChatService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: <short line>
Style: <short line>
```

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: <short line>
Style: <short line>
```

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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 2 additions & 1 deletion Dayflow/Dayflow/Core/AI/DashboardChatMemoryStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 7 additions & 4 deletions Dayflow/Dayflow/Core/Analysis/AnalysisManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
10 changes: 7 additions & 3 deletions Dayflow/Dayflow/Core/Recording/StorageManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 }

Expand Down
17 changes: 10 additions & 7 deletions Dayflow/Dayflow/Views/UI/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -934,7 +936,7 @@ struct ChatView: View {
AnalyticsService.shared.capture(
"chat_memory_manual_saved",
[
"chars": storedMemoryBlob.count,
"chars": storedMemoryBlob.count
])
}

Expand Down Expand Up @@ -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
}
Expand Down
17 changes: 9 additions & 8 deletions Dayflow/DayflowTests/TimeParsingTests.swift
Original file line number Diff line number Diff line change
@@ -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"))
}
}
60 changes: 30 additions & 30 deletions Dayflow/DayflowUITests/DayflowUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
}
34 changes: 17 additions & 17 deletions Dayflow/DayflowUITests/DayflowUITestsLaunchTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading