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:
Alan Wizemann
2026-04-24 13:12:25 +02:00
parent 742605d359
commit 8e14e0e776
3 changed files with 51 additions and 4 deletions
@@ -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