First public release v2.3.1
This commit is contained in:
@@ -57,6 +57,16 @@ class ChatViewModel {
|
||||
var commandHistory: [String] = []
|
||||
var historyIndex: Int = 0
|
||||
var isAutoContinuing: Bool = false
|
||||
|
||||
// Save tracking
|
||||
var currentConversationId: UUID? = nil
|
||||
var currentConversationName: String? = nil
|
||||
private var savedMessageCount: Int = 0
|
||||
|
||||
var hasUnsavedChanges: Bool {
|
||||
let chatCount = messages.filter { $0.role != .system }.count
|
||||
return chatCount > 0 && chatCount != savedMessageCount
|
||||
}
|
||||
var autoContinueCountdown: Int = 0
|
||||
|
||||
// MARK: - Auto-Save Tracking
|
||||
@@ -126,6 +136,13 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
- 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
|
||||
|
||||
**FILE EDITING — CRITICAL:**
|
||||
- NEVER use write_file to rewrite a large existing file (>200 lines or >8KB)
|
||||
- Large write_file calls exceed output token limits and will fail — the content will be truncated
|
||||
- For existing files: ALWAYS use edit_file to replace specific sections
|
||||
- Use write_file ONLY for new files or very small files (<100 lines)
|
||||
- If you need to make many changes to a large file, use multiple edit_file calls
|
||||
|
||||
**METHODOLOGY:**
|
||||
1. Use the tool to gather information (if needed)
|
||||
2. Use the tool to make changes (if needed)
|
||||
@@ -218,6 +235,9 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
messages = []
|
||||
sessionStats = SessionStats()
|
||||
inputText = ""
|
||||
currentConversationId = nil
|
||||
currentConversationName = nil
|
||||
savedMessageCount = 0
|
||||
}
|
||||
|
||||
/// Re-sync local state from SettingsService (called when Settings sheet dismisses)
|
||||
@@ -493,20 +513,81 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
}
|
||||
|
||||
func toggleMessageStar(messageId: UUID) {
|
||||
// Get current starred status
|
||||
let isStarred = (try? DatabaseService.shared.getMessageMetadata(messageId: messageId)?.user_starred == 1) ?? false
|
||||
// Update in-memory state first (works for both saved and unsaved messages)
|
||||
if let index = messages.firstIndex(where: { $0.id == messageId }) {
|
||||
messages[index].isStarred.toggle()
|
||||
let newStarred = messages[index].isStarred
|
||||
// Persist to DB if the message exists there (saved conversations only)
|
||||
do {
|
||||
try DatabaseService.shared.setMessageStarred(messageId: messageId, starred: newStarred)
|
||||
Log.ui.info("Message \(messageId) starred: \(newStarred)")
|
||||
} catch {
|
||||
// FK error is expected for unsaved messages — in-memory state is already updated
|
||||
Log.ui.debug("Star not persisted for unsaved message \(messageId): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle starred status
|
||||
do {
|
||||
try DatabaseService.shared.setMessageStarred(messageId: messageId, starred: !isStarred)
|
||||
Log.ui.info("Message \(messageId) starred: \(!isStarred)")
|
||||
} catch {
|
||||
Log.ui.error("Failed to toggle star for message \(messageId): \(error)")
|
||||
// MARK: - Quick Save
|
||||
|
||||
/// Called from the File menu — re-saves if already named, shows NSAlert to name if not.
|
||||
func saveFromMenu() {
|
||||
let chatMessages = messages.filter { $0.role != .system }
|
||||
guard !chatMessages.isEmpty else { return }
|
||||
|
||||
if currentConversationName != nil {
|
||||
quickSave()
|
||||
} else {
|
||||
#if os(macOS)
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Save Chat"
|
||||
alert.informativeText = "Enter a name for this conversation:"
|
||||
alert.addButton(withTitle: "Save")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 24))
|
||||
input.placeholderString = "Conversation name…"
|
||||
alert.accessoryView = input
|
||||
alert.window.initialFirstResponder = input
|
||||
guard alert.runModal() == .alertFirstButtonReturn else { return }
|
||||
let name = input.stringValue.trimmingCharacters(in: .whitespaces)
|
||||
guard !name.isEmpty else { return }
|
||||
do {
|
||||
let saved = try DatabaseService.shared.saveConversation(name: name, messages: chatMessages)
|
||||
currentConversationId = saved.id
|
||||
currentConversationName = name
|
||||
savedMessageCount = chatMessages.count
|
||||
showSystemMessage("Saved as \"\(name)\"")
|
||||
} catch {
|
||||
showSystemMessage("Save failed: \(error.localizedDescription)")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-save the current conversation under its existing name, or prompt if never saved.
|
||||
func quickSave() {
|
||||
let chatMessages = messages.filter { $0.role != .system }
|
||||
guard !chatMessages.isEmpty else { return }
|
||||
|
||||
if let id = currentConversationId, let name = currentConversationName {
|
||||
// Update existing saved conversation
|
||||
do {
|
||||
try DatabaseService.shared.updateConversation(
|
||||
id: id, name: name, messages: chatMessages,
|
||||
primaryModel: selectedModel?.id
|
||||
)
|
||||
savedMessageCount = chatMessages.count
|
||||
showSystemMessage("Saved \"\(name)\"")
|
||||
} catch {
|
||||
showSystemMessage("Save failed: \(error.localizedDescription)")
|
||||
}
|
||||
} else {
|
||||
showSystemMessage("No name yet — use /save <name> to save this conversation")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Command Handling
|
||||
|
||||
|
||||
private func handleCommand(_ command: String) {
|
||||
guard let (cmd, args) = command.parseCommand() else {
|
||||
showSystemMessage("Invalid command")
|
||||
@@ -571,13 +652,17 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
return
|
||||
}
|
||||
do {
|
||||
let _ = try DatabaseService.shared.saveConversation(name: name, messages: chatMessages)
|
||||
let saved = try DatabaseService.shared.saveConversation(name: name, messages: chatMessages)
|
||||
currentConversationId = saved.id
|
||||
currentConversationName = name
|
||||
savedMessageCount = chatMessages.count
|
||||
showSystemMessage("Conversation saved as '\(name)'")
|
||||
} catch {
|
||||
showSystemMessage("Failed to save: \(error.localizedDescription)")
|
||||
}
|
||||
} else {
|
||||
showSystemMessage("Usage: /save <name>")
|
||||
// Re-save without a name if already saved; otherwise prompt
|
||||
quickSave()
|
||||
}
|
||||
|
||||
case "/load", "/list":
|
||||
@@ -808,8 +893,11 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
if let usage = response.usage {
|
||||
messages[index].tokens = usage.completionTokens
|
||||
if let model = selectedModel {
|
||||
let cost = (Double(usage.promptTokens) * model.pricing.prompt / 1_000_000) +
|
||||
(Double(usage.completionTokens) * model.pricing.completion / 1_000_000)
|
||||
let hasPricing = model.pricing.prompt > 0 || model.pricing.completion > 0
|
||||
let cost: Double? = hasPricing
|
||||
? (Double(usage.promptTokens) * model.pricing.prompt / 1_000_000) +
|
||||
(Double(usage.completionTokens) * model.pricing.completion / 1_000_000)
|
||||
: nil
|
||||
messages[index].cost = cost
|
||||
sessionStats.addMessage(inputTokens: usage.promptTokens, outputTokens: usage.completionTokens, cost: cost)
|
||||
}
|
||||
@@ -856,8 +944,11 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
if let usage = totalTokens {
|
||||
messages[index].tokens = usage.completionTokens
|
||||
if let model = selectedModel {
|
||||
let cost = (Double(usage.promptTokens) * model.pricing.prompt / 1_000_000) +
|
||||
(Double(usage.completionTokens) * model.pricing.completion / 1_000_000)
|
||||
let hasPricing = model.pricing.prompt > 0 || model.pricing.completion > 0
|
||||
let cost: Double? = hasPricing
|
||||
? (Double(usage.promptTokens) * model.pricing.prompt / 1_000_000) +
|
||||
(Double(usage.completionTokens) * model.pricing.completion / 1_000_000)
|
||||
: nil
|
||||
messages[index].cost = cost
|
||||
sessionStats.addMessage(inputTokens: usage.promptTokens, outputTokens: usage.completionTokens, cost: cost)
|
||||
}
|
||||
@@ -1345,8 +1436,11 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
|
||||
// Calculate cost
|
||||
if let usage = totalUsage, let model = selectedModel {
|
||||
let cost = (Double(usage.promptTokens) * model.pricing.prompt / 1_000_000) +
|
||||
(Double(usage.completionTokens) * model.pricing.completion / 1_000_000)
|
||||
let hasPricing = model.pricing.prompt > 0 || model.pricing.completion > 0
|
||||
let cost: Double? = hasPricing
|
||||
? (Double(usage.promptTokens) * model.pricing.prompt / 1_000_000) +
|
||||
(Double(usage.completionTokens) * model.pricing.completion / 1_000_000)
|
||||
: nil
|
||||
if let index = messages.lastIndex(where: { $0.id == assistantMessage.id }) {
|
||||
messages[index].cost = cost
|
||||
}
|
||||
@@ -1453,7 +1547,7 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
modelInfoTarget = model
|
||||
}
|
||||
|
||||
private func exportConversation(format: String, filename: String) {
|
||||
func exportConversation(format: String, filename: String) {
|
||||
let chatMessages = messages.filter { $0.role != .system }
|
||||
guard !chatMessages.isEmpty else {
|
||||
showSystemMessage("Nothing to export — no messages")
|
||||
@@ -1655,6 +1749,9 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
messages: chatMessages
|
||||
)
|
||||
|
||||
currentConversationId = conversation.id
|
||||
currentConversationName = conversationName
|
||||
savedMessageCount = chatMessages.count
|
||||
Log.ui.info("Auto-saved conversation: \(conversationName)")
|
||||
|
||||
// Check if progressive summarization is needed
|
||||
|
||||
Reference in New Issue
Block a user