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:
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user