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.actionTraywith 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
skillsservice - Writes individual Skill toggles back via
skills.setSelectedand writes bulk selections viaskills.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
Thepackage.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
@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"onchat.composer.actionTray— completely replaces the default selected-Skill/MCP/Agent Mode tray. Theorder: 100value ensures this registration wins over any lower-priority replacement from another extension.mode: "hide"onchat.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. Ahideregistration needs norender— 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 nativeskillsservice. The page derivesdefaultIdsfrom 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 theset-skillaction with{ skill_id: skill.id }asargs, 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
Therender 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 derivesselectedSkillsfrom 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
expandedisfalseand 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”.
expanded is true, the tray renders a scrollable list of Skill toggles and a Clear button:
5. Reading Skills from the skills service
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
setSelectedtoggles 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 readsselected_skill_ids.setSelectionreplaces the entire selection with an explicit list of IDs. Theclearaction 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:
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) withregisterPage— you can suppress a built-in UI element entirely while simultaneously substituting your own replacement and adding a dedicated management page, all from a singleactivateAetherfactory 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 nativeskillsservice. 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 tostorageinside an action handler and want surfaces to reflect the new value immediately, callaether.invalidate()after the write. In this mod,toggle-expandedupdatesstorage.expandedand the framework re-renders the tray automatically; explicitinvalidate()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", ...).