TURBOWARP-PLUS-EDITOR · DEVELOPER GUIDE

TurboWarp-Plus-Editor Developer Guide

Three ways to add features to the editor

Four documents that compare Scratch addons, TurboWarp addons and the sockets of TurboWarp-Plus-Editor, specify how Plus builds in its added features, and explain how to build a new one.

In these documents, an added feature is something Plus builds into the editor itself on top of standard TurboWarp, and an extension is a Scratch or TurboWarp extension that a project loads.

Scratch Addons (for Scratch) TurboWarp bundled addons TurboWarp-Plus-Editor
01 · SHORT ARTICLE

Addons reach in from outside; Plus gets called from inside

There are three main ways to add features to a Scratch-family editor. The core difference is which side calls the other: the feature code or the editor.

The official Scratch 3 editor has no mechanism for users to add features. The community browser extension Scratch Addons fills that gap. It injects scripts into scratch.mit.edu pages, then looks for on-screen elements and internal state from the outside and adds features to them.

TurboWarp took the editor-oriented addons from Scratch Addons and bundled them into its own build. The code style and API are nearly the same, but they run as part of the editor instead of as a browser extension.

TurboWarp-Plus-Editor reverses the direction. Its fork of scratch-gui has named sockets that say "use this if something is supplied here", and the build binds Plus's implementation to them. When nothing is supplied, the fork behaves exactly like upstream TurboWarp.

Feature code Editor Addon Lives in a browser extension Official Scratch editor scratch.mit.edu Injected; searches from outside Finds DOM, Redux and VM, adds to them Same build 81 bundled addons Imported from Scratch Addons TurboWarp editor Upstream scratch-gui After startup, same API, from outside Plus implementation npm packages and site/ scratch-gui fork Has named sockets Asks through fixed sockets Bound at build time; empty = upstream
Fig. 1Addons reach from the feature side into the editor. In Plus, the editor calls Plus's implementation, one socket at a time.
Scratch Addons

A browser extension you install. It works not only on the editor of the official Scratch site but also on profile and forum pages.

Install
The user adds it to the browser
Toggle
Per addon, in the browser extension's settings
Access
DOM, Redux and VM, from outside
TurboWarp addons

The editor addons of Scratch Addons, bundled. TurboWarp reimplemented the API and adds six addons of its own, prefixed tw-.

Install
Bundled from the start
Toggle
The /addons settings page
Access
The same API, from outside
TurboWarp-Plus-Editor

Implementations are passed to the fork's named sockets at build time. The implementations themselves live in versioned npm packages.

Install
Built into the bundle
Toggle
URL flags at startup
Access
Called by the GUI

The addon approach doesn't touch the editor itself. In exchange, when the page structure changes, addons silently stop working. The Plus approach leaves call lines in the fork, but its contracts are pinned by types and tests. It can also handle work that must take effect before a project loads, and replace the whole layout.

Plus's main features are the layout, extension review and the save flow. None of them fit the "add from outside" model, so there is little point in Plus aiming for addon compatibility. Small features that can be removed again are written, like addons, so they can be attached later and detached.

The detailed comparison is in 02 Comparison and conclusion, and the list of sockets is in 03 Spec.

02 · COMPARISON AND CONCLUSION

Comparing the three approaches, and why Plus chooses sockets

This compares the mechanism, when each takes effect, how each breaks and how each is toggled. It ends with the advantages of the Plus approach and the reasons for them.

1. Scratch Addons: a browser extension injected into the page

Scratch Addons is a browser extension for Chrome or Firefox. Each addon has its own manifest that declares its name, description, tags, settings, whether it is on by default and which pages it runs on. When you open a matching page, the browser extension injects the addon's userscripts and userstyles into it.

A userscript is an async function that receives ({addon, console, msg}). It waits for elements to appear with addon.tab.waitForElement, obtains the VM and Blockly through traps, and watches Redux state. The official editor has no sockets, so everything has to be found from the outside.

Browser extension Manifests + scripts Settings page The user toggles On/off, values A scratch.mit.edu page (official editor) userscript ({addon, msg}) injects DOM (CSS classes) Redux store VM · Blockly waitForElement watch/dispatch traps
Fig. 2With Scratch Addons, the official editor knows nothing. Userscripts find on-screen elements and internal state from the outside and add to them.

