First public release v2.3.1

This commit is contained in:
2026-02-19 16:39:23 +01:00
parent 52e3d0c07e
commit f3d673ab27
15 changed files with 1032 additions and 60 deletions

View File

@@ -126,7 +126,12 @@ struct ChatView: View {
)
// Footer
FooterView(stats: viewModel.sessionStats)
FooterView(
stats: viewModel.sessionStats,
conversationName: viewModel.currentConversationName,
hasUnsavedChanges: viewModel.hasUnsavedChanges,
onQuickSave: viewModel.quickSave
)
}
.background(Color.oaiBackground)
.sheet(isPresented: $viewModel.showShortcuts) {

View File

@@ -46,6 +46,9 @@ struct ContentView: View {
}
.frame(minWidth: 640, minHeight: 400)
#if os(macOS)
.onAppear {
NSApplication.shared.windows.forEach { $0.tabbingMode = .disallowed }
}
.onKeyPress(.return, phases: .down) { press in
if press.modifiers.contains(.command) {
chatViewModel.sendMessage()
@@ -157,8 +160,7 @@ struct ContentView: View {
Button(action: { chatViewModel.showStats = true }) {
ToolbarLabel(title: "Stats", systemImage: "chart.bar", showLabels: showLabels, scale: scale)
}
.keyboardShortcut("s", modifiers: .command)
.help("Session statistics (Cmd+S)")
.help("Session statistics")
Button(action: { chatViewModel.showCredits = true }) {
ToolbarLabel(title: "Credits", systemImage: "creditcard", showLabels: showLabels, scale: scale)

View File

@@ -27,7 +27,20 @@ import SwiftUI
struct FooterView: View {
let stats: SessionStats
let conversationName: String?
let hasUnsavedChanges: Bool
let onQuickSave: (() -> Void)?
init(stats: SessionStats,
conversationName: String? = nil,
hasUnsavedChanges: Bool = false,
onQuickSave: (() -> Void)? = nil) {
self.stats = stats
self.conversationName = conversationName
self.hasUnsavedChanges = hasUnsavedChanges
self.onQuickSave = onQuickSave
}
var body: some View {
HStack(spacing: 20) {
// Session summary
@@ -58,9 +71,18 @@ struct FooterView: View {
Spacer()
// Save indicator (only when chat has messages)
if stats.messageCount > 0 {
SaveIndicator(
conversationName: conversationName,
hasUnsavedChanges: hasUnsavedChanges,
onSave: onQuickSave
)
}
// Shortcuts hint
#if os(macOS)
Text("M Model • ⌘K Clear • ⌘S Stats")
Text("N New • ⌘M Model • ⌘S Save")
.font(.caption2)
.foregroundColor(.oaiSecondary)
#endif
@@ -77,6 +99,62 @@ struct FooterView: View {
}
}
struct SaveIndicator: View {
let conversationName: String?
let hasUnsavedChanges: Bool
let onSave: (() -> Void)?
private let guiSize = SettingsService.shared.guiTextSize
private var isSaved: Bool { conversationName != nil }
private var isModified: Bool { isSaved && hasUnsavedChanges }
private var isUnsaved: Bool { !isSaved }
private var label: String {
if let name = conversationName {
return name.count > 20 ? String(name.prefix(20)) + "" : name
}
return "Unsaved"
}
private var icon: String {
if isModified { return "circle.fill" }
if isSaved { return "checkmark.circle.fill" }
return "exclamationmark.circle"
}
private var color: Color {
if isModified { return .orange }
if isSaved { return .green }
return .secondary
}
private var tooltip: String {
if isModified { return "Click to re-save \"\(conversationName ?? "")\"" }
if isSaved { return "Saved — no changes" }
return "Not saved — use /save <name>"
}
var body: some View {
Button(action: { if isModified { onSave?() } }) {
HStack(spacing: 4) {
Image(systemName: icon)
.font(.system(size: guiSize - 3))
.foregroundColor(color)
Text(label)
.font(.system(size: guiSize - 2))
.foregroundColor(isUnsaved ? .secondary : .oaiPrimary)
}
}
.buttonStyle(.plain)
.help(tooltip)
.disabled(!isModified)
.opacity(isUnsaved ? 0.6 : 1.0)
.animation(.easeInOut(duration: 0.2), value: conversationName)
.animation(.easeInOut(duration: 0.2), value: hasUnsavedChanges)
}
}
struct FooterItem: View {
let icon: String
let label: String
@@ -177,14 +255,17 @@ struct SyncStatusFooter: View {
}
#Preview {
VStack {
let stats = SessionStats(
totalInputTokens: 1250,
totalOutputTokens: 3420,
totalCost: 0.0152,
messageCount: 12
)
return VStack(spacing: 0) {
Spacer()
FooterView(stats: SessionStats(
totalInputTokens: 1250,
totalOutputTokens: 3420,
totalCost: 0.0152,
messageCount: 12
))
FooterView(stats: stats, conversationName: nil, hasUnsavedChanges: true)
FooterView(stats: stats, conversationName: "My Project", hasUnsavedChanges: true)
FooterView(stats: stats, conversationName: "My Project", hasUnsavedChanges: false)
}
.background(Color.oaiBackground)
}

View File

@@ -279,6 +279,9 @@ struct MessageRow: View {
private func loadStarredState() {
if let metadata = try? DatabaseService.shared.getMessageMetadata(messageId: message.id) {
isStarred = metadata.user_starred == 1
} else {
// Unsaved message use in-memory flag on the Message struct
isStarred = message.isStarred
}
}

View File

@@ -71,7 +71,7 @@ struct StatsView: View {
Section("Costs") {
StatRow(label: "Total Cost", value: stats.totalCostDisplay)
if stats.messageCount > 0 {
StatRow(label: "Avg per Message", value: String(format: "$%.4f", stats.averageCostPerMessage))
StatRow(label: "Avg per Message", value: stats.averageCostDisplay)
}
}