---
status: resolved
claimed-by: "019fbffd-3252-7067-a2e7-8fcab9cc7293"
claimed-at: "2026-08-01T21:01:28-04:00"
parent: "[[083-pi-subagent-result-delivery-map|Pi subagent result delivery]]"
blocked-by: []
resolved-at: "2026-08-01T21:03:56-04:00"
tags:
  - ticket/research
---

# Pi subagent result delivery UI research

## Question

What Pi-native UI affordances can show subagent completion, failure, unread results, or a lightweight result inbox without disrupting an active parent workflow such as a one-question-at-a-time grill session?

## Findings

The best Pi-native non-disruptive awareness surface is an extension widget plus footer status.
Pi documents `ctx.ui.setStatus()` as persistent footer or status-bar text, and `ctx.ui.setWidget()` as persistent content above or below the editor.
The TUI source stores extension widgets in separate above-editor and below-editor maps, limits string widgets to `InteractiveMode.MAX_WIDGET_LINES`, renders them into widget containers, and requests a render without replacing editor focus.
Sources: Pi `docs/extensions.md`, Custom UI and Widgets, Status, and Footer sections.
Sources: Pi `docs/tui.md`, Pattern 4 and Pattern 5.
Sources: Pi `dist/core/extensions/types.d.ts`, `ExtensionUIContext.setStatus()` and `setWidget()`.
Sources: Pi `dist/modes/interactive/interactive-mode.js`, `setExtensionStatus()`, `setExtensionWidget()`, `renderWidgets()`, and `renderWidgetContainer()`.

Footer status is good for the smallest completion and unread-result indicator.
The built-in footer renders extension statuses from `footerData.getExtensionStatuses()` on an extra sorted status line, and custom footers can consume the same `getExtensionStatuses()` data.
This supports a compact badge such as `subagents: 2 running · 1 unread · 1 failed` without opening a modal, injecting a message, or disturbing a grill prompt.
Sources: Pi `dist/modes/interactive/components/footer.js`, extension status rendering.
Sources: Pi `docs/tui.md`, Pattern 4 and Pattern 6.
Sources: Pi `examples/extensions/status-line.ts` and `examples/extensions/custom-footer.ts`.

A widget is the best Pi-native lightweight inbox surface.
Pi documents widgets as persistent content above or below the editor, with string-array or component-factory content and explicit clear semantics.
The current dotfiles subagent extension already uses this pattern by rendering a collapsed summary or expanded inspector through `ctx.ui.setWidget("subagents", ...)`, and its supervisor updates that widget through `onChange` whenever child status changes.
That proves the runtime state model can drive a non-blocking inbox-like view today.
Sources: Pi `docs/extensions.md`, `ctx.ui.setWidget()` examples.
Sources: Pi `docs/tui.md`, Pattern 5.
Sources: `modules/agents/pi/extensions/subagents/index.ts`, `updateUi()` and `onChange` wiring.
Sources: `modules/agents/pi/extensions/subagents/ui.ts`, `renderSummary()`, `renderInspector()`, and `widget()`.
Sources: `modules/agents/pi/extensions/subagents/supervisor.ts`, `emitChange()`, terminal state handling, and result availability updates.

A durable unread or milestone trail should use custom entries rather than ordinary messages.
Pi documents `pi.appendEntry()` as session-persistent extension data that does not participate in LLM context, and `pi.registerEntryRenderer()` as the TUI renderer for those entries.
The interactive source listens for `entry_appended`, renders custom entries with `addCustomEntryToChat()`, and inserts them before the current streaming component when one exists.
This is suitable for a compact history of completed, failed, or unread subagent results that is visible in the transcript but does not contaminate the parent model context.
Sources: Pi `docs/extensions.md`, `pi.appendEntry()` and Message and Entry Rendering sections.
Sources: Pi `dist/core/extensions/types.d.ts`, `registerEntryRenderer()` and `appendEntry()` definitions.
Sources: Pi `dist/modes/interactive/interactive-mode.js`, `entry_appended` handling and `addCustomEntryToChat()`.
Sources: Pi `examples/extensions/entry-renderer.ts`.

`ctx.ui.notify()` is Pi-native but should be secondary rather than authoritative.
The extension docs define `notify()` as non-blocking and typed as `info`, `warning`, or `error`.
The interactive implementation maps it to `showStatus()`, `showWarning()`, or `showError()`, and `showStatus()` appends or updates a dim chat status line to avoid log spam from back-to-back status messages.
This can announce `subagent sg-... completed` or `failed`, but it is transient and not enough for unread-result accounting.
Sources: Pi `docs/extensions.md`, Dialogs section.
Sources: Pi `dist/core/extensions/types.d.ts`, `ExtensionUIContext.notify()`.
Sources: Pi `dist/modes/interactive/interactive-mode.js`, `showExtensionNotify()` and `showStatus()`.
Sources: Pi `examples/extensions/notify.ts`.

