Communitygithub.com

jongio/skills

Um pacote de habilidades multiplataforma para agentes de IA de codificação — GitHub Copilot, Claude e Codex — instalável com a CLI skills (npx skills add). Inclui create-canvas-kit para construir extensões de canvas do Copilot.

O que é skills?

Este pacote de habilidades, `jongio/skills`, oferece um conjunto de ferramentas e habilidades reutilizáveis para agentes de IA de codificação, como GitHub Copilot, Claude e Codex. A habilidade principal incluída é o `create-canvas-kit`, que fornece uma solução completa e robusta para a criação de extensões de *canvas* (superfícies interativas) para o Copilot. O kit resolve problemas comuns encontrados em implementações manuais, como perda de foco em campos de texto, estado compartilhado inconsistente e problemas de tema. Ele oferece um modelo claro de arquitetura, separando a lógica de extensão, o canvas, a visualização em Preact e o servidor, garantindo que o agente e a interface do usuário compartilhem os mesmos manipuladores de ações. Além disso, fornece componentes de interface pré-construídos, helpers para formatação e sincronização automática de estado, tornando o desenvolvimento de canvases interativos e com estado muito mais eficiente. O pacote pode ser instalado globalmente via a CLI `skills`.

Funciona com✓Claude Code✓Codex CLI~Cursor
npx skills add jongio/skills

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Criar App de Canvas

Uma maneira completa de construir extensões de canvas para o Copilot App. Ele existe porque canvases feitos manualmente esbarram nos mesmos problemas: um loop de repintura innerHTML que engole pressionamentos de tecla, estado que vive apenas em um painel, estilos ad-hoc que ignoram o tema do host e ícones inconsistentes. O kit resolve todos os quatro.

O que é um canvas (e quando construir um)

Um canvas é uma superfície interativa que o agente abre em um painel lateral via open_canvas. Tanto o agente quanto o usuário atuam no mesmo estado através dos mesmos manipuladores de ação. Recorra a um canvas quando o texto do chat ou um diff não for suficiente: painéis ao vivo, editores, planilhas, rastreadores, visualizações kanban/board, pré-visualizações de documentos, fluxos de trabalho específicos de ferramentas.

Se o usuário precisar apenas de uma ferramenta de agente não visual, construa uma ferramenta de extensão normal — não um canvas.

Camada de renderização — decida primeiro

CamadaQuando usarComo
String HTML estáticaConteúdo somente leitura ou quase estático; sem entradas a protegerO scaffold vanilla do runtime (extensions_manage scaffold kind:canvas).
Preact + htm + este kitQualquer coisa interativa: entradas, atualizações ao vivo, listas, formulários, estado compartilhadoEste kit. Escolha padrão para canvases reais.

A razão mais importante para usar o kit: Preact difere o DOM, então um push de estado ao vivo do agente não destrói o foco, a posição do cursor ou o texto parcialmente digitado em uma entrada. O padrão innerHTML = ... que a maioria dos canvases iniciais usa repinta toda a árvore e perde pressionamentos de tecla a cada push. Não faça isso.

O modelo (leia isto antes de codificar)

extension.mjs   ── o ÚNICO arquivo que importa o Copilot SDK (adaptador fino)
canvas.mjs      ── seu canvas: id, schema, salvar/carregar estado, manipuladores de ação (sem SDK)
canvas-kit/     ── o kit (copiado ipsis litteris; não edite)
web/index.html  ── shell: carrega /kit/theme.css e ./app.mjs
web/app.mjs     ── sua visualização Preact
  • O estado é compartilhado e durável. Ele é indexado por um domain id resolvido a partir da entrada aberta (resolveDomainId), não por instanceId — abra o mesmo domínio em dois painéis e eles mostram os mesmos dados. A persistência passa por userStore(extName, file) → $COPILOT_HOME/extensions/<nome>/artifacts/<dominio>.json.
  • Agente e Interface compartilham manipuladores. Uma ação invocada pelo agente e a mesma ação invocada a partir de um botão executam o manipulador idêntico e produzem a mutação de estado idêntica. Escreva a lógica uma vez, em canvas.mjs.
  • Atualizações ao vivo são automáticas. O kit serve GET /state, GET /events (SSE) e POST /action. mountCanvas os conecta; cada mudança de estado se propaga para todos os painéis abertos.
  • server.mjs não tem SDK para que todo o runtime seja testável com Node HTTP puro (veja test/http.test.mjs). Mantenha as chamadas de SDK apenas em extension.mjs.

