Files
neuron/FISHBONE.md
T
will.anderson e58cdd124c
Neuron Soul CI / build (push) Failing after 4m34s
Neuron Soul CI / deploy (push) Failing after 12m13s
Park remaining local WIP on main. TUI archived, CLI/kernel edits, glass. Sort later. Broken is fine.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 14:07:23 -05:00

11 KiB
Raw Blame History

Fishbone: black TUI, split starts, leftover OpenCode

Fix bottom to top. Do not start at “blank screen” or a new script/dev.ts. Each layer can look like the same black window.

Date: 2026-08-24
Repo: neuron
Kit: sibling neuron-tui
Symptom: iTerm looks empty after launching the CLI, or the CLI dies before that with a loud error.


Unproven (do not skip)

We have not shown that a high-contrast cell landed in your iTerm alternate screen on the current command.

iTerm “session contents” (AppleScript) is usually the main buffer. The TUI draws on the alt buffer. Empty contents is not proof of a blank app.

Until a bright first frame or a dump of the renderer buffer, “always blank” is several failures wearing one coat.


Core lizard (cyclomatic)

Tool: lizard (CCN = cyclomatic complexity). Default warn is 15. Run 2026-08-24 on @neuron-tui/core + native Zig.

The JS presentation host is too big for lizard to see. renderer.ts is 5130 lines. Lizard reports avg CCN 1.9 and 0 warnings. That is a lie: it swallowed most of CliRenderer into resolveModes (595 lines). zig.ts is 6706 lines and lizard found 5 functions. If a metric cannot split the file, the file is the problem.

Hand count of the JS hot path (if/else/switch + &&/||): loop ~32 branches / 122 nloc; requestRender ~10; setupTerminal ~14; constructor ~39. Not 1.9.

Native is where lizard can see, and one function owns present:

CCN nloc fn file
189 394 prepareRenderFrameWithWriter native/src/renderer.zig
104 240 checkEnvironmentOverrides native/src/terminal.zig
58 107 processCapabilityResponse terminal.zig
57 109 writeSnapshotCommit renderer.zig
47 68 writeKittyImages renderer.zig

prepareRenderFrameWithWriter is 12× the warn line. It is the function that turns the JS buffer into TTY bytes. Skip lives next door: prepareFrame tryLock miss → finishSkippedFrame clears the buffer JS just drew. renderer-output.zig looks simple (avg CCN 2.6). The skip is small. The present is not.

JS outliers that are real parsers, not the blank path: Markdown.createInitialStyledText CCN 366 (lizard may have eaten the class), toNativeAudioStreamFormat 101, parseAlign 89, parseKeypress 84, parsePositiveDecimalPrefix 77.

Fix from this list first: split/log prepareRenderFrameWithWriter; do not wipe nextRenderBuffer on skip; do not run checkEnvironmentOverrides + alt-screen + first frame on the same mutex; split CliRenderer until lizard can name loop / requestRender / setupTerminal.


Bottom → top (fix in this order)

0. Hold one test still

This chat mixed runs: Cursor aborted serve, AppleScript 0×0 windows, fake script/dev.ts, OpenTUI then neuron-tui, port 4096 leftovers. “Still blank” compared different stacks.

Fix: one command, real iTerm (not Cursor job), leave the engine alone, do not AppleScript-scrape alt-screen.

Current command (from any folder, including ~):

bun --cwd /Users/will/Development/neuron-technologies/neuron/packages/cli --conditions=browser src/index.ts

--cwd packages/cli is required so packages/cli/bunfig.toml preloads @neuron-tui/solid/preload. That cwd also makes process.cwd() = packages/cli (the “project” the engine/TUI think you opened). That is a later layer.


1. Core: native library (neuron-tui)

What: Zig renderer. On darwin arm64 JS does import("@neuron-tui/core-darwin-arm64").
Fact: that name 404s on npm. OpenTUIs @opentui/core-darwin-arm64 is published. Rename did not ship binaries.
Error (loud): Failed to initialize OpenTUI render library: Cannot find package '@neuron-tui/core-darwin-arm64' — leftover OpenTUI string in your core.
Done locally: workspace package neuron-tui/packages/core-darwin-arm64 copies packages/native/lib/aarch64-macos/libopentui.dylib (built ~Aug 22). CLI/TUI depend on it.

Still open: JS is today; dylib is another day. ABI mismatch → black, no JS throw. Rebuild native from current neuron-tui when fixing core for real. Other platforms still 404 (linux/win optional deps).


2. Core: Mac thread vs Linux, skip vs fail

What: CliRenderer constructor sets useThread: true except Linux (forced off; comment: thread crashes). You are on Mac.

Two output machines:

Path What happens on native render status SKIPPED (1)
Tests / custom stdout NativeSpanFeed → wait feed.idle() → retry
iTerm process.stdout + thread No feed. stdout.write hijacked to lib.writeOut. Skip → "backpressured", no console.error

Alt-screen is lib.setupTerminal(..., alternate-screen) — wipe is native, before Solid has a tree.

Fix direction: useThread: false on darwin until skip is visible; or log skip; or use feed on TTY; rebuild native; prove lib.render returns presented not skipped.


3. Core: idle one-shot vs live loop

What: @neuron-tui/solid render() does engine.attach (timeline) + mount. Tests call renderer.start(). The app does not.

Paint is requestRender() → one activateFrame if _isRunning is false.

activateFrame no-ops if updateScheduled is already false.

