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
9318 skills1 agent2 commands1 MCP
Install
$ npx -y skills add deeleeramone/PyWry --skill events --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/events

Context preview

The summary Claude sees to decide when to auto-load this skill.

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

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
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.

Get the whole plugin

Other skills on pywry.