Dois tipos de estado na visualização — mantenha-os separados

  • Estado de domínio compartilhado chega via SSE e é re-renderizado a cada push. Trate-o como somente leitura na visualização; altere-o apenas chamando invoke(action, input).
  • Estado da interface local (um valor de entrada de rascunho, a guia/filtro ativo) vive em useState e nunca deve ser enviado ao servidor até que o usuário o confirme.

Resultados de ação e erros — como as falhas aparecem

Ambos os chamadores (agente e interface) atingem o mesmo manipulador, e o runtime envolve cada invocação no mesmo envelope, então modele as falhas deliberadamente:

  • Retorne um pequeno objeto simples para sucesso ({ id, status }, { count }). POST /action responde com { ok: true, result }.
  • Lance para uma falha grave (entrada inválida, id ausente). O runtime responde com { ok: false, code, message } — HTTP 400 para um erro conhecido do kit (ex.: uma ação desconhecida) e 500 para um throw do manipulador — e o invoke do mountCanvas rejeita com um CanvasActionError carregando esse code/message, para que um manipulador de botão possa try/catch isso. No lado do agente, extension.mjs mapeia o mesmo erro para um CanvasError. Um throw é superficializado, nunca uma quebra.
  • Para uma falha esperada e transitória (uma busca upstream instável em uma sondagem em segundo plano), não lance — capture-a em state.error e retorne para que o cartão de erro apareça enquanto a sondagem permanece silenciosa (veja o template data refresh).

Ícones — GitHub Lucide oficial, sempre

O kit fornece o conjunto exato de Lucide que o github-app usa ([email protected], bytes idênticos). Use-o para todos os ícones; não escreva SVGs manualmente ou puxe um CDN.

import { Icon } from "/kit/client.mjs";
html`<${Icon} name="circle-check" size=${16} />`

Contrato: nomes são em kebab-case (circle-check, trash-2, list-todo). viewBox é fixo 0 0 24 24, stroke-width é 2 — nunca passe strokeWidth. Use tamanhos 12 · 14 · 16 · 20 · 24 · 32. Corresponda às próprias escolhas de glifo do github-app quando houver um mapeamento óbvio (ex.: open=circle-dot, done=circle-check, bookmark=bookmark). iconNames() / hasIcon(name) estão disponíveis se você precisar validar.

Tema — tokens Primer, com fallbacks

Vincule /kit/theme.css e construa com base em suas classes ck-* (ck-card, ck-btn, ck-btn-primary, ck-input, ck-badge, ck-tabs, ck-status, ck-empty, …). Elas mapeiam os tokens Primer que o runtime injeta no documento do canvas, cada um com um fallback codificado para o tema escuro do GitHub, para que um canvas pareça correto mesmo se um token estiver ausente. Adicione CSS específico do canvas por cima; não reestilize os primitivos.

Badges são genéricos. Use as variantes semânticas ck-badge-success, ck-badge-accent, ck-badge-muted, ck-badge-danger, ck-badge-attention e mapeie seus status para elas na visualização (a referência faz open → success, decided → accent, parked → muted). Não adicione nomes de classe de status específicos do aplicativo ao tema compartilhado.

Primitivos de carregamento e erro (não crie estes manualmente):

  • ck-spinner — animação de giro; emparelhe com um loader Lucide, ex.: html\<${Icon} name="loader-circle" class="ck-spinner" />``.
  • ck-skeleton — bloco de placeholder cintilante para conteúdo que ainda está carregando.
  • ck-callout / ck-callout ck-error — uma notificação / superfície de erro embutida para uma busca com falha, ex.: html\<${Icon} name="circle-x" size=${16} />${state.error}``.

Segurança de movimento é global. theme.css já envia um guard @media (prefers-reduced-motion: reduce) que neutraliza todas as animações e transições (spinners/skeletons do kit e quaisquer que você adicione). Você não repete isso por canvas — mas mantenha o movimento decorativo, nunca o único sinal.

API da visualização — hooks, helpers, fragmentos

/kit/client.mjs re-exporta o conjunto completo de hooks Preact fornecidos, não apenas useState: useEffect, useRef, useMemo, useCallback, useReducer, useContext, useLayoutEffect, useImperativeHandle. Chame hooks incondicionalmente no topo de um componente — antes de qualquer return antecipado (ex.: a ramificação de carregamento if (!state) return … vem depois de seus hooks).

