Skip to content
Development
Skill

/events

The PyWry event system — namespaced events, request/response round-trips, widget IDs, component IDs, and how tool results flow back to the agent.

From plugin
pywry
9918 skills1 agent2 commands1 MCP

Claude Code

$ npx -y skills add deeleeramone/PyWry --skill events --agent claude-code

SKILL.md

events.SKILL.md
description: The PyWry event system — namespaced events, request/response round-trips, widget IDs, component IDs, and how tool results flow back to the agent.

PyWry Event System — Agent Reference

> **The event bus is the plumbing underneath every MCP tool.** You > rarely need to think about it — the typed tools wrap emit + wait + > state-poll for you — but when you reach for `send_event` or > interpret tool results, this is how it works.

Event names are namespaced

Every event has the form `namespace:event-name`, e.g.:

  • `tvchart:symbol-search` — ask the chart to open symbol search
  • `tvchart:state-response` — chart's reply with its current state
  • `tvchart:data-request` — chart asks Python for bars
  • `tvchart:data-response` — Python delivers bars
  • `toolbar:request-state` — ask a toolbar component for its value
  • `toolbar:state-response` — component's reply
  • `chat:user-message` — user typed something
  • `chat:ai-response` — model produced a token
  • `pywry:update-theme` — dark/light mode change

Never emit an event with a name that doesn't match `namespace:event-name` — the framework rejects it.

Widget IDs vs component IDs

**widget_id** — identifies the top-level PyWry widget (a chart, a grid, a chat panel, a dashboard). Every MCP tool takes `widget_id` as an argument because all events route to the widget first.

**componentId** — identifies a child *inside* a widget (a specific toolbar button, a marquee ticker slot, a chart pane). Component IDs are scoped to their containing widget.

When you call `send_event(widget_id, event_type, data)`, the `widget_id` picks the target widget; anything identifying a specific component goes in the `data` payload (typically as `data.componentId` or `data.chartId`).

Request / response pattern

Some events are fire-and-forget (e.g. `tvchart:symbol-search` — "please do this"). Others are request/response round-trips where the caller wants a reply (e.g. `tvchart:request-state` → `tvchart:state-response`).

The framework correlates request/response with a `context` token:

1. Emitter generates a random `context` token. 2. Emitter injects it into the request payload. 3. Listener sees the request, attaches the same `context` to its response, and emits the response event. 4. Emitter sees the matching `context` on the response and wakes up.

All of this is handled inside `request_response()` in `pywry.mcp.state` — you never construct tokens yourself. Typed MCP tools that need a reply (`tvchart_request_state`, `tvchart_list_indicators`) use this under the hood and return the stripped response (no `context` token) in their tool result.

How tool results reach the agent

Agent             MCP Server          PyWry Widget (JS)
  │                   │                       │
  │ tool call ──────► │                       │
  │                   │ widget.emit() ──────► │
  │                   │                       │ (updates chart)
  │                   │                       │
  │                   │ ◄───── bridge.emit()  │
  │                   │ (state-response)      │
  │                   │                       │
  │ ◄──── tool result │                       │
  │ (includes state)  │                       │

Mutating tools (`tvchart_symbol_search`, `tvchart_change_interval`) poll `tvchart:request-state` after emitting the mutation, wait for the chart to actually reflect the change, and return the real post-change state in the tool result. The `state` field in the tool result contains the SAME structure as a direct call to `tvchart_request_state`.

If the mutation didn't settle in time, the result contains a `note` field explaining the discrepancy — relay the note to the user, do not invent state.

Emitting events from tools — `send_event`

Only reach for `send_event` when no typed MCP tool exists for the target event. It's a raw passthrough with no state polling:

send_event(widget_id, event_type, data)
  → { "widget_id": ..., "event_sent": true, "event_type": ... }

Example — apply a rare tvchart option with no typed wrapper:

send_event(
  widget_id="chart",
  event_type="tvchart:apply-options",
  data={"chartOptions": {"timeScale": {"secondsVisible": False}}},
)

The returned `event_sent: true` means the event was successfully handed to the widget — it does NOT mean the JS handler ran successfully. For confirmation, follow up with `tvchart_request_state` to read the new state.

Event capture (`get_events`)

Some events fire from the widget *to* Python (e.g. the user clicked a bar, moved the crosshair). These are automatically captured into a per-widget event buffer and can be retrieved with:

get_events(widget_id, event_types=[...], clear=True)
  → { "events": [{ "event_type": ..., "data": ..., "label": ... }, ...] }

Default captured events for charts:

  • `tvchart:click`
  • `tvchart:crosshair-move`
  • `tvchart:visible-range-change`
  • `tvchart:drawing-added`
  • `tvchart:drawing-deleted`
  • `tvchart:open-layout-request`
  • `tvchart:interval-change`
  • `tvchart:chart-type-change`

Use `get_events` if the user asks "what did I just click" or "what was the last drawing I added".

Don'ts

  • Do NOT synthesise event payloads. Only report event data the

framework actually handed you.

  • Do NOT emit events whose name doesn't match `ns:name` — they're

rejected.

  • Do NOT emit to a widget id that isn't registered — the tool will

return an error listing the registered widgets; correct and retry.

  • Do NOT assume `event_sent: true` means the downstream JS succeeded.

When it matters, follow up with `tvchart_request_state` (or the relevant state query) to confirm.

Read more
@deeleeramone
Ships withpywry

PyWry is a cross-platform app factory, rendering engine and UI toolkit for Python that produces native desktop, web, and notebook experiences from a single API.

9918 skills1 agent1 MCP
Get the whole plugin

Stars on GitHub

Momentum

stars in total
99
Star history of deeleeramone/PyWry on GitHub, a chart by star-history.com

Other skills on pywry.

pywry-orientation

pywry-orientation

When to reach for PyWry MCP tools (native webview rendering, Plotly, TradingView, AgGrid, chat) instead of writing…

@deeleeramone@deeleeramoneInstall Skill
chat_agent

chat_agent

How an agent operates inside a PyWry chat widget — reading user messages, attachments, @-context, tool-call result cards,…

@deeleeramone@deeleeramoneInstall Skill
component_reference

component_reference

**STOP. THIS IS THE AUTHORITATIVE REFERENCE FOR ALL COMPONENTS.** **YOU MUST USE THE EXACT EVENT SIGNATURES DOCUMENTED HERE.**…

@deeleeramone@deeleeramoneInstall Skill

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills11d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc13d293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react2d252k
@facebook@facebookInstall Skill

Questions about events.

Which coding agents can use events?
events installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.