Aller au contenu

Contribuer au projet

Ce guide documente les patterns a suivre et les pieges a eviter pour contribuer a WebMCP Auto-UI. Il est mis a jour au fil des incidents de production — chaque regle correspond a un bug reel, pas a une convention theorique.

graph TD
subgraph "Packages (npm workspaces)"
CORE["core<br/>Types, MCP client, validation"]
SDK["sdk<br/>HyperSkills, canvas store"]
AGENT["agent<br/>Boucle agent, providers LLM"]
UI["ui<br/>30+ widgets, composants agent"]
end
subgraph "Apps"
FLEX[flex — Composeur principal]
VIEWER[viewer — Visionneur HyperSkills]
SHOWCASE[showcase — Galerie widgets]
RECIPES[recipes — Explorateur recettes]
HOME[home — Page d'accueil]
BOILER[boilerplate — Template starter]
TODO[todo — Demo todo]
end
CORE --> SDK
CORE --> AGENT
CORE --> UI
SDK --> FLEX
AGENT --> FLEX
UI --> FLEX

Avant d’ecrire du code dans une app (apps/*), verifier d’abord si la fonctionnalite existe deja dans un package. Le tableau ci-dessous liste les imports disponibles :

BesoinImportPackage
Serveur WebMCPcreateWebMcpServercore
Client MCPMcpClient, McpMultiClientcore
Validation JSON SchemavalidateJsonSchemacore
Montage widget vanillamountWidgetcore
Boucle agentrunAgentLoopagent
Provider LLM distantRemoteLLMProvideragent
Provider GemmaWasmProvideragent
Provider OllamaLocalLLMProvideragent
Outils de decouvertebuildDiscoveryTools, activateServerToolsagent
Resolution canoniqueresolveCanonicalTools, toolAliasMapagent
Suivi de tokensTokenTrackeragent
Nano-RAGContextRAGagent
Selecteur LLM<LLMSelector>ui
Chargement Gemma<ModelLoader>ui
Statut MCP<McpStatus>ui
Progression agent<AgentProgress>ui
Rendu widgets<WidgetRenderer>, <BlockRenderer>ui
Message bus FONCbusui
Encodage HyperSkillencode, decodesdk
Canvas storecanvas via @webmcp-auto-ui/sdk/canvassdk

Les regles suivantes viennent de bugs reels de production. Chaque regle est illustree par le code fautif et le code correct.

Regle 1 : un $effect ne doit pas lire ET ecrire les memes etats

Section intitulée « Regle 1 : un $effect ne doit pas lire ET ecrire les memes etats »

Symptome : effect_update_depth_exceeded en console, la page entiere se bloque (boutons sans effet, modals qui ne s’ouvrent pas).

Cause : Svelte 5 re-execute un $effect chaque fois qu’une de ses dependances reactives change. Si l’effet ecrit une valeur qu’il lit aussi, il se re-declenche lui-meme — boucle infinie — arret au bout de 5 iterations.

<!-- INCORRECT -- lit gemmaStatus ET l'ecrit -->
$effect(() => {
const llm = canvas.llm;
if (gemmaStatus === 'ready') { // lit gemmaStatus → tracke
gemmaStatus = 'idle'; // ecrit gemmaStatus → re-run !
}
canvas.addMsg('system', llm);
});
<!-- CORRECT -- untrack() pour les lectures non-declencheuses -->
import { untrack } from 'svelte';
$effect(() => {
const llm = canvas.llm; // seule dependance trackee
untrack(() => {
if (gemmaStatus === 'ready') { // lu mais pas tracke
gemmaStatus = 'idle';
}
canvas.addMsg('system', llm);
});
});

Regle : isoler la ou les dependances qui doivent declencher le re-run, et wrapper tout le reste dans untrack().


Regle 2 : preferer $derived a $effect + $state pour les valeurs calculees

Section intitulée « Regle 2 : preferer $derived a $effect + $state pour les valeurs calculees »
<!-- INCORRECT -- $state ecrit dans un $effect -->
let paletteOpen = $state(true);
$effect(() => { paletteOpen = canvas.mode === 'drag'; });
<!-- CORRECT si la valeur est read-only -->
const paletteOpen = $derived(canvas.mode === 'drag');
<!-- CORRECT si la valeur est aussi ecrite manuellement (toggle utilisateur) -->
let paletteOpen = $state(true);
$effect(() => { paletteOpen = canvas.mode === 'drag'; });
// OK car l'effet ne LIT PAS paletteOpen

$derived est toujours preferable quand la valeur ne depend que d’autres etats reactifs. Utiliser $effect + $state uniquement quand la valeur est aussi modifiee par l’utilisateur (toggle, drag, etc.).


Regle 3 : eviter les $effect redondants avec onMount

Section intitulée « Regle 3 : eviter les $effect redondants avec onMount »
<!-- INCORRECT -- double initialisation -->
let skills = $state([]);
$effect(() => { skills = listSkills(); }); // court-circuite par onMount
onMount(() => { skills = listSkills(); });
<!-- CORRECT -- une seule source de verite -->
let skills = $state([]);
onMount(() => { skills = listSkills(); });

Si listSkills() ne lit aucun etat reactif, le $effect ne se re-declenchera jamais apres le premier run. Il est strictement equivalent a onMount, mais plus trompeur a la lecture.


Regle 4 : passer le modele LLM explicitement au provider

Section intitulée « Regle 4 : passer le modele LLM explicitement au provider »
// INCORRECT -- utilise toujours 'haiku' par defaut
return new RemoteLLMProvider({ proxyUrl: `${base}/api/chat` });
// CORRECT -- transmet le choix de l'utilisateur
return new RemoteLLMProvider({
proxyUrl: `${base}/api/chat`,
model: canvas.llm,
});

RemoteLLMProvider utilise model ?? 'haiku' comme valeur par defaut. Si l’utilisateur selecte sonnet dans le <LLMSelector>, il recevra quand meme haiku sans message d’erreur. Toujours passer model explicitement.

onText n’est appele qu’a la derniere iteration

Section intitulée « onText n’est appele qu’a la derniere iteration »

Par defaut, callbacks.onText est appele uniquement quand le LLM repond sans tool_use. Comme le system prompt force l’usage d’outils, ce callback n’est jamais atteint dans le flux normal.

Consequence : la bulle “thinking” reste figee sur le dernier outil appele.

Correctif applique dans loop.ts : appeler onText aussi quand il y a du texte intermediaire avant les tool_use :

// Texte intermediaire avant tool_use -- mise a jour live
if (lastText) callbacks.onText?.(lastText);

Le script deploy.sh verifie automatiquement l’integrite apres chaque transfert. En cas de mismatch, le deploiement echoue avec rollback.

Pourquoi ? Sans cette verification, un deploiement peut “reussir” (pas d’erreur SCP, service active) mais servir l’ancien code si :

  • Le build local etait perime
  • Le SCP a ete interrompu silencieusement
  • Le fichier cible etait en lecture seule

Le script rebuild automatiquement les apps via npm run build avant de copier. Avant ce correctif, les packages etaient recompiles mais les apps conservaient leur ancien build/, et les correctifs Svelte etaient perdus.

Quand un package npm est en JavaScript pur (sans types TypeScript), ne jamais l’importer directement dans le code TypeScript du projet. Creer un wrapper type dans le SDK :

packages/sdk/src/hyperskills.ts
// @ts-ignore — hyperskills is intentionally pure JS
import * as hs from 'hyperskills';
export const encode: (sourceUrl: string, content: string) => Promise<string> = hs.encode;
export const decode: (urlOrParam: string) => Promise<{ sourceUrl: string; content: string }> = hs.decode;

Pourquoi pas declare module ? Ignore par moduleResolution: "NodeNext" quand le JS est deja resolu. Pourquoi pas .d.ts dans node_modules ? Supprime au prochain npm install. Pourquoi pas allowJs ? Ne suffit pas avec strict: true + NodeNext.

Fenêtre de terminal
npm run test # Tous les tests
npm run test:watch # Mode watch
npm run test:coverage # Avec couverture

Les tests e2e verifient les apps deployees sur https://demos.hyperskills.net :

Fenêtre de terminal
npx playwright test # Tous les tests
npx playwright test --grep "Composer" # Une suite
npx playwright test --grep "export" # Un test precis

Quand lancer les tests :

  • Apres chaque deploiement important
  • Avant de marquer un bug comme resolu
  • Apres un refactoring qui touche plusieurs composants
// INCORRECT -- peut cliquer avant hydratation
await page.goto(url);
await page.click('button:has-text("export")');
// CORRECT -- attendre un element client-side
await page.goto(url);
await page.waitForSelector('select', { state: 'visible' });
await page.waitForTimeout(500); // tick d'hydratation
await page.click('button:has-text("export")');
  • Que le code deploye correspond au code local (c’est le role du sha256 dans deploy.sh)
  • Les erreurs JavaScript silencieuses cote client (ajouter page.on('pageerror'))
  • Les boucles reactives Svelte qui n’affectent pas les selecteurs CSS testes

Debug — Checklist “le correctif n’est pas en production”

Section intitulée « Debug — Checklist “le correctif n’est pas en production” »

Quand un correctif semble applique localement mais pas visible en production, suivre cette checklist dans l’ordre :

  1. Le build local est-il a jour ?

    Fenêtre de terminal
    ls -la apps/flex/build/index.js # Verifier la date de modification
  2. Le fichier deploye correspond-il au build local ?

    Fenêtre de terminal
    sha256sum apps/flex/build/index.js
    ssh bot "sha256sum /opt/webmcp-demos/flex/index.js"
    # Les deux hashes doivent etre identiques
  3. Le service a-t-il redemarre avec le bon fichier ?

    Fenêtre de terminal
    ssh bot "systemctl status webmcp-flex --no-pager | head -20"
  4. Y a-t-il des erreurs JavaScript cote client ? Ouvrir la console du navigateur (F12) sur l’URL de production.

Apres toute modification de code qui change les exports, les tokens ou les types de widgets :

Fenêtre de terminal
npm run docs:sync

Le CI verifie que la documentation est a jour a chaque push.

Les diagrammes existants sont pre-rendus en SVG dans docs/starlight/public/diagrams/. Pour regenerer les SVG apres modification d’un diagramme :

Fenêtre de terminal
npx @mermaid-js/mermaid-cli -i fichier.mmd -o docs/starlight/public/diagrams/fichier.svg --backgroundColor transparent
Fenêtre de terminal
git checkout -b feat/ma-feature

Conventions de nommage :

  • feat/ : nouvelle fonctionnalite
  • fix/ : correction de bug
  • refactor/ : restructuration sans changement fonctionnel
  • docs/ : documentation uniquement
Fenêtre de terminal
npm run dev:flex # Developper sur l'app principale
npm run test # Lancer les tests unitaires
npm run check # Verifier les types TypeScript

Utiliser le format Conventional Commits :

feat(agent): add context compaction via Nano-RAG
fix(ui): prevent effect_update_depth_exceeded in ModelLoader
refactor(core): simplify JSON Schema validation
docs(guide): update architecture diagram
perf(onnxruntime): load WASM from CDN instead of bundling
  • Titre court et descriptif (< 70 caracteres)
  • Description avec le contexte et les changements
  • Lier les issues si applicable
  1. Creer le composant Svelte dans packages/ui/src/widgets/rich/ ou simple/.
  2. L’exporter dans packages/ui/src/index.ts.
  3. Ecrire une recette markdown avec frontmatter (schema JSON Schema).
  4. Enregistrer la recette dans packages/agent/src/autoui-server.ts.
  5. Lancer npm run docs:sync pour mettre a jour la doc.
  1. Creer un fichier dans packages/agent/src/providers/.
  2. Implementer l’interface LLMProvider (methode chat).
  3. L’exporter dans packages/agent/src/index.ts.
  4. Ajouter un cas dans la factory createProvider.
  1. Lancer le serveur MCP (doit exposer un endpoint SSE).
  2. Ajouter l’URL dans la configuration (.env.local ou parametres de l’app).
  3. Le resolver canonique gerera automatiquement le mapping des noms d’outils.

Pourquoi les tests e2e testent les apps deployees et pas locales ?

Section intitulée « Pourquoi les tests e2e testent les apps deployees et pas locales ? »

Parce que les bugs les plus dangereux surviennent entre le build local et le deploiement. Tester en production (avec des donnees de test) detecte les problemes d’integrite de build, de configuration nginx et de variables d’environnement manquantes.