2. TurboWarp addons: bundled, running on the same API

TurboWarp pulls the editor addons of Scratch Addons into its own source with a script called pull.js. It applies TurboWarp's patches to the upstream code (the tw branch of TurboWarp/addons), rewrites it further with regular expressions, and places the result in src/addons/. The current fork contains 81 addons, six of which are TurboWarp's own tw- addons.

At run time, api.js recreates the addon object of Scratch Addons and passes it in. APIs that only exist in a browser extension, such as login information, are absent. In their place there are spots tailored to TurboWarp's screen, such as the shared spaces of appendToSharedSpace (nine of them: stageHeader, fullscreenStageHeader, afterGreenFlag, afterStopButton, afterSoundTab, monitor, paintEditorZoomControls and two in the asset context menu). mediarecorder, which adds a "Record" button to the menu bar, also uses this mechanism: it looks for the element next to the File menu and inserts itself there.

Build time Scratch Addons Upstream TurboWarp/addons tw branch pull.js addons/ generated/ 81 addons + list patches clone rewrite Run time (after the GUI starts) Settings page /addons Runner entry.js · api.js userscript toggle passes addon reads the list DOM · shared spaces Redux store VM · Blockly
Fig. 3In TurboWarp, importing happens at build time and connecting at run time. The connection still works like Scratch Addons, by searching from the outside.

3. TurboWarp-Plus-Editor: bound to sockets at build time

Plus does not add a single file to its scratch-gui fork that upstream doesn't have. It puts getHostIntegration() in src/addons/hooks.js, a file upstream also has, and 28 files other than hooks.js ask for values by name, as in getHostIntegration()?.EditorLayout.

In the distributed build, webpack's ProvidePlugin binds __TW_GUI_HOST__ to this repository's site/gui-integration/index.ts. From there it connects to the integration object in site/plus-integration.ts and to the versioned @kubohiroya/turbowarp-* npm packages. When the GUI is built on its own, __TW_GUI_HOST__ doesn't exist, the getter returns null, and the GUI behaves like upstream.

turbowarp-plus-editor (this repository) and npm packages npm packages @kubohiroya/turbowarp-* Integration object site/plus-integration.ts Host bridge site/gui-integration/ import import webpack ProvidePlugin __TW_GUI_HOST__ binds scratch-gui fork (pinned by SHA; no files that upstream lacks) Call sites in 28 files getHostIntegration()?.X addons/hooks.js null if absent supplies the value asks by name
Fig. 4In Plus, each call site in the fork names a socket and receives the value supplied at build time. When there is no value, it falls back to upstream behavior.

4. When each takes effect

The three approaches differ in when a feature starts working. Addons attach asynchronously after the page or GUI starts running, so they are a poor fit for work that must be in effect before a project loads. Plus can pass such work, like extension review, synchronously through a socket, so it is settled before loading.

Build Page load GUI render Project load Editing Scratch Addons TurboWarp addons Plus Injected; attaches once elements appear User toggles Bundled Runs asynchronously after startup User toggles Bound at build; read synchronously while rendering Review applies before load
Fig. 5Addons attach later and users can toggle them. Plus is bound from the start, so it can also hold work that must take effect before a project loads.

5. How each breaks when upstream changes

Addons look for CSS module class names by prefix (scratchClass, [class*='menu-bar_file-group'] and so on). When upstream changes the page structure, the search fails and nothing happens. Plus's sockets are calls written into the fork's source, so a clash with an upstream change stops at the merge conflict, the type check or the integration tests.

Addons Upstream changes the menu bar structure The selector stops matching The button just vanishes No error, no warning Plus Upstream changes menu-bar.jsx The merge conflicts with the call line Stopped before it lands Type check · integration · E2E
Fig. 6Addons silently stop working. With Plus, you notice when you update the fork.

6. Side-by-side comparison

