Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-claude/.nornir/reasoning.md

facett-claude — why it is shaped this way (the idiot-guide)

Class: reasoning · What it's for: the why behind the seams — read this if the design.md "what" left you asking "but why not just call the API in the pane?". Plain-language, decision-by-decision.

Why a separate crate at all

nornir's 🤖 Claude tab lived welded inside src/viz/claude_tab.rs. Any other app that wanted a "chat with Claude" pane had to copy it — and a copied UI drifts. So the drivable pane (turns · prompt · Send · model) is lifted into ONE crate every app mounts. This is nordisk Law #1: one surface, no duplication. dwarves already has a "Claude" tab concept; nornir and holger can mount the same crate instead of forking.

Why the pane does NO HTTP

Two reasons, both fatal if ignored:

  1. Cycle. Apps depend on facett-claude to render the pane. If the pane depended on nornir (for its HTTP client) you'd have a dependency cycle. The pane must be a pure LEAF.
  2. Testability. A pane that calls api.anthropic.com can't be tested without a network and a secret. That makes CI flaky and the robot harness impossible.

So the network lives OUTSIDE the pane. On Send the pane emits ClaudeEffect::Send as data (model + the message list) and stops. The host runs the actual request and feeds the answer back as ReceivedOk/ReceivedErr. This is the standard Elm "effects are data" split (FC-8): the pane decides what should happen; the host does it. No async, no reqwest, no key ever enters this crate.

Why a ClaudeBackend trait (and an echo default)

The seam is one trait — ClaudeTransport, aliased ClaudeBackend for the brief's vocabulary — with one method: send(model, messages) -> Result<String, String>. The host implements it once over its real Anthropic client. Because the trait is the only coupling point, the crate ships a built-in EchoBackend that echoes the last user turn: it means the crate builds and every test runs with no network and no secrets. You get a working, deterministic pane out of the box; you upgrade to the real model by swapping one struct. An error is a returned Err(String), not a panic or a crash — an offline host or an unset ANTHROPIC_API_KEY shows an inline message and keeps running.

Why everything routes through update_json / state_json

The pane is a facett_core::Elm bridged to Facet/Panel by impl_facet_via_elm!. That gives it a text remote-control: send {"prompt_changed":…}, "send", {"received_ok":…} and read state_json() back. The robot harness drives the WHOLE conversation in code — no pixels, no human, and (because the reply is injected as data) no AI at runtime. The same update path the mouse drives is the path the robot drives, so "it works headless" and "it works on screen" can't diverge. The testmatrix cell (facett-claude/prompt) turns that proof into a functional-status row: green ⟺ the remote-drive is actually covered.

Why the model is the whole observable state

ClaudeModel (turns · prompt · model · sending · error) is serializable and is the only thing update mutates. That is what makes the pane round-trippable and the state_json assertions meaningful — there is no hidden state a driver can't see. sending also guards against a double-send and an empty-prompt send (both clean no-ops), so a fast driver or an impatient user can't wedge it.

What's deferred

Real Anthropic wiring (token resolution, streaming) is the host's HttpTransport — by design it is NOT in this crate. nornir still has to de-weld src/viz/claude_tab.rs to mount this pane; dwarves wires the Claude tab to it. Those are host-side follow-ups; the pane and its seam are done.