Skip to main content
The examples/global-skills-mod package is the reference implementation for global, persistent Skill selection across chats. It originated from Aether issue #27 and demonstrates combining UI replacement with native service calls in a single Script Mod — no Native Mod required.

What this mod does

  • Replaces chat.composer.actionTray with a custom tray that shows the current global Skill selection and an expand/collapse toggle
  • Hides chat.composer.skillPicker (the built-in per-chat Skill rows) to keep the UI clean
  • Reads the full Skill catalog from Aether’s native skills service
  • Writes individual Skill toggles back via skills.setSelected and writes bulk selections via skills.setSelection
  • Persists the expand/collapse state in extension storage
  • Adds a “Global Skills” full-screen page to the conversation drawer for dedicated management

Package declaration

The package.json registers a single entry point under aether.extensions. There is no pi key because this mod has no Pi agent component:

Full annotated code walkthrough

1. Imports

Because this is a Script-only Aether Mod (no Pi integration), there is no import from @earendil-works/pi-coding-agent. The local Skill type mirrors the shape returned by the native skills service’s list method, used throughout the render callbacks for type-safe destructuring.

2. Component registrations: replacing the action tray and hiding the skill picker

registerComponent targets named built-in Compose UI. Two registrations work together here:
  • mode: "replace" on chat.composer.actionTray — completely replaces the default selected-Skill/MCP/Agent Mode tray. The order: 100 value ensures this registration wins over any lower-priority replacement from another extension.
  • mode: "hide" on chat.composer.skillPicker — suppresses the built-in per-chat Skill rows that appear in the composer plus menu. Because the mod takes over global Skill selection through the tray, the native per-chat picker would be confusing and redundant. A hide registration needs no render — the target simply disappears.

3. Page registration: the Global Skills management page

registerPage adds a full-screen destination to the conversation drawer. Key points:
  • context.skills — comes from the live app state; Aether surfaces the Skill catalog alongside the rest of the UI state automatically.
  • context.default_skill_ids — reflects the current default selection from the native skills service. The page derives defaultIds from this to drive the switch states without maintaining a duplicate in extension storage.
  • ui.switch(label, checked, actionId, options) — renders a labeled toggle row. Each switch fires the set-skill action with { skill_id: skill.id } as args, passing the specific Skill ID to the action handler.
  • skill.enabled !== false — filters out Skills the platform has disabled; those should not appear in the management UI.
  • scroll: true — makes the column scrollable for large Skill catalogs.

4. Surface: showing the current global Skill selection in the tray

The render callback for the chat.composer.actionTray replacement reads live context and builds the tray UI:
  • context.selected_skill_ids — reflects the currently active Skill selection for the open chat, sourced from native state. The tray derives selectedSkills from this to show counts and names without duplicating state.
  • context.storage.expanded — the collapse/expand toggle state is the only thing stored in extension storage; everything else reads from native app state.
  • Collapsed state — when expanded is false and at least one Skill is selected, the tray shows the Skill names joined by · as a muted caption. When nothing is selected it shows “No defaults”.
When expanded is true, the tray renders a scrollable list of Skill toggles and a Clear button:

5. Reading Skills from the skills service

Aether injects the Skill catalog and current selection directly into the render context under context.skills and context.selected_skill_ids. You do not need to call aether.services.invoke("skills", "list") manually inside render — the platform keeps these fields up to date and re-renders surfaces whenever they change. When you need to read Skills imperatively (outside render, e.g., in an action handler), use:

6. Writing selections back via skills.setSelected and skills.setSelection

  • setSelected toggles a single Skill. scope: "both" updates both the current chat’s selection and the default selection at once. The native service propagates the change back into app state, which triggers a re-render of every surface that reads selected_skill_ids.
  • setSelection replaces the entire selection with an explicit list of IDs. The clear action passes an empty array to deselect all Skills in both scopes.

7. Persisting the expand/collapse preference in storage

The expand/collapse preference is the only value written to storage:
Because Skill selections are written directly into native state via setSelected / setSelection, there is no need to mirror them in extension storage. Aether’s native skills service persists default selections on its own. The tray reads context.storage.expanded on every render to restore the correct collapsed or expanded state across sessions.

Key patterns demonstrated

  • Combining registerComponent (replace + hide) with registerPage — you can suppress a built-in UI element entirely while simultaneously substituting your own replacement and adding a dedicated management page, all from a single activateAether factory call.
  • Using aether.services.invoke("skills", ...) for real native state — rather than maintaining a shadow copy of Skill data in extension storage, the mod reads from and writes to the actual native skills service. This means the selections are always consistent with what the rest of Aether sees.
  • aether.invalidate() after storage writes to trigger surface re-renders — if you write to storage inside an action handler and want surfaces to reflect the new value immediately, call aether.invalidate() after the write. In this mod, toggle-expanded updates storage.expanded and the framework re-renders the tray automatically; explicit invalidate() calls would be needed if storage writes happened outside an action registered with Aether.
This mod is a Script Mod only (aether.extensions) — it achieves full Skill management without needing a Native Mod, showing the power of the Script API. All persistent state lives in Aether’s native skills service; extension storage is used only for the tray’s expand/collapse preference. If you need to intercept Skill selections at a lower level (for example, to lock a Skill against deselection), combine this pattern with aether.intercept("skills.selection", ...).