AspectScratch AddonsTurboWarp addonsTurboWarp-Plus-Editor
DistributionBrowser extensionBundled in the editor buildnpm packages built into the bundle
Unit of a featureManifest + userscripts/userstylesThe same (converted to _manifest_entry.js)Package functions and components, plus entries in the integration object
Editor changesNoneOnly the runner (src/addons/)Call lines in 28 fork files
When it connectsRun time (injection)Run time (after the GUI starts)Bound at build time, read synchronously while rendering
Connection pointsDOM, Redux, VM and Blockly, from outsideThe same, plus 9 shared spacesNamed sockets (React components, reducer fragments, services)
What it can doAdditions, partial replacementThe sameAdditions, plus replacing the whole layout, review before load, the save flow
TogglingBy the user, in the settings pageBy the user, in /addonsURL flags at startup (?twPlus…=1)
When upstream changesSilently stops workingThe sameStopped by merge, type check and tests
Explicit contractDeveloper docsNone (the code is the spec)Typed sockets and unit tests
Sharing with playersNoneNoneThe same packages also work in Packager's single-HTML output

7. Can Plus's added features take the addon form?

Plot Plus's added features on two axes, "adds or replaces" and "the user may turn it off, or it must always be on". Only the top-left area can take the addon form.

Adds ←   → Replaces User may turn off Always on A Can take the addon form B Only look and toggling align C Not aligned (stays a socket) Context menu icons Workspace minimap Palette delete preview Script transfer menu Project-wide search Stage controls Horizontal block palette Code in separate windows Sprite list enhancements Fixed at startup; aligns if "reload required" Extension review, trust list Project state, modal reducers Packager target Whole layout via Dockview Stage panel, sprite list File menu and save flow Applies before load · must not be turned off · replaces structure
Fig. 7Only area A, small additive features, can take the addon form. Area C, at the center of Plus, lies outside the addon model.

8. Conclusion: why the Plus approach is better

For TurboWarp-Plus-Editor to add features to TurboWarp, the current approach of placing named sockets in the fork and binding implementations at build time suits it better than the addon approach. There are six reasons.

  1. It can take effect before the projectExtension review and the list of trusted extensions must be settled before a project loads. Addons attach asynchronously after startup and can't guarantee that order. Sockets are read synchronously at render time.
  2. It can replace whole structuresThe Dockview layout, stage panel, sprite list and save flow are not additions to the existing screen. Doing them as addons would mean hiding upstream's DOM and rebuilding it. A socket can take a React component or a reducer fragment as is.
  3. The contract is visible and testableAddons connect through hashed class names and on-screen text. Plus's sockets have fixed names and arguments, which test/plus-integration.test.ts and test:gui-integration can pin down. When upstream changes, tests stop the change instead of letting it break silently.
  4. The diff from upstream stays smallEvery socket is optional; with nothing passed in, the fork behaves like upstream. Only call lines remain in the fork, and it has zero files that upstream lacks. When upstream updates are merged, conflicts are limited to those call lines.
  5. Implementations are reusable, versioned partsThe implementations live in @kubohiroya/turbowarp-* npm packages, each with its own versions, tests and releases. The same packages work in the single-HTML player that Packager produces. Addons only run inside their editor.
  6. It connects to runtime extensionsSome sockets are exposed to extensions as Scratch.gui.stageControls and Scratch.gui.blockEditors, so extensions such as the camera can register their own controls in the Editor's control bar. The addon API was not designed to be used from extensions.
COSTS WE ACCEPT

There is no screen where users toggle individual features. The fork's call lines must be maintained with every upstream update, and adding a feature requires a rebuild. Third parties cannot add features at run time either. Features that aren't part of the editor itself, however, can be added as runtime extensions through the control bar API.

That said, small features in area A of Fig. 7 are written like addons: "attached later, removed with dispose". That makes them easy to move to the addon form if we ever want users to toggle them. We will not rebuild existing features to be addon-compatible.

Sources: the fork's src/addons/ (81 addons, 6 of them tw-), appendToSharedSpace in api.js, docs/plus-integration-sockets.md, docs/gui-integration/README.md, site/plus-integration.ts. Counts were taken on 14 November 2026 from the fork pinned by this repository (guiRevision in site/source.json, f1e8f96). Recount them when the fork is updated.

03 · SPECIFICATION

How TurboWarp-Plus-Editor builds in added features

This defines how Plus's added features reach the forked scratch-gui, the list of sockets, the rules they follow, startup flags and the API exposed to runtime extensions.

1. Three repositories and the direction of dependency

Plus's added features span three repositories. Dependencies go one way; upstream knows nothing about downstream.

