import { html, nothing } from 'lit';
import { t } from '../../lib/i18n.js';
/**
* The background-task strip: what the agent handed off to run on its own.
*
* Rendered just above the composer, and only when there is something to say —
* an empty strip would be a permanent slice of chrome for an occasional event.
* It sits outside the message flow on purpose: a task started twenty messages
* ago scrolls away exactly when it matters most, and the transcript is a record
* of what was said, not a dashboard.
*
* State lives on the chat component (`ChatSession._tasks`), which owns the
* WebSocket the updates arrive on; this file only knows how it looks. Shared by
* the desktop copilot and the mobile chat page.
*/
const STATE_ICON = {
running: 'bi-arrow-repeat agent-task-spin',
completed: 'bi-check-circle-fill',
failed: 'bi-exclamation-triangle-fill',
cancelled: 'bi-slash-circle',
};
/** Elapsed time since an ISO timestamp, coarse on purpose (`12s`, `4m 03s`). */
function elapsed(startedAt) {
if (!startedAt) return '';
const ms = Date.now() - new Date(startedAt).getTime();
if (!Number.isFinite(ms) || ms < 0) return '';
const s = Math.floor(ms / 1000);
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m ${String(s % 60).padStart(2, '0')}s`;
return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, '0')}m`;
}
function renderTask(host, task) {
const running = task.state === 'running';
// The task's own session page is the drill-in: it already shows, live, what a
// background agent is doing. Without it "a task is running" is a fact you can
// do nothing with.
const openable = task.session_id != null && host._canOpenTaskSession;
return html`
${running
? html``
: html``}
`;
}
/**
* What a background task is waiting on: the approval or question it raised.
*
* One at a time, on purpose. A task that is blocked stays blocked whether or not
* its card is on screen, so stacking every pending item here would trade a
* readable chat for a queue nobody asked to see all of — the count says how many
* are behind it, and resolving this one reveals the next.
*
* It sits above the strip rather than in the transcript because the transcript
* is a record of what was said: the task that is asking may have been started
* twenty messages ago, and a card that scrolls away is a card that gets missed.
*
* Which is also why it can be closed: a card that cannot be moved out of the way
* is a card that takes the chat hostage. The ✕ hides it and nothing more — the
* item stays pending and the Inbox stays the place to answer it.
*/
function renderPending(host) {
const pending = host._taskPending ?? [];
if (pending.length === 0) return nothing;
const [item] = pending;
return html`
`;
}
export function renderTaskStrip(host) {
const tasks = host._tasks ?? [];
const pending = host._taskPending ?? [];
const hidden = host._taskPendingHidden ?? 0;
// A pending item outlives the strip's own drop timers, so it keeps the
// container alive on its own: a task can still be waiting on a human after
// its row has been dismissed.
//
// Dismissed items deliberately do *not* keep it alive. Closing the last card
// with nothing else running has to leave a clean chat, or the ✕ would not be
// the promise it looks like — the sidebar badge and the Inbox are where those
// items live now.
if (tasks.length === 0 && pending.length === 0) return nothing;
const running = tasks.filter(x => x.state === 'running').length;
return html`