internalStart() clears updateScheduled so queued one-shots die when the live loop starts. Idle shots and the continuous loop cancel each other by design.

Fix direction: renderer.start() (or requestLive) after mount so skipped frame 0 is not the only chance. Do not “fix” this by adding another wrapper script.


4. Core: queries and pixels on the same hijack

setupTerminal returns immediately. It has already:

  1. switched alt-screen
  2. turned on capability probes (privateCapabilityRepliesActive)
  3. written those probes through the same lib.writeOut as frames

iTerm replies on stdin. Frame 0 is process.nextTick. Probes and first paint contend. Skip on frame 0 is “queue full,” not “JS forgot to draw.” The 5s timer is stop asking, not dont draw yet.

Constructor also replaces global requestAnimationFrame. If the renderer is idle, rAF UI sleeps too.

Fix direction: dont probe on the presentation path; or dont hijack stdout until after first present; or useThread: false so stdout.write is real TTY again.


5. Errors after core owns stdout can vanish

Loud errors you pasted (Failed to start server, react, missing dylib) happened before hijack.

Bootstrap (sync.tsx) uses throwOnError: true and exit(e) after the TUI is up. Those logs can go into Zig. No ERROR (#1) + black ≠ no error.

Fix direction: log bootstrap failures to a file, or restore realStdoutWrite for console.error.


6. --conditions=browser is a pun

Needed: solid-js client build (dist/solid.js), not dist/server.js.
Also: Bun treats it as “browser app” → default JSX factory React.

--cwd repo skips packages/cli/bunfig.toml → no @neuron-tui/solid/preloadCannot find package 'react'. Root bunfig preload failed: @opentui/solid/preload not installed at root (and that is not your kit).

Fix direction: keep CLI bunfig preload; do not put kit preload on repo root; or pass --jsx-import-source @neuron-tui/solid explicitly; split “solid client resolve” from “browser JSX.”


7. One --cwd, three jobs

Job Wants
bunfig / Solid preload packages/cli
process.cwd() = project for engine/TUI neuron repo (or the users real project)
Finding src/index.ts a path that exists from this shell (~ vs repo)

--cwd packages/cli from ~ → ENOENT (packages/cli is relative).
--cwd repo → JSX React.
Old script/dev.ts climbed twice (URL .. then dirname) → neuron-technologies/ not neuron/. Deleted; do not bring it back.

Fix direction: absolute CLI path + set process cwd to the repo after bun loads bunfig, or bun --cwd cli plus NEURON_CWD / engine cwd override. Never one relative --cwd for all three.


8. Two bosses for “is the engine up”

CLI daemon: alive = ~/.neuron/server.json and /global/health.
A live bun ./src/serve.ts on 4096 with no file = “no server.” Child spawn uses stdio: ignore. Wait → Failed to start server.

Daemon start() now probes default http://127.0.0.1:4096 if registration is missing.

Fix direction: one authority (health on the port, or always --register). Never hide child stderr. Dont spawn a second listener on 4096.


9. Painted empty ≠ unpainted

packages/tui is still OpenCodes client (many HTTP routes, plugins, home layout). Neuron answers a lot with empties so the client wont invent data.

Home: two flexGrow={1} spacers, 7-row logo, prompt. Wordmark PNG is cream on black; app clear #0a0a0a. StartupLoading only appears if ready() is false after 500ms; App ready starts trueno spinner.

A successful frame can look dead.

Settings ~/.local/state/opencode/kv.json used to hide the whole tree until a flock lock finished (createSimpleContext + Show when ready). That gate was removed; file is optional. Do not treat kv as the remaining blank.

Logo protocol="auto" (Kitty/iTerm handshake) could hang with no ASCII fallback. Now protocol="blocks" on the same PNG. Contrast still terrible.

Fix direction: first frame high contrast (e.g. yellow “neuron” on black) before logo/image; renderer.start(); then put the wordmark back. Do not use alt-screen scrape as pass/fail.


10. Product vs kit (do not confuse)

Piece What it is
neuron-tui Your kit (Zig + Solid reconciler). Not the Neuron chat app.
packages/tui OpenCode screens now importing @neuron-tui/*.
packages/cli Launcher + daemon; still @opencode-ai/cli.
packages/neuron Engine HTTP on 4096.
script/dev.ts Fake one-command runner. Deleted. Do not restore.

Root package.json workspaces include sibling ../neuron-tui/packages/{core,solid,keymap,react,core-darwin-arm64}. Nested workspace + npm 404s on other core-* platforms remain.


Loud errors already explained (not “blank”)

Paste Cause
Could not change directory to "packages/cli" Relative --cwd from ~
Failed to start server Daemon vs leftover 4096 / no server.json
Cannot find package 'react' JSX default React; CLI bunfig not loaded
preload not found "@opentui/solid/preload" Root bunfig; not your kit
Cannot find package '@neuron-tui/core-darwin-arm64' Native never published; now local package

Not the users fault

Did not delete packages/cli. Did not need React. Did not “fail to use the TS build.” Bun/package.json already was that. Did not turn on a blank-screen mode.


Suggested first patches (still bottom-up)

  1. Rebuild neuron-tui native for this machine; keep core-darwin-arm64 in workspace.
  2. In packages/tui createCliRenderer: useThread: false until skip is logged.
  3. After Solid render(), renderer.start().
  4. First paint: high-contrast text, not the black wordmark.
  5. Then: one cwd story, engine health-only, bootstrap errors to a file.

When those five are true in one iTerm run, re-judge “blank.” Not before.