turbowarp-plus-packages npm · MPL-2.0 and others · The implementations · Unit tests · Versions, releases (v tags) · Unaware of downstream turbowarp-plus-editor GPL · this repository · Wiring code in site/ · Startup flags in config/ · Build config, deployment · Integration tests, E2E scratch-gui fork GPL · pinned by SHA · Only files upstream has · Lines that call sockets · Small generic fixes · No custom code added imports npm releases pins, supplies values / asks via sockets Pages · player · Packager
Fig. 8Implementations live in the packages, wiring and deployment in the Editor, and only the socket call lines in the fork. The packages never import GPL code, so the direction is fixed: the GPL side calls the packages' sockets.

2. Binding at build time

The fork's src/addons/hooks.js, a file upstream also has, contains this getter.

/* global __TW_GUI_HOST__ */
// The distributing build supplies this module; standalone GUI builds have no extra host.
export const getHostIntegration = () =>
    (typeof __TW_GUI_HOST__ === 'undefined' ? null : __TW_GUI_HOST__);

The distribution build's scripts/site/webpack.config.cts uses ProvidePlugin to bind __TW_GUI_HOST__ to site/gui-integration/index.ts. Nothing depends on assigning a global at run time or on startup order. The values the fork reads come in two layers.

Fork call sites ?.EditorLayout ?.renderFileMenu(props) ?.hostModalNames( ?.hostIntegration()) site/gui-integration/index.ts Direct values Components, functions host* helpers tw-plus-integration-points.ts site/plus-integration.ts Integration object modals translations attachToWorkspace stageControls securityReview … plusIntegration() hostIntegration() If neither exists, returns null or an empty default ([], {}, null); the fork falls back to upstream
Fig. 9UI parts are read directly from the bridge; settings and services are read from the integration object through helpers. The helpers decide the "absent" default in one place.

3. List of sockets

The sockets the fork refers to, grouped by purpose. The left column is the name the fork writes after getHostIntegration()?.; the right column is the entry in the integration object. menu-bar.jsx and extension-library.jsx first store getHostIntegration() in a variable named host, so extract the current list by combining the two commands below in a checkout of the fork. hostIntegration is not a socket; it is the getter that hands the integration object to the host… helpers.

The table was extracted on 14 November 2026 from the pinned fork (f1e8f96).

grep -rhoE "getHostIntegration\(\)\?\.[A-Za-z]+" src | sort | uniq -c
grep -hoE "host\??\.[A-Za-z]+" src/components/menu-bar/menu-bar.jsx src/containers/extension-library.jsx | sort -u
PurposeName the fork readsIntegration object entryMain callers
Screen structureEditorLayout StagePanel StageDisplayMenu TargetListLayout TargetListPanel SpriteGrid BackdropTile BackdropListActions BackpackSurface(direct values)gui.jsx and others
Sprites and stagewrapSpriteInfo spriteListClasses useSpriteCodeWindows attachSpriteCodeWorkspace attachStageInput attachStageMonitorDrag(direct values)sprite-info.jsx, stage.jsx and others
MenusrenderFileMenu createFileMenuItem extensionInFileMenu renderProjectSearch ProjectInfoMenu renderStageControls(direct values)menu-bar.jsx, controls.jsx
Workspace and blockscreateWorkspaceAttachment registerHostBlockShapes createBlocksWrapper getBlockEditorService attachHostScriptTransferattachToWorkspace registerBlockShapes blockEditor attachWorkspaceMinimapgui.jsx, blocks.js, blocks.jsx
StatehostModalNames hostModalDefaults createCancelSecurityLoad recoverProjectLoad loadedProjectTitle restoreProjectTitlemodalsreducers/, vm-manager-hoc.jsx
Extension loading and reviewinstallSecurityHost getPlusReviewedMode createPreviewVm hostLibraryTabinstallExtensionHost securityReview trustedExtensions libraryTab extensionLoadErrorstw-security-manager.jsx, extension-library.jsx
Input and translationhostIgnoresKeyEvent hostTranslationsForshouldIgnoreKeyEvent translationsvm-listener-hoc.jsx, tw-translations/
Touchpoints with addonshostAwaitedAddonIds finalizeHostTransferMenu hostStageTransferMenuawaitStartupAddons scriptTransferaddons/api.js
PackagerhostProjectPackagerprojectPackagertw-packager-integration-hoc.jsx
API for extensionshostStageControls getBlockEditorService createPreviewVmstageControls blockEditortw-extension-gui-api.js

4. Socket rules

