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 |
A2UISkill |
Open a declarative form modal | /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 dddk = new DotDotDuck({
siteName: 'Acme',
llm: yourProvider,
skills: [introduce],
});
dddk.mount();
User types /introduce in the palette (or you call dddk.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> |
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: '🌐',
};
dddk.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)
A2UISkill — /new-customer
Opens a declarative form. dddk renders the form, validates input, and hands you the values.
import type { A2UISkill } from '@perhapxin/dddk';
import { z } from 'zod';
const newCustomer: A2UISkill = {
id: 'new-customer',
type: 'a2ui',
name: 'New customer',
icon: '👤',
build: async (ctx) => ({
fields: [
{ name: 'name', label: 'Name', type: 'text', required: true },
{ name: 'email', label: 'Email', type: 'email', required: true },
{ name: 'plan', label: 'Plan', type: 'select', options: ['free', 'pro', 'enterprise'] },
],
submit: { label: 'Create' },
}),
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. dddk 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, dddk shows the setup form. Subsequent runs read from storage.
Registering after construction
If your skills aren't known at construction time:
const dddk = new DotDotDuck({ /* …no skills yet… */ });
dddk.skills.register(introduce);
dddk.skills.register(clearClipboard);
dddk.mount();
You can also unregister at runtime via dddk.skills.remove('introduce').