feat: let a background task ask the chat that started it, not just the Inbox
Nightly Build / build (push) Successful in 7m42s

An async sub-agent runs in a session of its own, so the rich per-session events
that draw the inline approval card never reach the chat's socket — only the
id-only inbox lifecycle ones do. A task blocked on an approval was therefore
invisible in the conversation that started it, and the only way to unblock it
was to notice the sidebar badge and go to the Inbox.

The chat already shows what it handed off. This asks the same question of the
pending items: `GET /{source}/inbox` joins them against the sessions of this
conversation's running async jobs, so "whose is this" has one answer, in the
same place `/{source}/tasks` answers it for a task. The client is left with a
list to render, not a correlation to guess. The live path adds no event — the
existing `approval_requested` / `clarification_*` broadcasts already reach every
socket of the user, and re-reading the endpoint turns a nudge into something
renderable and survives a reload for free.

The card sits above the task strip rather than in the transcript: the task that
is asking may have been started twenty messages ago, and a card that scrolls
away is a card that gets missed. One at a time, with a count of what is behind
it — a blocked task stays blocked whether or not its card is on screen, so
stacking them would trade a readable chat for a queue nobody asked to see. And
it closes: the ✕ hides the card without resolving anything, leaving the item in
the Inbox, because a panel that cannot be moved takes the chat hostage.

`InboxCardsMixin` is the cards and their resolve calls, split out of
`InboxMixin` so the chat and the Inbox render the same approval rather than two
drifting copies of it; `_afterInboxResolve` is the only thing they disagree on.

Elicitations are left out: `PendingElicitationInfo` carries no `session_id`, so
there is nothing to attribute one to a task with.

Also: an async task's context label said "CronJob:", which sends whoever reads
the approval looking on the wrong page — and now says so next to the task's
real name.
This commit is contained in:
2026-08-04 21:00:45 +01:00
parent 3f74dc26f2
commit 01b8a187b5
14 changed files with 838 additions and 328 deletions
+12
View File
@@ -26,6 +26,17 @@ While a background task is running, a small strip appears **just above the compo
A finished task's line disappears from the strip after a few seconds. A **failed** one stays, so the reason can be read, until it is dismissed with the ✕.
## When a task needs the user
A background task can hit something it is not allowed to do on its own — running a command, writing outside its own area — or it can simply need to ask a question. Because the task is a separate conversation, it cannot interrupt the chat the way the assistant does mid-answer. Instead, **a card appears at the top of the task strip**, above the message box: the approval to grant, or the question to answer, labelled with the task that is asking.
- **It waits.** A task that has asked for something is stopped until it gets an answer. Nothing else of it moves in the meantime.
- **One at a time.** If several tasks are asking, the card shows the first and says how many are behind it (*1 of 3*); answering one brings up the next.
- **It can be closed.** The **✕** in the card's top-left corner puts it away — it does *not* approve or reject anything. The request is still pending, the task is still waiting, and it can be dealt with from the **Inbox** (sidebar → Inbox) whenever the user is ready. The strip keeps a small "waiting in the Inbox" pointer while any closed request is still outstanding.
- **Anywhere works.** The same request appears in the Inbox and, if the mobile app is paired, on the phone. Answering it in any one of those places settles it everywhere; the card disappears on its own.
The chat's *own* approvals are unaffected by all this — when the assistant itself needs permission mid-answer, the card still appears inline, in the transcript, where the work is happening.
## When a user asks about them
Common questions and the honest answers:
@@ -34,4 +45,5 @@ Common questions and the honest answers:
- *"What is it doing?"* — click the task's line.
- *"It has been going for ages."* — a task has no time limit; stopping it with ■ is always available, and stopping is not the same as failing.
- *"Where did the result go?"* — into this conversation, always. If it is not there yet, the task has not finished.
- *"It's stuck."* — check whether it is asking for something: an approval or a question waiting at the top of the strip, or in the Inbox if the card was closed earlier.
- *"Show me everything that ever ran."* — the **Tasks** page (sidebar → Tasks) has the full history, including scheduled jobs; the strip only covers the current conversation.