Ele também exporta helpers de formatação + id para que você pare de reinventá-los:

import { nid, relativeTime, compactNumber, percent } from "/kit/client.mjs";
nid();                       // "lq3k9f2a" — id único curto
relativeTime(iso);           // "5m atrás" / "3h atrás" / "2d atrás" / uma data
compactNumber(2_300_000);    // "2,3M"
percent(1.25);               // "+1,25%"

nid também é importável no lado do servidor em canvas.mjs via import { nid } from "./canvas-kit/format.mjs" (o gerador faz isso).

Os formatadores são sensíveis à localidade — relativeTime termina em um toLocaleDateString(), e compactNumber/percent passam por Number.prototype.toLocaleString, então sua saída depende da localidade do host ("1,5K" vs "1.5K", 2025-01-31 vs 31/01/2025). Mantenha os valores brutos no estado durável (uma string ISO, um Number) e formate na visualização. Nunca armazene uma string formatada e nunca analise uma de volta — a formatação é uma preocupação de apresentação, e um valor formatado sob uma localidade não pode ser lido de forma confiável sob outra.

Fragment NÃO é exportado. Para retornar nós irmãos sem um wrapper, use a sintaxe de tag vazia embutida do htm — html`<><${A} /><${B} /></>` — ou apenas retorne um array de vnodes. Não faça import { Fragment }; ele não está lá.

Canvases com dados externos — busca + atualização automática

A maioria dos canvases reais não são listas CRUD inseridas pelo usuário; eles puxam da rede e atualizam. A forma:

  1. fetch() vive no manipulador de ação (ou um helper que ele chama), nunca na visualização. Sempre vincule-o com um timeout para que um upstream lento não possa travar o painel:

    async function fetchItems(url) {
      const res = await fetch(url, { signal: AbortSignal.timeout(12000) });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return mapItems(await res.json());
    }
    

    A busca executa no lado do servidor no runtime de loopback, então o template data gerado a protege com uma verificação SSRF: ele permite apenas http/https para um host público e rejeita loopback, link-local (incl. metadados de nuvem) e intervalos privados — incluindo cada endereço para o qual o hostname resolve. Qualquer coisa que você alimente de volta ao agente (ex.: via list_items) ainda é texto influenciado por atacantes, então mantenha a fonte que você escolhe e trate o conteúdo buscado como não confiável (renderize-o como texto, nunca innerHTML).

  2. Exponha uma ação refresh que busca e escreve o resultado no estado compartilhado. Capture falhas em state.error e retorne (não lance) para que um tique de sondagem em segundo plano permaneça silencioso enquanto o cartão de erro aparece. Capture a entrada antes do await, e escreva de volta com um set funcional para que uma ação concorrente (outra sondagem, um set_source) não seja sobrescrita:

    refresh: {
      inputSchema: { type: "object", properties: {}, additionalProperties: false },
      handler: async ({ state, set }) => {
        const sourceUrl = state.sourceUrl;          // capture antes do await
        try {
          const items = await fetchItems(sourceUrl);
          set((cur) => cur.sourceUrl !== sourceUrl  // descarta uma resposta obsoleta
            ? cur
            : { ...cur, items, error: null, lastRefresh: new Date().toISOString() });
          return { count: items.length };
        } catch (err) {
          set((cur) => ({ ...cur, error: `Não foi possível carregar: ${err.message}`, lastRefresh: new Date().toISOString() }));
          return { ok: false, error: String(err.message) };
        }
      },
    },
    
  3. Sonde com o kit, não com um setInterval bruto. pollWhileVisible(tick, seconds) executa tick em um intervalo apenas enquanto o painel estiver visível (para que um canvas em segundo plano pare de atingir a rede) e retorna uma função de limpeza — um useEffect return imediato:

    import { pollWhileVisible } from "/kit/client.mjs";
    // intervalo vinculado ao estado durável; reconstruído quando muda:
    useEffect(() => pollWhileVisible(() => invoke("refresh"), state.autoRefreshSec || 0),
              [state.autoRefreshSec]);
    

    Para um intervalo fixo, você pode pular o efeito completamente: mountCanvas({ view, poll: { action: "refresh", seconds: 60, immediate: true } }).

Estampe toda esta forma com node scripts/new-canvas.mjs <nome> --template data.

