mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +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 messages: [HermesMessage] = []
|
||||||
public var currentSession: HermesSession?
|
public var currentSession: HermesSession?
|
||||||
public var messageGroups: [MessageGroup] = []
|
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 isAgentWorking = false
|
||||||
public var pendingPermission: PendingPermission?
|
public var pendingPermission: PendingPermission?
|
||||||
/// Mutated to trigger a scroll-to-bottom in the message list.
|
/// Mutated to trigger a scroll-to-bottom in the message list.
|
||||||
public var scrollTrigger = UUID()
|
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)
|
// Cumulative ACP token tracking (ACP returns tokens per prompt but DB has none)
|
||||||
public private(set) var acpInputTokens = 0
|
public private(set) var acpInputTokens = 0
|
||||||
public private(set) var acpOutputTokens = 0
|
public private(set) var acpOutputTokens = 0
|
||||||
|
|||||||
@@ -94,15 +94,26 @@ struct ChatView: View {
|
|||||||
MessageBubble(message: msg)
|
MessageBubble(message: msg)
|
||||||
.id(msg.id)
|
.id(msg.id)
|
||||||
}
|
}
|
||||||
if controller.vm.isAgentWorking {
|
if controller.vm.isGenerating {
|
||||||
HStack {
|
HStack {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
Text("Agent is working…")
|
Text("Agent is thinking…")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.horizontal)
|
.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
|
Color.clear
|
||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
|
|||||||
@@ -15,7 +15,13 @@ struct RichChatView: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SessionInfoBar(
|
SessionInfoBar(
|
||||||
session: richChat.currentSession,
|
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,
|
acpInputTokens: richChat.acpInputTokens,
|
||||||
acpOutputTokens: richChat.acpOutputTokens,
|
acpOutputTokens: richChat.acpOutputTokens,
|
||||||
acpThoughtTokens: richChat.acpThoughtTokens,
|
acpThoughtTokens: richChat.acpThoughtTokens,
|
||||||
@@ -34,7 +40,7 @@ struct RichChatView: View {
|
|||||||
// which manifests as a white flash.
|
// which manifests as a white flash.
|
||||||
RichChatMessageList(
|
RichChatMessageList(
|
||||||
groups: richChat.messageGroups,
|
groups: richChat.messageGroups,
|
||||||
isWorking: richChat.isAgentWorking,
|
isWorking: richChat.isGenerating,
|
||||||
isLoadingSession: chatViewModel.isPreparingSession,
|
isLoadingSession: chatViewModel.isPreparingSession,
|
||||||
scrollTrigger: richChat.scrollTrigger
|
scrollTrigger: richChat.scrollTrigger
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user