Every socket follows these rules, and so must any new socket.

  1. Optional. With no value, the fork behaves like upstream TurboWarp. Helpers return an empty default ([], {}, null).
  2. GUI internals arrive as arguments. The host never imports the fork's reducers or UI. The fork passes Modal, the lazily loaded ScratchBlocks, the logger and Redux action creators at call time (for example createCancelSecurityLoad(initialState, defaultProjectId, LoadingState, integration)).
  3. Anything attached can be detached. A socket that attaches something returns {dispose()}. Each time the VM changes, the fork calls dispose() on the previous return value before calling again, and never looks inside it.
  4. One service per VM. A service read from two places, like the control bar, is kept single with a WeakMap keyed by the VM.
  5. No CSS sockets. Upstream's CSS module class names are hashed at build time and can't be targeted from outside. New parts keep their own classes and styles in the package and use the GUI's public theme variables for color.
  6. No custom files in the fork. Only socket call lines stay in the fork. Tests aren't added to the fork either; they go in this repository's test/ and in the packages.

5. Attaching to the workspace

attachToWorkspace is the socket that attaches features to Blockly and the VM in one go. It is the closest thing to an addon userscript.

Fork (gui.jsx) Integration object Packages attachToWorkspace(context) Calls attach functions in order each returns {dispose} Combined {dispose} The VM changes dispose() Detaches in reverse; continues on failure attachToWorkspace(new context)
Fig. 10The fork owns the order of attaching and detaching; the integration object decides what to attach.
Context fieldMeaning
vmThe current VM. Called again each time it changes
localeDisplay language (ja, en and so on)
documentThe editor's document. Don't use window.document directly, so separate windows also work
getBlocklyA function returning ScratchBlocks. Waits until it is loaded
getMessagesA function returning translations for the current language

One-time registration once ScratchBlocks is available (block shapes, context menus, palette additions) goes in registerBlockShapes(ScratchBlocks). When you need to wait for an addon to start, list its id in awaitStartupAddons (currently editor-searchable-dropdowns).

6. API exposed to runtime extensions

Unsandboxed extensions can read the following from Scratch.gui. In addition to upstream's getBlockly and getBlocklyEagerly, these fields appear only when the host supplies a value. Otherwise they stay undefined, so an extension can tell that "this build doesn't have it".

FieldMeaning
Scratch.gui.stageControlsAn API for adding buttons to the stage control bar. addButton({id, label, text?, icon?, onClick}) returns {update, focus, dispose}
Scratch.gui.blockEditorsA service for editing blocks through forms
Scratch.gui.createPreviewVm()Creates a preview VM independent of the project
Unsandboxed extension Loaded by the project Extension using app-shell e.g. camera-source controls dock installControlsDock Symbol.for(…/v1) Control bar service One per VM WeakMap Scratch.gui.stageControls.addButton() service.api End of the menu bar renderStageControls subscribes Cleared on PROJECT_LOADED
Fig. 11Either path registers into the same service, and the buttons line up in the Editor's menu bar. The Editor itself knows nothing about individual extensions.

Buttons added with addButton start with visible: false. Call update({visible: true}) to show them. update can change four things: label, text, visible and expanded. To show an element when the button is expanded, hand it to the host with the service's placePanel(element). The registering side owns the element; the host only mounts it.

7. Startup flags

Whether a feature is on is decided once at startup by the page's twPlus… URL query. config/web-startup-feature-flags.ts reads the URL, and readStartupFeatureFlags in config/feature-flags.ts returns a frozen object. These values must never come from project data.

