mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
M7 #4: split isAgentWorking into isGenerating + isPostProcessing
Pass-1 showed the "Agent is working…" spinner persisting long after the reply had landed in the message list — Hermes delays the ACP `promptComplete` event while it does auxiliary post-work (title generation, usage accounting). Spinner stuck ~minute+ on a 2-second response. Fix without touching the ACP state machine: derive two computed properties from existing signals in RichChatViewModel: - `isGenerating`: agent is working AND we don't yet have a finalized assistant reply on the message list. Drives the prominent spinner. - `isPostProcessing`: agent is working AND the user CAN see the reply. Drives a subtle "Finishing up…" pill instead of the big spinner. When `promptComplete` finally arrives, `isAgentWorking` flips false and both derived props go quiet. `isAgentWorking` remains the canonical ACP-level flag (kept public for any consumer that really wants the raw value), just no longer the signal for visible "spinner now" UX. Applied to: - ScarfGo ChatView.swift — primary spinner + post-processing pill. - Mac RichChatView.swift — SessionInfoBar + RichChatMessageList now take `isGenerating` instead of `isAgentWorking`. Same UX win for the macOS app (pass-1 finding was cross-platform, just surfaced first on iOS). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,11 +44,41 @@ public final class RichChatViewModel {
|
||||
public var messages: [HermesMessage] = []
|
||||
public var currentSession: HermesSession?
|
||||
public var messageGroups: [MessageGroup] = []
|
||||
/// True from the moment the user sends a prompt until the ACP
|
||||
/// `promptComplete` event arrives. Covers the whole round-trip
|
||||
/// including auxiliary post-processing (title generation, usage
|
||||
/// accounting, etc.). UIs should prefer the `isGenerating` /
|
||||
/// `isPostProcessing` pair below — they distinguish "agent is
|
||||
/// thinking about your message" from "agent is closing out" and
|
||||
/// avoid the misleading "spinner after the reply has landed" UX
|
||||
/// we saw in pass-1 (M7 #4).
|
||||
public var isAgentWorking = false
|
||||
public var pendingPermission: PendingPermission?
|
||||
/// Mutated to trigger a scroll-to-bottom in the message list.
|
||||
public var scrollTrigger = UUID()
|
||||
|
||||
/// True while the assistant hasn't yet emitted a complete reply
|
||||
/// for the latest user prompt. Renders the prominent "Agent is
|
||||
/// thinking…" indicator in the chat. Flips false as soon as we've
|
||||
/// finalized an assistant message with content — even if the ACP
|
||||
/// `promptComplete` event hasn't arrived yet (Hermes auxiliary
|
||||
/// work like title generation delays that event).
|
||||
public var isGenerating: Bool {
|
||||
isAgentWorking && !isPostProcessing
|
||||
}
|
||||
|
||||
/// True while ACP hasn't closed out the prompt but the assistant
|
||||
/// has already finalized a reply the user can see. Renders a
|
||||
/// subtle "Finishing up…" pill instead of the prominent spinner.
|
||||
/// Avoids the pass-1 M7 #4 UX where users stared at "Agent is
|
||||
/// working…" forever because `promptComplete` was held up by
|
||||
/// auxiliary server-side work.
|
||||
public var isPostProcessing: Bool {
|
||||
guard isAgentWorking else { return false }
|
||||
guard let last = messages.last else { return false }
|
||||
return last.isAssistant && !last.content.isEmpty
|
||||
}
|
||||
|
||||
// Cumulative ACP token tracking (ACP returns tokens per prompt but DB has none)
|
||||
public private(set) var acpInputTokens = 0
|
||||
public private(set) var acpOutputTokens = 0
|
||||
|
||||
@@ -94,15 +94,26 @@ struct ChatView: View {
|
||||
MessageBubble(message: msg)
|
||||
.id(msg.id)
|
||||
}
|
||||
if controller.vm.isAgentWorking {
|
||||
if controller.vm.isGenerating {
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text("Agent is working…")
|
||||
Text("Agent is thinking…")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal)
|
||||
} else if controller.vm.isPostProcessing {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "ellipsis")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
Text("Finishing up…")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
Color.clear
|
||||
.frame(height: 1)
|
||||
|
||||
@@ -15,7 +15,13 @@ struct RichChatView: View {
|
||||
VStack(spacing: 0) {
|
||||
SessionInfoBar(
|
||||
session: richChat.currentSession,
|
||||
isWorking: richChat.isAgentWorking,
|
||||
// Prefer `isGenerating` over the raw `isAgentWorking`
|
||||
// so the info bar drops the spinner as soon as the
|
||||
// assistant's reply is visible, even while ACP
|
||||
// auxiliary work (title gen, usage accounting) is
|
||||
// still in flight. See RichChatViewModel docs — same
|
||||
// fix as ScarfGo for pass-1 M7 #4.
|
||||
isWorking: richChat.isGenerating,
|
||||
acpInputTokens: richChat.acpInputTokens,
|
||||
acpOutputTokens: richChat.acpOutputTokens,
|
||||
acpThoughtTokens: richChat.acpThoughtTokens,
|
||||
@@ -34,7 +40,7 @@ struct RichChatView: View {
|
||||
// which manifests as a white flash.
|
||||
RichChatMessageList(
|
||||
groups: richChat.messageGroups,
|
||||
isWorking: richChat.isAgentWorking,
|
||||
isWorking: richChat.isGenerating,
|
||||
isLoadingSession: chatViewModel.isPreparingSession,
|
||||
scrollTrigger: richChat.scrollTrigger
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user