Skip to content

fix(3ds): the runtime fixes Pocket Shell found — adjacent-swap reconcile, the guest's stack budget, a pinned emulator clock - #352

Draft
doodlewind wants to merge 4 commits into
mainfrom
feat/pocket-shell-3ds
Draft

fix(3ds): the runtime fixes Pocket Shell found — adjacent-swap reconcile, the guest's stack budget, a pinned emulator clock#352
doodlewind wants to merge 4 commits into
mainfrom
feat/pocket-shell-3ds

Conversation

@doodlewind

@doodlewind doodlewind commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

What

Pocket Shell now lives in its own repository — pocket-stack/pocket-shell, with this checkout as its vendor/pocketjs submodule, the same split Pocket Term took. This branch keeps only what belongs to the runtime.

Three fixes, each found by running a real UI on the console rather than a demo:

  • framework/src/native-tree.tsinsertBefore(node, node) is a no-op, like the DOM. Solid's reconcileArrays issues exactly that call when two adjacent <For> items swap; the old code unlinked the node first, so it became its own missing anchor and threw insert anchor is not a child of parent. Every adjacent swap crashed. Any app that reorders a <For> by one position hits this; tests/renderer.test.ts pins the case.
  • hosts/3ds/src/qjs.c — the guest's JS stack is 384 KiB, not 192. The console spends it on JSX nesting depth, not node count: a QuickJS call frame is expensive and mounting descends the tree. A UI four levels deeper than a demo overflowed it mid-frame, after which the runtime's recovery chain fell back to last-good — which on a shared SD card looks exactly like a different app booting. This is the same budget the Pocket Term work independently needed.
  • tests/e2e/azahar.ts — the fixture pins the emulated clock (init_clock=1, init_time = 2000-01-01), so a guest that shows the RTC can still be compared byte for byte. DEFAULT_SPEC_NAMES is back to the demo.

Verified

  • bunx tsc --noEmit clean; tests/renderer.test.ts 51/51 and tests/platform-contracts.test.ts 37/37 green after the removal.
  • The extracted app, built out-of-tree against this commit, reproduces all 26 pinned frames byte for byte against the recordings made while it still lived here — so the move changed nothing about what the console draws.

The app, its tests, its input tapes and those frames now live in the product repository, along with a frame recorder that turns the same tapes into the animations its README shows.

🤖 Generated with Claude Code

doodlewind and others added 4 commits September 1, 2026 13:04
Omarchy binds every window action to SUPER plus one key, and SUPER + K shows
the table. Pocket Shell keeps that grammar and changes the hardware: L is
super, R is shift, L+R addresses workspaces, and the chord map for whichever
shoulder is down replaces the minimap on the touch screen — the reference
appears exactly when the modifier that needs it is pressed, and disappears
when it is released.

The top screen is the stage: Omarchy's tokyo-night wallpaper under windows
tiled by dwindle (a binary split tree, longer side first, splits preserved
across a close) or scrolling (columns at 0.49 of the width on a strip that
pans to keep the focused column whole), five workspaces, a 14 px bar. The
touch screen is the deck: workspace strip, live minimap, dock, launcher, and
a hand-laid keyboard. Windows hold local applets — pocketsh, a clock, notes,
the key sheet, stats, about — so nothing needs a companion.

Touch does what the buttons do. Tap the minimap to focus, drag a window onto
another to swap or onto a tab to move it, drag the gap between two windows to
move that split. Closing is a hold, a slide and a release onto a bar, the
Pocket Term convention: the panel is resistive and single-contact, and a small
× is how a shell gets killed by accident.

wm.ts, chords.ts and shell.ts are pure and unit-tested; the render layer
replays the golden tape headlessly in the sim, which is what caught the
renderer bug below in 30 ms instead of an emulator boot. Ten Azahar goldens
cover both screens and reproduce byte-exactly across runs.

Also here, because the app was the first thing to need them:

- framework: insertBefore(node, node) is a no-op, as in the DOM. Solid's
  reconcileArrays issues exactly that call when two adjacent <For> items swap,
  and unlinking first left the node as its own missing anchor — every
  adjacent window swap threw. Covered in tests/renderer.test.ts.
- e2e: the Azahar fixture pins the emulated clock so a guest that shows the
  time stays byte-exact, and the default 3DS spec set now runs both apps.

Nothing is bound to ZL/ZR: they are New-3DS-only, arrive through ir:rst rather
than the HID pad, and have no BTN constant in the spec yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y applet in a golden