An overlay or custom component can provide an explicit inbox viewer, but it should be user-invoked.
Pi documents `ctx.ui.custom()` as temporarily replacing the editor unless `{ overlay: true }` is passed, and overlay mode renders a floating component on top of existing content.
The interactive source preserves editor text, restores editor focus after non-overlay custom UI, and in overlay mode calls `ui.showOverlay()` instead of clearing the editor container.
This is appropriate for a `/subagent-inbox` or shortcut that opens an inspect-and-mark-read panel, but automatic popup overlays would still interrupt keyboard flow in a one-question-at-a-time grill.
Sources: Pi `docs/extensions.md`, Custom Components and Overlay Mode sections.
Sources: Pi `docs/tui.md`, Overlays and Overlay Focus sections.
Sources: Pi `dist/modes/interactive/interactive-mode.js`, `showExtensionCustom()`.
Sources: Pi `examples/extensions/overlay-test.ts`.

Custom tool rendering is useful for blocking or in-turn subagent results, but it does not solve background result delivery by itself.
Pi supports `renderCall()` and `renderResult()` for tool rows, partial results via `onUpdate`, and compact or expanded tool output.
The bundled Pi subagent example uses custom renderers to show single, chain, and parallel results with success, failure, running, usage, and expanded details.
That pattern is valuable for `subagent_wait` or old blocking subagent calls, but a spawned background child that completes after the parent turn needs a stateful widget, footer badge, entry renderer, or command because no tool row is actively updating at completion time.
Sources: Pi `docs/extensions.md`, Custom Rendering section.
Sources: Pi `examples/extensions/subagent/index.ts`, `renderCall()`, `renderResult()`, and parallel update handling.

Message injection APIs are available but are not the default workflow-safe route.
Pi documents `pi.sendMessage()` as injecting a custom message that participates in LLM context, with delivery modes `steer`, `followUp`, and `nextTurn`.
It separately says TUI-only durable content should use `pi.appendEntry()` with `pi.registerEntryRenderer()` instead.
For AFK worker result awareness during a grill, ordinary or custom message injection risks changing the active parent context and should be reserved for explicit user-approved reconciliation or selected result import.
Sources: Pi `docs/extensions.md`, `pi.sendMessage()` and `pi.appendEntry()` sections.
Sources: Pi `dist/core/extensions/types.d.ts`, `sendMessage()` and `appendEntry()` definitions.

Mode behavior limits which affordances exist outside interactive TUI.
Pi documents that `ctx.hasUI` is true in TUI and RPC modes, false in print and JSON modes, and that TUI-specific features need `ctx.mode === "tui"`.
It also states that in RPC mode some TUI-specific methods are no-ops or return defaults.
The no-op UI context in the extension runner implements notification, status, widget, footer, custom UI, editor, and autocomplete methods as no-ops when no UI is bound.
Therefore, result delivery should keep the authoritative unread/result state in supervisor/session data and treat TUI surfaces as projections.
Sources: Pi `docs/extensions.md`, ExtensionContext and Mode Behavior sections.
Sources: Pi `dist/core/extensions/runner.js`, `noOpUIContext`.

## Answer

Use a layered Pi-native design.
Keep authoritative result and unread state in the subagent supervisor and persisted custom entries.
Project awareness through a footer status badge and the existing subagent widget.
Add a user-invoked inbox command or shortcut using either an expanded widget state or an overlay custom component.
Use `notify()` only as a best-effort completion toast or chat status line.
Use custom entry rendering for durable milestone cards that do not enter LLM context.
Do not inject ordinary messages into the active parent conversation by default.

## Limitations

No primary Pi source found a built-in unread badge, inbox widget, notification center, or read/unread model for extensions.
Those semantics must be implemented by the subagent extension on top of `setStatus()`, `setWidget()`, `appendEntry()`, commands, and optional overlay UI.

No primary Pi source found a way for a background process completion to update an already-finalized tool row after the parent turn has moved on.
Background completion should therefore update extension-owned state surfaces rather than rely on tool result rendering.

Automatic overlays are technically possible, but the cited Pi sources do not define a non-disruptive policy for showing one during active typing or an ongoing HITL flow.
Treat overlays as explicit inbox viewers unless a later design decision chooses interruption rules.

Terminal-level desktop notifications are possible as an example extension writes OSC 777, OSC 99, or Windows toast sequences, but that is not a Pi-owned notification API beyond extension code writing to stdout or spawning a command.
It should not be the primary Pi-native answer.

## Citations

- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/docs/extensions.md`.
- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/docs/tui.md`.
- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/dist/core/extensions/types.d.ts`.
- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/dist/core/extensions/runner.js`.
- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/dist/modes/interactive/interactive-mode.js`.
- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/dist/modes/interactive/components/footer.js`.
- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/examples/extensions/status-line.ts`.
- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/examples/extensions/widget-placement.ts`.
- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/examples/extensions/entry-renderer.ts`.
- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/examples/extensions/message-renderer.ts`.
- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/examples/extensions/overlay-test.ts`.
- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/examples/extensions/notify.ts`.
- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/examples/extensions/custom-footer.ts`.
- `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/examples/extensions/subagent/index.ts`.
- `/home/alexion/wrk/dotfiles/modules/agents/pi/extensions/subagents/index.ts`.
- `/home/alexion/wrk/dotfiles/modules/agents/pi/extensions/subagents/ui.ts`.
- `/home/alexion/wrk/dotfiles/modules/agents/pi/extensions/subagents/supervisor.ts`.
