agentsclimarketplace

Xcratch extension palette refresh

Skill xcratch/xcratch-skills/skills/xcratch-extension-palette-refresh

Xcratch extension development skills for AI agents

Install
npx -y skills add xcratch/xcratch-skills --skill xcratch-extension-palette-refresh

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when a scratch-vm / Xcratch extension creates, deletes, or renames a variable or list programmatically (e.g. target.lookupOrCreateList / createVariable / deleteVariable) and the change does NOT appear in the editor block palette until the user re-opens the Code tab or switches sprites. Make sure to use this whenever programmatic changes to the VM variable/list model (or custom My Blocks procedures) are not reflected in the Scratch editor palette right away, or whenever someone asks how to force the Blockly toolbox / variable-list flyout to refresh from extension code. Also covers the companion bug this fix introduces: calling requestBlocksUpdate mid-execution orphans the running script's yellow stack-glow highlight (the outline stays lit until the Code tab is reopened, or disappears while still running). Trigger phrases: list created but not in palette, variable not showing until tab switch, requestBlocksUpdate / emitWorkspaceUpdate did not update the palette, refresh toolbox flyout, force variable category refresh, lookupOrCreateList not visible, deleted variable still in palette, yellow outline stays after block runs, stack glow stuck, script highlight not cleared, ハロー表示が消えない.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

16.8 KB, ~3.6k tokens by cl100k_base, as published. Nobody here has run it

Xcratch Extension: Refresh the Palette After Programmatic Variable/List Creation

When an Xcratch / scratch-vm extension creates a variable or list programmatically — typically target.lookupOrCreateList(id, name) or target.createVariable(...) — the new entry often does not appear in the editor's block palette (the "Variables" category flyout) until the user re-opens the Code tab or switches sprites. This skill explains why that happens and gives a verified, copy-pasteable fix.

The mirror cases — deleting or renaming a variable/list (e.g. target.deleteVariable(id)) — have the exact same cause and the exact same fix: the stale entry lingers, or the old name keeps showing, in the flyout until a refresh. The same symptom also appears for custom procedures ("My Blocks") and any other dynamic toolbox category populated from the workspace at flyout-show time. Throughout this skill, "create" stands in for any of create / delete / rename.

Symptom checklist

Use this skill when all of these are true:

  • Your extension changed the VM model directly (not via vm.loadProject).
  • The change adds, removes, or renames a variable, list, or procedure that should be reflected in the palette.
  • The palette only updates after a Code-tab switch / sprite switch, not immediately.
  • You may have already tried runtime.emitProjectChanged() or runtime.requestBlocksUpdate() and it "did nothing" visible.

Also use it for the companion symptom: after adopting this fix (or any mid-execution requestBlocksUpdate()), the yellow stack-glow outline of the script that ran your block stays lit until the Code tab is reopened — see "The glow race" below.

TL;DR — the fix

Three parts are required (the third compensates for a side effect of the first):

  1. Sync the new variable into the Blockly workspace so the palette has data to show: runtime.requestBlocksUpdate() (the VM turns this into a workspace XML reload).
  2. Force the dynamic flyout to actually re-render — the GUI will not do this on its own for a variable-only change (see "Why" below). Reach the scratch-blocks workspace from the main thread and refresh its toolbox.
  3. Resync the script-glow highlights afterwards. An extension block always runs inside a VM step, so step (1) reloads the workspace mid-execution — destroying and re-creating every Blockly block and racing the VM's SCRIPT_GLOW_ON/SCRIPT_GLOW_OFF events. Without this step, the yellow outline of the running script gets orphaned (stays lit until the Code tab is reopened) or is visually lost while still running. See "The glow race" below.

Drop this helper into your extension and call it right after creating the variable/list:

