Files
scarf/scarf/scarf/Features/Chat/Views/RichChatView.swift
T
Alan Wizemann c8208dedb1 fix(chat): slash-menu filter, auto-scroll on send/complete, loading state
- Slash menu: filter at the parent and pass the pre-filtered list to
  SlashCommandMenu (pure-prefix match, no description fallback). Adds
  `.id(menuQuery)` to force a fresh view on every query so SwiftUI can't
  render stale props — this was the cause of "typing /mo still shows
  /help" (the old description fallback plus a cached child view kept
  /help pinned regardless of query).
- Auto-scroll to bottom when the user submits a message and again when
  the prompt completes. `.defaultScrollAnchor(.bottom)` handles slow
  streaming fine, but rapid slash-command responses outran the anchor
  and left the response off-screen.
- Loading state: add `ChatViewModel.isPreparingSession` (true during
  Starting / Creating / Loading / Reconnecting). While true, the message
  list swaps its placeholder for a ProgressView — non-blocking, just a
  view inside the ScrollView.
- Center the empty-state placeholder properly: replace
  `.padding(.vertical, 80)` with Spacers inside
  `.containerRelativeFrame(.vertical)` so the placeholder sits in the
  true vertical center of the chat pane at any window size.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:15:46 -07:00

53 lines
2.0 KiB
Swift

import SwiftUI
struct RichChatView: View {
@Bindable var richChat: RichChatViewModel
var onSend: (String) -> Void
var isEnabled: Bool
@Environment(HermesFileWatcher.self) private var fileWatcher
@Environment(ChatViewModel.self) private var chatViewModel
/// In ACP mode, events drive updates directly no DB polling needed.
private var isACPMode: Bool { chatViewModel.isACPConnected }
var body: some View {
VStack(spacing: 0) {
SessionInfoBar(
session: richChat.currentSession,
isWorking: richChat.isAgentWorking,
acpInputTokens: richChat.acpInputTokens,
acpOutputTokens: richChat.acpOutputTokens,
acpThoughtTokens: richChat.acpThoughtTokens
)
Divider()
// Always mount RichChatMessageList; empty state lives inside it.
// Swapping between a ContentUnavailableView and the ScrollView
// hierarchy on first message caused a full view tree rebuild,
// which manifests as a white flash.
RichChatMessageList(
groups: richChat.messageGroups,
isWorking: richChat.isAgentWorking,
isLoadingSession: chatViewModel.isPreparingSession,
scrollTrigger: richChat.scrollTrigger
)
Divider()
RichChatInputBar(
onSend: { text in
onSend(text)
},
isEnabled: isEnabled,
commands: richChat.availableCommands,
showCompressButton: richChat.supportsCompress && !richChat.hasBroaderCommandMenu
)
}
// DB polling fallback for terminal mode only never overwrite ACP messages
.onChange(of: fileWatcher.lastChangeDate) {
if !isACPMode, !richChat.hasMessages, richChat.sessionId != nil {
richChat.scheduleRefresh()
}
}
}
}