Small feature changes and bug fixes
This commit is contained in:
@@ -32,7 +32,7 @@ struct ChatView: View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 12) {
|
||||
ForEach(viewModel.messages) { message in
|
||||
MessageRow(message: message)
|
||||
MessageRow(message: message, viewModel: viewModel)
|
||||
.id(message.id)
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ struct ContentView: View {
|
||||
.sheet(isPresented: $vm.showSettings, onDismiss: {
|
||||
chatViewModel.syncFromSettings()
|
||||
}) {
|
||||
SettingsView()
|
||||
SettingsView(chatViewModel: chatViewModel)
|
||||
}
|
||||
.sheet(isPresented: $vm.showStats) {
|
||||
StatsView(
|
||||
|
||||
@@ -84,6 +84,7 @@ struct FooterItem: View {
|
||||
|
||||
struct SyncStatusFooter: View {
|
||||
private let gitSync = GitSyncService.shared
|
||||
private let settings = SettingsService.shared
|
||||
private let guiSize = SettingsService.shared.guiTextSize
|
||||
@State private var syncText = "Not Synced"
|
||||
@State private var syncColor: Color = .secondary
|
||||
@@ -104,17 +105,23 @@ struct SyncStatusFooter: View {
|
||||
.onChange(of: gitSync.syncStatus.lastSyncTime) {
|
||||
updateSyncStatus()
|
||||
}
|
||||
.onChange(of: gitSync.syncStatus.isCloned) {
|
||||
updateSyncStatus()
|
||||
}
|
||||
.onChange(of: gitSync.lastSyncError) {
|
||||
updateSyncStatus()
|
||||
}
|
||||
.onChange(of: gitSync.isSyncing) {
|
||||
updateSyncStatus()
|
||||
}
|
||||
.onChange(of: settings.syncConfigured) {
|
||||
updateSyncStatus()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSyncStatus() {
|
||||
if let error = gitSync.lastSyncError {
|
||||
syncText = "Error With Sync"
|
||||
syncText = "Sync Error"
|
||||
syncColor = .red
|
||||
} else if gitSync.isSyncing {
|
||||
syncText = "Syncing..."
|
||||
@@ -123,10 +130,13 @@ struct SyncStatusFooter: View {
|
||||
syncText = "Last Sync: \(timeAgo(lastSync))"
|
||||
syncColor = .green
|
||||
} else if gitSync.syncStatus.isCloned {
|
||||
syncText = "Not Synced"
|
||||
syncText = "Sync: Ready"
|
||||
syncColor = .secondary
|
||||
} else if settings.syncConfigured {
|
||||
syncText = "Sync: Not Initialized"
|
||||
syncColor = .orange
|
||||
} else {
|
||||
syncText = "Not Configured"
|
||||
syncText = "Sync: Off"
|
||||
syncColor = .secondary
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,20 @@ import AppKit
|
||||
|
||||
struct MessageRow: View {
|
||||
let message: Message
|
||||
let viewModel: ChatViewModel?
|
||||
private let settings = SettingsService.shared
|
||||
|
||||
#if os(macOS)
|
||||
@State private var isHovering = false
|
||||
@State private var showCopied = false
|
||||
@State private var isStarred = false
|
||||
#endif
|
||||
|
||||
init(message: Message, viewModel: ChatViewModel? = nil) {
|
||||
self.message = message
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// Compact layout for system messages (tool calls)
|
||||
if message.role == .system && !isErrorMessage {
|
||||
@@ -45,6 +52,18 @@ struct MessageRow: View {
|
||||
Spacer()
|
||||
|
||||
#if os(macOS)
|
||||
// Star button (user/assistant messages only, visible on hover)
|
||||
if (message.role == .user || message.role == .assistant) && isHovering {
|
||||
Button(action: toggleStar) {
|
||||
Image(systemName: isStarred ? "star.fill" : "star")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(isStarred ? .yellow : .oaiSecondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.transition(.opacity)
|
||||
.help("Star this message to always include it in context")
|
||||
}
|
||||
|
||||
// Copy button (assistant messages only, visible on hover)
|
||||
if message.role == .assistant && isHovering && !message.content.isEmpty {
|
||||
Button(action: copyContent) {
|
||||
@@ -138,6 +157,9 @@ struct MessageRow: View {
|
||||
isHovering = hovering
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
loadStarredState()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -235,6 +257,17 @@ struct MessageRow: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadStarredState() {
|
||||
if let metadata = try? DatabaseService.shared.getMessageMetadata(messageId: message.id) {
|
||||
isStarred = metadata.user_starred == 1
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleStar() {
|
||||
viewModel?.toggleMessageStar(messageId: message.id)
|
||||
isStarred.toggle()
|
||||
}
|
||||
#endif
|
||||
|
||||
private var roleIcon: some View {
|
||||
|
||||
@@ -14,14 +14,23 @@ struct ConversationListView: View {
|
||||
@State private var conversations: [Conversation] = []
|
||||
@State private var selectedConversations: Set<UUID> = []
|
||||
@State private var isSelecting = false
|
||||
@State private var useSemanticSearch = false
|
||||
@State private var semanticResults: [Conversation] = []
|
||||
@State private var isSearching = false
|
||||
private let settings = SettingsService.shared
|
||||
var onLoad: ((Conversation) -> Void)?
|
||||
|
||||
private var filteredConversations: [Conversation] {
|
||||
if searchText.isEmpty {
|
||||
return conversations
|
||||
}
|
||||
return conversations.filter {
|
||||
$0.name.lowercased().contains(searchText.lowercased())
|
||||
|
||||
if useSemanticSearch && settings.embeddingsEnabled {
|
||||
return semanticResults
|
||||
} else {
|
||||
return conversations.filter {
|
||||
$0.name.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +88,11 @@ struct ConversationListView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
TextField("Search conversations...", text: $searchText)
|
||||
.textFieldStyle(.plain)
|
||||
.onChange(of: searchText) {
|
||||
if useSemanticSearch && settings.embeddingsEnabled && !searchText.isEmpty {
|
||||
performSemanticSearch()
|
||||
}
|
||||
}
|
||||
if !searchText.isEmpty {
|
||||
Button { searchText = "" } label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
@@ -86,6 +100,25 @@ struct ConversationListView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
if settings.embeddingsEnabled {
|
||||
Divider()
|
||||
.frame(height: 16)
|
||||
Toggle("Semantic", isOn: $useSemanticSearch)
|
||||
.toggleStyle(.switch)
|
||||
.controlSize(.small)
|
||||
.onChange(of: useSemanticSearch) {
|
||||
if useSemanticSearch && !searchText.isEmpty {
|
||||
performSemanticSearch()
|
||||
}
|
||||
}
|
||||
.help("Use AI-powered semantic search instead of keyword matching")
|
||||
}
|
||||
|
||||
if isSearching {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 8))
|
||||
@@ -231,6 +264,52 @@ struct ConversationListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func performSemanticSearch() {
|
||||
guard !searchText.isEmpty else {
|
||||
semanticResults = []
|
||||
return
|
||||
}
|
||||
|
||||
isSearching = true
|
||||
|
||||
Task {
|
||||
do {
|
||||
// Use user's selected provider, or fall back to best available
|
||||
guard let provider = EmbeddingService.shared.getSelectedProvider() else {
|
||||
Log.api.warning("No embedding providers available - skipping semantic search")
|
||||
await MainActor.run {
|
||||
isSearching = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Generate embedding for search query
|
||||
let embedding = try await EmbeddingService.shared.generateEmbedding(
|
||||
text: searchText,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
// Search conversations
|
||||
let results = try DatabaseService.shared.searchConversationsBySemantic(
|
||||
queryEmbedding: embedding,
|
||||
limit: 20
|
||||
)
|
||||
|
||||
await MainActor.run {
|
||||
semanticResults = results.map { $0.0 }
|
||||
isSearching = false
|
||||
Log.ui.info("Semantic search found \(results.count) results using \(provider.displayName)")
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
semanticResults = []
|
||||
isSearching = false
|
||||
Log.ui.error("Semantic search failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func exportConversation(_ conversation: Conversation) {
|
||||
guard let (_, loadedMessages) = try? DatabaseService.shared.loadConversation(id: conversation.id),
|
||||
!loadedMessages.isEmpty else {
|
||||
|
||||
@@ -12,6 +12,12 @@ struct SettingsView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Bindable private var settingsService = SettingsService.shared
|
||||
private var mcpService = MCPService.shared
|
||||
private let gitSync = GitSyncService.shared
|
||||
var chatViewModel: ChatViewModel?
|
||||
|
||||
init(chatViewModel: ChatViewModel? = nil) {
|
||||
self.chatViewModel = chatViewModel
|
||||
}
|
||||
|
||||
@State private var openrouterKey = ""
|
||||
@State private var anthropicKey = ""
|
||||
@@ -31,6 +37,7 @@ struct SettingsView: View {
|
||||
@State private var showSyncToken = false
|
||||
@State private var isTestingSync = false
|
||||
@State private var syncTestResult: String?
|
||||
@State private var isSyncing = false
|
||||
|
||||
// OAuth state
|
||||
@State private var oauthCode = ""
|
||||
@@ -769,6 +776,141 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
|
||||
divider()
|
||||
|
||||
sectionHeader("Memory & Context")
|
||||
|
||||
row("Smart Context Selection") {
|
||||
Toggle("", isOn: $settingsService.contextSelectionEnabled)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
Text("Automatically select relevant messages instead of sending all history. Reduces token usage and improves response quality for long conversations.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
if settingsService.contextSelectionEnabled {
|
||||
row("Max Context Tokens") {
|
||||
HStack(spacing: 8) {
|
||||
TextField("", value: $settingsService.contextMaxTokens, format: .number)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(width: 120)
|
||||
Text("tokens")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Spacer().frame(width: labelWidth + 12)
|
||||
Text("Maximum context window size. Default is 100,000 tokens. Smart selection will prioritize recent and starred messages within this limit.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
divider()
|
||||
|
||||
sectionHeader("Semantic Search")
|
||||
|
||||
row("Enable Embeddings") {
|
||||
Toggle("", isOn: $settingsService.embeddingsEnabled)
|
||||
.toggleStyle(.switch)
|
||||
.disabled(!EmbeddingService.shared.isAvailable)
|
||||
}
|
||||
|
||||
// Show status based on available providers
|
||||
if let provider = EmbeddingService.shared.getBestAvailableProvider() {
|
||||
Text("Enable AI-powered semantic search across conversations using \(provider.displayName) embeddings.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} else {
|
||||
Text("⚠️ No embedding providers available. Please configure an API key for OpenAI, OpenRouter, or Google in the General tab.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.orange)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
if settingsService.embeddingsEnabled {
|
||||
row("Model") {
|
||||
Picker("", selection: $settingsService.embeddingProvider) {
|
||||
if settingsService.openaiAPIKey != nil && !settingsService.openaiAPIKey!.isEmpty {
|
||||
Text("OpenAI (text-embedding-3-small)").tag("openai-small")
|
||||
Text("OpenAI (text-embedding-3-large)").tag("openai-large")
|
||||
}
|
||||
if settingsService.openrouterAPIKey != nil && !settingsService.openrouterAPIKey!.isEmpty {
|
||||
Text("OpenRouter (OpenAI small)").tag("openrouter-openai-small")
|
||||
Text("OpenRouter (OpenAI large)").tag("openrouter-openai-large")
|
||||
Text("OpenRouter (Qwen 8B)").tag("openrouter-qwen")
|
||||
}
|
||||
if settingsService.googleAPIKey != nil && !settingsService.googleAPIKey!.isEmpty {
|
||||
Text("Google (Gemini embedding)").tag("google-gemini")
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
}
|
||||
HStack {
|
||||
Spacer().frame(width: labelWidth + 12)
|
||||
Text("Cost: OpenAI ~$0.02-0.13/1M tokens, OpenRouter similar, Google ~$0.15/1M tokens")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer().frame(width: labelWidth + 12)
|
||||
Button("Embed All Conversations") {
|
||||
Task {
|
||||
if let chatVM = chatViewModel {
|
||||
await chatVM.batchEmbedAllConversations()
|
||||
}
|
||||
}
|
||||
}
|
||||
.help("Generate embeddings for all existing messages (one-time operation)")
|
||||
}
|
||||
HStack {
|
||||
Spacer().frame(width: labelWidth + 12)
|
||||
Text("⚠️ This will generate embeddings for all messages in all conversations. Estimated cost: ~$0.04 for 10,000 messages.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.orange)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
divider()
|
||||
|
||||
sectionHeader("Progressive Summarization")
|
||||
|
||||
row("Enable Summarization") {
|
||||
Toggle("", isOn: $settingsService.progressiveSummarizationEnabled)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
Text("Automatically summarize old portions of long conversations to save tokens and improve context efficiency.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
if settingsService.progressiveSummarizationEnabled {
|
||||
row("Message Threshold") {
|
||||
HStack(spacing: 8) {
|
||||
TextField("", value: $settingsService.summarizationThreshold, format: .number)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(width: 80)
|
||||
Text("messages")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Spacer().frame(width: labelWidth + 12)
|
||||
Text("When a conversation exceeds this many messages, older messages will be summarized. Default: 50 messages.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
divider()
|
||||
|
||||
sectionHeader("Info")
|
||||
HStack {
|
||||
Spacer().frame(width: labelWidth + 12)
|
||||
@@ -1066,57 +1208,72 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
|
||||
row("") {
|
||||
HStack(spacing: 12) {
|
||||
Button("Clone Repository") {
|
||||
Task { await cloneRepo() }
|
||||
if !gitSync.syncStatus.isCloned {
|
||||
// Not cloned yet - show initialize button
|
||||
Button {
|
||||
Task { await cloneRepo() }
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
if isSyncing {
|
||||
ProgressView()
|
||||
.scaleEffect(0.7)
|
||||
.frame(width: 16, height: 16)
|
||||
} else {
|
||||
Image(systemName: "arrow.down.circle")
|
||||
}
|
||||
Text("Initialize Repository")
|
||||
}
|
||||
.frame(minWidth: 160)
|
||||
}
|
||||
.disabled(!settingsService.syncConfigured || isSyncing)
|
||||
} else {
|
||||
// Already cloned - show sync button
|
||||
Button {
|
||||
Task { await syncNow() }
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
if isSyncing {
|
||||
ProgressView()
|
||||
.scaleEffect(0.7)
|
||||
.frame(width: 16, height: 16)
|
||||
} else {
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
}
|
||||
Text("Sync Now")
|
||||
}
|
||||
.frame(minWidth: 160)
|
||||
}
|
||||
.disabled(isSyncing)
|
||||
}
|
||||
.disabled(!settingsService.syncConfigured)
|
||||
|
||||
Button("Export All") {
|
||||
Task { await exportConversations() }
|
||||
}
|
||||
.disabled(!GitSyncService.shared.syncStatus.isCloned)
|
||||
|
||||
Button("Push") {
|
||||
Task { await pushToGit() }
|
||||
}
|
||||
.disabled(!GitSyncService.shared.syncStatus.isCloned)
|
||||
|
||||
Button("Pull") {
|
||||
Task { await pullFromGit() }
|
||||
}
|
||||
.disabled(!GitSyncService.shared.syncStatus.isCloned)
|
||||
|
||||
Button("Import") {
|
||||
Task { await importConversations() }
|
||||
}
|
||||
.disabled(!GitSyncService.shared.syncStatus.isCloned)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
// Status
|
||||
if GitSyncService.shared.syncStatus.isCloned {
|
||||
if gitSync.syncStatus.isCloned {
|
||||
HStack {
|
||||
Spacer().frame(width: labelWidth + 12)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if let lastSync = GitSyncService.shared.syncStatus.lastSyncTime {
|
||||
if let lastSync = gitSync.syncStatus.lastSyncTime {
|
||||
Text("Last sync: \(timeAgo(lastSync))")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if GitSyncService.shared.syncStatus.uncommittedChanges > 0 {
|
||||
Text("Uncommitted changes: \(GitSyncService.shared.syncStatus.uncommittedChanges)")
|
||||
if gitSync.syncStatus.uncommittedChanges > 0 {
|
||||
Text("Uncommitted changes: \(gitSync.syncStatus.uncommittedChanges)")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
|
||||
if let branch = GitSyncService.shared.syncStatus.currentBranch {
|
||||
if let branch = gitSync.syncStatus.currentBranch {
|
||||
Text("Branch: \(branch)")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if let status = GitSyncService.shared.syncStatus.remoteStatus {
|
||||
if let status = gitSync.syncStatus.remoteStatus {
|
||||
Text("Remote: \(status)")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -1133,6 +1290,11 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
syncUsername = settingsService.syncUsername ?? ""
|
||||
syncPassword = settingsService.syncPassword ?? ""
|
||||
syncAccessToken = settingsService.syncAccessToken ?? ""
|
||||
|
||||
// Update sync status to check if repository is already cloned
|
||||
Task {
|
||||
await gitSync.updateStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1691,7 +1853,7 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
syncTestResult = nil
|
||||
|
||||
do {
|
||||
let result = try await GitSyncService.shared.testConnection()
|
||||
let result = try await gitSync.testConnection()
|
||||
syncTestResult = "✓ \(result)"
|
||||
} catch {
|
||||
syncTestResult = "✗ \(error.localizedDescription)"
|
||||
@@ -1703,27 +1865,27 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
private var syncStatusIcon: String {
|
||||
guard settingsService.syncEnabled else { return "externaldrive.slash" }
|
||||
guard settingsService.syncConfigured else { return "exclamationmark.triangle" }
|
||||
guard GitSyncService.shared.syncStatus.isCloned else { return "externaldrive.badge.questionmark" }
|
||||
guard gitSync.syncStatus.isCloned else { return "externaldrive.badge.questionmark" }
|
||||
return "externaldrive.badge.checkmark"
|
||||
}
|
||||
|
||||
private var syncStatusColor: Color {
|
||||
guard settingsService.syncEnabled else { return .secondary }
|
||||
guard settingsService.syncConfigured else { return .orange }
|
||||
guard GitSyncService.shared.syncStatus.isCloned else { return .orange }
|
||||
guard gitSync.syncStatus.isCloned else { return .orange }
|
||||
return .green
|
||||
}
|
||||
|
||||
private var syncStatusText: String {
|
||||
guard settingsService.syncEnabled else { return "Disabled" }
|
||||
guard settingsService.syncConfigured else { return "Not configured" }
|
||||
guard GitSyncService.shared.syncStatus.isCloned else { return "Not cloned" }
|
||||
guard gitSync.syncStatus.isCloned else { return "Not cloned" }
|
||||
return "Ready"
|
||||
}
|
||||
|
||||
private func cloneRepo() async {
|
||||
do {
|
||||
try await GitSyncService.shared.cloneRepository()
|
||||
try await gitSync.cloneRepository()
|
||||
syncTestResult = "✓ Repository cloned successfully"
|
||||
} catch {
|
||||
syncTestResult = "✗ \(error.localizedDescription)"
|
||||
@@ -1732,7 +1894,7 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
|
||||
private func exportConversations() async {
|
||||
do {
|
||||
try await GitSyncService.shared.exportAllConversations()
|
||||
try await gitSync.exportAllConversations()
|
||||
syncTestResult = "✓ Conversations exported"
|
||||
} catch {
|
||||
syncTestResult = "✗ \(error.localizedDescription)"
|
||||
@@ -1742,9 +1904,9 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
private func pushToGit() async {
|
||||
do {
|
||||
// First export conversations
|
||||
try await GitSyncService.shared.exportAllConversations()
|
||||
try await gitSync.exportAllConversations()
|
||||
// Then push
|
||||
try await GitSyncService.shared.push()
|
||||
try await gitSync.push()
|
||||
syncTestResult = "✓ Changes pushed successfully"
|
||||
} catch {
|
||||
syncTestResult = "✗ \(error.localizedDescription)"
|
||||
@@ -1753,7 +1915,7 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
|
||||
private func pullFromGit() async {
|
||||
do {
|
||||
try await GitSyncService.shared.pull()
|
||||
try await gitSync.pull()
|
||||
syncTestResult = "✓ Changes pulled successfully"
|
||||
} catch {
|
||||
syncTestResult = "✗ \(error.localizedDescription)"
|
||||
@@ -1762,13 +1924,44 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
|
||||
private func importConversations() async {
|
||||
do {
|
||||
let result = try await GitSyncService.shared.importAllConversations()
|
||||
let result = try await gitSync.importAllConversations()
|
||||
syncTestResult = "✓ Imported \(result.imported) conversations (skipped \(result.skipped) duplicates)"
|
||||
} catch {
|
||||
syncTestResult = "✗ \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
|
||||
private func syncNow() async {
|
||||
isSyncing = true
|
||||
syncTestResult = nil
|
||||
|
||||
do {
|
||||
// Step 1: Export all conversations
|
||||
syncTestResult = "Exporting conversations..."
|
||||
try await gitSync.exportAllConversations()
|
||||
|
||||
// Step 2: Pull from remote
|
||||
syncTestResult = "Pulling changes..."
|
||||
try await gitSync.pull()
|
||||
|
||||
// Step 3: Import any new conversations
|
||||
syncTestResult = "Importing conversations..."
|
||||
let result = try await gitSync.importAllConversations()
|
||||
|
||||
// Step 4: Push to remote
|
||||
syncTestResult = "Pushing changes..."
|
||||
try await gitSync.push()
|
||||
|
||||
// Success
|
||||
await gitSync.updateStatus()
|
||||
syncTestResult = "✓ Sync complete: \(result.imported) imported, \(result.skipped) skipped"
|
||||
} catch {
|
||||
syncTestResult = "✗ Sync failed: \(error.localizedDescription)"
|
||||
}
|
||||
|
||||
isSyncing = false
|
||||
}
|
||||
|
||||
private var tokenGenerationURL: String? {
|
||||
let url = settingsService.syncRepoURL.lowercased()
|
||||
if url.contains("github.com") {
|
||||
|
||||
Reference in New Issue
Block a user