Skip to content

feat(tui): generate InputCore via fluessig single_threaded handle (#80)#341

Draft
zmaril wants to merge 1 commit into
napi-fluessig/bytes-image-mimefrom
napi-fluessig/single-threaded-tui
Draft

feat(tui): generate InputCore via fluessig single_threaded handle (#80)#341
zmaril wants to merge 1 commit into
napi-fluessig/bytes-image-mimefrom
napi-fluessig/single-threaded-tui

Conversation

@zmaril

@zmaril zmaril commented Jul 21, 2026

Copy link
Copy Markdown
Owner

What

Swaps the hand-written InputCore napi class (packages/tui/src/components/input.ts) for a fluessig-generated one, proving the last unproven fluessig path in the pidgin campaign: #80 single_threaded — a thread-confined, !Send handle that holds its core in RefCell<Impl> without Arc/Send/Sync.

Stacked on napi-fluessig/bytes-image-mime (which stacks on the pin-walk to fluessig e056bb9).

Why InputCore

Of the three !Send UI-state cores (TuiCore, InputCore, SelectListCore), InputCore is the simplest: a zero-arg ctor, five all-sync / all-infallible ops, one small DTO (InputEvent), and a dedicated status=native oracle (test/input.test.ts). It is genuinely !Send: it owns pi's Input (whose on_submit/on_escape are non-Send Box<dyn FnMut> closures) plus an Rc<RefCell<InputEventState>> event cell captured by those closures.

The #80 path

Authoring the interface single_threaded: true makes fluessig lower it to:

  • pub struct InputCore { core: RefCell<InputCoreImpl> } — no Arc, no Send/Sync.
  • pub trait InputCoreCore: Sized + 'static — sheds the Send + Sync supertraits, so a !Send core implements it.
  • &mut self core ops, reached from the handle's &self methods through RefCell::borrow_mut().

InputCoreImpl (in core_impl.rs) does the same onSubmit/onEscape wiring the deleted hand-written class did — now legal to hold Rc/RefCell directly since the trait carries no Send bound.

Byte-transparency

Self-mutability isn't in the TS type, so RefCell-vs-&mut self is invisible at the boundary. The generated InputCore class .d.ts block, the InputEvent interface, the .js export, and the nativeBinding entry are byte-identical to the pre-swap build — the only file movement is InputEvent and the InputCore export relocating from the hand-written region into the generated cluster (identical tokens).

Gates

  • cargo build -p pidgin-napi, cargo fmt --all --check, cargo clippy -p pidgin-napi --all-targets -- -D warnings — clean (no RefCell-layer complaints).
  • codespell — 0 on tracked files.
  • straitjacket v0.2.3 on git archive HEAD0 errors.
  • pi's test/input.test.ts oracle against the locally-built addon — 35/35 pass.

Generated files (src/generated.rs) are regenerated via regen.sh against fluessig e056bb9; not hand-edited.

🤖 Generated with Claude Code

https://claude.ai/code/session_017xL2W7dkhPsTSRuXQNCDeY


Generated by Claude Code

Swap the hand-written `InputCore` napi class (packages/tui/src/components/
input.ts) for a fluessig-generated one, proving the #80 single_threaded
(thread-confined `!Send`) handle path at the bumped pin (e056bb9):

- Author the ctor + five sync ops + the `InputEvent` model in schema/
  api.json, marking the interface `single_threaded: true`. fluessig lowers
  it to a THREAD-CONFINED handle: `pub struct InputCore { core:
  RefCell<InputCoreImpl> }` — no `Arc`, no `Send`/`Sync` — and the
  `InputCoreCore` trait sheds its `Send + Sync` supertraits (`Sized +
  'static`) with `&mut self` ops reached through `borrow_mut()`.
- This is exactly what lets a `!Send` core compile: `InputCoreImpl`
  (core_impl.rs) owns pi's `Input` (whose `on_submit`/`on_escape` are
  non-`Send` boxed closures) plus an `Rc<RefCell<InputEventState>>` event
  cell captured by those closures — the same wiring the deleted hand-written
  class used.
- regen src/generated.rs (fluessig e056bb9); delete the hand-written
  `#[napi]` struct/impl + `InputEvent`/`InputEventState` from lib.rs; keep
  `pub mod generated;`.

Self-mutability is invisible to the TS type, so the RefCell-vs-`&mut self`
handle is byte-transparent: the generated `InputCore` class `.d.ts` block,
the `InputEvent` interface, the `.js` export, and the nativeBinding entry
are all byte-identical to the pre-swap build. pi's own test/input.test.ts
oracle passes 35/35 against the locally-built addon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xL2W7dkhPsTSRuXQNCDeY
@github-actions

Copy link
Copy Markdown

Conformance smoke: agent + ai + tui

  • pi_sha: 3da591ab74ab9ab407e72ed882600b2c851fae21
  • manifest_native_modules: 45

Headline is rust-backed: passing cases in files whose module-under-test is a native (Rust addon) module. Raw all-pass is shown secondary — it is inflated by unflipped TypeScript that passes without touching any Rust.

Package rust-backed Native modules raw pass (secondary) Failing Skipped
agent 55/180 (30.6%) 5 180/180 0 0
ai 74/1295 (5.7%) 8 553/1295 3 739
tui 375/678 (55.3%) 14 678/678 0 0
smoke total 504/2153 (23.4%) 27 1411/2153 3 739

agent: 55/180 (30.6%) · ai: 74/1295 (5.7%) · tui: 375/678 (55.3%)

rust-backed = passing / total tests run, per the manifest's per-native-row tests lists. The Native modules column counts modules served by the Rust addon. This is the agent+ai+tui smoke subset; the full baseline lives in committed conformance.json.

Attribution

A pi test file is rust-backed iff the module it primarily exercises — its module-under-test — is status=native in conformance/manifest.json. A package's rust-backed count is the passing cases in its rust-backed files. Transitive or infrastructure use does not count (e.g. ~30 ai files construct the native faux provider but test other things — only faux-provider.test.ts, whose subject is faux, counts). A file that substantially tests both a native and an original module is excluded rather than counted, so the number under-reports rather than over-claims.

Per-file decisions (from each native manifest row's tests list):

Native module Test file Decision
ai/api/anthropic-messages.ts test/anthropic-sse-parsing.test.ts counted
ai/providers/faux.ts test/faux-provider.test.ts counted
coding-agent/utils/ansi.ts test/ansi-utils.test.ts counted
coding-agent/utils/changelog.ts test/changelog.test.ts counted
coding-agent/utils/git.ts test/git-ssh-url.test.ts counted
coding-agent/core/tools/path-utils.ts test/path-utils.test.ts counted
tui/keys.ts test/keys.test.ts counted
tui/utils.ts test/truncate-to-width.test.ts, test/regression-regional-indicator-width.test.ts counted
coding-agent/utils/mime.ts test/image-process.test.ts mixed — excluded (2 of 3 cases test processImage/original)
coding-agent/utils/version-check.ts test/version-check.test.ts mixed — excluded (fetch cases mock the original; not separable per-file)
coding-agent/core/export-html/ansi-to-html.ts test/export-html-whitespace.test.ts mixed — excluded (asset-grep + tool-renderer/original dominate)
coding-agent/core/tools/truncate.ts no dedicated test (exercised via tools.test.ts, subject is the read/edit factory/original)
coding-agent/core/tools/edit-diff.ts no dedicated test (edit-tool tests' subject is the edit tool/original)

CLI conformance (black-box, against the pidgin binary)

  • CLI conformance: 15/15 pass against target/release/pidgin (pass delta ±0, fail delta ±0).
File Passing Failing Skipped
packages/coding-agent/test/session-file-invalid.test.ts 1 0 0
packages/coding-agent/test/session-id-readonly.test.ts 7 0 0
packages/coding-agent/test/startup-session-name.test.ts 2 0 0
packages/coding-agent/test/stdout-cleanliness.test.ts 5 0 0

The four repointed coding-agent CLI test files spawn the compiled pidgin binary via $PIDGIN_BIN instead of pi's own cli.ts. This is a separate signal from the module smoke table and is never folded into the per-package Native count.

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.

2 participants