Skip to content

feat(widget): passive indicator for auto-resuming durable pauses#349

Open
bookernath wants to merge 2 commits into
mainfrom
feat/durable-pause-await-reason
Open

feat(widget): passive indicator for auto-resuming durable pauses#349
bookernath wants to merge 2 commits into
mainfrom
feat/durable-pause-await-reason

Conversation

@bookernath

@bookernath bookernath commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

The unified await SSE event can now carry an awaitReason. When present, the pause resumes on its own — so the widget shows a passive, non-interactive "working in the background" indicator (new "pause" message variant) instead of a resume button, and clears it once the stream continues.

Awaits without awaitReason (local-tool / WebMCP) keep today's interactive behavior unchanged. Suppression keys on awaitReason being present (not a specific value), so future pause kinds work without a release. Indicator copy is overridable via config.copy.durablePauseLabels.

Try it

A new mock-driven demo: Advanced Examples → Durable Pause (apps/web/durable-pause-demo.html), with a toggle for the two pause kinds. No backend needed.

Notes

  • New unit tests for the event handler and the pause bubble; full widget suite, lint, and typecheck pass.
  • Changeset included (minor).

Greptile Summary

This PR adds a passive "working in the background" indicator for auto-resuming durable pauses on the unified await SSE event. When awaitReason is present, the widget renders a non-interactive variant: "pause" bubble instead of a resume control, and settles it once the stream continues — with a try/finally guard ensuring the indicator is always cleared even on HTTP-level stream failure.

  • New "pause" message variant (AgentWidgetDurablePause type, createPauseBubble component) handles crawl_pending / durable_poll with per-reason copy overridable via config.copy.durablePauseLabels, falling back gracefully to a generic label for unknown future reasons.
  • SSE loop wrapped in try/finally so settleDurablePause() runs on normal completion, network drop, or reader throw — the per-frame check settles eagerly on content/completion frames while pings are intentionally ignored.
  • Full unit coverage for the new event handler and pause bubble component, including a dedicated test for the stream-failure settle path.

Confidence Score: 5/5

Safe to merge. The change is additive and self-contained: existing interactive tool-await paths are untouched, the new passive indicator path is well-guarded, and the SSE loop's try/finally ensures the bubble is always cleared.

The two concerns flagged in the previous round have both been addressed: the SSE loop is now wrapped in try/finally so a mid-stream network failure clears the indicator, and the stream-failure test case is included to lock in that behaviour. The core durable-pause discriminator (presence of a non-empty awaitReason) is applied consistently across the frame handler, the per-frame early-settle check, and the oracle fixture. No new logic gaps were found.

No files require special attention.

Important Files Changed

Filename Overview
packages/widget/src/client.ts Core change: adds durablePauseMessage state, settleDurablePause() helper, per-frame early settle check (skips pings/durable-await refreshes), durable-await frame handler, and wraps SSE loop in try/finally for stream-failure cleanup. Logic is correct and well-tested.
packages/widget/src/components/pause-bubble.ts New passive indicator component. Label resolution priority (user-specific > built-in-specific > user-default > built-in-default) is intentional and documented. Correctly collapses to display:none when resolved or pause is absent.
packages/widget/src/types.ts Adds AgentWidgetDurablePause type (resolved required), 'pause' variant, and durablePauseLabels config key. Types are accurate and well-documented.
packages/widget/src/client.test.ts New test suite covers: passive bubble emitted for known and unknown awaitReason values, interactive tool await unchanged, settle-on-resume (pings don't settle prematurely), and settle-on-stream-failure via the finally block.
packages/widget/src/components/pause-bubble.test.ts Unit tests for createPauseBubble: non-interactive DOM (no buttons), role=status, reason-specific copy, forward-compat fallback, hide-on-resolved, and config override priority.
packages/widget/src/utils/message-fingerprint.ts Adds awaitReason and resolved to the fingerprint so the cache correctly invalidates on settle. The FingerprintableMessage type uses optional fields (structural type), matching the broader pattern used for approval and toolCall.
packages/widget/src/ui.ts Adds pause variant branch to the main render path, mirroring the identical addition in messages.ts. Correctly passes config for label overrides.
packages/widget/src/utils/fixtures/unified-translator.oracle.ts Additive: passes awaitReason, crawlId, stepId through the oracle translator. Undefined values are dropped by JSON.stringify, so no impact on existing test fixtures.
apps/web/src/durable-pause-demo.ts New mock-driven demo using createMockSSEResponse. Scripted stream covers the full pause/ping/resume/complete lifecycle and supports switching between crawl_pending and durable_poll kinds.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Server
    participant SSELoop as SSE Reader
    participant Settler as settleDurablePause
    participant UI as Pause Bubble

    Server->>SSELoop: "await frame (awaitReason present)"
    SSELoop->>UI: "emitMessage: variant=pause, resolved=false"
    UI-->>UI: "Show passive indicator — no resume button"

    Server->>SSELoop: "ping"
    Note over SSELoop: "ping — skip settle check"

    alt Stream resumes with content
        Server->>SSELoop: "turn_start / text frames"
        SSELoop->>Settler: "per-frame check triggers settle"
        Settler->>UI: "emitMessage: resolved=true, display none"
        Server->>SSELoop: "execution_complete"
        Note over Settler: "no-op: already null"
        Note over SSELoop: "finally block — no-op"
    else Network failure before content
        SSELoop->>Settler: "finally block fires on reader throw"
        Settler->>UI: "emitMessage: resolved=true, display none"
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Server
    participant SSELoop as SSE Reader
    participant Settler as settleDurablePause
    participant UI as Pause Bubble

    Server->>SSELoop: "await frame (awaitReason present)"
    SSELoop->>UI: "emitMessage: variant=pause, resolved=false"
    UI-->>UI: "Show passive indicator — no resume button"

    Server->>SSELoop: "ping"
    Note over SSELoop: "ping — skip settle check"

    alt Stream resumes with content
        Server->>SSELoop: "turn_start / text frames"
        SSELoop->>Settler: "per-frame check triggers settle"
        Settler->>UI: "emitMessage: resolved=true, display none"
        Server->>SSELoop: "execution_complete"
        Note over Settler: "no-op: already null"
        Note over SSELoop: "finally block — no-op"
    else Network failure before content
        SSELoop->>Settler: "finally block fires on reader throw"
        Settler->>UI: "emitMessage: resolved=true, display none"
    end
Loading

Reviews (2): Last reviewed commit: "fix: address Greptile findings" | Re-trigger Greptile

Branch the unified `await` event on `awaitReason`: when present, the pause
resumes server-side, so render a non-interactive "working in the background"
indicator (new "pause" message variant) instead of a resume affordance, and
settle it once the stream resumes. Awaits without `awaitReason` keep the
existing interactive local-tool/WebMCP behavior.

Adds an apps/web durable-pause demo driven by mock SSE.
@vercel

vercel Bot commented Jun 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ai-sdk-webmcp Ready Ready Preview, Comment Jun 28, 2026 11:16pm
persona Ready Ready Preview, Comment Jun 28, 2026 11:16pm
persona-proxy Ready Ready Preview, Comment Jun 28, 2026 11:16pm

Request Review

Comment thread packages/widget/src/types.ts Outdated
Comment thread packages/widget/src/client.ts
- Settle the durable pause in streamResponse's finally so the passive
  indicator clears even if the stream fails at the HTTP/read layer before
  another frame arrives (network drop / reader throw / abort).
- Make AgentWidgetDurablePause.resolved required (every call site sets it).
- Add a regression test for the stream-failure settle path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant