Files
Skald-Circle/web/css/agent-tasks.css
T
dguiducci 01b8a187b5
Nightly Build / build (push) Successful in 7m42s
feat: let a background task ask the chat that started it, not just the Inbox
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.
2026-08-04 21:00:45 +01:00

258 lines
6.2 KiB
CSS

/* ── Background-task strip ──────────────────────────────────────────────────
The list of `execute_task mode=async` tasks the current conversation started,
rendered above the composer by `renderTaskStrip` (shared by the desktop
copilot and the mobile chat page). Quiet by default: it sits next to the
composer, so it must read as a status line, never as a second UI. */
.agent-tasks {
display: flex;
flex-direction: column;
gap: 0.25rem;
margin-bottom: 0.5rem;
padding: 0.4rem 0.45rem 0.45rem;
border: 1px solid var(--toolbar-border);
border-radius: var(--radius-sm);
background: var(--msg-assistant-bg);
}
.agent-tasks-head {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0 0.25rem 0.1rem;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--placeholder-color);
}
.agent-tasks-all {
margin-left: auto;
font-size: 0.7rem;
font-weight: 500;
text-transform: none;
letter-spacing: 0;
color: var(--placeholder-color);
text-decoration: none;
}
.agent-tasks-all:hover { color: var(--accent); text-decoration: underline; }
/* Pointer to the pending items the user closed with the ✕: still waiting,
just not here. */
.agent-tasks-hidden {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.7rem;
font-weight: 500;
text-transform: none;
letter-spacing: 0;
color: var(--accent);
text-decoration: none;
}
.agent-tasks-hidden:hover { text-decoration: underline; }
/* When both links are present the "all tasks" one gives up its auto margin. */
.agent-tasks-hidden ~ .agent-tasks-all { margin-left: 0.6rem; }
/* ── What a task is waiting on ──────────────────────────────────────────────
An approval or a question raised by a background task, shown above the strip
and outside the transcript. It carries the full Inbox card, so it must read
as an interruption worth answering — accent border, not the quiet chrome the
task rows use — while still capping its own height: an `execute_cmd` card can
be tall, and the conversation underneath has to stay reachable. */
.agent-task-ask {
display: flex;
flex-direction: column;
gap: 0.3rem;
max-height: min(22rem, 45vh);
overflow-y: auto;
margin-bottom: 0.35rem;
padding: 0.35rem;
border: 1px solid var(--accent);
border-radius: var(--radius-sm);
background: var(--accent-soft);
}
.agent-task-ask-head {
position: sticky;
top: 0;
z-index: 1;
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.1rem 0.15rem;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--accent);
background: var(--accent-soft);
}
/* Leading, so closing the panel is the first thing the corner offers. */
.agent-task-ask-close {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 1.35rem;
height: 1.35rem;
padding: 0;
border: none;
border-radius: var(--radius-sm);
background: none;
color: var(--accent);
font-size: 0.9rem;
cursor: pointer;
transition: background 0.15s;
}
.agent-task-ask-close:hover { background: var(--card-bg); }
.agent-task-ask-job {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
text-transform: none;
letter-spacing: 0;
color: var(--placeholder-color);
}
.agent-task-ask-count {
margin-left: auto;
flex-shrink: 0;
font-size: 0.68rem;
font-weight: 500;
text-transform: none;
letter-spacing: 0;
color: var(--placeholder-color);
}
.agent-task-ask-error {
padding: 0.2rem 0.35rem;
font-size: 0.72rem;
color: var(--bs-danger, #dc3545);
}
/* The Inbox card sizes itself for a full-width page; here it is a panel. */
.agent-task-ask .inbox-card { margin: 0; }
/* ── One task ── */
.agent-task {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.3rem 0.4rem;
border-radius: var(--radius-sm);
background: var(--card-bg);
border: 1px solid transparent;
}
.agent-task--failed {
border-color: rgba(var(--bs-danger-rgb, 220, 53, 69), 0.35);
}
.agent-task--completed,
.agent-task--cancelled { opacity: 0.7; }
.agent-task-icon {
flex-shrink: 0;
font-size: 0.85rem;
color: var(--accent);
}
.agent-task--completed .agent-task-icon { color: var(--tool-subagent); }
.agent-task--failed .agent-task-icon { color: var(--bs-danger, #dc3545); }
.agent-task--cancelled .agent-task-icon { color: var(--placeholder-color); }
/* The body is a button: clicking opens the task's own session page. */
.agent-task-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.05rem;
padding: 0;
border: none;
background: none;
text-align: left;
color: inherit;
cursor: pointer;
}
.agent-task-body:disabled { cursor: default; }
.agent-task-title {
max-width: 100%;
font-size: 0.8rem;
font-weight: 500;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-task-body:not(:disabled):hover .agent-task-title {
color: var(--accent);
text-decoration: underline;
}
.agent-task-meta,
.agent-task-error {
max-width: 100%;
font-size: 0.7rem;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--placeholder-color);
}
.agent-task-error { color: var(--bs-danger, #dc3545); }
.agent-task-action {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
padding: 0;
border: none;
border-radius: var(--radius-sm);
background: none;
color: var(--placeholder-color);
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.agent-task-action:hover {
background: var(--accent-soft);
color: var(--accent);
}
/* ── The running spinner ── */
.agent-task-spin {
display: inline-block;
animation: agent-task-spin 1.4s linear infinite;
}
@keyframes agent-task-spin {
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.agent-task-spin { animation: none; }
}