Hold agent activation in its loading state until the agent responds

Clicking Activate reverted to idle (with a success toast) as soon as
the start API returned — but a launched process is not a responding
agent, so the card lingered and read as a failed activation. The card's
button now stays in its loading state from click until the chat's turn
actually starts (the card unmounts then), with a 60s give-up window,
and the copy switches to "Starting {agent}… It will pick up your
message as soon as it connects." The premature success toast is gone —
the agent visibly starting work is the confirmation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
klopez4212
2026-07-07 07:51:57 +01:00
co-authored by Claude Fable 5
parent 47b67066c1
commit 5cf7a86e5e
3 changed files with 38 additions and 9 deletions
@@ -232,11 +232,14 @@ export function AgentActivationCard({
</div>
<div className="min-w-0">
<p className="text-sm font-medium">
Activate {agentName} to get a response
{isActivating
? `Starting ${agentName}…`
: `Activate ${agentName} to get a response`}
</p>
<p className="mt-1 text-sm text-muted-foreground">
Your message was sent, but this agent is not active in this
chat yet.
{isActivating
? "It will pick up your message as soon as it connects."
: "Your message was sent, but this agent is not active in this chat yet."}
</p>
</div>
</div>
+28 -2
View File
@@ -470,6 +470,30 @@ export function ChatDetail({
// message, and start its turn. Without this grace window the card re-shows
// ~1s after activation and reads as "activation didn't work".
const AGENT_ACTIVATION_GRACE_MS = 20_000;
// "Activated" isn't done until the agent responds: the card's button holds
// its loading state from click until this chat has a live turn (the card
// unmounts then) or the give-up window expires — the start API returning
// only means the process launched, not that it's connected and replaying
// the pending message.
const ACTIVATION_PENDING_MS = 60_000;
const [isActivationPending, setIsActivationPending] = React.useState(false);
const handleActivateAgent = React.useCallback(() => {
setIsActivationPending(true);
onActivateAgent();
}, [onActivateAgent]);
React.useEffect(() => {
if (!isActivationPending) {
return;
}
if (isChatTurnActive) {
setIsActivationPending(false);
return;
}
const timeout = window.setTimeout(() => {
setIsActivationPending(false);
}, ACTIVATION_PENDING_MS);
return () => window.clearTimeout(timeout);
}, [isActivationPending, isChatTurnActive]);
const [activationGraceUntil, setActivationGraceUntil] = React.useState(0);
const wasActivatingRef = React.useRef(false);
React.useEffect(() => {
@@ -620,8 +644,10 @@ export function ChatDetail({
>
<AgentActivationCard
agentName={defaultAgent?.name ?? "Fizz"}
isActivating={isActivatingAgent}
onActivate={onActivateAgent}
isActivating={
isActivatingAgent || isActivationPending
}
onActivate={handleActivateAgent}
/>
</MessageScrollerItem>
) : null}
@@ -516,10 +516,10 @@ export function ChatsScreen({
defaultAgent.status !== "running" &&
defaultAgent.status !== "deployed"
) {
const updatedAgent = await startManagedAgentMutation.mutateAsync(
defaultAgent.pubkey,
);
toast.success(`${updatedAgent.name || defaultAgent.name} activated`);
// No success toast: the process starting is not the same as the
// agent responding — the activation card holds its loading state
// until the agent's turn actually begins.
await startManagedAgentMutation.mutateAsync(defaultAgent.pubkey);
} else {
await managedAgentsQuery.refetch();
toast.success(`${defaultAgent.name} activated`);