Opening one `keys` window on hardware took the whole guest down: the runtime
rolled back to last-good, which on a card that also holds Pocket Term is
Pocket Term, so the shell appeared to turn into another app and stay there.
The guest had hit `InternalError: stack overflow`.

The 3DS spends its JS stack on JSX NESTING DEPTH, not node count — a QuickJS
call frame is expensive and mounting descends the tree — and an applet sits
under stage, windows, chrome and content before it draws anything. 192 KiB
did not cover a windowed applet whose rows each carried a wrapper view with a
Show inside. The sibling Pocket Term work already found 384 KiB; the main
thread's stack is 1 MiB and the limit costs nothing until it is used.

Two changes, because either alone leaves a trap:

- hosts/3ds/src/qjs.c raises the budget to 384 KiB. This is the root cause,
  and it needs a reflash to reach a card already carrying the old binary.
- Keys and Stats render each column as a flat pass of absolutely-positioned
  Text under the applet root — a row is an offset, not a node — which is what
  lets the fix reach an unflashed console over the dev wire. The README now
  states the budget so the next applet starts the right shape.

The first tape never opened these applets, so three of the six reached a
device for the first time in Evan's hands. `pocket-shell-applets` taps every
dock cell and is in the default spec set; it reproduces the crash in ~90 s
against the old budget, which is how this was diagnosed rather than guessed.
An emulator with a generous stack would not have warned us, so the tape is
the guard, not the sim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ress

Four things Evan found on the console, three of them real defects.

**The clock flickered.** Not a rendering fault: two wrong times alternating
frame to frame. The RTC's epoch is sound — Date.now() is monotonic and
correct on hardware — but the console's breakdown of it is not, and
getHours() intermittently disagreed with the epoch by whole hours, applying
the timezone on some reads and not others. Measured over the dev wire, the
big time text changed by ~1600 px between consecutive frames; it now changes
by zero. Nothing calls a Date breakdown method any more: civilFromEpoch
derives the civil date and time by arithmetic, tested across leap years, the
1900/2100 century rules and the exact reading the device produced. The zone
is sampled once at boot and kept only if it looks like a real one, so a bad
read cannot be frozen in — and because this console reports none, pocketsh
gained `tz +8` to state the offset it could not.

This also explains the one golden frame that read 06:47 among neighbours
reading 00:00. It reproduced byte-exactly, so I let it stand; it was the same
bug, visible and dismissed. A capture that reproduces is not thereby correct.

**The term icon drew a tofu box** — `❯` is not in the baked face, so a Text
rendered the placeholder. It is pixels now. Rotation was the obvious
alternative and would have been worse: `-rotate-45` is not a utility this
compiler accepts, and an unknown token makes the whole class literal unknown,
which paints nothing at all.

**The keys icon read as a row of dots.** Two flex rows in a container with no
`flex-col` — the default direction here is row, so they sat side by side. It
is a keyboard now, placed absolutely.

**Painted buttons say nothing when pressed.** A physical button reports
itself; on a resistive panel with no hover, nothing else tells you the panel
heard you. The strip pills, layout badge, workspace tabs, gutter buttons,
dock cells, chord rows and launcher cards now darken or invert under the
finger and hold it a few frames after release, so a quick tap is still seen.
A pan or a long press withdraws it, because those are not taps. The applets
tape captures a frame mid-tap to keep it honest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ime fixes it found

The shell is a product, not a demo of the runtime, so it now lives in
pocket-stack/pocket-shell with the PocketJS checkout as its vendor submodule
— the same split Pocket Term took. Its app, tests, tapes and byte-exact
frames move there; this branch keeps only what belongs to the runtime.

What stays, and why it is worth keeping on its own:

- framework/src/native-tree.ts: insertBefore(node, node) is a no-op, like the
  DOM. Solid's reconcileArrays issues exactly that call when two ADJACENT
  <For> items swap, and the old code unlinked first, so the node became its
  own missing anchor and threw. Any app that reorders a <For> by one position
  hit it; tests/renderer.test.ts pins the case.
- hosts/3ds/src/qjs.c: the guest's JS stack is 384 KiB, not 192. The console
  spends it on JSX nesting depth rather than node count, and a UI four levels
  deeper than a demo overflowed it mid-frame — after which the runtime rolled
  back to last-good, which on a shared SD card looks like a different app
  booting.
- tests/e2e/azahar.ts: the fixture pins the emulated clock, so a guest that
  shows the RTC can still be compared byte for byte.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doodlewind doodlewind changed the title feat(3ds): Pocket Shell — a tiling window shell for the console fix(3ds): the runtime fixes Pocket Shell found — adjacent-swap reconcile, the guest's stack budget, a pinned emulator clock Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant