Skill launcher
One reusable affordance any package drops into its UI to hand a skill to the agent — it prefills the chat composer exactly like a slash pick, never sends, and derives its own visibility so it can never advertise a skill the caller cannot run.
A package surface often wants to say, in one click, "this is a skill — let the agent do it for you." The Skill launcher is the single reusable control for that. It is not tied to any one route or widget: a package places it anywhere in its UI as an additive affordance, and every instance behaves identically.
Clicking a Skill launcher prefills the chat composer with the skill — and
optionally its scripts and the file it should act on — exactly as if the user
had picked the skill from the / slash menu. It then focuses chat. It does
not start a stream, call a route, or send the message. The user reviews the
prefilled composer and presses Send themselves. This keeps the human in control
of every agent turn while removing the "what do I type?" friction.
It is exported from @neuralis/package-system/client, so any package — not just
the chat package — can use it with a dependency only on the package system.
The three shapes
import { SkillLauncher } from '@neuralis/package-system/client';
// Icon-only — for toolbars, widget headers, chip rows
<SkillLauncher variant="icon" skill="manage-files" packageId="@neuralis/brain-core"
label="Organize this folder" />
// Icon + label — for empty states and side panels
<SkillLauncher variant="label" skill="manage-workflows" packageId="@neuralis/agent-core"
label="Schedule a workflow" icon="calendar" />
// Full-width list row — for skill-launcher lists and menus
<SkillLauncher variant="row" skill="manage-sources" packageId="@neuralis/brain-core"
label="Manage sources" />Notice what is not there: a feature list. The launcher works out for itself whether this caller may see it — see Visibility.
All three are the same button behind the same handoff — pick the
presentation that fits the surface; variant="row" is a list line, not a
separate component:
variant="icon"— a compact, icon-only chip. Usesize="sm"for a20×20widget-header slot, the default for composer-chip-sized rows.variant="label"— an icon + text pill for roomier surfaces.variant="row"— a full-width, left-aligned list line (transparent with a hover wash) for skill-launcher lists: a searchable skills menu, or a per-package breakdown where each skill — and each of its scripts — is its own launchable row.attached="left" | "right"— flattens the adjacent corners so the button sits flush against an existing control inside aninline-flex gap-0.5wrapper, blending the two into one visual unit (the way the composer's model + effort chips merge).
Props
| Prop | Meaning |
|---|---|
skill | The skill activation id (its frontmatter name, or its directory name) — what execute({ action: 'skill' }) resolves. Never a display title. |
packageId | The owning package's registry id (@neuralis/brain-core, i.e. its manifest neuralis.id) — not the directory name. It is matched as an exact pair with skill. |
scripts? | Skill-bundle-relative paths (e.g. scripts/build.sh) the agent should run through the skill's directory after activation. These are intent references, not attachments — see below. |
attachments? | Real, addressable URIs the skill operates on (e.g. a data:// file a row represents). Attached as ordinary read references. |
agentId? | Target agent; defaults to the active agent. |
hint? | One short line of plain text naming the job, appended after the chips — so the agent's first move is the work rather than a lookup call to find out which object you meant. Keep it to what the caller can already read: it lands in a draft they see and can edit. It is plain text by contract (≤200 characters), sanitized and clamped by the chat surface that receives it, so it can never become a chip of its own. |
label | Visible text (label variant) and the tooltip / accessible name (both variants). |
icon? | A registered icon name; defaults to a wand glyph. |
alsoRequires? | Extra features required by the route behind the skill, when it is gated more tightly than the skill itself. Narrow-only: it can hide the launcher, never reveal one. |
visible? | An explicit visibility answer from a surface that already holds a stronger, per-caller projection. Omit it to let the launcher derive its own. |
variant (icon | label | row) · size · attached · accent | Presentation, as above. |
port? | An explicit workspace host port. Omit it inside a widget and the launcher takes the one from its provider context; pass it only where no provider is above the button. |
className? | Extra classes on the rendered control, for spacing and placement within your own layout. |
How it works
The button does one thing: it pushes a structured handoff through the workspace's existing cross-widget navigation seam, the same mechanism a widget uses to deep-link another widget. The chat surface receives the handoff and turns it into composer content:
- the skill becomes a skill chip in the composer (identical to a
/pick); - each script becomes a small file chip that tells the agent to run it from the activated skill's directory;
- each attachment becomes an ordinary file-read chip;
- the hint, if you set one, is appended after them as plain text — never a chip. The chat surface strips anything that could turn into one and clamps the length, because a hint routinely carries user-written text (a workflow title, a file name) that the package did not author.
Because the script chips point inside the skill bundle, the package system stays out of the file-addressing business: the agent reaches them through the skill's own directory once it activates the skill, using its normal filesystem and execute tools. The package that places the button only needs to name the skill and, optionally, which of its scripts are relevant, plus the one-line hint — no URIs, no prompt to write.
Visibility is derived, not declared
The launcher renders only if the (packageId, skill) pair comes back in the
skill catalog the host resolves for the current caller — the same per-caller
filtered projection the workspace already runs on. Membership in that answer is
the proof of visibility.
This is deliberately not something the author states. An earlier version took a
hand-typed feature list, and nothing linked it to the skill's own
requiredFeatures frontmatter, so it drifted silently whenever a skill changed.
A stale entry does real harm: it advertises a privileged skill's existence to
a caller who cannot run it, when the platform's rule is that a feature-gated
contribution is invisible — never shown locked or disabled. Deriving removes the
second copy of the truth, so there is nothing left to drift.
It also fails in the safe direction. An unresolved, denied or still-loading catalog is empty, so the failure mode is no button, never a button that should not be there.
Two details worth knowing:
- The pair is exact. Activation ids are not globally unique — a package
dropped into a project can declare one that collides with a privileged
built-in — so a launcher never matches on the skill id alone. This is also why
packageIdmust be the registry id: the catalog carries that form, and a directory name matches nothing. alsoRequiresnarrows, never widens. When the route behind a skill is gated more tightly than the skill itself, list the extra features there. It can only remove the launcher, so a wrong value is a UX bug and never a disclosure.
Visibility is still not authorization
None of the above is a security boundary. The real authorization happens when the user sends the message: activating the skill re-evaluates the caller's actual granted features server-side and fails closed, and any attached URI is checked against the per-path URI policy before it is read. A control that prefills the composer confers no privilege; it only saves typing.
Placing it well
The Skill launcher is additive. Add it where it genuinely helps — a widget toolbar, a panel's empty state, a context-menu row — rather than replacing a surface's existing actions. A few well-chosen placements beat many. Match each button to a skill that fits its surface: a files panel to a file-organizing skill, a calendar to a workflow-scheduling skill, an agent's settings to an agent-management skill.
To merge the icon variant flush with an existing control, wrap both in an
inline-flex gap-0.5 container and set attached on the matching side. To wire
the same handoff into a control you already have, call useSkillHandoff() and
invoke composeSkill(...) from its onClick — but note that you then own the
visibility question yourself.
The launcher resolves the workspace host port from a provider its package mounts once, in its host install entry. Forget that provider and every launcher under it renders nothing at all — with no error — which is indistinguishable from "not added yet". Sandboxed (iframe) surfaces cannot host one: they are separate documents with no access to the port.
For broad discoverability — so every skill the agent can run has a trace, not
only the ones a package author hand-placed — the chat surface also offers a
skill launcher: a searchable menu (and the empty-state package view) that
lists every in-scope package's skills with their scripts as variant="row"
launchers. Those are filled from the live, access-filtered package overview — a
stricter projection than the default catalog, because it also accounts for the
per-agent package switches the caller set themselves — so a skill the caller
can't use never appears.