Companion to malleability-today.html.
The unification shipped as a four-axis model — runtime × trigger × anchor ×
presentation — projected into a block_extensions registry, with a BehaviorEditor,
runtime:template notes, an on_load trigger, and dead-end gating all live. This pass
reconciles the design with the metal: where the build is richer than the "two settings" framing,
where it's narrower, and the gaps that still hurt the note-taking experience. Surfaces below carry a
shipped planned or gap badge.
trigger/anchor) is shown in 4+ places, none authoritative.show / spawn / insert / nothing — not drawer/callout/chip.runtime:template notes.sidebar/topbar slots — you'd said remove it. Flagged below to settle.block_extensions registry shipped
The hard rule — runtime decides which landings are coherent (shipped as coherentPresentations — the editor never offers an impossible combo)
| the block is a… | show | spawn | insert | nothing |
|---|---|---|---|---|
| code lua / python | ✓ | ✓ | ✓ | ✓ |
| jsx renderer | ✓ | — | — | — |
| template note w/ {vars} | ✓ | ✓ | ✓ | — |
| note plain (none) | ✓ | ✓ | dead-end | — |
Only a runtime executes. A jsx block is a component, so it can only show a live view.
A plain note is passive — it can copy itself into a new block (spawn) but can't reach into
another block to insert; mark it template (the editor's plain↔template toggle) and
it can. This is the "verb from type" idea, now enforced top-to-bottom: the editor greys impossible verbs, CI
(check-no-dead-ends.sh) fails if the gating regresses, and an ops scan finds raw-API rows that
slipped a dead-end past the UI.
Concepts are still presets — now over four axes
Named concepts → their coordinate
The four-axis registry made the empty cells reachable: (event, lua, sidebar) — the
classifier-labeler, once a hook-in-disguise — is now just a coordinate. template + insert is
the instant-template you asked for.
/extensionsblock_extensions registry not built — registry exists, no UI
/drawers and /automation already read the same block_extensions table —
so they become one spatial overview. This page is for seeing what's mounted where,
toggling, and troubleshooting — not authoring. There's no search, no "+ new block", no
drag-to-reorder; a new extension is born as a code block in the notebook and simply appears here.
Click any item to open the editor (§03). To relocate spatially, drag it in the page view (§05).
sidebar (context)
Classifier labeler · ghost ✎ Daily brief · pin ✎slot usage
anchor: null a legitimate place to be, not a confusing absence. Editing happens by
click → editor; moving happens by drag in the page (§05). The map itself
stays calm.
BehaviorEditor.tsx — WHEN/WHERE chips + live sentence + dead-end gating shipped
metadata, rendered inline on the block (the canonical store) or
summoned as a popover when the block isn't on-screen. The block's runtime isn't here — that's
the block's own type. Every choice rewrites the sentence and shows a plain-English note.
The editor — description, live sentence, and a note on every choice
page_create, is an instant template — it fires and inserts itself into the new
page. Two rules come from the block's type: a jsx block always
renders a live view (only show — it's a component, not
text), and code that returns nothing shows nothing — it just did
its work.
A note set to Insert = a template
The four landings, side by side
The editor only offers the landings a block can actually do: a plain note offers spawn, a template adds insert, a jsx block offers show, a value-less script offers nothing. You never pick an impossible combo.
Are the sections independent? Mostly — with one friendly rule
Some triggers produce output (a /command, a tag, a manual run); some just change data quietly
(ingest, on-update). When you pick a quiet one, the WHERE section dims — there's
nothing to place. It does this non-destructively: your "where" choice is remembered, the
section just greys. Switch the trigger back and it returns exactly as it was. So "does setting ingest wipe my
anchor?" — no, it's only suspended.
inline is remembered — pick a showing trigger to bring it back.
Feasibility: trivial — keep all three values always, derive whereActive = triggerRenders(...), grey the section and skip the render when false. No destructive writes.
How the inline form is summoned
Any block whose metadata has a trigger or anchor shows a one-line behavior sentence under its title — that's the affordance. Click the line (or its ▸) to expand the editor in place; the ✎ on the map and right-click → Edit open the same thing. Plain blocks show nothing until you give them behavior — so ordinary notes stay clean.
Collapsed — the behavior line (click to open)
On a plain note block, the same spot reads a faint "+ give this block behavior" on hover — nothing permanent until you do.
Expanded — the drawers, in place
Identical to the popover above — same drawers, same writes. "Show Metadata" just toggles this open instead of dumping JSON.
BlockContextMenu.tsx partial — editor opens, no Pin-to submenu yet
A "show it here" pin is a single choice — one click, perfect for a menu, so Pin to ▸ is a submenu of places. But choosing when something runs needs sub-fields (which event, which filter, the command name) — too much for a submenu. So Edit behavior… spawns the drawers right under the block. No "Fire on…" submenu — picking a trigger lives in the editor, where there's room to explain it.
Right-click a block · "Pin to ▸" opens a submenu
"Edit behavior…" → the editor, inline
So your instinct is the rule: right-click → editor for the part that needs explaining, with one-click "Pin to" places kept as a submenu. Quick when it can be quick, guided when it can't.
Hard to build, worth it. Grab a block by its * gutter handle and every valid WHERE
lights up at once — the topbar/header, the context sidebar, the block's own
title slot, and any tag chip in the body. Drop, a small confirm sets the
anchor (presentation inferred from the target), done. Drag sets WHERE only — it's the one spatial axis; HOW
comes after via the ✎ or the drawers.
anchor: sidebar · presentation: pin
context · sidebar
Classifier labeler
Four target kinds, one gesture, all reversible (drag back out, or toggle off in the map). Dropping on the
:food: chip sets anchor: tag:food and the block becomes that tag's
drawer — anywhere that tag appears. The "Drawers…" button on the confirm is the escape hatch into §03 when you
want to set HOW in the same motion.
The scoreboard
| Piece | State | Note |
|---|---|---|
Four-axis model + block_extensions registry | shipped | registry table + partial indexes, not generated columns |
| BehaviorEditor — chips + live sentence | shipped | our §03, essentially as drawn |
show / spawn / insert / nothing | shipped | our "insert vs show", expanded to four |
runtime: template notes ({vars}) | shipped | the instant-template; plain↔template toggle |
on_load trigger | shipped | page-granular, fired on block load |
| Dead-end gating + CI/ops guards | shipped | coherentPresentations — the matrix in §01 |
| Visual chrome (drawer / callout / chip) | cut | presentation became a verb, not a style |
| Slot-aware positioning | stub | spawn always appends to page tail |
| Lazy / visibility trigger | planned | tag renderers run for every host till then |
| Schedule execution | planned | stored, not run |
JSX = all 39 ui.* primitives | planned | only 5/39 today — forces 2-block splits |
| The mount map (§02) + page-drag (§05) | not built | registry has no spatial UI yet |
The five gaps that actually bite the note-taking experience
show — style: drawer|callout|chip|plain — orthogonal to the verb. Doesn't touch
the axis model.
page_create appends to the bottom of the page, not the
top. Templates that don't land where you'd write feel broken. The anchor's
slot (title/top/body/after) is parsed but not yet mapped to a real position
(TODO(slot-positioning)). tag:project Python renderer with an expensive query executes for every tagged block
on screen at once — no lazy/visibility trigger yet. Tag-drawers on real data will
stutter, which trains people to avoid the best feature. list_hooks is also type-agnostic, so the list is
noisy. block_extensions; it needs no new model.
insert can only hit self / the active block — no "into that block"
picker. And the Run button is still baked chrome, not the title-slot chip we proposed — so
"code blocks declare what actions they expose" didn't land (it depended on the cut chrome layer).
template and note — one kindruntime:template and runtime:none are the same block —
a note, just with {vars} sprinkled in; a plain note is a template with no variables. Collapse
them into one note runtime that always interpolates (verbatim when there are no vars). Then every note can insert, not just spawn — which removes the
note(none)+insert dead-end and a whole row from the coherence matrix. The plain↔template
toggle disappears: you just write, and blanks fill if present. docs/TODO-unify-note-template.md — one code check first: confirm render_template
is a byte-exact no-op on variable-free content.
anchor.approved for scarce
sidebar/topbar slots (to stop the AI/MCP self-promoting into global real estate).
Worth settling: drop it entirely (your call), or keep it only for
AI-authored claims and auto-approve anything you place by hand?