UI redesign Phase 1: NavigationSplitView with collapsible sidebar

- Replace root VStack with NavigationSplitView (2-column, collapsible sidebar)
- Add SidebarView: new chat button, conversation search, list with swipe actions
- Slim HeaderView to text-only (provider + model + star); remove all icon rows
- Move status pills (Online, MCP, Synced) to footer right side
- Remove version number and shortcut hints from footer
- Add resizable InputBar with drag handle (persisted height) and globe/network.slash online toggle
- Fix Norwegian menu appearing on English systems (CFBundleLocalizations in Info.plist)
- Add View menu (Model Info, History, Stats, Credits, Online Mode toggle ⌘⇧O)
- Add ⌘L as alias for Search Conversations (muscle memory for /load users)
- Add Check for Updates to Help menu with download URL from Gitea API
- Add one-time Intel/Rosetta deprecation warning on first launch
- Swift 6: fix self.Self.isoString() call sites in DatabaseService

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 11:18:48 +02:00
co-authored by Claude Sonnet 4.6
parent cd0ceeab41
commit 8451db1142
19 changed files with 702 additions and 363 deletions
+7 -66
View File
@@ -2,7 +2,8 @@
// HeaderView.swift
// oAI
//
// Header bar with provider, model, and stats
// Slim header provider, model name, star only.
// Status pills and stats live in SidebarView and FooterView respectively.
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Rune Olsen
@@ -28,18 +29,13 @@ import SwiftUI
struct HeaderView: View {
let provider: Settings.Provider
let model: ModelInfo?
let stats: SessionStats
let onlineMode: Bool
let mcpEnabled: Bool
let mcpStatus: String?
let onModelSelect: () -> Void
let onProviderChange: (Settings.Provider) -> Void
private let settings = SettingsService.shared
private let registry = ProviderRegistry.shared
private let gitSync = GitSyncService.shared
var body: some View {
HStack(spacing: 20) {
HStack(spacing: 12) {
// Provider picker dropdown only shows configured providers
Menu {
ForEach(registry.configuredProviders, id: \.self) { p in
@@ -75,7 +71,7 @@ struct HeaderView: View {
.fixedSize()
.help("Switch provider")
// Model info (clickable model selector)
// Model name (clickable model selector)
Button(action: onModelSelect) {
if let model = model {
HStack(spacing: 6) {
@@ -116,7 +112,6 @@ struct HeaderView: View {
Text("No model selected")
.font(.system(size: settings.guiTextSize))
.foregroundColor(.oaiSecondary)
Image(systemName: "chevron.down")
.font(.caption2)
.foregroundColor(.oaiSecondary)
@@ -126,6 +121,7 @@ struct HeaderView: View {
.buttonStyle(.plain)
.help("Select model")
// Favourite star
if let model = model {
let isFav = settings.favoriteModelIds.contains(model.id)
Button(action: { settings.toggleFavoriteModel(model.id) }) {
@@ -138,40 +134,9 @@ struct HeaderView: View {
}
Spacer()
// Status indicators
HStack(spacing: 8) {
if model?.capabilities.imageGeneration == true {
StatusPill(icon: "paintbrush", label: "Image", color: .purple)
}
if onlineMode {
StatusPill(icon: "globe", label: "Online", color: .green)
}
if mcpEnabled {
StatusPill(icon: "folder", label: "MCP", color: .blue)
}
if settings.syncEnabled && settings.syncAutoSave {
SyncStatusPill()
}
}
// Divider between status and stats
if onlineMode || mcpEnabled || model?.capabilities.imageGeneration == true || (settings.syncEnabled && settings.syncAutoSave) {
Divider()
.frame(height: 16)
.opacity(0.5)
}
// Quick stats
HStack(spacing: 16) {
StatItem(icon: "message", value: "\(stats.messageCount)")
StatItem(icon: "arrow.up.arrow.down", value: stats.totalTokensDisplay)
StatItem(icon: "dollarsign", value: stats.totalCostDisplay)
}
.font(.caption)
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
.padding(.vertical, 8)
.background(.ultraThinMaterial)
.overlay(
Rectangle()
@@ -182,22 +147,7 @@ struct HeaderView: View {
}
}
struct StatItem: View {
let icon: String
let value: String
private let settings = SettingsService.shared
var body: some View {
HStack(spacing: 4) {
Image(systemName: icon)
.font(.system(size: settings.guiTextSize - 3))
.foregroundColor(.oaiSecondary)
Text(value)
.font(.system(size: settings.guiTextSize - 1, weight: .medium))
.foregroundColor(.oaiPrimary)
}
}
}
// MARK: - Status Pills (used by SidebarView)
struct StatusPill: View {
let icon: String
@@ -284,15 +234,6 @@ struct SyncStatusPill: View {
HeaderView(
provider: .openrouter,
model: ModelInfo.mockModels.first,
stats: SessionStats(
totalInputTokens: 125,
totalOutputTokens: 434,
totalCost: 0.00111,
messageCount: 4
),
onlineMode: true,
mcpEnabled: true,
mcpStatus: "MCP",
onModelSelect: {},
onProviderChange: { _ in }
)