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 declared preferences)
  • ctx.agent(task) to delegate to the webagent
  • ctx.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').