?twPlusXxx=1 Page URL readWebStartup FeatureFlags readStartup Returns frozen Decides what to pass site/* Never read from project data (SB3)
Fig. 12Flags are read once from the URL at startup and never change afterwards.
DefaultFlags (twPlus prefix omitted)
On (=0 turns off)ExtensionLoader ProjectTools PayloadFormats Splitter StandardLayout (web default) PaletteDeletePreview PaletteCategoryReorder ContextMenuIcons LocalGit WorkspaceMinimap StageCodePrewarm
Off (=1 turns on)SpriteProperties WindowLayout HorizontalBlockPalette StageDisplayMenu (on with StandardLayout)
Needs another flag (on by default)ManualProjectURI (needs ExtensionLoader); ExtensionFileMenu (needs StandardLayout)
Needs another flag (=1 turns on)GitRestoreVersion (needs LocalGit); BackpackPopout InternalWindows (need WindowLayout); StagePanel TargetList SpriteCodeWindows (need WindowLayout and InternalWindows)

8. Relationship with bundled addons

  • Default enablement: site/addon-manifests.ts replaces the fork's generated/addon-manifests through a webpack alias. It enables addons tagged "recommended" plus hide-delete-button and block-palette-icons by default, and keeps hide-flyout off. Choices the user makes in the settings page take precedence.
  • Startup wait: For addons listed in awaitStartupAddons, the runner waits for them to start before moving on.
  • Context menus: createBlockContextMenu in api.js passes items through finalizeHostTransferMenu before finalizing them. This is where the script transfer menu is added.

9. Verification

CommandWhat it checks
npm testUnit tests for this repository and packages/. test/plus-integration.test.ts pins each entry of the integration object
E2E_GUI_ROOT=… npm run test:gui-integrationThe pinned fork has no files upstream lacks; socket semantics; relocated tests
npm run typecheckType checking
E2E_GUI_ROOT=… npm run build:pagesThe distribution build. revision and siteRevision in build-info.json must match the current HEAD and site/source.json
npm run test:e2eIntegration E2E with Playwright
E2E_GUI_ROOT

The absolute path of a checkout of the pinned fork. As in CI, the usual place is .host/scratch-gui. The checkout's HEAD must match guiRevision in site/source.json; if it doesn't, test:gui-integration stops and names both revisions.

04 · BUILDING A FEATURE

How to build a new added feature

The steps from deciding where a feature belongs to implementing it in a package, wiring it in the Editor, verifying and shipping it.

1. Decide where it goes

First, decide which path the feature takes to reach the editor. Walk the chart below from the top; the first "yes" is where it goes.

Feature to build Runs in the project (blocks, I/O)? No Reachable via an existing socket? No Attaches to Blockly /VM and detaches? No D Open a new socket One call line in the fork + a host helper A Build a runtime extension Controls: Scratch.gui.stageControls / app-shell Yes B Package + wiring in site/ No change to the fork Yes C Attach inside attachToWorkspace Returns {dispose}; no change to the fork Yes
Fig. 13A to C need no fork changes. Only D changes the fork, so first make sure A to C can't reach it.

2. Order of work

Publish the implementation from the packages repository first; the Editor upgrades to that npm release and then wires it in. When a new socket is needed, work on the fork change in parallel and pin its SHA in the Editor.

packages fork (D only) plus-editor ① Implement and unit-test ② bump the version, publish via v tag ①' PR adding socket call lines No custom files or tests ③ Raise version, SHA ④ Wire in site/ ⑤ Verify, open a PR npm release SHA in source.json
Fig. 14Work flows one way, from upstream to downstream. Editor changes start by using a published version.

3. Steps

  1. Decide the placement and the socket

    Use Fig. 13 to decide which of A to D applies, and pick the socket from the socket list in the spec. Also decide now which package the implementation goes into. Existing code in the Editor's packages/ is mid-migration; don't use it as a home for new features.

  2. Implement it in a package

    Implement it in the relevant package of turbowarp-plus-packages (or a new package if none fits). Don't import GPL code; receive everything from the editor as arguments. Features that attach something return {dispose}.

    // Package side: receive the document and translations, return a way to detach
    export const attachSpriteBadges = (hostDocument, {vm, getMessages}) => {
        const onTargetsUpdate = () => render(hostDocument, vm, getMessages());
        vm.on('targetsUpdate', onTargetsUpdate);
        return {
            dispose () {
                vm.removeListener('targetsUpdate', onTargetsUpdate);
                clear(hostDocument);
            }
        };
    };

    Keep styles inside the package under your own classes, and use the GUI's public theme variables for color. Put unit tests in the package too.

  3. Raise the version and publish to npm

    In turbowarp-plus-packages, decide the version with npm run bump (from upstream) or npm run bump:downstream (to propagate downstream), and publish to npm with a v<version> tag. Don't bundle tgz files into the Editor.

  4. (D only) Open a socket in the fork

    Add a line that calls the socket inside a file upstream also has. Write it so that with no value, it behaves like upstream.

    // scratch-gui fork: lines of this shape are the only additions to existing files
    const SpriteBadges = getHostIntegration()?.SpriteBadges || null;
    // ...
    {SpriteBadges ? <SpriteBadges vm={vm} /> : null}

    Don't add new files or tests to the fork. Cover the socket's semantics with tests on the Editor side. If the value should be read as an entry of the integration object, add a host… helper that returns a default to site/gui-integration/lib/tw-plus-integration-points.ts.

  5. Raise the version and SHA in the Editor

    Upgrade package.json and package-lock.json to the npm release. For D, replace the fork SHA in site/source.json with the full 40-character SHA of the commit that opened the socket.

  6. Wire it in site/

    For C, add one line to attachToWorkspace in site/plus-integration.ts.

    attached.push(attachSpriteBadges(hostDocument, {vm, getMessages}));

    For B and D, when passing a UI part, export it from site/gui-integration/index.ts. To toggle it with a startup flag, add one twPlus… flag each to config/feature-flags.ts and config/web-startup-feature-flags.ts, and pass nothing when the flag is off.

    // site/gui-integration/index.ts
    export const SpriteBadges = startup.spriteBadges ? SpriteBadgesView : undefined;
  7. For A: register controls from the extension

    A runtime extension can use the control bar without knowing the Editor's internals. In builds without Scratch.gui.stageControls, it should do nothing.

    const controls = Scratch.gui?.stageControls;
    if (controls) {
        const button = controls.addButton({
            id: 'my-camera.calibrate', label: 'Calibrate camera', icon: '◎',
            onClick: () => startCalibration()
        });
        button.update({visible: true}); // hidden right after adding
    }

    Extensions distributed as packages can use the control bar of @kubohiroya/turbowarp-app-shell, so the same controls also appear in the single-HTML player, which has no toolbar.

  8. Verify

    Pass npm test, npm run typecheck and npm run test:gui-integration, then rebuild with E2E_GUI_ROOT=… npm run build:pages before checking the screen (see Verification in the spec for E2E_GUI_ROOT). Don't use stale output left in scratch-gui/build. Check that revision and siteRevision in build-info.json match the current state.

  9. Open a PR

    Open a PR that describes the change and what you checked, and merge it after looking at the CI results.

4. UI conventions

Dialogs and panels shown by added features follow these conventions.

  • Action buttons at the bottom: group them at the bottom right, with Cancel to the left of the primary action such as Save or Apply. Use the shared tw-plus-dialog-actions and set each button's data-dialog-action to cancel, close, confirm or delete. Keep the text labels and pair them with an X for Cancel and Close, a check for Save and confirmation, or a trash can for Delete.
  • Close button next to the title: the close button at the top right of a panel or dialog shows no "Close" text, only an X icon. Name it with a translated title and aria-label.
  • Strings: displayed strings have ja and en translations (plus ja-hira where hiragana display is needed).

5. Before you ship

  • The implementation lives in a package, and Editor changes are limited to version bumps and a few wiring lines
  • Everything attached comes off with dispose(), and nothing is attached twice when the VM changes
  • With no value passed in, the fork behaves like upstream TurboWarp
  • GUI internals arrive as arguments; no fork modules are imported
  • Displayed strings have ja and en translations (plus ja-hira where hiragana display is needed)
  • Startup flags are read only from the URL, never from project data
  • Unit tests are in the package; socket tests are in the Editor's test/
  • You checked the screen with freshly rebuilt output

Don't

  • Add new files, custom tests, or CSS that targets upstream class names to the fork
  • Find and rewrite upstream's DOM by class name (that is the addon approach; use a socket if one exists)
  • Import GPL code (the fork, or the Editor's site/) from a package
  • Bundle packages into the Editor as tgz files

6. Notes for maintainers of this repository

FOR MAINTAINERS AND AI AGENTS

These are working rules for people (including AI agents) who change turbowarp-plus-editor directly. AGENTS.md is the source of truth.

  • Don't use --auto on PRs. PRs in this repository have no required checks, so --auto merges at once without waiting for CI
  • Don't run E2E tests that move the OS mouse cursor (TW_PLUS_NATIVE_INPUT=1, test:e2e:native) unless explicitly asked to. A general request to run E2E or to verify a PR doesn't count

Related documents: docs/plus-integration-sockets.md (history of the socket design), docs/gui-integration/README.md (relocation and build-time wiring), docs/architecture.md (responsibilities and deployment).