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
@@ -1,7 +1,8 @@
import { i18n } from "@/i18n/i18next";
import { fr } from "@/i18n/resources/fr";
import { describe, expect, it } from "vitest";
import type { WorkspaceDescriptor } from "@/stores/session-store";
import type { Agent, WorkspaceDescriptor } from "@/stores/session-store";
import { normalizeAgentSnapshot, projectAgentSnapshot } from "@/utils/agent-snapshots";
import {
createPluginAgentSnapshot,
createPluginWorkspaceSnapshot,
@@ -109,6 +110,26 @@ describe("sidebar entity snapshots", () => {
).toBe(after);
});
it("carries the background work count the host publishes for the agent", () => {
// Round-trip through the wire payload, the way the app receives each agent update.
const published = (backgroundWork: Agent["backgroundWork"]) =>
createPluginAgentSnapshot(
normalizeAgentSnapshot({ ...projectAgentSnapshot(agent), backgroundWork }, agent.serverId),
workspace.id,
).backgroundWork;
expect(published(undefined)).toEqual({ count: 0 });
expect(published({ count: 2 })).toEqual({ count: 2 });
expect(published({ count: 0 })).toEqual({ count: 0 });
expect(Object.isFrozen(published({ count: 1 }))).toBe(true);
});
it("marks a background agent", () => {
expect(createPluginAgentSnapshot(agent, workspace.id).background).toBe(false);
expect(createPluginAgentSnapshot({ ...agent, background: true }, workspace.id).background).toBe(
true,
);
});
it("reads projects without requiring a workspace", () => {
const project = {
projectId: "empty",
@@ -86,6 +86,8 @@ export function createPluginAgentSnapshot(agent: Agent, workspaceId: string): Pl
workspaceId,
provider: agent.provider,
status: agent.status,
backgroundWork: Object.freeze({ count: agent.backgroundWork?.count ?? 0 }),
background: agent.background ?? false,
createdAt: agent.createdAt.toISOString(),
updatedAt: agent.updatedAt.toISOString(),
lastActivityAt: agent.lastActivityAt.toISOString(),
@@ -51,6 +51,8 @@ const agent: PluginAgentSnapshot = {
workspaceId: workspace.id,
provider: "codex",
status: "running",
backgroundWork: { count: 0 },
background: false,
createdAt: "2026-08-16T10:00:00.000Z",
updatedAt: "2026-08-16T10:00:00.000Z",
lastActivityAt: "2026-08-16T10:00:00.000Z",