{
  "schema": "leumas.docs.page/1",
  "id": "skill:leviathan-page-capabilities",
  "slug": "skills/leviathan-page-capabilities",
  "kind": "tools",
  "bucket": "skill",
  "title": "leviathan-page-capabilities",
  "name": "Leviathan Page Capabilities",
  "eyebrow": null,
  "chip": null,
  "summary": "Give the Leviathan widget live tools to read, edit, save and drive a page's own data — the save/edit pattern every Leumas surface repeats.",
  "keywords": [
    "leviathan-page-capabilities",
    "widget",
    "pattern",
    "reference implementations",
    "drive",
    "leumas leviathan page capabilities",
    "save",
    "checklist new surface"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "\n# Leviathan page capabilities — the save/edit pattern\n\nEvery Leumas product mounts the Leviathan widget (`@leumas/leviathan`) inside a\n`CapabilityProvider` (`@leumas/capability-sdk`). A page can register **live\ntools** with that provider; the widget sends them as function-call tools on\nevery chat turn, executes returned `clientToolCalls` **in the page** via the\nregistered handlers, and feeds the results back (up to 4 tool rounds per turn).\nThe [fast] badge on the widget shows the live count. Navigate away → the scope\nunregisters → the tools vanish.\n\nThat loop is what turns Leviathan from \"RAG over my data\" into an **operator of\nthe page**: it reads real state, mutates it through the page's own save path,\nand the user watches the UI update mid-conversation.\n\n## The one hook\n\n```jsx\nimport { useRegisterCapability } from '@leumas/capability-sdk';\n\nuseRegisterCapability([\n  {\n    name: 'add_statements',                       // snake_case verb\n    description: 'What it does + WHEN to use it', // the model reads this\n    parameters: { type: 'object', properties: { texts: { type: 'array', items: { type: 'string' } } }, required: ['texts'] },\n    call: async ({ texts }) => {                  // may be async; return JSON-able\n      await addStatements(texts, { source: 'ai' });\n      return { ok: true, added: texts.length };\n    },\n  },\n], {\n  scopeId: 'graphs:editor',        // 'domain:page' convention — stable, explicit\n  enabled: Boolean(doc),           // gate until data is loaded\n  pageTitle: `Reasoning graph: ${doc?.name}`,\n  pageInstructions: 'Context + ground rules the bot gets while THIS page is open.',\n});\n```\n\nFresh array literals per render are fine — registration is keyed on the tools'\n*shape* (names/descriptions/parameters), and `call` always sees the latest\nclosures. Unregister-on-unmount is automatic.\n\n## The save/edit rules (what makes the pattern solid)\n\n1. **Tools call the page's EXISTING mutators** — the exact functions the\n   buttons call (optimistic update → API verbs → refresh). Never write a\n   parallel persistence path for the AI; then AI edits inherit every invariant,\n   race guard and stats refresh for free, and are indistinguishable from hand\n   edits.\n2. **Read tools first, write tools second.** Ship a `get_*_overview` that\n   returns the page's real state compactly, and tell the bot (in\n   `pageInstructions`) to call it before editing. Grounded reads are what make\n   the edits sensible.\n3. **Return small JSON, always `{ ok, ... }` on writes** — errors as\n   `{ ok: false, error }`, never throws the model can't read. Cap list sizes.\n4. **\"Show\" tools drive the UI** (select, focus, navigate) so the user *sees*\n   what the bot means. Selection/spotlight setters are capabilities too.\n5. **Mark AI writes** where the data model supports it (e.g. statements carry\n   `source: 'ai'`) so provenance survives.\n6. **One scope per page**, `domain:page` id, `enabled:` gated on data readiness.\n   Keep 5–12 tools; more dilutes tool choice.\n7. **Extract to a `use<Domain>Capabilities.js` hook** beside the page — the\n   page stays lean and the capability surface is reviewable in one file.\n\n## Reference implementations\n\n- **Canonical toy:** `products/leumas-studio/src/admin/demos/DemoContacts.jsx`\n  (add/find/tag contacts — the minimal shape).\n- **Real save/edit surface:** `products/leumas-studio/src/admin/graphs/useGraphCapabilities.js`\n  (reasoning graphs: overview/list/inspect reads + add/edit/delete/rename/mute\n  writes + select/focus canvas drivers), registered in `GraphEditor.jsx`; the\n  gallery registers a lighter `list/open/create` set in `GraphsHome.jsx`.\n- SDK internals: `shared/packages/capability-sdk/src/{CapabilityProvider.jsx,useRegisterCapability.js}`;\n  widget tool loop: `shared/packages/leviathan/src/Widget.jsx`.\n\n## Checklist for a new surface\n\n1. Identify the page's mutators + state (the ones the UI already uses).\n2. Write `use<Domain>Capabilities.js`: overview read → detail read →\n   writes-via-mutators → show tools. snake_case names, JSON-schema parameters,\n   `{ ok }` returns.\n3. Call it from the page component; scope `domain:page`; `enabled` on data\n   readiness; `pageInstructions` = context + \"read overview first\" + any\n   domain grammar (e.g. \"statements are single declarative sentences\").\n4. Verify: open the page → widget [fast] count rises → ask the bot to read, then to\n   make one small edit → the UI updates and the change persists on reload →\n navigate away → [fast] drops.\n5. Server-side twins: if the domain also has an adapter (functioncalls/MCP),\n   keep page tools for *stateful, open-document* work and adapter tools for\n   *stateless* work — don't duplicate one as the other.\n",
  "source": {
    "path": ".claude/skills/leviathan-page-capabilities/SKILL.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 4925,
    "hash": "9dca90ae84227e534d54718c74773bd522d8f403"
  },
  "urls": {
    "html": "/p/skills/leviathan-page-capabilities",
    "json": "/docs/skills/leviathan-page-capabilities.json",
    "md": "/docs/skills/leviathan-page-capabilities.md"
  },
  "links": {
    "composes": [],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  }
}
