New version v2.3.6
This commit is contained in:
@@ -38,7 +38,7 @@ struct AgentSkillsView: View {
|
||||
@Bindable private var settings = SettingsService.shared
|
||||
|
||||
@State private var editContext: SkillEditContext? = nil
|
||||
@State private var statusMessage: String? = nil
|
||||
@State private var statusMessage: LocalizedStringKey? = nil
|
||||
|
||||
private var activeCount: Int { settings.agentSkills.filter { $0.isActive }.count }
|
||||
|
||||
@@ -166,7 +166,7 @@ struct AgentSkillsView: View {
|
||||
if importMarkdown(url) { imported += 1 }
|
||||
}
|
||||
}
|
||||
if imported > 0 { show("Imported \(imported) skill\(imported == 1 ? "" : "s")") }
|
||||
if imported > 0 { show("^[\(imported) skill](inflect: true) imported") }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -256,7 +256,7 @@ struct AgentSkillsView: View {
|
||||
}
|
||||
exported += 1
|
||||
}
|
||||
show("Exported \(exported) skill\(exported == 1 ? "" : "s") to Downloads")
|
||||
show("^[\(exported) skill](inflect: true) exported to Downloads")
|
||||
}
|
||||
|
||||
private func exportOne(_ skill: AgentSkill) {
|
||||
@@ -335,7 +335,7 @@ struct AgentSkillsView: View {
|
||||
return ""
|
||||
}
|
||||
|
||||
private func show(_ text: String) {
|
||||
private func show(_ text: LocalizedStringKey) {
|
||||
statusMessage = text
|
||||
Task { try? await Task.sleep(for: .seconds(3)); statusMessage = nil }
|
||||
}
|
||||
@@ -380,7 +380,7 @@ private struct AgentSkillRow: View {
|
||||
|
||||
// File count badge
|
||||
if fileCount > 0 {
|
||||
Label("\(fileCount) file\(fileCount == 1 ? "" : "s")", systemImage: "doc")
|
||||
Label("^[\(fileCount) file](inflect: true)", systemImage: "doc")
|
||||
.font(.caption2).foregroundStyle(.secondary)
|
||||
.padding(.horizontal, 6).padding(.vertical, 3)
|
||||
.background(.blue.opacity(0.1), in: Capsule())
|
||||
@@ -411,7 +411,7 @@ struct AgentSkillsTabContent: View {
|
||||
@Bindable private var settings = SettingsService.shared
|
||||
|
||||
@State private var editContext: SkillEditContext? = nil
|
||||
@State private var statusMessage: String? = nil
|
||||
@State private var statusMessage: LocalizedStringKey? = nil
|
||||
|
||||
private var activeCount: Int { settings.agentSkills.filter { $0.isActive }.count }
|
||||
|
||||
@@ -431,7 +431,7 @@ struct AgentSkillsTabContent: View {
|
||||
.background(.purple.opacity(0.07), in: RoundedRectangle(cornerRadius: 8))
|
||||
|
||||
if activeCount > 0 {
|
||||
Label("\(activeCount) skill\(activeCount == 1 ? "" : "s") active — appended to every system prompt", systemImage: "checkmark.circle.fill")
|
||||
Label("^[\(activeCount) skill](inflect: true) active — appended to every system prompt", systemImage: "checkmark.circle.fill")
|
||||
.font(.caption).foregroundStyle(.green)
|
||||
}
|
||||
|
||||
@@ -516,7 +516,7 @@ struct AgentSkillsTabContent: View {
|
||||
if importMarkdown(url) { imported += 1 }
|
||||
}
|
||||
}
|
||||
if imported > 0 { show("Imported \(imported) skill\(imported == 1 ? "" : "s")") }
|
||||
if imported > 0 { show("^[\(imported) skill](inflect: true) imported") }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -602,7 +602,7 @@ struct AgentSkillsTabContent: View {
|
||||
}
|
||||
exported += 1
|
||||
}
|
||||
show("Exported \(exported) skill\(exported == 1 ? "" : "s") to Downloads")
|
||||
show("^[\(exported) skill](inflect: true) exported to Downloads")
|
||||
}
|
||||
|
||||
private func exportOne(_ skill: AgentSkill) {
|
||||
@@ -671,7 +671,7 @@ struct AgentSkillsTabContent: View {
|
||||
return ""
|
||||
}
|
||||
|
||||
private func show(_ text: String) {
|
||||
private func show(_ text: LocalizedStringKey) {
|
||||
statusMessage = text
|
||||
Task { try? await Task.sleep(for: .seconds(3)); statusMessage = nil }
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ struct CreditsView: View {
|
||||
}
|
||||
|
||||
struct CreditRow: View {
|
||||
let label: String
|
||||
let label: LocalizedStringKey
|
||||
let value: String
|
||||
var highlight: Bool = false
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ struct EmailLogView: View {
|
||||
|
||||
private var bottomActions: some View {
|
||||
HStack {
|
||||
Text("\(logs.count) \(logs.count == 1 ? "entry" : "entries")")
|
||||
Text("^[\(logs.count) entry](inflect: true)")
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
@@ -327,7 +327,7 @@ struct EmailLogRow: View {
|
||||
// MARK: - Stat Item
|
||||
|
||||
struct EmailStatItem: View {
|
||||
let title: String
|
||||
let title: LocalizedStringKey
|
||||
let value: String
|
||||
let color: Color
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ struct CommandDetail: Identifiable {
|
||||
|
||||
struct CommandCategory: Identifiable {
|
||||
let id = UUID()
|
||||
let name: String
|
||||
let name: LocalizedStringKey
|
||||
let icon: String
|
||||
let commands: [CommandDetail]
|
||||
}
|
||||
@@ -358,7 +358,7 @@ struct HelpView: View {
|
||||
.padding(.vertical, 4)
|
||||
.background(.quaternary, in: RoundedRectangle(cornerRadius: 5))
|
||||
Spacer()
|
||||
Text(shortcut.description)
|
||||
Text(LocalizedStringKey(shortcut.description))
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -463,7 +463,7 @@ private struct CommandRow: View {
|
||||
.font(.system(size: 13, weight: .medium, design: .monospaced))
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text(command.brief)
|
||||
Text(LocalizedStringKey(command.brief))
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
@@ -493,14 +493,14 @@ private struct CommandRow: View {
|
||||
// Expanded detail
|
||||
if isExpanded {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(command.detail)
|
||||
Text(LocalizedStringKey(command.detail))
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
if !command.examples.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(command.examples.count == 1 ? "Example" : "Examples")
|
||||
Text(command.examples.count == 1 ? "Example" as LocalizedStringKey : "Examples")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fontWeight(.medium)
|
||||
|
||||
@@ -132,6 +132,7 @@ struct ModelInfoView: View {
|
||||
capabilityBadge(icon: "wrench.fill", label: "Tools", active: model.capabilities.tools)
|
||||
capabilityBadge(icon: "globe", label: "Online", active: model.capabilities.online)
|
||||
capabilityBadge(icon: "photo.fill", label: "Image Gen", active: model.capabilities.imageGeneration)
|
||||
capabilityBadge(icon: "brain", label: "Thinking", active: model.capabilities.thinking)
|
||||
}
|
||||
|
||||
// Architecture (if available)
|
||||
@@ -172,14 +173,14 @@ struct ModelInfoView: View {
|
||||
|
||||
// MARK: - Layout Helpers
|
||||
|
||||
private func sectionHeader(_ title: String) -> some View {
|
||||
private func sectionHeader(_ title: LocalizedStringKey) -> some View {
|
||||
Text(title)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
.textCase(.uppercase)
|
||||
}
|
||||
|
||||
private func infoRow(_ label: String, _ value: String) -> some View {
|
||||
private func infoRow(_ label: LocalizedStringKey, _ value: String) -> some View {
|
||||
HStack {
|
||||
Text(label)
|
||||
.font(.body)
|
||||
@@ -193,7 +194,7 @@ struct ModelInfoView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func costExample(label: String, inputTokens: Int) -> some View {
|
||||
private func costExample(label: LocalizedStringKey, inputTokens: Int) -> some View {
|
||||
let cost = (Double(inputTokens) * model.pricing.prompt / 1_000_000) +
|
||||
(Double(inputTokens) * model.pricing.completion / 1_000_000)
|
||||
VStack(spacing: 2) {
|
||||
@@ -211,7 +212,7 @@ struct ModelInfoView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func capabilityBadge(icon: String, label: String, active: Bool) -> some View {
|
||||
private func capabilityBadge(icon: String, label: LocalizedStringKey, active: Bool) -> some View {
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: icon)
|
||||
.font(.title3)
|
||||
|
||||
@@ -36,20 +36,37 @@ struct ModelSelectorView: View {
|
||||
@State private var filterTools = false
|
||||
@State private var filterOnline = false
|
||||
@State private var filterImageGen = false
|
||||
@State private var filterThinking = false
|
||||
@State private var keyboardIndex: Int = -1
|
||||
@State private var sortOrder: ModelSortOrder = .default
|
||||
@State private var selectedInfoModel: ModelInfo? = nil
|
||||
|
||||
private var filteredModels: [ModelInfo] {
|
||||
models.filter { model in
|
||||
let q = searchText.lowercased()
|
||||
let filtered = models.filter { model in
|
||||
let matchesSearch = searchText.isEmpty ||
|
||||
model.name.lowercased().contains(searchText.lowercased()) ||
|
||||
model.id.lowercased().contains(searchText.lowercased())
|
||||
model.name.lowercased().contains(q) ||
|
||||
model.id.lowercased().contains(q) ||
|
||||
model.description?.lowercased().contains(q) == true
|
||||
|
||||
let matchesVision = !filterVision || model.capabilities.vision
|
||||
let matchesTools = !filterTools || model.capabilities.tools
|
||||
let matchesOnline = !filterOnline || model.capabilities.online
|
||||
let matchesImageGen = !filterImageGen || model.capabilities.imageGeneration
|
||||
let matchesThinking = !filterThinking || model.capabilities.thinking
|
||||
|
||||
return matchesSearch && matchesVision && matchesTools && matchesOnline && matchesImageGen
|
||||
return matchesSearch && matchesVision && matchesTools && matchesOnline && matchesImageGen && matchesThinking
|
||||
}
|
||||
|
||||
switch sortOrder {
|
||||
case .default:
|
||||
return filtered
|
||||
case .priceLowHigh:
|
||||
return filtered.sorted { $0.pricing.prompt < $1.pricing.prompt }
|
||||
case .priceHighLow:
|
||||
return filtered.sorted { $0.pricing.prompt > $1.pricing.prompt }
|
||||
case .contextHighLow:
|
||||
return filtered.sorted { $0.contextLength > $1.contextLength }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,16 +78,47 @@ struct ModelSelectorView: View {
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding()
|
||||
.onChange(of: searchText) {
|
||||
// Reset keyboard index when search changes
|
||||
keyboardIndex = -1
|
||||
}
|
||||
|
||||
// Filters
|
||||
// Filters + Sort
|
||||
HStack(spacing: 12) {
|
||||
FilterToggle(isOn: $filterVision, icon: "\u{1F441}\u{FE0F}", label: "Vision")
|
||||
FilterToggle(isOn: $filterTools, icon: "\u{1F527}", label: "Tools")
|
||||
FilterToggle(isOn: $filterOnline, icon: "\u{1F310}", label: "Online")
|
||||
FilterToggle(isOn: $filterImageGen, icon: "\u{1F3A8}", label: "Image Gen")
|
||||
FilterToggle(isOn: $filterThinking, icon: "\u{1F9E0}", label: "Thinking")
|
||||
|
||||
Spacer()
|
||||
|
||||
// Sort menu
|
||||
Menu {
|
||||
ForEach(ModelSortOrder.allCases, id: \.rawValue) { order in
|
||||
Button {
|
||||
sortOrder = order
|
||||
keyboardIndex = -1
|
||||
} label: {
|
||||
if sortOrder == order {
|
||||
Label(order.label, systemImage: "checkmark")
|
||||
} else {
|
||||
Text(order.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "arrow.up.arrow.down")
|
||||
Text("Sort")
|
||||
}
|
||||
.font(.caption)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(sortOrder != .default ? Color.blue.opacity(0.2) : Color.gray.opacity(0.1))
|
||||
.foregroundColor(sortOrder != .default ? .blue : .secondary)
|
||||
.cornerRadius(6)
|
||||
}
|
||||
.menuStyle(.borderlessButton)
|
||||
.fixedSize()
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 12)
|
||||
@@ -91,13 +139,11 @@ struct ModelSelectorView: View {
|
||||
ModelRowView(
|
||||
model: model,
|
||||
isSelected: model.id == selectedModel?.id,
|
||||
isKeyboardHighlighted: index == keyboardIndex
|
||||
isKeyboardHighlighted: index == keyboardIndex,
|
||||
onSelect: { onSelect(model) },
|
||||
onInfo: { selectedInfoModel = model }
|
||||
)
|
||||
.id(model.id)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
onSelect(model)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.onChange(of: keyboardIndex) { _, newIndex in
|
||||
@@ -143,20 +189,42 @@ struct ModelSelectorView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Initialize keyboard index to current selection
|
||||
if let selected = selectedModel,
|
||||
let index = filteredModels.firstIndex(where: { $0.id == selected.id }) {
|
||||
keyboardIndex = index
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedInfoModel) { model in
|
||||
ModelInfoView(model: model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sort Order
|
||||
|
||||
enum ModelSortOrder: String, CaseIterable {
|
||||
case `default`
|
||||
case priceLowHigh
|
||||
case priceHighLow
|
||||
case contextHighLow
|
||||
|
||||
var label: LocalizedStringKey {
|
||||
switch self {
|
||||
case .default: return "Default"
|
||||
case .priceLowHigh: return "Price: Low to High"
|
||||
case .priceHighLow: return "Price: High to Low"
|
||||
case .contextHighLow: return "Context: High to Low"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Filter Toggle
|
||||
|
||||
struct FilterToggle: View {
|
||||
@Binding var isOn: Bool
|
||||
let icon: String
|
||||
let label: String
|
||||
let label: LocalizedStringKey
|
||||
|
||||
var body: some View {
|
||||
Button(action: { isOn.toggle() }) {
|
||||
@@ -175,51 +243,74 @@ struct FilterToggle: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Model Row
|
||||
|
||||
struct ModelRowView: View {
|
||||
let model: ModelInfo
|
||||
let isSelected: Bool
|
||||
var isKeyboardHighlighted: Bool = false
|
||||
let onSelect: () -> Void
|
||||
let onInfo: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Text(model.name)
|
||||
.font(.headline)
|
||||
.foregroundColor(isSelected ? .blue : .primary)
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
// Selectable main content
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Text(model.name)
|
||||
.font(.headline)
|
||||
.foregroundColor(isSelected ? .blue : .primary)
|
||||
|
||||
Spacer()
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
|
||||
// Capabilities
|
||||
Text(model.id)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if let description = model.description {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Label(model.contextLengthDisplay, systemImage: "text.alignleft")
|
||||
Label(model.promptPriceDisplay + "/1M", systemImage: "dollarsign.circle")
|
||||
}
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { onSelect() }
|
||||
|
||||
// Right side: capabilities + info button
|
||||
VStack(alignment: .trailing, spacing: 6) {
|
||||
// Capability icons
|
||||
HStack(spacing: 4) {
|
||||
if model.capabilities.vision { Text("\u{1F441}\u{FE0F}").font(.caption) }
|
||||
if model.capabilities.tools { Text("\u{1F527}").font(.caption) }
|
||||
if model.capabilities.online { Text("\u{1F310}").font(.caption) }
|
||||
if model.capabilities.imageGeneration { Text("\u{1F3A8}").font(.caption) }
|
||||
if model.capabilities.thinking { Text("\u{1F9E0}").font(.caption) }
|
||||
}
|
||||
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.blue)
|
||||
// Info button
|
||||
Button(action: onInfo) {
|
||||
Image(systemName: "info.circle")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Show model info")
|
||||
}
|
||||
|
||||
Text(model.id)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if let description = model.description {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Label(model.contextLengthDisplay, systemImage: "text.alignleft")
|
||||
Label(model.promptPriceDisplay + "/1M", systemImage: "dollarsign.circle")
|
||||
}
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, isKeyboardHighlighted ? 4 : 0)
|
||||
|
||||
@@ -142,7 +142,7 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
tabButton(7, icon: "brain", label: "Skills")
|
||||
tabButton(4, icon: "arrow.triangle.2.circlepath", label: "Sync")
|
||||
tabButton(5, icon: "envelope", label: "Email")
|
||||
tabButton(8, icon: "doc.text", label: "Paperless")
|
||||
tabButton(8, icon: "doc.text", label: "Paperless", beta: true)
|
||||
tabButton(9, icon: "icloud.and.arrow.up", label: "Backup")
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
@@ -283,10 +283,42 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
rowDivider()
|
||||
row("MCP (File Access)") {
|
||||
Toggle("", isOn: $settingsService.mcpEnabled)
|
||||
row("Reasoning (Thinking)") {
|
||||
Toggle("", isOn: $settingsService.reasoningEnabled)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
if settingsService.reasoningEnabled {
|
||||
rowDivider()
|
||||
row("Reasoning Effort") {
|
||||
Picker("", selection: $settingsService.reasoningEffort) {
|
||||
Text("High (~80%)").tag("high")
|
||||
Text("Medium (~50%)").tag("medium")
|
||||
Text("Low (~20%)").tag("low")
|
||||
Text("Minimal (~10%)").tag("minimal")
|
||||
}
|
||||
.labelsHidden()
|
||||
.fixedSize()
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(reasoningEffortDescription)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.bottom, 4)
|
||||
rowDivider()
|
||||
row("Hide Reasoning in Response") {
|
||||
Toggle("", isOn: $settingsService.reasoningExclude)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Model thinks internally but reasoning is not shown in chat")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2050,13 +2082,25 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
|
||||
// MARK: - Tab Navigation
|
||||
|
||||
private func tabButton(_ tag: Int, icon: String, label: String) -> some View {
|
||||
private func tabButton(_ tag: Int, icon: String, label: LocalizedStringKey, beta: Bool = false) -> some View {
|
||||
Button(action: { selectedTab = tag }) {
|
||||
VStack(spacing: 3) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 22))
|
||||
.frame(height: 28)
|
||||
.foregroundStyle(selectedTab == tag ? .blue : .secondary)
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 22))
|
||||
.frame(height: 28)
|
||||
.foregroundStyle(selectedTab == tag ? .blue : .secondary)
|
||||
if beta {
|
||||
Text("β")
|
||||
.font(.system(size: 8, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 3)
|
||||
.padding(.vertical, 1)
|
||||
.background(Color.orange)
|
||||
.clipShape(Capsule())
|
||||
.offset(x: 6, y: -2)
|
||||
}
|
||||
}
|
||||
Text(label)
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(selectedTab == tag ? .blue : .secondary)
|
||||
@@ -2070,7 +2114,7 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private func tabTitle(_ tag: Int) -> String {
|
||||
private func tabTitle(_ tag: Int) -> LocalizedStringKey {
|
||||
switch tag {
|
||||
case 0: return "General"
|
||||
case 1: return "MCP"
|
||||
@@ -2086,13 +2130,23 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reasoning Helpers
|
||||
|
||||
private var reasoningEffortDescription: LocalizedStringKey {
|
||||
switch settingsService.reasoningEffort {
|
||||
case "high": return "Uses ~80% of max tokens for reasoning — best for hard problems"
|
||||
case "medium": return "Uses ~50% of max tokens for reasoning — balanced default"
|
||||
case "low": return "Uses ~20% of max tokens for reasoning — faster, cheaper"
|
||||
case "minimal": return "Uses ~10% of max tokens for reasoning — lightest thinking"
|
||||
default: return "Uses ~50% of max tokens for reasoning — balanced default"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Layout Helpers
|
||||
|
||||
private func row<Content: View>(_ label: String, @ViewBuilder content: () -> Content) -> some View {
|
||||
private func row<Content: View>(_ label: LocalizedStringKey, @ViewBuilder content: () -> Content) -> some View {
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
if !label.isEmpty {
|
||||
Text(label).font(.system(size: 14))
|
||||
}
|
||||
Text(label).font(.system(size: 14))
|
||||
Spacer()
|
||||
content()
|
||||
}
|
||||
@@ -2100,7 +2154,7 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
|
||||
private func sectionHeader(_ title: String) -> some View {
|
||||
private func sectionHeader(_ title: LocalizedStringKey) -> some View {
|
||||
Text(title)
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -2205,7 +2259,7 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
return .green
|
||||
}
|
||||
|
||||
private var syncStatusText: String {
|
||||
private var syncStatusText: LocalizedStringKey {
|
||||
guard settingsService.syncEnabled else { return "Disabled" }
|
||||
guard settingsService.syncConfigured else { return "Not configured" }
|
||||
guard gitSync.syncStatus.isCloned else { return "Not cloned" }
|
||||
@@ -2318,19 +2372,9 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
}
|
||||
|
||||
private func timeAgo(_ date: Date) -> String {
|
||||
let seconds = Int(Date().timeIntervalSince(date))
|
||||
if seconds < 60 {
|
||||
return "just now"
|
||||
} else if seconds < 3600 {
|
||||
let minutes = seconds / 60
|
||||
return "\(minutes) minute\(minutes == 1 ? "" : "s") ago"
|
||||
} else if seconds < 86400 {
|
||||
let hours = seconds / 3600
|
||||
return "\(hours) hour\(hours == 1 ? "" : "s") ago"
|
||||
} else {
|
||||
let days = seconds / 86400
|
||||
return "\(days) day\(days == 1 ? "" : "s") ago"
|
||||
}
|
||||
let formatter = RelativeDateTimeFormatter()
|
||||
formatter.unitsStyle = .full
|
||||
return formatter.localizedString(for: date, relativeTo: .now)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ struct StatsView: View {
|
||||
}
|
||||
|
||||
struct StatRow: View {
|
||||
let label: String
|
||||
let label: LocalizedStringKey
|
||||
let value: String
|
||||
|
||||
var body: some View {
|
||||
@@ -137,7 +137,7 @@ struct StatRow: View {
|
||||
|
||||
struct CapabilityBadge: View {
|
||||
let icon: String
|
||||
let label: String
|
||||
let label: LocalizedStringKey
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 2) {
|
||||
|
||||
Reference in New Issue
Block a user