Added a lot of functionality. Bugfixes and changes
This commit is contained in:
@@ -36,51 +36,109 @@ class ChatViewModel {
|
||||
var modelInfoTarget: ModelInfo? = nil
|
||||
var commandHistory: [String] = []
|
||||
var historyIndex: Int = 0
|
||||
var isAutoContinuing: Bool = false
|
||||
var autoContinueCountdown: Int = 0
|
||||
|
||||
// MARK: - Auto-Save Tracking
|
||||
|
||||
private var conversationStartTime: Date?
|
||||
private var lastMessageTime: Date?
|
||||
private var idleCheckTimer: Timer?
|
||||
|
||||
// MARK: - Private State
|
||||
|
||||
private var streamingTask: Task<Void, Never>?
|
||||
private var autoContinueTask: Task<Void, Never>?
|
||||
private let settings = SettingsService.shared
|
||||
private let providerRegistry = ProviderRegistry.shared
|
||||
|
||||
// Default system prompt
|
||||
// Default system prompt - generic for all models
|
||||
private let defaultSystemPrompt = """
|
||||
You are a helpful AI assistant. Follow these guidelines:
|
||||
You are a helpful AI assistant. Follow these core principles:
|
||||
|
||||
1. **Accuracy First**: Never invent information or make assumptions. If you're unsure about something, say so clearly.
|
||||
## CORE BEHAVIOR
|
||||
|
||||
2. **Ask for Clarification**: When a request is ambiguous or lacks necessary details, ask clarifying questions before proceeding.
|
||||
- **Accuracy First**: Never invent information. If unsure, say so clearly.
|
||||
- **Ask for Clarification**: When ambiguous, ask questions before proceeding.
|
||||
- **Be Direct**: Provide concise, relevant answers. No unnecessary preambles.
|
||||
- **Show Your Work**: If you use capabilities (tools, web search, etc.), demonstrate what you did.
|
||||
- **Complete Tasks Properly**: If you start something, finish it correctly.
|
||||
|
||||
3. **Be Honest About Limitations**: If you cannot or should not help with a request, explain why clearly and politely. Don't attempt tasks outside your capabilities.
|
||||
## FORMATTING
|
||||
|
||||
4. **Stay Grounded**: Base responses on facts and known information. Clearly distinguish between facts, informed opinions, and speculation.
|
||||
Always use Markdown formatting:
|
||||
- **Bold** for emphasis
|
||||
- Code blocks with language tags: ```python
|
||||
- Headings (##, ###) for structure
|
||||
- Lists for organization
|
||||
|
||||
5. **Be Direct**: Provide concise, relevant answers. Avoid unnecessary preambles or apologies.
|
||||
## HONESTY
|
||||
|
||||
6. **Use Markdown Formatting**: Always format your responses using standard Markdown syntax:
|
||||
- Use **bold** for emphasis
|
||||
- Use bullet points and numbered lists for organization
|
||||
- Use code blocks with language tags for code (e.g., ```python)
|
||||
- Use proper headings (##, ###) to structure long responses
|
||||
- If the user requests output in other formats (HTML, JSON, XML, etc.), wrap those in appropriate code blocks
|
||||
|
||||
7. **Break Down Complex Tasks**: When working with tools (file access, search, etc.), break complex tasks into smaller, manageable steps. If a task requires many operations:
|
||||
- Complete one logical step at a time
|
||||
- Present findings or progress after each step
|
||||
- Ask the user if you should continue to the next step
|
||||
- Be mindful of tool usage limits (typically 25-30 tool calls per request)
|
||||
|
||||
8. **Incremental Progress**: For large codebases or complex analyses, work incrementally. Don't try to explore everything at once. Focus on what's immediately relevant to the user's question.
|
||||
|
||||
Remember: It's better to ask questions or admit uncertainty than to provide incorrect or fabricated information.
|
||||
It's better to admit "I need more information" or "I cannot do that" than to fake completion or invent answers.
|
||||
"""
|
||||
|
||||
/// Builds the complete system prompt by combining default + custom
|
||||
// Tool-specific instructions (added when tools are available)
|
||||
private let toolUsageGuidelines = """
|
||||
|
||||
## TOOL USAGE (You have access to tools)
|
||||
|
||||
**CRITICAL: Never claim to have done something without actually using the tools.**
|
||||
|
||||
**BAD Examples (NEVER do this):**
|
||||
❌ "I've fixed the issue" → No tool calls shown
|
||||
❌ "The file has been updated" → Didn't actually use the tool
|
||||
❌ "Done!" → Claimed completion without evidence
|
||||
|
||||
**GOOD Examples (ALWAYS do this):**
|
||||
✅ [Uses tool silently] → Then explain what you did
|
||||
✅ Shows actual work through tool calls
|
||||
✅ If you can't complete: "I found the issue, but need clarification..."
|
||||
|
||||
**ENFORCEMENT:**
|
||||
- If a task requires using a tool → YOU MUST actually use it
|
||||
- Don't just describe what you would do - DO IT
|
||||
- The user can see your tool calls - they are proof you did the work
|
||||
- NEVER say "fixed" or "done" without showing tool usage
|
||||
|
||||
**EFFICIENCY:**
|
||||
- Some models have tool call limits (~25-30 per request)
|
||||
- Make comprehensive changes in fewer tool calls when possible
|
||||
- If approaching limits, tell the user you need to continue in phases
|
||||
- Don't use tools to "verify" your work unless asked - trust your edits
|
||||
|
||||
**METHODOLOGY:**
|
||||
1. Use the tool to gather information (if needed)
|
||||
2. Use the tool to make changes (if needed)
|
||||
3. Explain what you did and why
|
||||
|
||||
Don't narrate future actions ("Let me...") - just use the tools.
|
||||
"""
|
||||
|
||||
/// Builds the complete system prompt by combining default + conditional sections + custom
|
||||
private var effectiveSystemPrompt: String {
|
||||
// Check if user wants to replace the default prompt entirely (BYOP mode)
|
||||
if settings.customPromptMode == .replace,
|
||||
let customPrompt = settings.systemPrompt,
|
||||
!customPrompt.isEmpty {
|
||||
// BYOP: Use ONLY the custom prompt
|
||||
return customPrompt
|
||||
}
|
||||
|
||||
// Otherwise, build the prompt: default + conditional sections + custom (if append mode)
|
||||
var prompt = defaultSystemPrompt
|
||||
if let customPrompt = settings.systemPrompt, !customPrompt.isEmpty {
|
||||
|
||||
// Add tool-specific guidelines if MCP is enabled (tools are available)
|
||||
if mcpEnabled {
|
||||
prompt += toolUsageGuidelines
|
||||
}
|
||||
|
||||
// Append custom prompt if in append mode and custom prompt exists
|
||||
if settings.customPromptMode == .append,
|
||||
let customPrompt = settings.systemPrompt,
|
||||
!customPrompt.isEmpty {
|
||||
prompt += "\n\n---\n\nAdditional Instructions:\n" + customPrompt
|
||||
}
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
@@ -201,7 +259,8 @@ Remember: It's better to ask questions or admit uncertainty than to provide inco
|
||||
tokens: cleanText.estimateTokens(),
|
||||
cost: nil,
|
||||
timestamp: Date(),
|
||||
attachments: attachments
|
||||
attachments: attachments,
|
||||
modelId: selectedModel?.id
|
||||
)
|
||||
|
||||
messages.append(userMessage)
|
||||
@@ -215,6 +274,11 @@ Remember: It's better to ask questions or admit uncertainty than to provide inco
|
||||
// Clear input
|
||||
inputText = ""
|
||||
|
||||
// Check auto-save triggers in background
|
||||
Task {
|
||||
await checkAutoSaveTriggersAfterMessage(cleanText)
|
||||
}
|
||||
|
||||
// Generate real AI response
|
||||
generateAIResponse(to: cleanText, attachments: userMessage.attachments)
|
||||
}
|
||||
@@ -223,8 +287,56 @@ Remember: It's better to ask questions or admit uncertainty than to provide inco
|
||||
streamingTask?.cancel()
|
||||
streamingTask = nil
|
||||
isGenerating = false
|
||||
cancelAutoContinue() // Also cancel any pending auto-continue
|
||||
}
|
||||
|
||||
|
||||
func startAutoContinue() {
|
||||
isAutoContinuing = true
|
||||
autoContinueCountdown = 5
|
||||
|
||||
autoContinueTask = Task { @MainActor in
|
||||
// Countdown from 5 to 1
|
||||
for i in (1...5).reversed() {
|
||||
if Task.isCancelled {
|
||||
isAutoContinuing = false
|
||||
return
|
||||
}
|
||||
autoContinueCountdown = i
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
}
|
||||
|
||||
// Continue the task
|
||||
isAutoContinuing = false
|
||||
autoContinueCountdown = 0
|
||||
|
||||
let continuePrompt = "Please continue from where you left off."
|
||||
|
||||
// Add user message
|
||||
let userMessage = Message(
|
||||
role: .user,
|
||||
content: continuePrompt,
|
||||
tokens: nil,
|
||||
cost: nil,
|
||||
timestamp: Date(),
|
||||
attachments: nil,
|
||||
responseTime: nil,
|
||||
wasInterrupted: false,
|
||||
modelId: selectedModel?.id
|
||||
)
|
||||
messages.append(userMessage)
|
||||
|
||||
// Continue generation
|
||||
generateAIResponse(to: continuePrompt, attachments: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func cancelAutoContinue() {
|
||||
autoContinueTask?.cancel()
|
||||
autoContinueTask = nil
|
||||
isAutoContinuing = false
|
||||
autoContinueCountdown = 0
|
||||
}
|
||||
|
||||
func clearChat() {
|
||||
messages.removeAll()
|
||||
sessionStats.reset()
|
||||
@@ -444,6 +556,7 @@ Remember: It's better to ask questions or admit uncertainty than to provide inco
|
||||
cost: nil,
|
||||
timestamp: Date(),
|
||||
attachments: nil,
|
||||
modelId: modelId,
|
||||
isStreaming: true
|
||||
)
|
||||
messageId = assistantMessage.id
|
||||
@@ -455,9 +568,9 @@ Remember: It's better to ask questions or admit uncertainty than to provide inco
|
||||
// Only include messages up to (but not including) the streaming assistant message
|
||||
var messagesToSend = Array(messages.dropLast()) // Remove the empty assistant message
|
||||
|
||||
// Web search via our WebSearchService (skip Anthropic — uses native search tool)
|
||||
// Web search via our WebSearchService
|
||||
// Append results to last user message content (matching Python oAI approach)
|
||||
if onlineMode && currentProvider != .anthropic && currentProvider != .openrouter && !messagesToSend.isEmpty {
|
||||
if onlineMode && currentProvider != .openrouter && !messagesToSend.isEmpty {
|
||||
if let lastUserIdx = messagesToSend.lastIndex(where: { $0.role == .user }) {
|
||||
Log.search.info("Running web search for \(currentProvider.displayName)")
|
||||
let results = await WebSearchService.shared.search(query: messagesToSend[lastUserIdx].content)
|
||||
@@ -490,7 +603,7 @@ Remember: It's better to ask questions or admit uncertainty than to provide inco
|
||||
// Image generation: use non-streaming request
|
||||
// Image models don't reliably support streaming
|
||||
if let index = messages.firstIndex(where: { $0.id == messageId }) {
|
||||
messages[index].content = "Generating image..."
|
||||
messages[index].content = ThinkingVerbs.random()
|
||||
}
|
||||
|
||||
let nonStreamRequest = ChatRequest(
|
||||
@@ -810,9 +923,9 @@ Remember: It's better to ask questions or admit uncertainty than to provide inco
|
||||
? messages.filter { $0.role != .system }
|
||||
: [messages.last(where: { $0.role == .user })].compactMap { $0 }
|
||||
|
||||
// Web search via our WebSearchService (skip Anthropic — uses native search tool)
|
||||
// Web search via our WebSearchService
|
||||
// Append results to last user message content (matching Python oAI approach)
|
||||
if onlineMode && currentProvider != .anthropic && currentProvider != .openrouter {
|
||||
if onlineMode && currentProvider != .openrouter {
|
||||
if let lastUserIdx = messagesToSend.lastIndex(where: { $0.role == .user }) {
|
||||
Log.search.info("Running web search for tool-aware path (\(currentProvider.displayName))")
|
||||
let results = await WebSearchService.shared.search(query: messagesToSend[lastUserIdx].content)
|
||||
@@ -833,9 +946,10 @@ Remember: It's better to ask questions or admit uncertainty than to provide inco
|
||||
["role": msg.role.rawValue, "content": msg.content]
|
||||
}
|
||||
|
||||
let maxIterations = 5
|
||||
let maxIterations = 10 // Increased from 5 to reduce hitting client-side limit
|
||||
var finalContent = ""
|
||||
var totalUsage: ChatResponse.Usage?
|
||||
var hitIterationLimit = false // Track if we exited due to hitting the limit
|
||||
|
||||
for iteration in 0..<maxIterations {
|
||||
if Task.isCancelled {
|
||||
@@ -908,6 +1022,7 @@ Remember: It's better to ask questions or admit uncertainty than to provide inco
|
||||
|
||||
// If this was the last iteration, note it
|
||||
if iteration == maxIterations - 1 {
|
||||
hitIterationLimit = true // We're exiting with pending tool calls
|
||||
finalContent = response.content.isEmpty
|
||||
? "[Tool loop reached maximum iterations]"
|
||||
: response.content
|
||||
@@ -929,7 +1044,8 @@ Remember: It's better to ask questions or admit uncertainty than to provide inco
|
||||
timestamp: Date(),
|
||||
attachments: nil,
|
||||
responseTime: responseTime,
|
||||
wasInterrupted: wasCancelled
|
||||
wasInterrupted: wasCancelled,
|
||||
modelId: modelId
|
||||
)
|
||||
messages.append(assistantMessage)
|
||||
|
||||
@@ -950,6 +1066,11 @@ Remember: It's better to ask questions or admit uncertainty than to provide inco
|
||||
isGenerating = false
|
||||
streamingTask = nil
|
||||
|
||||
// If we hit the iteration limit and weren't cancelled, start auto-continue
|
||||
if hitIterationLimit && !wasCancelled {
|
||||
startAutoContinue()
|
||||
}
|
||||
|
||||
} catch {
|
||||
let responseTime = Date().timeIntervalSince(startTime)
|
||||
|
||||
@@ -963,7 +1084,8 @@ Remember: It's better to ask questions or admit uncertainty than to provide inco
|
||||
content: "",
|
||||
timestamp: Date(),
|
||||
responseTime: responseTime,
|
||||
wasInterrupted: true
|
||||
wasInterrupted: true,
|
||||
modelId: modelId
|
||||
)
|
||||
messages.append(assistantMessage)
|
||||
} else {
|
||||
@@ -1079,4 +1201,283 @@ Remember: It's better to ask questions or admit uncertainty than to provide inco
|
||||
showSystemMessage("Export failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Auto-Save & Background Summarization
|
||||
|
||||
/// Summarize the current conversation in the background (hidden from user)
|
||||
/// Returns a 3-5 word title, or nil on failure
|
||||
func summarizeConversationInBackground() async -> String? {
|
||||
// Need at least a few messages to summarize
|
||||
let chatMessages = messages.filter { $0.role == .user || $0.role == .assistant }
|
||||
guard chatMessages.count >= 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get current provider
|
||||
guard let provider = providerRegistry.getCurrentProvider() else {
|
||||
Log.ui.warning("Cannot summarize: no provider configured")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let modelId = selectedModel?.id else {
|
||||
Log.ui.warning("Cannot summarize: no model selected")
|
||||
return nil
|
||||
}
|
||||
|
||||
Log.ui.info("Background summarization: model=\(modelId), messages=\(chatMessages.count)")
|
||||
|
||||
do {
|
||||
// Simplified summarization prompt
|
||||
let summaryPrompt = "Create a brief 3-5 word title for this conversation. Just the title, nothing else."
|
||||
|
||||
// Build chat request with just the last few messages for context
|
||||
let recentMessages = Array(chatMessages.suffix(10)) // Last 10 messages for context
|
||||
var summaryMessages = recentMessages.map { msg in
|
||||
Message(role: msg.role, content: msg.content, tokens: nil, cost: nil, timestamp: Date())
|
||||
}
|
||||
|
||||
// Add the summary request as a user message
|
||||
summaryMessages.append(Message(
|
||||
role: .user,
|
||||
content: summaryPrompt,
|
||||
tokens: nil,
|
||||
cost: nil,
|
||||
timestamp: Date()
|
||||
))
|
||||
|
||||
let chatRequest = ChatRequest(
|
||||
messages: summaryMessages,
|
||||
model: modelId,
|
||||
stream: false, // Non-streaming for background request
|
||||
maxTokens: 100, // Increased for better response
|
||||
temperature: 0.3, // Lower for more focused response
|
||||
topP: nil,
|
||||
systemPrompt: "You are a helpful assistant that creates concise conversation titles.",
|
||||
tools: nil,
|
||||
onlineMode: false,
|
||||
imageGeneration: false
|
||||
)
|
||||
|
||||
// Make the request (hidden from user)
|
||||
let response = try await provider.chat(request: chatRequest)
|
||||
|
||||
Log.ui.info("Raw summary response: '\(response.content)'")
|
||||
|
||||
// Extract and clean the summary
|
||||
var summary = response.content
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.replacingOccurrences(of: "\"", with: "")
|
||||
.replacingOccurrences(of: "'", with: "")
|
||||
.replacingOccurrences(of: "Title:", with: "", options: .caseInsensitive)
|
||||
.replacingOccurrences(of: "title:", with: "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// Take only first line if multi-line response
|
||||
if let firstLine = summary.components(separatedBy: .newlines).first {
|
||||
summary = firstLine.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
// Limit length
|
||||
summary = String(summary.prefix(60))
|
||||
|
||||
Log.ui.info("Cleaned summary: '\(summary)'")
|
||||
|
||||
// Return nil if empty
|
||||
guard !summary.isEmpty else {
|
||||
Log.ui.warning("Summary is empty after cleaning")
|
||||
return nil
|
||||
}
|
||||
|
||||
return summary
|
||||
|
||||
} catch {
|
||||
Log.ui.error("Background summarization failed: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if conversation should be auto-saved based on criteria
|
||||
func shouldAutoSave() -> Bool {
|
||||
// Check if auto-save is enabled
|
||||
guard settings.syncEnabled && settings.syncAutoSave else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if sync is configured
|
||||
guard settings.syncConfigured else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if repository is cloned
|
||||
guard GitSyncService.shared.syncStatus.isCloned else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check minimum message count
|
||||
let chatMessages = messages.filter { $0.role == .user || $0.role == .assistant }
|
||||
guard chatMessages.count >= settings.syncAutoSaveMinMessages else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if already auto-saved this conversation
|
||||
// (prevent duplicate saves)
|
||||
if let lastSavedId = settings.syncLastAutoSaveConversationId {
|
||||
// Create a hash of current conversation to detect if it's the same one
|
||||
let currentHash = chatMessages.map { $0.content }.joined()
|
||||
if lastSavedId == currentHash {
|
||||
return false // Already saved this exact conversation
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/// Auto-save the current conversation with background summarization
|
||||
func autoSaveConversation() async {
|
||||
guard shouldAutoSave() else {
|
||||
return
|
||||
}
|
||||
|
||||
Log.ui.info("Auto-saving conversation...")
|
||||
|
||||
// Get summary in background (hidden from user)
|
||||
let summary = await summarizeConversationInBackground()
|
||||
|
||||
// Use summary as name, or fallback to timestamp
|
||||
let conversationName: String
|
||||
if let summary = summary, !summary.isEmpty {
|
||||
conversationName = summary
|
||||
} else {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd HH:mm"
|
||||
conversationName = "Conversation - \(formatter.string(from: Date()))"
|
||||
}
|
||||
|
||||
// Save the conversation
|
||||
do {
|
||||
let chatMessages = messages.filter { $0.role == .user || $0.role == .assistant }
|
||||
let conversation = try DatabaseService.shared.saveConversation(
|
||||
name: conversationName,
|
||||
messages: chatMessages
|
||||
)
|
||||
|
||||
Log.ui.info("Auto-saved conversation: \(conversationName)")
|
||||
|
||||
// Mark as saved to prevent duplicate saves
|
||||
let conversationHash = chatMessages.map { $0.content }.joined()
|
||||
settings.syncLastAutoSaveConversationId = conversationHash
|
||||
|
||||
// Trigger auto-sync (export + push)
|
||||
Task {
|
||||
await performAutoSync()
|
||||
}
|
||||
|
||||
} catch {
|
||||
Log.ui.error("Auto-save failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform auto-sync: export + push to git (debounced)
|
||||
private func performAutoSync() async {
|
||||
await GitSyncService.shared.autoSync()
|
||||
}
|
||||
|
||||
// MARK: - Smart Triggers
|
||||
|
||||
/// Update conversation tracking times
|
||||
func updateConversationTracking() {
|
||||
let now = Date()
|
||||
|
||||
if conversationStartTime == nil {
|
||||
conversationStartTime = now
|
||||
}
|
||||
|
||||
lastMessageTime = now
|
||||
|
||||
// Restart idle timer if enabled
|
||||
if settings.syncAutoSaveOnIdle {
|
||||
startIdleTimer()
|
||||
}
|
||||
}
|
||||
|
||||
/// Start or restart the idle timer
|
||||
private func startIdleTimer() {
|
||||
// Cancel existing timer
|
||||
idleCheckTimer?.invalidate()
|
||||
|
||||
let idleMinutes = settings.syncAutoSaveIdleMinutes
|
||||
let idleSeconds = TimeInterval(idleMinutes * 60)
|
||||
|
||||
// Schedule new timer
|
||||
idleCheckTimer = Timer.scheduledTimer(withTimeInterval: idleSeconds, repeats: false) { [weak self] _ in
|
||||
Task { @MainActor [weak self] in
|
||||
await self?.onIdleTimeout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when idle timeout is reached
|
||||
private func onIdleTimeout() async {
|
||||
guard settings.syncAutoSaveOnIdle else { return }
|
||||
|
||||
Log.ui.info("Idle timeout reached - triggering auto-save")
|
||||
await autoSaveConversation()
|
||||
}
|
||||
|
||||
/// Detect goodbye phrases in user message
|
||||
func detectGoodbyePhrase(in text: String) -> Bool {
|
||||
let lowercased = text.lowercased()
|
||||
let goodbyePhrases = [
|
||||
"bye", "goodbye", "bye bye",
|
||||
"thanks", "thank you", "thx", "ty",
|
||||
"that's all", "thats all", "that'll be all",
|
||||
"done", "i'm done", "we're done",
|
||||
"see you", "see ya", "catch you later",
|
||||
"have a good", "have a nice"
|
||||
]
|
||||
|
||||
return goodbyePhrases.contains { phrase in
|
||||
// Check for whole word match (not substring)
|
||||
let pattern = "\\b\(NSRegularExpression.escapedPattern(for: phrase))\\b"
|
||||
return lowercased.range(of: pattern, options: .regularExpression) != nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger auto-save when user switches models
|
||||
func onModelSwitch(from oldModel: ModelInfo?, to newModel: ModelInfo?) async {
|
||||
guard settings.syncAutoSaveOnModelSwitch else { return }
|
||||
guard oldModel != nil else { return } // Don't save on first model selection
|
||||
|
||||
Log.ui.info("Model switch detected - triggering auto-save")
|
||||
await autoSaveConversation()
|
||||
}
|
||||
|
||||
/// Trigger auto-save on app quit
|
||||
func onAppWillTerminate() async {
|
||||
guard settings.syncAutoSaveOnAppQuit else { return }
|
||||
|
||||
Log.ui.info("App quit detected - triggering auto-save")
|
||||
await autoSaveConversation()
|
||||
}
|
||||
|
||||
/// Check and trigger auto-save after user message
|
||||
func checkAutoSaveTriggersAfterMessage(_ text: String) async {
|
||||
// Update tracking
|
||||
updateConversationTracking()
|
||||
|
||||
// Check for goodbye phrase
|
||||
if detectGoodbyePhrase(in: text) {
|
||||
Log.ui.info("Goodbye phrase detected - triggering auto-save")
|
||||
// Wait a bit to see if user continues
|
||||
try? await Task.sleep(for: .seconds(30))
|
||||
|
||||
// Check if they sent another message in the meantime
|
||||
if let lastTime = lastMessageTime, Date().timeIntervalSince(lastTime) < 25 {
|
||||
Log.ui.info("User continued chatting - skipping goodbye auto-save")
|
||||
return
|
||||
}
|
||||
|
||||
await autoSaveConversation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user