Harness MaxxingDocumentación
📚 Todas las páginas

🧩 Ampliar · 5 min de lectura

🧱Extensiones de página

Una extensión de página añade una página completa a la app, servida en /x/<id>. Es un módulo de JavaScript normal –sin paso de compilación– más una API opcional en Python.

Dónde aparece: todas las páginas se listan en Extensiones en la barra lateral (la página /x). Haz clic en la chincheta de la parte superior derecha de una página (o en la página de Extensiones, o activa En la barra lateral en Ajustes -> Extensiones) para darle su propia entrada en la barra lateral, justo debajo de Extensiones; vuelve a hacer clic para quitarla.

/root/maxxing/extensions/notes-board/
  manifest.json
  page.js        the page (ES module)
  api.py         optional: its HTTP API

Pídele a Claude "crea una página que ..." y la skill maxxing-extension la escribe por ti, o sigue estos pasos.

Una instalación nueva incluye una página, Tus páginas (extensions/pages/): qué es una extensión de página, y ejemplos (archivo de cliente, seguimiento de pedidos, facturas...) que abren una sesión con la petición ya escrita. Es una extensión normal: borra su carpeta, o desactívala en Ajustes -> Extensiones, cuando ya no la necesites - las actualizaciones no la devuelven.

1. manifest.json

{"id": "notes-board", "name": "Notes board", "type": "page", "icon": "sticky-note", "version": "1.0.0"}

name también puede ser {"en": "Notes board", "fr": "Tableau de notes"}: la app muestra el de su idioma. "api": 1 (opcional, 1 por defecto) es la versión del contrato de extensiones para la que está escrita la página: si una versión futura deja de admitirla, la comprobación previa a la actualización te lo avisa antes de actualizar (Ajustes -> Sistema).

2. api.py (opcional)

Cada petición a /api/ext/<id>/<path> llama a:

def handle(ctx, method, path, query, body):
    ...
    return {...}            # sent as JSON (200)
    # or: return 404, {"error": "not_found", "message": "..."}   to choose the status code
Argumento Valor
ctx igual que en los widgets: id, workspace, state_dir, secret(name), config
method GET, POST, PUT, PATCH, DELETE
path la parte posterior a /api/ext/<id>/ (por ejemplo, notes, notes/42)
query parámetros de la query string como un diccionario
body JSON interpretado (o texto), None para GET; máximo 5 MB

Se ejecuta en un hilo de trabajo con un límite de 30 segundos. Las peticiones las autentica la app: solo un usuario con sesión iniciada puede llamarla.

Ejemplo:

import json
import time
from pathlib import Path


def handle(ctx, method, path, query, body):
    f = Path(ctx["state_dir"]) / "notes.json"
    notes = json.loads(f.read_text()) if f.exists() else []
    if path == "notes" and method == "GET":
        return {"notes": notes}
    if path == "notes" and method == "POST":
        text = str((body or {}).get("text", "")).strip()[:500]
        if text:
            notes.insert(0, {"text": text, "at": time.time()})
            tmp = f.with_suffix(".tmp")
            tmp.write_text(json.dumps(notes[:200]))
            tmp.replace(f)
        return {"notes": notes}
    return 404, {"error": "not_found", "message": f"{method} {path}"}

3. page.js

La app carga /ext/<id>/page.js y llama a mount(el, ctx):

ctx
api(path, opts?) llama a /api/ext/<id>/<path> con autenticación y cabecera CSRF; opts = {method, body} (el body se envía como JSON); devuelve el JSON interpretado y se rechaza ante errores HTTP
navigate(route) ir a otra página de la app, por ejemplo navigate("/tasks"); /sessions?path=<topic path>&new=1&prompt=<text> abre una sesión nueva con el texto ya escrito
theme "dark" o "light"
lang el idioma de la app, "en" o "fr"
t(key) función de traducción de la app
icon(name) un icono de la app como elemento <svg> (mismos nombres que el icon del manifest), por ejemplo button.prepend(ctx.icon("plus"))

Devuelve una función de limpieza: se ejecuta cuando el usuario sale de la página.

Por seguridad, la app solo ejecuta scripts que ella misma sirve: page.js no puede añadir etiquetas <script> en línea a la página ni construir código a partir de texto (eval); en su lugar, divide el código en módulos de la carpeta de la extensión. Los widgets HTML siguen funcionando como antes.

