Skip to content

[P2][UX/A11y] Tool actions must return promises/results instead of fire-and-forget async work #308

Description

@baixiangcpp

Summary

Several tool actions still wrap async work with fire-and-forget handlers such as onClick: () => { void verify() } or onClick: () => void copyOutput(). This bypasses the shared ToolActionBar pending/success/failure semantics.

ToolActionBar only treats an action as pending when action.onClick() returns a Promise. If the handler starts async work and returns undefined, the toolbar immediately marks the action as successful and announces success to assistive technology, even if the actual async operation is still running, fails later, or does nothing because required input is missing.

Why this matters

The recent action-state audit standardized pending/success/failure feedback. Fire-and-forget handlers undermine that standardization:

  • screen-reader users can hear a success announcement before verification/copy work completes;
  • pending state is skipped;
  • failures are not reflected in the shared toolbar state;
  • analytics/action feedback can report a run even when validation short-circuits;
  • disabled/empty-input states become inconsistent across tools.

Current behavior

ToolActionBar does this:

constmaybeResult=action.onClick()if(!isPromiseLike(maybeResult)){setLastActionStatus(... "success")setActionAnnouncement(getResultAnnouncement(...))return}

So any action that returns undefined is treated as a completed success.

Known examples:

  • src/features/tools/jwt-verifier/page.tsx
    • onClick: () => { void verify() }
    • verify() is async and may perform Web Crypto work.
  • src/features/tools/oauth-jwks-workbench/page.tsx
    • copy action uses onClick: () => void copyOutput().
    • copy may fail, but the toolbar receives undefined immediately.

There may be additional occurrences; search for fire-and-forget patterns around ToolAction definitions.

Expected behavior

Tool actions should return one of:

  • void only for genuinely synchronous state updates that cannot fail;
  • ToolActionResult for synchronous success/failure;
  • Promise<void | ToolActionResult> for async actions.

Async tool work should not be hidden behind void wrappers.

Suggested implementation plan

  1. Update known actions:
onClick: verify
onClick: copyOutput
  1. Where an async function currently only shows toast, make it return ToolActionResult or use shared helpers such as copyTextWithToolFeedback().
  2. Add disabled conditions and disabled reasons for required input:
    • JWT verifier Verify should be disabled when token is empty.
    • HMAC verification should guide the user when secret is empty for HS algorithms.
  3. Add/extend a guard to detect onClick: () => void ... inside ToolAction arrays unless explicitly allowlisted.
  4. Add component tests:
    • JWT verifier Verify enters pending state or at least does not announce success before async verification resolves.
    • Copy failure returns failed state through the shared toolbar.
    • Empty required input does not produce a false success announcement.

Acceptance criteria

  • No async ToolAction uses fire-and-forget void wrappers without an explicit allowlist and justification.
  • JWT Verifier verify action does not announce success before verification completes.
  • Copy actions that can fail return failed state through the shared action feedback path.
  • Required-input actions have disabled states or actionable failure results.
  • A guard prevents reintroducing fire-and-forget action handlers.
  • npm run test -- tests/component/tool-shell-status-output.test.tsx tests/component/jwt-verifier-page.test.tsx tests/component/oauth-jwks-workbench-page.test.tsx passes if updated.
  • npm run check:types and npm run lint pass.

Related code pointers

  • src/features/tool-shell/tool-action-bar.tsx
  • src/features/tools/jwt-verifier/page.tsx
  • src/features/tools/oauth-jwks-workbench/page.tsx
  • src/features/tool-shell/tool-action-feedback.ts

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions