A personal workspace for widget previews, themes, and visual assets.
DesignTypeScript · JavaScriptGitHub @guinhoshutoOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A personal, code-accessible workbench for StreamElements Custom Widgets. Submit HTML, CSS, JavaScript, and the real FIELDS schema through the API or web import screen, then use a private editing link to preview themes, run synthetic tests, and generate visual assets.
The Next.js application lives at this repository's root. Consumer widgets remain plain HTML/CSS/JS/FIELDS; they do not need React, a bundler, or changes to their production files. The shared simulation and renderer also remain available as a local CLI.
{{field}} and {field} placeholders in HTML, CSS, and JavaScript, and reloads the widget when a field changes, as the StreamElements editor does.There are no user accounts and no external database. In production, private Vercel Blob stores projects, revisions, uploads, and final artifacts; Workflows orchestrate jobs; short-lived Sandboxes run the browser/media worker. The editing link is a secret capability, not a public sharing link.
Requirements: Node.js >=22.20 <23 or >=24 <25, npm 10.9.3, and the committed lockfile. Browser jobs also require an already-installed Chrome/Chromium. Final video jobs require existing FFmpeg and ffprobe executables.
From the repository root, after authorizing dependency installation:
npm ci
npm run dev
Open http://127.0.0.1:3000 and choose Open demo project, or import a WidgetSnapshot JSON document. No Vercel credentials are needed locally. The default storage directory is .studio-data; keep it private and backed up if the projects matter.
Both npm run dev and npm start bind to 127.0.0.1. The Studio that agents use by default is the production build on port 4310, which the se-widget-studio skill's client targets:
npm run build
npm run serve # http://127.0.0.1:4310
Outside Vercel the Studio drops the hosted deployment's quotas (daily budgets, ten-minute jobs, 15-second videos, four video variants, 900 frames per job); a job instead waits for the machine-wide render slot and then for the machine check, and keeps the render disk guard (lib/limits.ts). The hosted deployment is paused since 2026-10-01.
A local job runs inside the server process, whichever session submitted it, so stop the server only once node skills/se-widget-studio/scripts/studio-client.mjs active prints "activeJobs":0. A job that a stopped server took along fails, and frees its project, the next time the restarted Studio reads it (Jobs).
The repository uses playwright-core. Normal startup, tests, and rendering never download Chromium, FFmpeg, codecs, or fonts. Install or select these tools deliberately; SE_WIDGET_STUDIO_BROWSER, STUDIO_FFMPEG_PATH, and STUDIO_FFPROBE_PATH can point to existing local executables.
| Setting | Local development | Vercel deployment |
|---|---|---|
STUDIO_STORAGE | local (default outside Vercel) | blob |
STUDIO_EXECUTION | local (default outside Vercel) | vercel |
STUDIO_DATA_DIR | Optional; defaults to .studio-data | Not used as a fallback |
| Blob credentials | Not required | Private Blob integration token, or configured store ID and Vercel OIDC identity |
STUDIO_SANDBOX_SNAPSHOT_ID | Not required | Required for jobs; trusted browser/media baseline |
STUDIO_CREATE_KEY | Optional creation gate | Optional creation gate; clients send X-Studio-Key |
.env.example documents local values. Do not copy its local mode settings into Vercel. Hosted deployments reject local storage and local workers rather than silently using ephemeral disk. Configure production resources and variables, then redeploy; see Vercel setup. Installing a plugin or successfully building the app does not provision or validate those resources.
POST /api/v1/projects with a complete versioned snapshot.editorUrl only in a trusted browser.Authorization: Bearer <capability>.PUT and the latest JSON response's etag as If-Match.The API reference includes runnable import examples, schemas, uploads, previews, history, limits, and error handling. A 201 response may contain a blocked revision: inspect its diagnostics before attempting a preview or job.
sample-media/ holds 3 gallery images (webcam-style streamer photos, two of them blurred, 1672x941) and 7 stage backdrops (1254x1254), all AI-generated. Reference one in catalog data as the whole string sws-sample:<file>, for example "image": "sws-sample:gallery/streamer-1-blur.jpg", an array for multiple image fields, or "background": {"id": "plants", "image": "sws-sample:backdrops/plants.jpg"}. The local CLI, the hosted preview, and hosted jobs resolve the same verified files; nothing is copied into the widget. Empty image fields stay empty unless you choose a sample or press Fill empty image fields in an editor, because StreamElements shows them empty too. The list of references, alt text, and backdrop tones is in sample-media/manifest.json.
Editing capabilities travel in an initial URL fragment and authorized request headers, not query parameters. Keep them out of Git, screenshots, logs, prompts, and widget data. Anyone holding a capability can read and edit that project. There is currently no account recovery, capability rotation, or project deletion API.
Source is retained as submitted in immutable revisions. A separate derived snapshot captures supported dependencies for offline rendering; uploaded code is never evaluated as Node.js configuration. Public HTTPS dependencies may be fetched during preparation under address, size, and time limits. Runtime fetches, module imports, and dynamic resource discovery are not supported, except for Google Fonts, which the Studio resolves from its cache. Use synthetic identities/events only—never real StreamElements tokens, cookies, webhooks, private messages, or channel data.
FIELDS defaults, theme, fixture, scene, and explicit overrides are separate layers. Presentation settings such as viewport, background, crop, and visual zoom are not injected into fieldData. Marketplace rules remain in dated presets under presets/marketplaces, with source URLs; they are not baked into the renderer.
This is a partial local simulation, not a guarantee of real StreamElements or OBS compatibility. Keep a final check in the actual target runtime. Hosted credentials, quotas, storage, and Sandbox execution also need separate hosted validation.
npm run typecheck
npm test
npm run build
npm run verify:bundle
verify:bundle checks that the traced sample media list, import, preview, job, and workflow functions and the npm package contain every file in sample-media/manifest.json.
For the production-browser workflow, keep this server running in one terminal:
STUDIO_STORAGE=local STUDIO_EXECUTION=local STUDIO_DATA_DIR=.studio-data/validation-server npm start -- --port 4317
Then run in another terminal, with an existing browser and FFmpeg/ffprobe available:
node scripts/verify-web.mjs
node --import tsx scripts/verify-field-precedence.mjs
node scripts/verify-font-control.mjs
The script accepts only loopback targets, creates synthetic projects/jobs, and writes screenshots, generated media, and a JSON report into a new .studio-data/verification-* directory. It is not a hosted-deployment check. Repeated runs consume the local store's personal daily budgets; use a new deliberate validation store when needed, without deleting unrelated data.
The field regression script verifies fixture precedence, scene persistence, and temporary overrides. For protected creation, start a separate local server on port 4318 with STUDIO_CREATE_KEY=test-only-local-creation-key, then run node scripts/verify-protected-create.mjs. This deliberately synthetic key must never be used on a hosted deployment. See the validation record for the latest local and hosted evidence.
After configuring and deploying the trusted Sandbox snapshot, run the bounded production check explicitly:
node scripts/verify-hosted.mjs \
--base-url https://your-studio.example/ \
--allow-hosted \
--ffprobe /absolute/path/to/an/existing/ffprobe
It creates one synthetic project and at most three jobs (smoke, one 320×240 PNG, and one one-second silent MP4), then verifies authorization, optimistic concurrency, iframe policy, artifact hashes, image dimensions, and video streams. Evidence and the private resume capability are written under ignored .studio-data/; never publish access.private.json. The command neither downloads tools nor deletes remote data. Use --resume with that private file after an interrupted run instead of creating another project.
To wait for a push to go live instead of sleeping, run it first with --wait-for $(git rev-parse HEAD) in place of --ffprobe: it asks GET /api/v1/version every 10 seconds (--wait-interval), creates nothing, returns when the deployed commit matches, and exits 1 with the last answer after 10 minutes (--wait-timeout, in seconds). Chain the verification after it with &&.
With --integration, the same script calls no hosted API: it runs tests/integration inside a clone of the renderer snapshot, on its HeadlessChrome, to reproduce a failure of that browser without downloading it (see Vercel).
dist/ and skills/ are live for every agent session: the global skill links here, and consumer sessions run this dist/. Prove a test with npm run mutate in a worktree (.claude/worktrees/<name>, node_modules linked): it swaps a literal, rebuilds that worktree's dist/ for a src/ file (and from the restored sources before the next mutation outside src/, so a src/ mutant cannot kill it), runs the test with a timeout, counts failures and cancellations, and restores the file byte for byte; in this checkout it refuses src/ and skills/ files. A test under tests/integration starts Chrome, so mutate runs it behind the gate of npm test's browser suites, without their wait: only once the machine check says the machine is free, holding the render slot (taken without waiting) until its last test ends; otherwise it stops before anything runs and, when waiting can end that (the slot held, a busy machine), prints the command that runs it again waiting in the render slot queue, node dist/shared/render-slot.js run -- node scripts/mutate.mjs …, under whose slot it runs (RENDER_SLOT_HELD). A chain of the harness that takes the slot step by step and cedes (hold --ceder, each round of the vault's mutar_copia.py) lets that command, and a browser suite, in between two of its steps: on 2026-10-28 a plan stopped six times in three hours before it said so (#14). Until #15, npm run wait-free and a local Studio client polled the machine check and never saw such a chain's slot free; now they wait in the queue with a product ticket while the check names only the slot, its queue and memory. Without a worktree, mutate a copy, never dist/ or the skill in place: copy dist/ and package.json into a scratch folder, link node_modules, sample-media, and examples beside them, copy the test file to the same relative path, mutate the copy, and run the test there.npm run mutate starts can outlive it: Playwright starts Chrome in a process group of its own, and a mutate killed with SIGKILL (an outer mutate's watchdog, a crash) runs no cleanup. So mutate runs every test and build under a leash, a node process that leads the run's group and reads a pipe from mutate; when the pipe closes, it kills the whole tree read from ps, then the group, and when the run exits, what the run left behind (noted every 0.5 s, since launchd adopts an orphan at once). A process it kills that leads a group of its own, as Chrome or a nested leash does, takes that group along: an orphan adopted there is reached no other way. kills mutate mid-hang and requires a clean within 5 s; its plan is . A test that may die before its names its temporary folders with its pid and sweeps those of dead owners.The CLI guides describe trusted local configuration and its wider limits. Hosted JSON imports accept only the data schemas and stricter limits in the API reference.
tests/unit/mutate.test.mjspstests/unit/mutate.mutations.jsont.aftertests/unit/mutate.test.mjs require launchd to adopt their orphan within 2.5 s, which a machine at load 10 or more on 8 cores misses: on 2026-10-28 they failed in npm test behind a vault mutation chain and passed 3 of 3 alone (npm run test:repeat).npm run mutate read a plan, so a plan whose from the code had moved away from claimed a mutation it no longer tested until someone ran it: a tests/web/held-slot.mutations.json entry stayed stale from a08f5bd until #15 ran it (#20). Now tests/unit/mutation-plans.test.mjs checks every tests/**/*.mutations.json as a run checks its plan before the first swap (checkPlans in scripts/mutate.mjs): each entry's file and test exist and its from occurs exactly occurrences times (default 1), and a failure names the plan, the entry, its from, and the count. A change that edits a mutated line updates the from in the same commit.npm run mutate swaps literals in the checkout's own files and rebuilds its dist/ from each src/ mutant, and nothing stops another command from reading them meanwhile. So run nothing else on src/ or dist/ in that checkout while it runs: npm run typecheck or npm run build beside it can check or build a mutant and still pass, and a build that rewrites dist/ can decide a mutation for the wrong reason. On 2026-11-02 #28 started the three at once: all of them passed, which proved nothing, so they ran again one after another. Run the gate in sequence, or mutate in another worktree.GIT_INDEX_FILE=<file> git read-tree main, git update-index --cacheinfo, git commit-tree, git update-ref) instead of switching this checkout to the branch.#layout reads back from the DOM, such as a field's rect to place a popup, has to be written earlier in the same #layout, never left over from the frame before. Test it by drawing frame 0 and then the frame in question: a test that renders in time order passes either way.stylesheet-readiness.test.mjs already fails on 139. Check a change to the frame runtime or the capture on 139 with verify-hosted --integration (docs/VERCEL.md), and make a timing test assert that its scenario happened. That command uploads this checkout's dist/, so a mutation run through it mutates the live dist/ in place: back the files up first, restore them as soon as the run ends, and say so.<video> in a widget, the sws-sample:clips/neon-road.webm sample included, plays on Chrome's wall clock, not the render's virtual clock: two record runs of the same scene matched in 3 of 150 frames on 2026-10-07. Never assert on a video frame's pixels; assert that the video loaded (readyState) or mask it.sampleFrameAnimations in src/scenarios/runner.ts, pauses every animation that document.getAnimations() returns and sets its currentTime; the virtual clock does not drive the document timeline, which runs CSS animations, transitions, and Web Animations. Two holes follow. A paused animation never finishes, so its finished promise never resolves and no finish event fires, even once the sampler moves it past its end: whatever a widget chains on the end of an animation (an idle loop after the entry, the removal after the exit) never runs in a capture. And between samples the timeline runs on the wall clock, so an animation shorter than the stall between its creation and the next sample ends for real and drops out of getAnimations() before the sampler sees it: the frame shows it already over. A capture harness that copied this sampler for se-jelly-chat on 2026-10-16, a widget the Studio cannot import, never left the entry for the idle loop and lost a 160 ms fade and a 280 ms FLIP in 2 of 28 entries. It closed both holes by calling finish() on an animation sampled past its end and by freezing the timeline with the DevTools protocol's Animation.setPlaybackRate at 0 (it checks that document.timeline.currentTime stands still): with a forced 400 ms stall after each event, the capture fails on the real timeline and, frozen, matches a capture without the stall frame for frame.tests/fixtures/css-transition shows partway in the 14 frames from 333 to 767 ms. But Chrome starts no transition on a property that a CSS animation holds, fill included, so an element that keeps an entrance keyframe with both or forwards never fades: se-wheel's .wrap (animation: widget-in 600ms ease both under transition: opacity 500ms ease) stayed whole through its !results exit and cut when visibility turned hidden, and on 2026-10-20 that cut was taken for a capture bug (SDK-53). element.getAnimations() right after the change shows it: no CSSTransition for that property. tests/integration/css-transition.test.mjs checks both cases, in a video and in Chrome on its own clock.node --import tsx --test also runs its [browser] tests, which open Chrome outside the render slot; pass --test-skip-pattern='^\[browser\] ', as the web suite does.runJob must be named [browser] … even when its job stops before Chrome (tests/unit/run-tests.test.mjs), and npm run mutate skips [browser] tests; test the worker directly, as limits.test.ts does, and mutate that test, and prove what only the runJob test sees by hand.capture, record, or render asks the machine check before each recipe, and a local Studio job before it starts, and the check counts only the caller's own process tree as its work, so two CLI renders that tests start in parallel see each other as another session's render and wait for each other. The test runner gives every suite a MACHINE_CHECK that does not exist, as it gives each its own render slot, and localWorkerEnvironment passes both to a job's worker; outside the runner (node --test on several files, npm run mutate), set MACHINE_CHECK to a missing path, or to a fake check when a test is about the wait.cutLoop()) or in a child (a CLI capture, record, or render) uses the machine-wide slot under npm run mutate and node --test: set RENDER_SLOT_DIR and MACHINE_CHECK in a temporary folder atop the file, removed in after (tests/unit/cut-loop.test.mjs). A child inherits them, and tests/unit/own-render-slot.test.mjs fails a file that starts such a CLI without them. The runner gives every suite a slot of its own; outside it, the file would otherwise queue on the machine's real slot. Its jobs would also land in the real history, ~/.cache/render-slot.historico/, where they look like product jobs from se-dev-kit.npm run mutate shares) and wait-free fall back to their own process list, which must see what the check sees: HEAVY_PATTERNS and WRAPPERS in scripts/lib/machine.mjs are PESADO and EMBRULHO of ~/obsidian/AI/scripts/maquina_livre.py, string for string, and tests/unit/machine.test.mjs fails while they differ (skipped without the vault). Change a pattern in the vault first, then copy it here with its literal cases and a mutation in tests/unit/machine.mutations.json. Until 2026-10-09 the fallback missed ffmpeg and took caffeinate and Claude Code's ugrep for renders (HAR-32).npm test, npm run test:repeat, npm run mutate) refuses while this checkout has an orphan test or job worker: PPID 1, a working directory inside the checkout, and node itself as the executable (isTestWorker and isJobWorker in scripts/lib/machine.mjs read argv[0]). Until 2026-10-28 they read only the command line, node and .test.mjs anywhere in it, so a detached nohup /bin/zsh -c wrapper started inside the checkout counted as an orphan worker: on 2026-10-25 one around render-slot.ts run -- npm run test:repeat -- tests/integration/css-transition.test.mjs 1, adopted by launchd, stopped its own run twice with this checkout left 1 orphan process(es), and the npm run kill-stale that the message suggests would have killed it (#12). tests/unit/machine.test.mjs keeps that wrapper's line.vercel blob list-stores shows Suspended, and vercel api /v1/storage/stores/<store id> shows "status": "limits-exceeded-suspended". Code cannot bring it back: the quota resets or the plan changes. Job polls (every 5 s from studio-client.mjs run, every 2.5 s from the editor) used to list the project's jobs (an advanced operation) and read every job record it ever had; they now call GET /api/studio/projects/<id>/jobs/<jobId>, which reads the project and that one record. The job list, the project view (status, the editor's load), and each artifact download still list and read every job, so never poll them.skills/se-widget-studio/scripts/cut-loop.mjs without --period refuses a chat that repeats exactly. On 2026-10-08, three of four iMessage chats with one message every 1–2 s on a 12 s cycle came out NO-LOOP … uneven cycles. Studio video was then x264 at its defaults (CRF 23), so a keyframe inside a rest reads as a change, a message can land a frame early or late, and every still stretch keeps the compression error of its first frame: the rest after the cycle differed from the rest before by a mean of 1.2–1.7 with the same content. When the period is known, pass it (--period 12000): the loop ends on the frame before the original's own continuation and blends its first 8 frames from that continuation into the first rest, so the wrap is two consecutive frames of the original.cut-loop.mjs --beat takes a change as a beat only between two holds of the beat's length: with a hold only before it, a message that popped in on a beat hid a skipped boil beat at 356 frames of se-crayon-chat's video, and the loop passed with a stutter (#19).cut-loop.mjs encodes a loop as soon as it finds one, in the render slot: check a period with --check, which measures without encoding or writing; before it, on 2026-10-28, a quick check of --period 11867 --beat 4 encoded a 1080p cut outside the slot while mutation chains held their turns in it (#21).~/.claude/skills/se-widget-studio, where argv[1] keeps the linked path and import.meta.url is the real one, so their main guard compares real paths (isEntryPoint() in studio-client.mjs and cut-loop.mjs). Until 2026-10-11 cut-loop.mjs compared resolve(argv[1]) and, run as the skill's docs say, exited 0 without a word or a file. A test of a script's command line runs it through a link (tests/unit/cut-loop.test.mjs); the repo scripts that keep the old guard are in docs/review-followups.md.~/.claude/skills/se-widget-studio, <skill>/../.. is ~/.claude, because node and the shell's cd drop the .. from the text, not from the real path. Sessions' validate failed with "Cannot find module", and on 2026-10-20 a cd there broke git status, the BUILD_ID check, and dist, until they ran realpath (SDK-52). The skill names the checkout <checkout>, what realpath <skill>/../.. prints ($STUDIO_DIR in studio-workflow.md), and tests/unit/skill-checkout-path.test.mjs runs each of those commands through a link and fails on a textual ../.. in a reference.xcrun_db in its temporary folder, with every test green. python3 on this Mac is the Apple shim /usr/bin/python3 (the machine check in src/shared/machine-check.ts runs it), and the shim sometimes writes its lookup cache, xcrun_db, into $TMPDIR: the unit suite did twice on 2026-10-09 under a load average above 300, while a quiet run of the same suite, and TMPDIR=$(mktemp -d) /usr/bin/python3 -c pass, write nothing. Run the suite again once the machine is quiet before hunting for a test that leaks.performance.now() when its script ran) draws them out of phase. Keep motion on the system clock, read as Date.now() - performance.now() once at load plus each frame's performance.now(), and check it as tests/integration/two-copies.test.mjs does: load the copies 700 ms apart and compare both canvases in one task. se-curvetext's two copies differed by a mean of 29.4 per channel before its fix on 2026-10-14 and by 0.0005 after (SDK-51). Until 2026-10-27 this item advised performance.timeOrigin + performance.now(), which Chrome gives too but Playwright's fake clock does not: its timeOrigin stays at the first time the clock was set, while Date.now() follows every setSystemTime. The two Studio captures of a layered setup also came out of step on either expression (#5): the capture installed the clock running and paused it after the host loaded, and every document replays the clock's log counting the real time between entries, so the widget frame's performance.now() started at 36 to 1782 ms instead of 0, which timeOrigin kept and which moved the 16 ms requestAnimationFrame grid (two captures of the fixture differed by a mean of 0.31 on Date.now() and 0.51 on timeOrigin). pauseClockFromStart in src/scenarios/runner.ts now pauses the clock with its first call and asks it in each document's first task, before the clock's own 100 ms real-time timer can add 101 ms; tests/integration/capture-clock.test.mjs checks both, and the two-copies test requires two identical captures on each expression.cat or a full Read) costs a consumer session its context: on 2026-10-15 one read studio-workflow.md (23 KB) whole because SKILL.md linked the bare file. Every link from SKILL.md to references/*.md names a heading anchor, and only a file of 16 KB or less may be linked by its title (whole); tests/unit/skill-sections.test.mjs fails on a bare link, an anchor that is not a heading, or a reference no link reaches, so a new reference or a renamed heading updates SKILL.md too.src/shared/render-slot.ts appends every grant and release to <RENDER_SLOT_DIR>.historico/<UTC day>.jsonl and never removes it (it prunes days older than a week). A test that puts RENDER_SLOT_DIR in a folder it lists, or asserts empty, expects that entry. The runner exempts only the history of the suite's own slot (leftovers in scripts/run-tests.mjs); after the sync, unit failed with every test green because render-slot.historico was left in its temporary folder.tests/integration/css-transition.test.mjs can fail with one frame off: on 2026-11-05 npm test read 247, not 255, at 300 ms, every other frame right, then the file passed 5 of 5 with npm run test:repeat. No cause known yet; repeat it before blaming a change.