Give plugins an agent's background work count and a background flag on hooks (#9362)

* feat(plugins): give plugins the agent's sidebar indicator and background flag

Plugin UI re-derived agent state from status and attention fields and could
not see background work, so it showed agents as done while a shell they left
running was still live. Agent snapshots now carry the sidebar's own bucket as
indicator/indicatorLabel, computed by the same function the sidebar uses so
the two cannot drift.

Lifecycle hooks and the agent.open.before request now carry `background`, so a
hook can skip a hidden helper agent without a lookup. A hook cannot change it;
an omitted value means unchanged, like metadata.

`finished` attention still means the turn finished; background work is the
indicator's concern.

* fix(plugins): keep session-open hooks that omit background typechecking

A hook that returns its own object instead of spreading the request has no
background field; the result type now treats it as optional, like metadata,
matching the runtime where an omitted value means unchanged.

* refactor(plugins): give agent snapshots the background work count instead of an indicator

* fix(plugins): keep legacy session-open hooks that omit background typechecking
This commit is contained in:
Mohamed Boudra
2026-11-11 15:10:13 +07:00
committed by GitHub
parent 88b46c6781
commit a886bfbcdd
24 changed files with 283 additions and 28 deletions
@@ -4,13 +4,21 @@ import { useAgents, type PluginSidebarViewProps } from "@getpaseo/plugin/client"
import { AgentRow, Heading } from "./rows";
export function agentTask(agent: PluginAgentSnapshot) {
if (agent.attentionReason === "permission") return "Respond";
if (agent.requiresAttention || agent.status === "error") return "Review";
if (agent.status === "running" || agent.status === "initializing") return "Wait";
if (agent.status === "error") return "Review";
// An agent whose turn ended still works while shells or monitors it started are live.
if (
agent.status === "running" ||
agent.status === "initializing" ||
agent.backgroundWork.count > 0
)
return "Wait";
if (agent.requiresAttention) return "Review";
return "Start";
}
function Task({ title, view }: { title: string; view: PluginSidebarViewProps }) {
const agents = useAgents({
where: (item) => agentTask(item) === title,
// Background agents are helpers the user never opens; leave them out like the sidebar does.
where: (item) => !item.background && agentTask(item) === title,
orderBy: ["lastActivityAt", "desc"],
});
return (
@@ -91,7 +91,12 @@ export function AgentRow({
() => ({ color: view.theme.colors.foreground, fontSize: 12 }),
[view.theme],
);
const detail = useMemo(
() => ({ color: view.theme.colors.foregroundMuted, fontSize: 11 }),
[view.theme],
);
if (!agent) return null;
const tasks = agent.backgroundWork.count;
return (
<Pressable
accessibilityRole="button"
@@ -102,6 +107,11 @@ export function AgentRow({
<Text numberOfLines={1} style={style}>
{agent.title ?? agent.provider}
</Text>
{tasks > 0 ? (
<Text numberOfLines={1} style={detail}>
{tasks === 1 ? "1 background task" : `${tasks} background tasks`}
</Text>
) : null}
</Pressable>
);
}