How to add a skill
A skill is one named, invokable thing your product can do via the palette. There are four kinds — pick the one that matches your task:
| Type | Use when | Example |
|---|---|---|
ScriptSkill |
Walk the user through a guided tour | /introduce |
PromptSkill |
Send a templated prompt to the LLM | /translate-prompt |
ActionSkill |
Run a TypeScript callback (no LLM) | /clear-clipboard |
SurfaceSkill |
Open a declarative form / dialog rendered as a Piece tree | /new-customer |
You register skills on the DotDotDuck config. They appear in the palette as /skillId — Name automatically.
ScriptSkill — /introduce
A scripted tour. Each step shows a subtitle and waits for the user to press space (or sets waitForUser: false to auto-advance).
import { DotDotDuck, type ScriptSkill } from '@perhapxin/dddk';
const introduce: ScriptSkill = {
id: 'introduce',
type: 'script',
name: 'Tour the product',
description: '1-minute walk-through',
icon: '▶',
steps: [
{ subtitle: 'Welcome! Press space to continue.' },
{
subtitle: 'This is your dashboard. The top bar is your nav.',
action: (t) => { t.border('nav', '#ec4899'); },
},
{
subtitle: 'Press Ctrl+K anytime to open the palette.',
},
{
subtitle: 'That`s it. Have fun.',
waitForUser: false,
},
],
};
const dotdotduck = new DotDotDuck({
siteName: 'Acme',
llm: yourProvider,
skills: [introduce],
});
dotdotduck.mount();
User types /introduce in the palette (or you call dotdotduck.runSkill('introduce') programmatically).
Step API (the t parameter)
| Method | What it does |
|---|---|
t.subtitle(text) |
Replace the floating subtitle |
t.border(selector, color?, label?) |
Highlight an element with a colored border |
t.spotlight(selector) |
Dim everything except this element |
t.highlight(selector) |
Soft highlight (less intrusive than spotlight) |
t.navigate(path) |
Programmatically navigate (routes through your onNavigate callback) |
t.wait(ms) |
Pause ms before the next step |
t.clearOverlays() |
Remove all border / spotlight decorations |
t.ask(question) |
Ask a question, returns the user's answer as Promise<string> |
t.surface(surface, opts?) |
Show an ad-hoc Surface mid-step; resolves with form data (or null on cancel) |
Mid-script Surface (form / picker / confirmation)
A step can pause the script to collect structured input via t.surface(...). The SDK emits the surface event with a PieceSurface payload; your host renders it, then calls dddk.submitSurface(data) (or dddk.cancelSurface()) to resume.
const onboarding: ScriptSkill = {
id: 'onboarding',
type: 'script',
name: 'Onboard new account',
steps: [
{ subtitle: 'Welcome — let me grab a couple of details.' },
{
action: async (t) => {
const data = await t.surface({
root: {
kind: 'Card',
children: [
{ kind: 'Heading', text: 'Your account' },
{ kind: 'TextInput', bind: '/name', label: 'Name', required: true },
{ kind: 'TextInput', bind: '/email', label: 'Email', required: true },
{ kind: 'Button', text: 'Continue', action: 'submit' },
],
},
data: { name: '', email: '' },
});
if (!data) return; // user cancelled
await fetch('/api/account', { method: 'POST', body: JSON.stringify(data) });
},
},
{ subtitle: 'Done — your account is set up.' },
],
};
Host wiring (one-time, where you already handle the surface event):
dddk.on('surface', ({ surface }) => {
mountYourRenderer(surface, {
onSubmit: (data) => dddk.submitSurface(data),
onCancel: () => dddk.cancelSurface(),
});
});
See surface-renderer for the full Surface contract.
PromptSkill — /translate-prompt
Sends a prompt to the agent. Variables in {braces} get filled from vars you pass at invocation.
import type { PromptSkill } from '@perhapxin/dddk';
const translateSkill: PromptSkill = {
id: 'translate-prompt',
type: 'prompt',
name: 'Translate clipboard',
prompt: 'Translate the following to {target}: {args}',
icon: '文',
};
dotdotduck.registerSkill(translateSkill);
// User: /translate-prompt es Hello world
// → prompt becomes "Translate the following to es: Hello world"
Args after the skill name are joined into {args}. For more control, the skill can read the user's first positional arg as a named variable — see parseArgs in the SkillRegistry.
ActionSkill — /clear-clipboard
Runs a TypeScript callback. No LLM involved.
import type { ActionSkill } from '@perhapxin/dddk';
const clearClipboard: ActionSkill = {
id: 'clear-clipboard',
type: 'action',
name: 'Clear clipboard',
icon: '✕',
handler: async (ctx) => {
await navigator.clipboard.writeText('');
ctx.subtitle.show({ text: 'Clipboard cleared.', type: 'info', autoHide: 1500 });
},
};
ctx gives you:
ctx.palette.close()/ctx.palette.replace(items)ctx.subtitle.show(...)/ctx.subtitle.hide()ctx.storage.get(key)/ctx.storage.set(key, value)(persists in localStorage by default)ctx.getPreferences<T>()(if your skill declaredpreferences)ctx.agent(task)to delegate to the webagentctx.navigate(path)
SurfaceSkill — /new-customer
Returns a PieceSurface — a tree of UI primitives + an optional data model. dotdotduck emits the surface event; the host renders via PieceRenderer. See surface-renderer for the full rendering contract.
import type { SurfaceSkill } from '@perhapxin/dddk';
const newCustomer: SurfaceSkill = {
id: 'new-customer',
type: 'surface',
name: 'New customer',
icon: '◉',
build: async (ctx) => ({
root: {
kind: 'Stack',
children: [
{ kind: 'Heading', text: 'New customer' },
{ kind: 'TextInput', id: 'name', bind: '/name', label: 'Name', required: true },
{ kind: 'TextInput', id: 'email', bind: '/email', label: 'Email', required: true },
{
kind: 'Select', id: 'plan', bind: '/plan', label: 'Plan',
options: [
{ value: 'free', label: 'Free' },
{ value: 'pro', label: 'Pro' },
{ value: 'enterprise', label: 'Enterprise' },
],
},
{ kind: 'Button', text: 'Create', action: 'submit' },
],
},
data: { name: '', email: '', plan: 'free' },
}),
onSubmit: async (values, ctx) => {
const { name, email, plan } = values;
await fetch('/api/customers', { method: 'POST', body: JSON.stringify({ name, email, plan }) });
ctx.subtitle.show({ text: `Created ${name}.`, type: 'info', autoHide: 2000 });
},
};
Per-skill preferences (settings)
A skill can declare fields the user needs to set once before it runs. dotdotduck auto-opens a setup form and persists the values to storage.
const summarizeSkill: ActionSkill = {
id: 'summarize',
type: 'action',
name: 'Summarize selection',
preferences: [
{ name: 'targetLang', label: 'Target language', type: 'select', options: ['en', 'zh-TW', 'ja'], default: 'en' },
{ name: 'tone', label: 'Tone', type: 'text', default: 'professional' },
],
handler: async (ctx) => {
const prefs = ctx.getPreferences<{ targetLang: string; tone: string }>();
// use prefs.targetLang / prefs.tone
},
};
First time the skill runs, dotdotduck shows the setup form. Subsequent runs read from storage.
Registering after construction
If your skills aren't known at construction time:
const dotdotduck = new DotDotDuck({ /* …no skills yet… */ });
dotdotduck.skills.register(introduce);
dotdotduck.skills.register(clearClipboard);
dotdotduck.mount();
You can also unregister at runtime via dotdotduck.skills.remove('introduce').