Estratégia de domínio — quadro compartilhado vs perfil pessoal

O estado é indexado pelo id de domínio que resolveDomainId retorna. Duas intenções, maquinário idêntico (fileFor sanitiza o id para um nome de arquivo — copie-o ipsis litteris):

  • Quadro compartilhado (a maioria dos canvases): indexado por um tópico que o agente e o usuário nomeiam. resolveDomainId: (input) => (input?.domain ? String(input.domain) : "default"). Qualquer um que abrir domain: "tech" vê o mesmo quadro.
  • Armazenamento pessoal: indexado por uma identidade, não um tópico. resolveDomainId: (input) => (input?.profile ? String(input.profile) : "default"), com a propriedade do schema de entrada nomeada profile. Use isso para dados por aluno / por usuário que não devem vazar entre tópicos.

Escolha o substantivo (domain vs profile) deliberadamente — é todo o contrato para quem compartilha o quê.

Padrões e limitações (conheça estes)

  • Catálogo empacotado. Para dados iniciais selecionados (cursos, predefinições), envie um catalog.mjs separado: dados simples mais um construtor do tipo buildCourse(...) que atribui ids posicionais determinísticos (es-u1-l2) para que o mesmo catálogo sempre produza os mesmos ids. Mantenha os dados em massa concisos com pequenos helpers de construtor.
  • statusLine é calculado uma vez, na abertura. A string de status do painel é definida em openInstance e não é re-enviada quando uma ação muta o estado, então uma contagem em statusLine fica obsoleta. Mostre contagens ao vivo dentro do corpo do canvas (que é re-renderizado a cada push SSE) e trate statusLine como um resumo apenas no momento da abertura.
  • Ações com payload rico. inputSchema das ações padrão tem additionalProperties: false propositalmente: o schema é o contrato que o agente preenche, e um schema estrito impede o modelo de contrabandear campos não validados/com erro de digitação — apenas as propriedades que você declara alcançam seu manipulador. Quando a interface genuinamente precisa passar um objeto inteiro para o manipulador (ex.: o artigo sendo salvo) em vez de campos escalares, defina additionalProperties: true no inputSchema daquela ação e desnormalize o que você precisa no estado durável no manipulador. Ative-o deliberadamente, por ação — não como um padrão geral.
  • Layout de instalação / resolução do SDK. Uma pasta de extensão enviada contém apenas copilot-extension.json ({ "name", "version": 1 } — sem package.json), extension.mjs, canvas.mjs, web/ e o canvas-kit/ aninhado. O kit é ESM puro que o Node executa diretamente — sem bundler, sem transpilação, sem etapa de build, sem package.json na extensão (o test/smoke.test.mjs incluso é executado com node simples). O host resolve @github/copilot-sdk/extension para você; extension.mjs é o único arquivo que o importa. Não adicione um package.json ou uma etapa de build a um canvas enviado.

Construa um canvas — caminho mais rápido

  1. Gere-o (um canvas funcional, kit já aninhado, um teste de fumaça incluído):
    node scripts/new-canvas.mjs <nome> --template data
    
  2. Ajuste canvas.mjs: defina id, openInstance (com inputSchema), e, pelo menos, um manipulador de ação. Veja examples/ para referência.
  3. Escreva web/app.mjs: useEffect para invocar na montagem e renderizar com html.
  4. Adicione ícones: use <${Icon} name=... /> do /kit/client.mjs.
  5. Teste: node test/smoke.test.mjs (HTTP sem SDK) e uma instalação real extensions_manage install <caminho>.

Estrutura de suporte & teste

  • Testes sem SDK: O kit exporta test/http.test.mjs que inicia o server.mjs com um canvas fictício e envia requisições HTTP reais contra ele. Execute com node --test test/http.test.mjs. Nenhum Copilot SDK está envolvido; isso testa a lógica do canvas, serialização de estado e roteamento de ações em isolamento.

    Extensões geradas também recebem um test/smoke.test.mjs que verifica a sanidade da estrutura: importa canvas.mjs, verifica openInstance e action handlers, tudo sem envolver a rede. Execute-o com node --test test/smoke.test.mjs.

  • Exemplos (em examples/): um canvas mínimo e um com dados externos. Copie-os como ponto de partida.

Canal de feedback

Problemas, ideias ou pedidos de recursos? Abra uma issue no repositório do kit.

Habilidades Relacionadas