diff --git a/scarf/scarf/Features/Chat/Views/ChatView.swift b/scarf/scarf/Features/Chat/Views/ChatView.swift index b28a8f5..b70e107 100644 --- a/scarf/scarf/Features/Chat/Views/ChatView.swift +++ b/scarf/scarf/Features/Chat/Views/ChatView.swift @@ -2,6 +2,7 @@ import SwiftUI struct ChatView: View { @Environment(ChatViewModel.self) private var viewModel + @Environment(HermesFileWatcher.self) private var fileWatcher var body: some View { VStack(spacing: 0) { @@ -11,6 +12,9 @@ struct ChatView: View { } .navigationTitle("Chat") .task { await viewModel.loadRecentSessions() } + .onChange(of: fileWatcher.lastChangeDate) { + Task { await viewModel.loadRecentSessions() } + } } private var toolbar: some View { @@ -36,6 +40,10 @@ struct ChatView: View { Spacer() + if viewModel.hasActiveProcess { + voiceControls + } + if !viewModel.hermesBinaryExists { Label("Hermes binary not found", systemImage: "exclamationmark.triangle") .font(.caption) @@ -80,6 +88,41 @@ struct ChatView: View { .padding(.vertical, 6) } + private var voiceControls: some View { + HStack(spacing: 8) { + Button { + viewModel.toggleVoice() + } label: { + HStack(spacing: 4) { + Image(systemName: viewModel.voiceEnabled ? "mic.fill" : "mic.slash") + .foregroundStyle(viewModel.voiceEnabled ? .green : .secondary) + Text(viewModel.voiceEnabled ? "Voice On" : "Voice Off") + .font(.caption) + .foregroundStyle(viewModel.voiceEnabled ? .primary : .secondary) + } + } + .buttonStyle(.plain) + .help("Toggle voice mode (/voice)") + + if viewModel.voiceEnabled { + Button { + viewModel.pushToTalk() + } label: { + HStack(spacing: 4) { + Image(systemName: viewModel.isRecording ? "waveform.circle.fill" : "waveform.circle") + .foregroundStyle(viewModel.isRecording ? .red : .accentColor) + .symbolEffect(.pulse, isActive: viewModel.isRecording) + Text(viewModel.isRecording ? "Recording..." : "Push to Talk") + .font(.caption) + } + } + .buttonStyle(.plain) + .help("Push to talk (Ctrl+B)") + .keyboardShortcut("b", modifiers: .control) + } + } + } + @ViewBuilder private var terminalArea: some View { if let terminal = viewModel.terminalView { diff --git a/scarf/scarf/Features/Dashboard/Views/DashboardView.swift b/scarf/scarf/Features/Dashboard/Views/DashboardView.swift index a773b19..47fc502 100644 --- a/scarf/scarf/Features/Dashboard/Views/DashboardView.swift +++ b/scarf/scarf/Features/Dashboard/Views/DashboardView.swift @@ -3,6 +3,7 @@ import SwiftUI struct DashboardView: View { @State private var viewModel = DashboardViewModel() @Environment(AppCoordinator.self) private var coordinator + @Environment(HermesFileWatcher.self) private var fileWatcher var body: some View { ScrollView { @@ -16,6 +17,9 @@ struct DashboardView: View { } .navigationTitle("Dashboard") .task { await viewModel.load() } + .onChange(of: fileWatcher.lastChangeDate) { + Task { await viewModel.load() } + } } private var statusSection: some View { diff --git a/scarf/scarf/Features/Sessions/ViewModels/SessionsViewModel.swift b/scarf/scarf/Features/Sessions/ViewModels/SessionsViewModel.swift index 6809a94..6aad57c 100644 --- a/scarf/scarf/Features/Sessions/ViewModels/SessionsViewModel.swift +++ b/scarf/scarf/Features/Sessions/ViewModels/SessionsViewModel.swift @@ -83,7 +83,7 @@ final class SessionsViewModel { let result = runHermes(["sessions", "rename", sessionId, title]) if result.exitCode == 0 { if let idx = sessions.firstIndex(where: { $0.id == sessionId }) { - sessions[idx] = HermesSession( + let updated = HermesSession( id: sessions[idx].id, source: sessions[idx].source, userId: sessions[idx].userId, model: sessions[idx].model, title: title, parentSessionId: sessions[idx].parentSessionId, @@ -94,7 +94,12 @@ final class SessionsViewModel { cacheWriteTokens: sessions[idx].cacheWriteTokens, estimatedCostUSD: sessions[idx].estimatedCostUSD ) + sessions[idx] = updated + if selectedSession?.id == sessionId { + selectedSession = updated + } } + sessionPreviews[sessionId] = title } showRenameSheet = false renameSessionId = nil