Move conversation name to header (macOS document-title style)

The save indicator was sitting in the bottom-right corner of the footer.
Moved it to the center of the header bar, where macOS apps conventionally
show the document/conversation title. An orange dot appears when there are
unsaved changes; clicking saves. Removed the indicator from the footer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 14:44:32 +02:00
parent b3bb7c4a59
commit 22f745762f
3 changed files with 129 additions and 107 deletions
+4 -1
View File
@@ -38,7 +38,10 @@ struct ChatView: View {
provider: viewModel.currentProvider, provider: viewModel.currentProvider,
model: viewModel.selectedModel, model: viewModel.selectedModel,
onModelSelect: onModelSelect, onModelSelect: onModelSelect,
onProviderChange: onProviderChange onProviderChange: onProviderChange,
conversationName: viewModel.currentConversationName,
hasUnsavedChanges: viewModel.hasUnsavedChanges,
onQuickSave: viewModel.quickSave
) )
// Messages // Messages
-9
View File
@@ -93,15 +93,6 @@ struct FooterView: View {
} }
#endif #endif
// Save indicator (only when chat has messages)
if stats.messageCount > 0 {
SaveIndicator(
conversationName: conversationName,
hasUnsavedChanges: hasUnsavedChanges,
onSave: onQuickSave
)
}
// Update available badge (shows only when an update exists no version number) // Update available badge (shows only when an update exists no version number)
#if os(macOS) #if os(macOS)
UpdateBadge() UpdateBadge()
+67 -39
View File
@@ -31,12 +31,66 @@ struct HeaderView: View {
let model: ModelInfo? let model: ModelInfo?
let onModelSelect: () -> Void let onModelSelect: () -> Void
let onProviderChange: (Settings.Provider) -> Void let onProviderChange: (Settings.Provider) -> Void
var conversationName: String? = nil
var hasUnsavedChanges: Bool = false
var onQuickSave: (() -> Void)? = nil
private let settings = SettingsService.shared private let settings = SettingsService.shared
private let registry = ProviderRegistry.shared private let registry = ProviderRegistry.shared
var body: some View { var body: some View {
ZStack {
// Left: provider + model + star
HStack(spacing: 12) { HStack(spacing: 12) {
// Provider picker dropdown only shows configured providers providerMenu
modelButton
starButton
Spacer()
}
// Center: conversation title (macOS document-title style)
conversationTitle
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(.ultraThinMaterial)
.overlay(
Rectangle()
.fill(Color.oaiBorder.opacity(0.5))
.frame(height: 1),
alignment: .bottom
)
}
// MARK: - Conversation title (center)
@ViewBuilder
private var conversationTitle: some View {
if let name = conversationName {
Button(action: { if hasUnsavedChanges { onQuickSave?() } }) {
HStack(spacing: 5) {
if hasUnsavedChanges {
Circle()
.fill(Color.orange)
.frame(width: 6, height: 6)
}
Text(name)
.font(.system(size: settings.guiTextSize - 1, weight: .medium))
.foregroundColor(.oaiPrimary)
.lineLimit(1)
.frame(maxWidth: 300)
}
}
.buttonStyle(.plain)
.disabled(!hasUnsavedChanges)
.help(hasUnsavedChanges ? "Unsaved changes — click to save" : "Saved")
.animation(.easeInOut(duration: 0.2), value: hasUnsavedChanges)
.animation(.easeInOut(duration: 0.2), value: name)
}
}
// MARK: - Subviews (extracted so ZStack stays readable)
private var providerMenu: some View {
Menu { Menu {
ForEach(registry.configuredProviders, id: \.self) { p in ForEach(registry.configuredProviders, id: \.self) { p in
Button { Button {
@@ -45,9 +99,7 @@ struct HeaderView: View {
HStack { HStack {
Image(systemName: p.iconName) Image(systemName: p.iconName)
Text(p.displayName) Text(p.displayName)
if p == provider { if p == provider { Image(systemName: "checkmark") }
Image(systemName: "checkmark")
}
} }
} }
} }
@@ -70,58 +122,46 @@ struct HeaderView: View {
.menuStyle(.borderlessButton) .menuStyle(.borderlessButton)
.fixedSize() .fixedSize()
.help("Switch provider") .help("Switch provider")
}
// Model name (clickable model selector) private var modelButton: some View {
Button(action: onModelSelect) { Button(action: onModelSelect) {
if let model = model { if let model = model {
HStack(spacing: 6) { HStack(spacing: 6) {
Text(model.name) Text(model.name)
.font(.system(size: settings.guiTextSize, weight: .medium)) .font(.system(size: settings.guiTextSize, weight: .medium))
.foregroundColor(.oaiPrimary) .foregroundColor(.oaiPrimary)
// Capability badges
HStack(spacing: 3) { HStack(spacing: 3) {
if model.capabilities.vision { if model.capabilities.vision {
Image(systemName: "eye") Image(systemName: "eye").font(.system(size: 9)).foregroundColor(.oaiSecondary)
.font(.system(size: 9))
.foregroundColor(.oaiSecondary)
} }
if model.capabilities.tools { if model.capabilities.tools {
Image(systemName: "wrench") Image(systemName: "wrench").font(.system(size: 9)).foregroundColor(.oaiSecondary)
.font(.system(size: 9))
.foregroundColor(.oaiSecondary)
} }
if model.capabilities.online { if model.capabilities.online {
Image(systemName: "globe") Image(systemName: "globe").font(.system(size: 9)).foregroundColor(.oaiSecondary)
.font(.system(size: 9))
.foregroundColor(.oaiSecondary)
} }
if model.capabilities.imageGeneration { if model.capabilities.imageGeneration {
Image(systemName: "paintbrush") Image(systemName: "paintbrush").font(.system(size: 9)).foregroundColor(.oaiSecondary)
.font(.system(size: 9))
.foregroundColor(.oaiSecondary)
} }
} }
Image(systemName: "chevron.down").font(.caption2).foregroundColor(.oaiSecondary)
Image(systemName: "chevron.down")
.font(.caption2)
.foregroundColor(.oaiSecondary)
} }
} else { } else {
HStack(spacing: 4) { HStack(spacing: 4) {
Text("No model selected") Text("No model selected")
.font(.system(size: settings.guiTextSize)) .font(.system(size: settings.guiTextSize))
.foregroundColor(.oaiSecondary) .foregroundColor(.oaiSecondary)
Image(systemName: "chevron.down") Image(systemName: "chevron.down").font(.caption2).foregroundColor(.oaiSecondary)
.font(.caption2)
.foregroundColor(.oaiSecondary)
} }
} }
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.help("Select model") .help("Select model")
}
// Favourite star @ViewBuilder
private var starButton: some View {
if let model = model { if let model = model {
let isFav = settings.favoriteModelIds.contains(model.id) let isFav = settings.favoriteModelIds.contains(model.id)
Button(action: { settings.toggleFavoriteModel(model.id) }) { Button(action: { settings.toggleFavoriteModel(model.id) }) {
@@ -132,18 +172,6 @@ struct HeaderView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
.help(isFav ? "Remove from favorites" : "Add to favorites") .help(isFav ? "Remove from favorites" : "Add to favorites")
} }
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(.ultraThinMaterial)
.overlay(
Rectangle()
.fill(Color.oaiBorder.opacity(0.5))
.frame(height: 1),
alignment: .bottom
)
} }
} }