| name | building-lingbot-world-2-frontends |
| description | Extend this cloned LingBot World 2 example app โ add new controls, scenes, motion patterns, or knobs on top of `@reactor-models/lingbot-world-2` without breaking the patterns the existing code uses. Covers the SDK's connection / events / messages model, the layered prompt system and its override store, the per-chunk `set_camera_pose` motion channel, the WASD + mouse-look driving model, the auth route, and clip capture. |
Building on this LingBot World 2 app
You've cloned this folder and now you want to extend it โ a new control, a new scene, a new motion pattern, a different UX. This guide explains the patterns the existing code uses and the rules to follow so your additions feel native instead of bolted on.
All the code referenced below already exists in this folder. Read this guide alongside the source โ especially The camera-pose channel and Jump and crouch before touching anything in the motion system.
What LingBot World 2 actually is, in three sentences
LingBot World 2 is a real-time interactive world model. Given a starting image and a composed prose prompt, it streams an unending first/third-person video on a single track (main_video) โ and while it generates, the client continuously steers it with movement actions (set_move_longitudinal, set_move_lateral), per-chunk camera deltas (set_camera_pose), and live prompt swaps (set_prompt).
The frontend's job is to (a) start the generation with a valid image + prompt, (b) translate keyboard/mouse/joystick input into those steering commands every chunk, and (c) keep the prose prompt in sync with what the user is doing.
The four concepts you'll touch
| Concept | What it is | Hook / API |
|---|
| Connection | The lifecycle of the model session (disconnected โ connecting โ waiting โ ready) | useLingbotWorld2().status, .connect(), .disconnect() |
| Events | Things you send TO the model. Always async. | useLingbotWorld2().setPrompt({...}), .setImage({...}), .setMoveLongitudinal({...}), .setMoveLateral({...}), .setCameraPose({...}), .setRotationSpeedDeg({...}), .setSeed({...}), .setAttnWindow({...}), .start(), .pause(), .resume(), .reset() |
| Messages | Things the model sends BACK to you โ acks, chunk ticks, and the state snapshot. | useLingbotWorld2Message((m) => โฆ) (this app uses the catch-all; per-message typed hooks also exist) |
| Tracks | The model's video output, rendered as a live MediaStreamTrack. | <LingbotWorld2MainVideoView /> |
You almost never have to drop below this surface. If you find yourself reaching for @reactor-team/js-sdk directly, stop and re-read the typed hooks list โ there's likely a typed hook you're missing. The one documented exception is the recording surface (see Capturing clips below), which is a base-SDK feature that the typed packages deliberately do not re-export.
Two commands the published schema doesn't declare yet (set_kv_cache_reset, trigger_kv_cache_reset) go through the raw sendCommand("โฆ", {...}) escape hatch โ with a comment explaining why. When the schema catches up, migrate them to the typed methods and delete the comment.
The UI phase model
A real-time session is a state machine, and the UI mirrors it with two visible phases:
โโโโโโโโโโโโโโโโ setImage โ setPrompt โ start โโโโโโโโโโโโโโโโโโ
โ WAITING โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโถ โ GENERATING โ
โ (Setup UI) โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ (Live UI) โ
โโโโโโโโโโโโโโโโ reset โโโโโโโฌโโโโโโโโโโโ
โ โฒ
pauseโ โresume
โผ โ
โโโโโโโโโโโโโโโโโโ
โ PAUSED โ
โ (Live UI) โ
โโโโโโโโโโโโโโโโโโ
| UI phase | When | What's visible |
|---|
| Setup | not generating (fresh page, post-reset) | Quick Start example cards ยท custom scene card ยท Start button with its blocker reason |
| Live | isGenerating === true | WASD pad + joystick ยท mouse-look ยท jump/crouch ยท hold-key world events ยท prompt inspector ยท Advanced knobs |
Unlike the sibling examples (which split every phase into its own sidebar component), this app centralizes input handling in one component โ LingbotWorldController.tsx โ because every control feeds the same three output channels (movement actions, camera pose, composed prompt) and they must stay coherent. The controller returns { sidebar, controls } and the page lays them out.
When you add a new control, decide which channel it feeds first:
- A new steering input (a key binding, a gamepad axis, a gesture) โ extend the controller. It must participate in
sendCameraPoseChunk() / the movement stacks so it composes with existing inputs instead of fighting them.
- A new prompt ingredient (a weather layer, a mood selector) โ extend the layered scene model in
lib/lingbot-world-prompts.ts and composePrompt(). Never call setPrompt from a random component โ all prompt sends flow through recomputePromptAndSend() so the inspector and dedupe logic stay truthful.
- A standalone panel (stats, capture, diagnostics) โ a separate component like
SnapClip.tsx, gated on status === "ready", dropped into the page layout.
Auth โ getJwt resolver + cacheable GET route
Two pieces work together: a Next.js GET route that mints (and caches) a session-scoped JWT server-side, and a getJwt resolver prop on <LingbotWorld2Provider> that calls it on every Reactor API HTTP hop.
getJwt, not jwtToken
@reactor-team/js-sdk โฅ 2.10.1 accepts a resolver anywhere it used to take a static string:
type JwtSource = string | (() => string | Promise<string>);
The app passes getJwt={fetchToken} to <LingbotWorld2Provider> (see app/LingbotWorld2App.tsx). The SDK re-invokes that function on every Reactor API HTTP call โ image uploads, clip manifests, ICE refresh, SDP renegotiation โ so a token aging out mid-session can't 401 those hops. The legacy jwtToken="..." string prop still works but caches one value at construction time and breaks the moment that value expires.
The provider auto-stabilizes the resolver via useRef + useMemo, so an inline arrow is safe too โ a parent re-render does not tear the session down. Do not wrap it in useCallback.
The route โ app/api/reactor/token/route.ts
Already implemented. You usually don't need to touch it, but here's why it works the way it does so you don't accidentally break it:
- GET, not POST. Browsers don't cache POST responses. The route handler still POSTs to the Reactor API internally; the public route exposes itself as GET so the browser's HTTP cache can transparently serve repeat calls.
Cache-Control: private. Never public โ JWTs are per-user and must not be shared across users by any CDN or proxy.
max-age derived from the server's expires_at, not a hardcoded number. The Reactor /tokens endpoint accepts an expires_after body and returns { jwt, expires_at }. The route uses expires_at to set the cache window so it always tracks what the server actually granted.
authorization_details scopes the token. The mint pins the JWT to this app's model with a bounded session budget (max_sessions): the browser's token can only create sessions for that one model and act on the sessions it created โ everything else on the account answers 403. Never hand a browser an unscoped token; that is the API key's full user-level access in cookie-jar form.
Because the route is GET + cacheable, the getJwt resolver is also dumb on the wire โ every Reactor API hop calls fetch("/api/reactor/token"), which 99% of the time comes back from the browser's HTTP cache without ever touching your server.
Wiring an identity-provider JWT instead (Clerk, Auth0, โฆ)
If your app uses Clerk session tokens or any other short-TTL identity JWT, getJwt is the hook for that:
import { useAuth } from "@clerk/nextjs";
function App() {
const { getToken } = useAuth();
return (
<LingbotWorld2Provider
getJwt={async () => (await getToken({ template: "reactor" })) ?? ""}
>
{/* ... */}
</LingbotWorld2Provider>
);
}
Returning "" suppresses the Authorization header entirely. getJwt wins over jwtToken when both are passed.
Configuring autoConnect
<LingbotWorld2Provider> is initialized without autoConnect. The user clicks "Connect" so they see the disconnected โ connecting โ waiting โ ready transitions. If you're shipping a polished product where you'd rather the connection happen on page load:
<LingbotWorld2Provider getJwt={fetchToken} connectOptions={{ autoConnect: true }}>
Just make sure your status indicator still surfaces the intermediate states (connecting, waiting for GPU) โ sessions don't reach ready instantly, and you don't want users staring at an unexplained loading state.
Messages โ what the model sends back
The controller subscribes once with the catch-all useLingbotWorld2Message and switches on msg.type. The messages you'll handle:
| Message | What it means / what the app does with it |
|---|
state | Periodic full snapshot: has_prompt, has_image, started, running, paused, camera_pose_active. Mirrors into local state. |
prompt_accepted / image_accepted | Per-command acks. image_accepted also carries the accepted width / height. |
conditions_ready | Fires after each conditioning command with has_prompt / has_image flags. Lower-level than the specific acks; prefer those. |
generation_started | Generation began; carries chunk_num. |
chunk_complete | The heartbeat of the live phase. Carries chunk_index and active_action. The app advances jump arcs, consumes crouch dips, and sends the next set_camera_pose from here. |
generation_paused / resumed / complete / reset | Lifecycle transitions. generation_reset is where the app clears all per-session scene state. |
command_error | A command was rejected. Always surfaced โ the app shows it as a 4-second toast (errorToast). |
Two rules to keep when extending:
- Clear everything on disconnect. The controller has a
useEffect on status === "disconnected" that resets every piece of session state โ held keys, arcs, refs, previews. If you add new session state (a new ref, a new held-input), add its reset there too, or the next session starts haunted by the last one.
- Surface
command_error for new commands automatically. The existing toast handles any command. Don't swallow errors in your own catch(console.error) without also letting the toast fire.
Sending events โ the right ordering
Events are fire-and-forget over a data channel. The canonical start flow lives in applyScene() (in the controller):
- Clear all held inputs (never carry a held key across a scene switch).
- If generating,
reset() first and give the model a beat to process it.
await uploadFile(file) โ FileRef, then await setImage({ image: ref }).
await setPrompt({ prompt: composePrompt(scene, โฆ) }).
- Brief settle, then
await start().
For a stricter chain, wait for the model's explicit ack between the slow conditioning step and start() โ the image_accepted message fires exactly once when the uploaded image has been fully processed. Park a one-shot resolver in a ref before sending the command (registering it after would race the response):
const imageReadyRef = useRef<(() => void) | null>(null);
useLingbotWorld2Message((m) => {
if (m.type === "image_accepted") {
imageReadyRef.current?.();
imageReadyRef.current = null;
}
});
async function startScene(scene: StructuredScene, file: File) {
const imageReady = new Promise<void>((r) => (imageReadyRef.current = r));
const ref = await uploadFile(file);
await setImage({ image: ref });
await imageReady;
await setPrompt({ prompt: composePrompt(scene, false, []) });
await start();
}
Prompt sends don't need the wait โ they're picked up at the next chunk boundary.
Movement commands are state-based โ always send the neutral value on release
set_move_longitudinal / set_move_lateral hold their last value until you change it. Send "forward" and never send "idle" and the world keeps scrolling forever. The controller's key handlers maintain key stacks (moveLStackRef / moveLatStackRef) so overlapping presses resolve correctly (hold W, tap S, release S โ back to forward, not idle). When you add a new binding, push/pop the stack โ don't send raw values from your own handler.
The camera-pose channel
This is the model's signature capability and the part most worth understanding before extending. Everything the driving UI does flows through three commands:
| Command | Carries | Notes |
|---|
set_move_longitudinal / set_move_lateral | WASD action (forward/back/strafe/still) | a discrete action string, not a vector |
set_camera_pose | per-step 6-DoF deltas [rx,ry,rz,tx,ty,tz] | the interesting channel; drives look / roll / jump / crouch / joystick |
set_prompt | the composed text prompt | recomposed whenever anything changes |
The camera_pose contract (the physics everything obeys)
Four wire-observable facts shape every decision in the motion system:
- Granularity is per-step within a chunk, not per-chunk. The app sends
CHUNK_LATENTS (3) deltas per chunk โ one per generation step, โ12 pixel frames total. Send 3 deltas and each steers its own step; send 1 and the model repeats it across the chunk. The app always sends 3 so jump arcs and crouch dips can shape motion inside a single chunk.
- Axis convention is y-down:
โty = UP, +ty = DOWN. In code this is JUMP_UP_SIGN = -1; "up" intent maps to a negative ty.
- Translation magnitude is normalized away per chunk. Sending
ty = 0.3 vs ty = 100 gives the same result โ only the sign (direction) and the within-chunk shape survive. Consequences: a motion's size is its step count (duration), not a magnitude; a smooth velocity envelope cannot carry across chunks. Motion is therefore authored as small integer intent patterns (+1 up / 0 still / -1 down), not velocity curves.
- Rotation OVERRIDES the arrow-key look; translation ADDS to WASD movement. A pose with
ty = down and zero rotation does not erase forward motion โ the model sums the WASD action in. This is why crouch-walking works without re-sending forward, and it drives every design decision in the jump/crouch system.
One consequence worth a warning: a sustained vertical hold (jump or crouch hold mode) freezes arrow-key look. The model's override rule applies whenever a pose is active, even when it carries zero rotation. Mouse-look is unaffected (it does carry rotation).
One sender per chunk
The single send site is sendCameraPoseChunk(), called on every chunk_complete (and once when a control engages). Mouse-look, roll, orbit, joystick, jump arcs, and crouch dips all deposit their intent into refs, and that one function composes them into the next chunk's pose โ rotation (rx,ry,rz) and horizontal translation (tx,tz) uniform across the 3 steps, only ty authored per-step. When nothing is active it sends an empty pose once, handing rotation back to the arrow keys and translation back to WASD. Never add a second setCameraPose call site โ two senders per chunk means the second overwrites the first.
If you add a new motion (a dodge, a lean, a head-bob), follow the jump-arc shape: author it as a per-step intent pattern, store it in a ref, consume it inside sendCameraPoseChunk(), and advance/expire it on chunk_complete.
Jump and crouch โ the button โ event model
Each button represents a triggerable event, and each event is a (camera pose, prompt) pair: a bit of motion on the camera_pose channel plus a sentence woven into set_prompt, delivered together so the prose matches the motion. Press and release are independent triggers โ crouch fires a "down" dip + crouchPrompt on press and a mirrored "up" dip + standPrompt on release, making a button a small state machine rather than a momentary toggle.
Symmetry โ return to origin
Because magnitude is normalized away (fact 3 above), "equal up and down" means equal counts of up-steps and down-steps. #up == #down lands the camera back at its starting height; more up than down and the character ends up higher (never fully lands), more down and it sinks. This is why the default charge-level arcs are symmetric โ L1 is [1, 0, -1], L2 is [1,1,0, -1,-1,0], L3 is [1,1,1,1, 0, -1,-1,-1,-1] (the 0s are the hang at the apex and don't affect balance). Hand-edit an asymmetric pattern if you want deliberate vertical drift; just know that's what you're authoring.
Trigger semantics โ one-shot, held, and charge
- Jump is one-shot and locked while airborne.
onJumpDown returns early while an arc is in flight (jumpArcRef.length > 0) โ no double-jump.
- Crouch can be held. The press dip fires only on the idle โ held transition (key auto-repeat is ignored); releasing fires exactly one release dip. Holding C while walking gives a crouch-walk because translation is additive.
- Charge is hold-to-charge. While held, the level meter steps through
NUM_CHARGE_LEVELS discrete levels (dwelling LEVEL_DWELL_MS on each); nothing is emitted until release, which fires that level's arc + jumpPrompt.
Both dips last one chunk and are consumed on the next chunk_complete, so they're inherently transient.
Modes, editors, and per-scene prompts
Jump and crouch each have a mode switch: hold (sustained translation while held โ the way to move straight up/down), prompt (sentence only, no pose), and charge / camera (the arc/dip patterns). The patterns are hand-editable per-step grids (Lvl buttons for jump levels, โ dip for crouch press/release) โ click a cell to cycle โ up (+1) โ โ down (โ1) โ ยท still (0); the number of still cells is the hang duration. Edits persist in localStorage (chargePatterns / crouchPatterns), never hardcoded in logic.
The jump/crouch/stand sentences are scene fields, not code constants โ StructuredScene.jumpPrompt / crouchPrompt / standPrompt, editable per example in the scene editor's "Vertical" tab. At runtime recomputePromptAndSend() picks the active vertical sentence and the inspector shows it as the vertical segment.
One keyboard gotcha to preserve: crouch is on C, not Ctrl. macOS reserves Ctrl+arrows for Mission Control and swallows the keydown before the page sees it โ a Ctrl-held crouch would silently kill arrow-look, and no preventDefault can override a system shortcut. Don't move it back.
Motion-system code map
| Concern | Where |
|---|
| All input handling, pose emission, modes | LingbotWorldController.tsx |
Per-chunk pose builder (per-step ty) | sendCameraPoseChunk() |
| Prompt composition + active vertical sentence | recomputePromptAndSend() |
| Jump press/release + charge meter + arc | onJumpDown / onJumpUp / the charge-meter useEffect |
| Arc / dip advancement + consumption | case "chunk_complete" in the message handler |
| Pattern defaults + grid editors | defaultChargePattern / defaultCrouchPatterns, cycleChargeCell / cycleCrouchCell |
Prompt model + composePrompt | lib/lingbot-world-prompts.ts |
| Prompt segments (incl. vertical) | prompt-segments.ts |
Tunables sit at the top of the controller: CHUNK_LATENTS, NUM_CHARGE_LEVELS, LEVEL_DWELL_MS, JUMP_SPEED, JUMP_UP_SIGN, CROUCH_DIP.
The layered prompt system
The model only ever sees one prose string, but the app authors it in layers โ that's the StructuredScene in lib/lingbot-world-prompts.ts:
| Layer | Role |
|---|
base | World identity: subject, environment, style. Always present. |
camera / movement | Each has static and dynamic variants โ selected by whether the user is currently moving. |
events[] | Hold-key detail clauses (keys 1โ9). Stack while held, drop on release. |
| vertical | The jump / crouch / stand sentence while those controls are engaged. |
composePrompt(scene, isMoving, heldSlots, verticalPrompt) flattens the active selection to prose. recomputePromptAndSend() calls it whenever any input changes, dedupes against the last sent string, and sends set_prompt. The prompt therefore always narrates what the controls are doing โ that text/motion coherence is why driving feels responsive.
Rules when extending:
- All prompt mutations go through
recomputePromptAndSend(). If you send prompts from anywhere else, the dedupe ref (lastSentPromptRef) and the inspector both start lying.
- New layers get a segment in
prompt-segments.ts so the Show-prompt inspector and the editor preview render them. The inspector is the debugging surface for "why does the world look like that" โ keep it complete.
- Write event clauses as single present-continuous sentences about the environment ("rain begins to fallโฆ"), not about the subject. They must compose onto any base without contradicting it.
- Keep prompts paragraph-length with explicit camera framing. Short prompts produce choppy, unstable output. Look at the bundled scenes in
lib/lingbot-cases/ for the density to match.
Scenes, examples, and the override store
Curated examples live as JSON in lib/lingbot-cases/ (one file per scene) and are loaded by lib/lingbot-cases-examples.ts. Starting images ship in public/lingbot-cases/.
Adding a new example = one JSON file + one image + one registry entry. No component changes.
User edits to any example go into an override store (overrides state, persisted to localStorage), keyed by example id. The pristine JSON constants are never mutated. Rules the store enforces โ keep them if you touch it:
- An override that is byte-equal to the pristine scene is dropped, not stored (otherwise cards show a misleading "edited" badge).
- Overrides survive
reset() and disconnects โ they're presets, not session state.
effectiveSceneFor(id) is the only read path; it clones so callers can mutate freely.
Capturing clips
The Reactor base SDK exposes a recording surface that works for every model: ask for the last N seconds of the live stream, get back a Clip, and either preview it with <ClipPlayer> or download it with <ClipDownloadButton>. The model SDK does not own this โ it lives on @reactor-team/js-sdk because it is the same call for every model with recording enabled.
The app ships a drop-in components/SnapClip.tsx panel that wires this together: a "Capture" button that calls requestClip(durationSeconds) off the store, opens a modal with the SDK's preview player, and offers an MP4 download. It is model-agnostic โ the same file ships (modulo theme classes) in every example.
When to reach for @reactor-team/js-sdk directly
The default rule still applies: do everything via @reactor-models/lingbot-world-2. But the typed package only re-exports model-specific surface (events, messages, the typed provider/hook). The recording surface is base-SDK only, so for that one feature you import directly:
import {
ClipDownloadButton,
ClipPlayer,
RecordingError,
useReactor,
type Clip,
} from "@reactor-team/js-sdk";
When you scaffold a new component, ask: "Does this depend on LingBot-World-2-specific events, messages, or commands?" If yes โ typed package only. If no, and it would work the same on any model (recording, generic stats, generic connection state) โ @reactor-team/js-sdk is fine.
The pattern
- Destructure the recording action off the store.
useReactor((s) => s.requestClip) โ requestClip, requestRecording, and downloadClipAsFile are first-class store actions on @reactor-team/js-sdk โฅ 2.11.1.
- Gate on connection status. Return
null when status !== "ready", so the panel disappears on disconnect like every other live-only control.
- Catch
RecordingError. Typed reasons: DISCONNECTED, RECORDER_DISABLED, INVALID_DURATION, REQUEST_TIMEOUT. Surface them inline.
- Compose
<ClipPlayer> + <ClipDownloadButton> in a modal, routing their onError / onSuccess callbacks into the same inline error line.
- No
getJwt plumbing. Both clip components auto-inherit the resolver from <LingbotWorld2Provider getJwt={โฆ}> via React context. The one case where you'd pass it explicitly is a portal rendered outside the provider subtree (e.g. a Sonner toast in app/layout.tsx) โ capture reactor.getJwtResolver() inside the provider at action time and thread it down as a prop.
hls.js
<ClipPlayer> plays HLS natively on Safari/iOS. On Chrome / Firefox / Edge it dynamically imports hls.js โ which is why the app declares it as a direct dep (hls.js@^1.6.0). If it weren't installed, the player would surface an inline error (downloads still work); the dep keeps the preview path functional for the majority of users.
Extending
<SnapClip durationSeconds={30} label="Save 30s highlight" /> โ most extensions are one prop. requestRecording() (no args, also on the store) grabs everything since recording started instead of a trailing window; swap the call for a "Save the whole session" button. The headless useClipDownload hook is what to use for a custom progress UI.
Clips are short-lived. The URL on a Clip expires after a few minutes. Don't store Clip objects long-term and don't hand clip.playlistUrl to users for sharing โ download the MP4 and host the result yourself.
Brand alignment โ tokens through the theme, components from components/ui
The app pulls Reactor's design tokens (fonts + brand colors) from @reactor-team/ui's stylesheet and maps them into the shadcn theme variables in app/globals.css โ --primary, --background, --font-sans, etc. all resolve to Reactor brand values. The reusable primitives in components/ui/ (button, input) are shadcn-generated and read those variables.
Rules:
- Use the theme utilities (
bg-primary, text-muted-foreground, font-mono) or the existing components/ui primitives. Don't invent parallel color systems with raw hex values.
- Don't import
@reactor-team/ui React components. They use hooks internally โ importing one into a Server Component (like SetupRequired) dies at runtime, not build time. The stylesheet import gives you everything you need.
- The app is dark-only: the
dark class is set on <html> in app/layout.tsx so server-rendered pages get the dark tokens too. Don't move it into a client effect.
Common mistakes when extending
- Reaching for
@reactor-team/js-sdk directly for model work. Everything model-specific is on @reactor-models/lingbot-world-2. The one allowed exception is the recording surface โ see Capturing clips.
- Adding a second
setCameraPose call site. One sender per chunk (sendCameraPoseChunk()), fed by refs. A second sender silently overwrites the first and motion becomes non-deterministic.
- Sending prompts outside
recomputePromptAndSend(). Breaks dedupe and makes the Show-prompt inspector lie.
- Forgetting the neutral value on key release. Movement axes hold their last value. Pair every
"forward" with an "idle", and use the key stacks so overlapping presses resolve correctly.
- Forgetting to reset new session state on disconnect. Extend the
status === "disconnected" effect in the controller with every new ref/state you add.
- Keyboard listeners that hijack typing. Every handler early-returns when the event originates in an
INPUT / TEXTAREA / contentEditable. Copy that guard into new bindings, and preventDefault() on handled keys so arrows don't scroll the page.
- Asymmetric vertical patterns by accident. Equal up-steps and down-steps returns the camera to its starting height; unequal counts drift it. Fine if intentional โ see Symmetry.
- Overwriting the user's scene edits. Reads go through
effectiveSceneFor(id); writes go into the override store. Never mutate the pristine constants from lib/lingbot-cases/.
- Blocking
start() on nothing. start requires both a prompt and an image; the model rejects it with command_error otherwise. The Start button's startBlockerReason tells the user which half is missing โ keep that pattern for new preconditions.
- Importing
@reactor-team/ui React components into Server Components. Runtime error, not build error. Use the theme tokens.
- One-line prompts. Paragraph length minimum, explicit camera framing, present-continuous event clauses. The bundled scenes are the reference density.
- Storing
Clip objects or sharing clip URLs. They expire in minutes. Download the MP4 if you need an artifact.
Checklist for new components
Before merging a new control or feature: