Files

229 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `~`):
```bash
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/preload``Cannot 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 **true****no 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.