El módulo se descarga una vez por versión de page.js y se conserva: el código de la parte superior del archivo se ejecuta una vez, mount se ejecuta en cada visita. Al guardar page.js se crea una versión nueva, que se recoge en la siguiente visita.

export function mount(el, ctx) {
  el.innerHTML = `
    <div class="ui-page" style="max-width:640px">
      <h1 class="ui-h1">Notes board</h1>
      <form class="ui-row">
        <input class="ui-input" name="text" placeholder="New note" style="flex:1">
        <button class="ui-btn">Add</button>
      </form>
      <ul class="ui-list"></ul>
      <p class="ui-muted" hidden>No note yet: write the first one above.</p>
    </div>`;
  const list = el.querySelector("ul");
  const empty = el.querySelector("p");
  const form = el.querySelector("form");
  form.querySelector("button").prepend(ctx.icon("plus"));
  const render = ({ notes }) => {
    list.hidden = !notes.length;
    empty.hidden = !!notes.length;
    list.replaceChildren(...notes.map((n) => {
      const li = document.createElement("li");
      li.textContent = n.text;            // never inject data with innerHTML
      return li;
    }));
  };
  ctx.api("notes").then(render);
  const onSubmit = async (e) => {
    e.preventDefault();
    const text = form.text.value.trim();
    if (!text) return;
    render(await ctx.api("notes", { method: "POST", body: { text } }));
    form.reset();
  };
  form.addEventListener("submit", onSubmit);
  return () => form.removeEventListener("submit", onSubmit);
}

Estilo

La página recibe el kit de interfaz de la app: clases CSS que se ven exactamente igual que el resto de la app y siguen por sí solas el tema, el color de acento y la densidad. Úsalas mejor que los estilos escritos a mano:

Clases
ui-page ui-section ui-grid ui-row ui-stack ui-spacer diseño: columna de página, sección, cuadrícula adaptable de tarjetas, fila, columna
ui-h1 ui-h2 ui-h3 ui-text ui-muted ui-small ui-label ui-num texto (título, título de sección, cuerpo, secundario, etiquetas, cifras en columnas)
ui-card ui-card-header ui-card-footer una tarjeta; la cabecera lleva un ui-icon-box y un <div> con el título y una línea
ui-tile una tarjeta en la que se puede hacer clic: <button class="ui-tile"><div class="ui-icon-box">svg</div><div><b>Title</b><span>line</span></div></button>
ui-icon-box (+ ui-lg, ui-neutral) un icono sobre un cuadrado suave del color de acento
ui-btn (+ ui-secondary ui-outline ui-ghost ui-danger, ui-sm, ui-icon-only) botones (ui-btn solo = acción principal)
ui-field ui-label ui-input ui-select ui-textarea ui-check formularios
ui-badge ui-dot (+ ui-success ui-warning ui-error ui-primary ui-outline) estados
ui-list ui-table ui-table-wrap ui-stat ui-stat-value ui-stat-label ui-progress datos
ui-steps ui-step-num ui-callout (+ ui-info ui-success ui-warning ui-error) ui-empty ui-skeleton pasos, avisos, estados vacíos y de carga
ui-code ui-kbd ui-link ui-separator el resto

Los iconos vienen de ctx.icon(name); úsalos en lugar de emojis. Tu propio CSS siempre se impone al del kit, y las variables CSS de la app están disponibles para el resto: --background, --foreground, --card, --border, --muted, --muted-foreground, --primary, --primary-foreground, --success, --warning, --destructive. Los widgets HTML reciben el mismo kit. Los campos del kit son de 16 px en móviles (sin zoom en iPhone).

4. Probar y cargar

  • Lado Python: llama a handle() directamente (mira el fragmento de prueba en Widgets del panel, con api.handle(ctx, "GET", "notes", {}, None)).
  • Sintaxis de JavaScript: node --check page.js si Node está instalado.
  • Ajustes -> Extensiones -> Recargar, y luego abre la página desde la barra lateral (Extensiones, o su propia entrada una vez fijada). Los errores aparecen en la fila de la extensión.
💡 ¿Atascado? Abre una sesión en el topic system › help y pregunta con tus palabras: Claude lee esta documentación en tu servidor y te responde.✏️ Mejorar esta página