/**
 * Force the Blockly variable/list flyout to re-render so a just-created list or
 * variable shows up in the palette immediately, without re-opening the Code tab.
 *
 * Best-effort: Xcratch extensions run on the main thread with DOM access, so we
 * reach the scratch-blocks workspace through the rendered DOM + React fiber. If
 * the editor internals differ, the entry still appears on the next natural
 * toolbox refresh (tab switch), so failure here is non-fatal.
 */
const refreshVariablePalette = function () {
    if (typeof document === 'undefined') return;
    try {
        const svg = document.querySelector('svg.blocklyWorkspace') ||
            document.querySelector('.blocklyWorkspace');
        let el = svg && svg.parentElement;
        for (let depth = 0; depth < 20 && el; depth++) {
            const fiberKey = Object.keys(el).find(k =>
                k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$'));
            if (fiberKey) {
                let fiber = el[fiberKey];
                while (fiber) {
                    const ws = fiber.stateNode && fiber.stateNode.workspace;
                    if (ws && typeof ws.refreshToolboxSelection_ === 'function') {
                        // The workspace XML reload (from requestBlocksUpdate) leaves
                        // toolboxRefreshEnabled_ === false, which makes
                        // refreshToolboxSelection_ a no-op. Re-enable it first — the
                        // same thing the GUI's own updateToolbox() does.
                        ws.toolboxRefreshEnabled_ = true;
                        ws.refreshToolboxSelection_();
                        return;
                    }
                    fiber = fiber.return;
                }
            }
            el = el.parentElement;
        }
    } catch (e) {
        // ignore: best-effort only
    }
};

And this pair for step (3) — extract the fiber walk from the helper above into a shared findBlocklyWorkspace() returning ws when both are used:

// Delays (ms) for the post-reload glow resync passes: one right after the
// current VM step (0), one after the script-glow OFF that normally follows a
// finished script by a frame or two (150), and one late safety pass (450).
const GLOW_RESYNC_DELAYS_MS = [0, 150, 450];

/**
 * Re-align the yellow stack-glow outlines with the VM's actual glow
 * bookkeeping (`runtime._scriptGlowsPreviousFrame`): remove glow filters the
 * VM no longer tracks (orphans left by the mid-execution workspace reload),
 * re-apply filters for scripts the VM still considers running.
 */
const resyncScriptGlows = function (runtime) {
    const ws = findBlocklyWorkspace();   // the same DOM + React-fiber walk as above
    if (!ws || !runtime) return;
    try {
        const active = new Set(runtime._scriptGlowsPreviousFrame || []);
        ws.getAllBlocks().forEach(block => {
            if (typeof block.setGlowStack !== 'function') return;
            const svg = block.getSvgRoot && block.getSvgRoot();
            const filterValue = svg && svg.getAttribute && svg.getAttribute('filter');
            if (filterValue && (/stackglow/i).test(filterValue) && !active.has(block.id)) {
                block.setGlowStack(false);
            }
        });
        active.forEach(id => {
            const block = ws.getBlockById(id);
            if (block && typeof block.setGlowStack === 'function') block.setGlowStack(true);
        });
    } catch (e) {
        // best-effort only
    }
};

const scheduleGlowResync = function (runtime) {
    if (typeof document === 'undefined' || typeof setTimeout !== 'function') return;
    GLOW_RESYNC_DELAYS_MS.forEach(delay => {
        setTimeout(() => resyncScriptGlows(runtime), delay);
    });
};

Call site (only when something was actually created — see "Do it once"):

const list = target.lookupVariableByNameAndType(name, 'list') ||
    target.lookupOrCreateList(id, name);

if (/* it was newly created */ target.runtime) {
    target.runtime.emitProjectChanged();          // mark dirty (save prompt)
    if (typeof target.runtime.requestBlocksUpdate === 'function') {
        target.runtime.requestBlocksUpdate();     // (1) sync into Blockly var map
    }
    refreshVariablePalette();                      // (2) re-render the flyout
    scheduleGlowResync(target.runtime);            // (3) fix the orphaned stack glow
}

requestBlocksUpdate() runs synchronously through the GUI's onWorkspaceUpdate handler, so by the time it returns the workspace variable map already contains the new entry. That is why calling refreshVariablePalette() immediately after works.

The same three-step call applies unchanged to deletes and renames — run runtime.requestBlocksUpdate() then refreshVariablePalette() then scheduleGlowResync() right after target.deleteVariable(id) (or the rename) so the removed/renamed entry stops showing the stale value in the flyout.

Why it happens (root cause)

Knowing the mechanism helps you adapt the fix and avoid dead ends:

  1. emitProjectChanged() is not visual. It only flags the project dirty for the "unsaved changes" prompt. It refreshes nothing.

  2. requestBlocksUpdate()emitWorkspaceUpdate() syncs data but not the flyout. It emits BLOCKS_NEED_UPDATE; the VM turns that into emitWorkspaceUpdate(); the GUI's onWorkspaceUpdate reloads the workspace XML (clearWorkspaceAndLoadFromXml). That does sync the new variable into the Blockly workspace variable map and redraws the script canvas — but it does not redraw the palette's variable/list flyout.

  3. The Variables/Lists category is a dynamic toolbox category (<category custom="VARIABLE">). Its contents are generated by a callback that reads the workspace variable map at flyout-show time. Therefore the static toolbox XML string is identical before and after you add a variable.

  4. The GUI's toolbox refresh is diff-gated. In scratch-gui's blocks.jsx, componentDidUpdate only calls updateToolbox() when this.props.toolboxXML !== this._renderedToolboxXML. Since the XML didn't change (step 3), updateToolbox() is skipped and the dynamic flyout is never re-read.

  5. Tab switching works because the visibility-change branch of componentDidUpdate calls refreshWorkspace() + updateToolbox() unconditionally — which is the manual refresh we reproduce in the helper.

So the helper does what the GUI's own updateToolbox() does for this case: set toolboxRefreshEnabled_ = true and call refreshToolboxSelection_() on the live workspace.

The glow race: the running script's yellow highlight gets orphaned

A blocks extension's opcode always executes inside a VM step, so step (1)'s workspace reload happens mid-execution — while the script containing your create/delete block is glowing (yellow stack outline). The reload's clearWorkspaceAndLoadFromXml destroys and re-creates every Blockly block (ids are preserved, glow state is not), racing the VM's glow lifecycle:

  • The VM tracks glowing scripts in runtime._scriptGlowsPreviousFrame and only emits SCRIPT_GLOW_OFF on a diff at the end of each step (runtime._updateGlows). The GUI's onWorkspaceUpdate does not restore glow visuals after a reload.
  • Depending on where the reload lands relative to the GLOW_ON/OFF events, the re-created block ends up with a glow filter the VM no longer tracks — so no OFF ever removes it (observed empirically: reload → GLOW_ON → no GLOW_OFF ever, with _scriptGlowsPreviousFrame ending empty while the SVG filter attribute persists). The user sees the yellow outline stay until the Code tab is reopened.
  • The race also runs the other way: a still-running script's glow can be visually wiped by the reload (the VM thinks it is still glowing, so it never re-emits GLOW_ON).

Because the VM's bookkeeping ends up clean, no future VM event will fix the visuals — the only remedy is to diff the visuals against the VM state and correct both directions, which is exactly what resyncScriptGlows above does (orphan filters → setGlowStack(false), missing filters for tracked scripts → setGlowStack(true)). It matches only filter values containing stackglow, so other SVG filter uses are untouched. The three delayed passes (0/150/450 ms) cover both orderings: the orphan created right after the reload, and the one created when the script finishes a frame or two later.

Gotchas

  • toolboxRefreshEnabled_ must be true. The XML reload from step (1) sets it to false; if you skip the flag, refreshToolboxSelection_() silently no-ops. This is the single most common reason a "refresh" attempt appears to do nothing.

  • Do it once, on creation only. Look the variable up first and create only if missing; refresh only when you actually created it. Refreshing on every block run triggers needless workspace reloads and can disrupt the user mid-interaction.

  • Sprite-local variables only sync for the editing target. emitWorkspaceUpdate() builds the workspace XML from editingTarget.variables. A sprite-local list created on a non-editing target will not show until that sprite is selected — which is the correct behavior anyway (sprite-local lists belong to that sprite's palette).

  • Keep it best-effort. The helper touches scratch-blocks internals (toolboxRefreshEnabled_, refreshToolboxSelection_) and walks React fiber — inherently version-fragile. Guard typeof document and wrap in try/catch so a future editor change degrades to "appears on next tab switch" rather than throwing.

  • Pure block-model edits (scripts) don't need part (2) — but they DO need part (3). Injecting blocks with target.blocks.createBlock(...) / vm.shareBlocksToTarget(...) and then calling emitWorkspaceUpdate() / requestBlocksUpdate() re-renders the script canvas on its own. The extra flyout refresh is only needed for dynamic categories — variables, lists, and custom procedures. The glow resync, however, is needed for any mid-execution requestBlocksUpdate(), block injection included.

  • Lazy per-target setup is a common hidden trigger. If your extension creates its config variables/lists lazily on first use (e.g. an adapter object built inside a running chat-style block), that creation path also runs mid-execution and needs all three steps — the glow orphan then appears on whatever user script happened to call your block.

How to verify

In the running editor (e.g. via the xcratch-extension-debug-auto skill or playwright-cli), reproduce the worst case and assert the flyout updated without a tab switch:

// after grabbing the scratch-blocks workspace `ws` (same fiber walk as the helper)
const flyoutLists = () => ws.getFlyout().getWorkspace().getAllBlocks()
    .filter(b => b.type === 'data_listcontents')
    .map(b => b.getField('LIST') && b.getField('LIST').getText());

ws.toolbox_.selectCategoryById('variables');   // open Variables (stale flyout)
// ... run the extension block that creates the list ...
flyoutLists();                                  // should now include your list name

Pass condition: the new list/variable name appears in flyoutLists() (or in the .blocklyFlyout DOM text) immediately after the creating block runs, with no Code-tab switch and no manual refresh.

For the glow race, exercise the worst case — click-run a stack whose execution creates a new variable (an existing name early-returns and never reloads, silently passing) — and assert no orphan filter remains:

const glowing = () => ws.getAllBlocks()
    .filter(b => b.getSvgRoot().hasAttribute('filter'))
    .map(b => b.type);

vm.runtime.toggleScript(topBlockId, {target, stackClick: true});  // = user click
// poll glowing() at ~150/400/700/1200 ms after the script ends

Pass condition: glowing() is empty once the script has finished (and, for longer scripts, contains the running script's hat while it runs).

Reference: where this lives in the editor source

If you have a scratch-editor checkout and want to read the exact code paths:

  • scratch-gui/src/containers/blocks.jsxonWorkspaceUpdate, componentDidUpdate (the diff gate), updateToolbox() (sets toolboxRefreshEnabled_ = true); also onScriptGlowOn/Off (plain glowStack calls, no state restore after a reload).
  • scratch-gui/src/lib/make-toolbox-xml.js — the custom="VARIABLE" dynamic category.
  • scratch-vm/src/engine/runtime.jsrequestBlocksUpdate() emits BLOCKS_NEED_UPDATE; _updateGlows() (the per-step glow diff against _scriptGlowsPreviousFrame).
  • scratch-vm/src/virtual-machine.js — listens for BLOCKS_NEED_UPDATE and calls emitWorkspaceUpdate().
  • scratch-blocks/core/block_svg.jssetGlowStack() (adds/removes the filter="url(#blocklyStackGlowFilter)" attribute on the block's SVG root); core/workspace_svg.jsglowStack() (throws on unknown block id).

What ships with it: 1 file

4.5 KB alongside SKILL.md

evals/

Keep looking

Skills are one crate of 326,984. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.