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.

EventWhen it firesData
hook:task-completeClaude Code task completesundefined
hook:needs-attentionClaude Code needs user inputundefined
hook:session-startClaude Code session startsundefined
hook:session-endClaude Code session endsundefined
pet:clickedUser clicks the petundefined
pet:interactionAny user interaction with the petundefined
pet:visibilityPet hidden or shown{ visible: bool }
plugin:urlExternal app opens petty://<your-plugin-id>/<path>?<query>{ path: string, query: {…} } — dispatched to the addressed plugin only
settings:changedA setting for this plugin changes{ key: string, value: any }
system:app-focusedA different app becomes frontmost (also fires once at plugin load for the current app){ bundleId, name }
system:app-unfocusedAn app loses focus{ bundleId, name }
system:clipboard-changedThe clipboard contents change{ text?, hasImage }
system:display-lockedUser locks the screenundefined
system:display-unlockedUser unlocks the screenundefined
system:sleepSystem is about to sleepundefined
system:wakeSystem has woken from sleepundefined

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.