Guide · Lifecycle & events
Lifecycle & events
How your JavaScript gets loaded, how to react to things, and how to push context in from outside.
Lifecycle
Your plugin's JS file is loaded once when the plugin is enabled. You can define optional lifecycle functions at the top level.
function onLoad() {
// Called once when the plugin is loaded and enabled.
// Set up timers, subscribe to events, do initial setup.
}
function onUnload() {
// Called when the plugin is disabled or Petty is quitting.
// Clean up any external resources. Timers and event handlers
// are cleaned up automatically — you don't need to cancel them.
}Neither function is required. Top-level subscriptions work too:
// Equivalent to doing this inside onLoad()
petty.events.on("hook:task-complete", function () {
petty.pet.showBubble("Nice!", 3);
});Event bus
Plugins communicate via an event bus. Subscribe with petty.events.on(name, handler); emit with petty.events.emit(name, data). Use "*" as the name to catch everything — the handler receives (name, data) instead of just data.
petty.events.on("hook:task-complete", function (data) {
petty.pet.animate("excited");
});
petty.events.on("*", function (name, data) {
petty.log("event:", name);
});Host-dispatched events
The host broker fires these automatically. No permission is required to subscribe — a single host-side poll replaces every plugin that would otherwise watch the same state on its own timer.
| Event | When it fires | Data |
|---|---|---|
hook:task-complete | Claude Code task completes | undefined |
hook:needs-attention | Claude Code needs user input | undefined |
hook:session-start | Claude Code session starts | undefined |
hook:session-end | Claude Code session ends | undefined |
pet:clicked | User clicks the pet | undefined |
pet:interaction | Any user interaction with the pet | undefined |
pet:visibility | Pet hidden or shown | { visible: bool } |
plugin:url | External app opens petty://<your-plugin-id>/<path>?<query> | { path: string, query: {…} } — dispatched to the addressed plugin only |
settings:changed | A setting for this plugin changes | { key: string, value: any } |
system:app-focused | A different app becomes frontmost (also fires once at plugin load for the current app) | { bundleId, name } |
system:app-unfocused | An app loses focus | { bundleId, name } |
system:clipboard-changed | The clipboard contents change | { text?, hasImage } |
system:display-locked | User locks the screen | undefined |
system:display-unlocked | User unlocks the screen | undefined |
system:sleep | System is about to sleep | undefined |
system:wake | System has woken from sleep | undefined |
Plugin-emitted events
emit dispatches locally by default — only handlers inside the same plugin receive it. Pass { scope: "global" } to reach every loaded plugin (including your own).
// Local (default) — only this plugin's handlers see it
petty.events.emit("cache:refreshed", { count: items.length });
// Global — every loaded plugin receives it
petty.events.emit("spotify:track-changed", { title, artist }, { scope: "global" });Plugin URL handlers
External tools (CLIs, IDE extensions, shortcuts) can push context into a plugin via open "petty://<plugin-id>/<path>?<query>". The host routes the URL to that plugin's JS context as a plugin:url event. Unmatched plugin ids fall through to the reserved host-level routes (petty://event/* for Claude Code hooks).
// In your plugin:
petty.events.on("plugin:url", function (data) {
if (data.path === "share") {
petty.pet.showBubble("Shared: " + (data.query.title || "something"), 3);
}
});# From anywhere on the host machine:
open "petty://com.example.my-plugin/share?title=Hello%20world"Reacting in bulk
For patterns that involve many events from the same source, subscribe once to "*" and route on the name. Keep handlers fast — blocking the JS thread blocks every other event going to your plugin.