# Inkhaven 1.2.10 — standalone editors for `inkhaven.hjson` and `prompts.hjson`

1.2.10 turns two of inkhaven's HJSON files —
`inkhaven.hjson` (project config) and
`prompts.hjson` (prompt library) — into
first-class editing surfaces with dedicated
TUIs.

  * **`inkhaven config -p <dir>`** — schema-aware
    editor for the project's `inkhaven.hjson`.
    Auto-derived schema from the Rust `Config`
    struct.  Typed widgets (bool / int / float /
    string / colour with live preview / path /
    enum / list).  Build-time `///` doc-comment
    extraction so every field has docs in the
    help pane.  Comment-preserving surgical save
    + timestamped `.config-backups/` rollback.
    Per-field free-text annotations (`Ctrl+A`)
    + a comment inspector (`Ctrl+I`).
    Add / delete map entries for
    `llm.providers` from the TUI.

  * **`inkhaven prompts-editor -p <dir>`** —
    four-pane workbench for the project's
    `prompts.hjson`.  Prompts list (left) +
    prompt editor (centre, full tui-textarea
    chord set) + AI response (right) + AI prompt
    input (bottom).  The LLM acts as a
    prompt-engineering **reviewer** — it
    analyses your template against your typed
    instruction; it does NOT execute the
    template.  Placeholders like `{{selection}}`
    are shown to the reviewer literally so it
    can comment on them.  `Ctrl+B G` lifts the
    reviewer's response into the editor at the
    cursor.  Both editors auto-populate from
    inkhaven's embedded defaults on first
    launch.

Both editors share design conventions: atomic
write + timestamped backups + rollback picker
(`Ctrl+R`) + `Ctrl+S` confirmation modal that
itemises pending changes + safety net on Esc /
Ctrl+Q with unsaved work.

Two new tutorials:
  * `Documentation/Tutorials/44-prompts-editor.md`
  * `Documentation/Tutorials/45-config-tui.md`
  * `Documentation/KEYBINDING.md` — new "inkhaven
    prompts-editor" section at the end.

This document is a living scratchpad — entries land
here as the work lands.  Each item starts as a one-
line plan; gets fleshed out when the corresponding
feature ships; gets a chord / config / hook callout
when relevant.

## Planned themes

### Carryovers from 1.2.9

- **AI-driven sentence-rhythm rewrite** — a chord
  that sends the open paragraph to the LLM with a
  prompt asking it to break a MONOTONE verdict.
  Pairs with `Ctrl+B Shift+H` the same way the AI
  show-don't-tell scan pairs with the regex
  overlay.
- **Per-language emotion + linking-verb lists**
  for Russian / French / German / Spanish in the
  show-don't-tell detector.  English ships
  built-in; the others have empty scaffolds — the
  community + dogfooding fills them in this cycle.
- **Concordance: jump-to-occurrence on Enter**.
  Currently the modal is read-only; pressing
  Enter on a row should close the modal and
  open the source paragraph at the line of the
  first sample.

### Split view — the deferred 1.3 work

1.3 has been "next release" since 1.2.5.  It keeps
slipping because each polish cycle finds enough
single-pane work to fill itself.  If 1.2.10 is
genuinely a quiet cycle, the split-view rework
lands here.

### Other candidates

- Mouse-driven scene-break selection (drag from
  one break-line to the next selects the scene).
- Concordance export — `inkhaven export
  concordance` for the CSV / JSON variant of the
  modal data, so users can pipe it into their own
  analysis.
- Show-don't-tell theme audit — per-detector
  underline style override (DIM / BOLD /
  REVERSED / NONE), since the teal underline
  reads differently on different terminal
  palettes.

### Standalone TUI configuration editor

Design proposal under
[`PROPOSALS/CONFIG_TUI.md`](../PROPOSALS/CONFIG_TUI.md).

Headlines:

  * New CLI: `inkhaven config
    --project-directory <dir>`.
  * Tree pane (left) + edit pane (right) layout
    matching the main app's conventions.
  * Per-type widgets (bool toggle, int with bounds,
    enum select, color picker, path picker, list
    editor) that catch errors at edit time.
  * Defaults shown inline as ghost values when a
    field isn't present in HJSON.
  * `Ctrl+S` saves with confirmation; every save
    snapshots to
    `<project>/.config-backups/inkhaven_YYYYMMDD_HHMMSS.hjson`.
  * `Ctrl+R` opens a rollback picker (preview +
    restore from backup history).
  * `Ctrl+A` attaches a free-text annotation to
    the current field (stored in a sidecar
    `.config-annotations.hjson`).
  * `Ctrl+B h` floats a CONFIGURATION.md slice for
    the focused field; `Ctrl+B i` floats the
    inspector showing the user's `#` / `//`
    comments attached to the field's stanza.
  * Comments **and** unknown / user-added fields
    survive every save via a surgical text-rewrite
    pipeline (path → byte-range index;
    splice-only, no re-emit).
  * The existing `Ctrl+B 0` in-app HJSON editor
    stays as the power-user fallback.

All four flagged questions resolved 2026-05-27.
Implementation underway against the four-phase
plan in the proposal (§11).  Estimated 7–9 focused
days.

## Implementation log

### Phase 1 — config-TUI read-only walk-through (shipped)

`inkhaven config -p <dir>` (also accepts
`--project-directory`) now launches a standalone TUI
that:

  * Walks `Config::default()` via
    `serde_json::Value` to derive the full schema
    tree automatically — every leaf gets its type
    inferred from the JSON shape, every default is
    pulled from `Config::default()`.  No hand-rolled
    schema table needed for Phase 1; metadata table
    lands in Phase 2.
  * Loads the live `inkhaven.hjson` (when present)
    via `serde_hjson` — same parser the main
    `Config::load` uses, so HJSON quirks behave
    identically.  Missing file is fine; the TUI
    runs defaults-only and says so.
  * Classifies every leaf as `Configured` (present
    in HJSON, value matches what was read) or
    `Default` (not present, value is the built-in
    default).  Stanzas roll up to `Configured` if
    any descendant leaf is.
  * Detects unknown fields (present in HJSON but
    absent from the schema) and surfaces the count
    as a yellow chip on the top bar.  Per the
    proposal §6.6 these are preserved on save (when
    Phase 2 lands) but not editable.
  * Renders a two-pane TUI: tree on the left (35%
    width, expand/collapse with Enter/Space, scroll
    with arrow keys), detail pane on the right
    (path, type, source, current value, default
    value when they differ, plus a yellow callout
    when unknown sub-fields exist under the focused
    stanza).
  * `Ctrl+H` or `?` opens a floating help pane
    showing the matching CONFIGURATION.md row.
    The help index is built at runtime from the
    embedded `include_str!` copy of the docs.
    Path lookup walks the dotted hierarchy so
    `editor.style_warnings.show_dont_tell.enabled`
    falls back through `style_warnings.show_dont_tell.enabled`
    → `show_dont_tell.enabled` → `enabled` until a
    match lands.
  * Esc / Ctrl+Q exit.

No widgets that mutate, no save, no backup —
strictly an explorer.  390 tests pass (was 381, +9
new tests covering schema build, type inference,
unknown-field collection, flatten with collapsed
subtrees, path-walking help lookup,
CONFIGURATION.md row extraction).

Phase 1 commit lands the module skeleton (`src/config_tui/`
with `mod.rs`, `schema.rs`, `help.rs`, `app.rs`)
ready for Phase 2 to extend.

### Phase 2 — typed widgets + comment-preserving save (shipped)

`inkhaven config` becomes editable.  The full editing
loop:

  * `Enter` on a leaf (or `e` from anywhere) opens
    a typed widget for that field.  Phase 2 ships
    four widget kinds:
      * `bool` — `Space` / `t` / `f` toggles;
        `y` / `n` snap; `Enter` commits.
      * `int` — digit / sign / Backspace input;
        `↑` / `↓` increment/decrement; bounds-
        check on commit.
      * `float` — same shape with `.` / `e` / `E`
        accepted; rejects NaN / infinity.
      * `string` — free text; multibyte clean.
    Anything else (color / path / enum / list /
    nested map) shows an "edit via Ctrl+B 0 HJSON
    editor" fallback — richer pickers ship later.
  * `r` resets the focused leaf to its built-in
    default — useful for trying a value, deciding
    you preferred the default, and rolling back
    without typing.
  * Every staged edit lights the leaf with a red
    `✱ STAGED` chip + bumps the top-bar
    `N changed` counter.

The save half is the technical centrepiece —
**comment-preserving surgical text rewrite**:

  * New module `src/config_tui/hjson_index.rs`: a
    hand-rolled HJSON walker (~470 LOC) that
    records, for every leaf in the source text, the
    byte range of its value, the byte range of any
    leading-comment block attached to it, plus
    the open / close brace positions of every
    stanza.  Tolerates `#` line comments, `//` line
    comments, `/* */` block comments, quoted +
    unquoted strings, multi-line `'''` strings,
    arrays (recorded as opaque spans), implicit
    top-level objects, trailing commas, and
    arbitrary nesting depth.  Blank-line gap rule:
    comments are detached from a key by any blank
    line between them.
  * New module `src/config_tui/save.rs`: diffs the
    schema tree's `current` against the on-disk
    values, classifies each change as **splice**
    (existing path) or **append** (new path), then
    rewrites the source byte-string in
    descending-offset order so earlier splices
    don't shift later ones.  Appends batch by
    parent stanza and insert just inside the
    closing `}`.  Unknown / user-added fields
    aren't touched — they survive by virtue of
    falling outside every recorded byte range.
    Comments survive for the same reason.
  * Render pipeline: bool / int / float values
    re-emit unquoted; strings re-emit unquoted
    when they're plain identifiers (no whitespace
    / commas / braces / comment markers / reserved
    words / numeric-lookalikes), else quoted with
    proper escaping.

`Ctrl+S` save flow:

  1. **Confirmation modal** lists every staged
     edit with its splice/append kind, path, and
     new value.  `y` / `Enter` commits; `n` / `Esc`
     cancels.
  2. **Atomic write**: contents → `inkhaven.hjson.tmp`
     → `rename(.tmp → inkhaven.hjson)`.
  3. **Backup snapshot**: a copy of the post-save
     contents lands in
     `<project>/.config-backups/inkhaven_YYYYMMDD_HHMMSS.hjson`.
     ISO date order; `.config-backups/` created on
     first save.
  4. **Schema reload**: the schema + index +
     original-values map all re-read from the
     freshly-written file so the next save diffs
     against current disk state.
  5. **Restart-required overlay**: top-bar chip
     paints purple `restart required` from this
     point until exit, plus the same banner shows
     in the post-save confirmation modal.

Discard-confirm modal triggers on `Esc` / `Ctrl+Q`
when there are unsaved changes.

416 tests pass (was 390, +26 new):

  * `hjson_index`: 14 walker tests covering flat /
    nested / commented / quoted / unquoted /
    arrays / trailing comments / blank-line gaps /
    block comments / deep nesting / stanza brace
    spans / realistic real-project HJSON.
  * `save`: 11 tests covering splice / nested-
    splice / unknown-field preservation / append
    insertion / value-rendering type coverage
    (string with spaces / identifier / reserved
    words / numeric-lookalike) + 3 end-to-end
    "splice the chunk_size, leave everything else
    byte-stable" tests.
  * `widgets`: pure UX; no unit tests yet (Phase
    3 will add interaction tests where reasonable).

Wrap-up:

  * Phase 1's `current_node` accessor + the
    `index_by_path` builder kept; new methods
    added directly to the `App` state.
  * No regressions: 390 pre-existing tests still
    pass.
  * Zero build warnings.

Phase 2 does NOT yet ship: the providers map
editor (Q9), the `Ctrl+B i` comment inspector
(Phase 3), rollback picker (Phase 3),
annotations (Phase 3).  Those land in the next
commit batch.

### Phase 3 — rollback + inspector + annotations (shipped)

Three new on-demand surfaces, all wired against
the Phase 2 byte-range index + the existing
`.config-backups/` snapshots.

**`Ctrl+R` — rollback picker.**  Lists every
`inkhaven_YYYYMMDD_HHMMSS.hjson` file under
`<project>/.config-backups/`, newest-first.  Each
row shows the absolute timestamp + a relative-time
chip ("12 minutes ago", "yesterday", "4 days ago")
+ file size.

  * `Enter` — *stages* the backup's leaf values
    into the working schema (no disk write).  The
    user sees N changed paths in the top-bar chip
    and can `Ctrl+S` to commit (which writes a
    fresh backup of the pre-rollback state on the
    way through, so the safety chain stays
    intact).
  * `v` — preview the backup file contents in a
    full-pane scrollable view (`↑↓` / `PgUp` /
    `PgDn` / `Home` / `End`).
  * `d` — delete with `y` / `Enter` confirm.
    Refreshes the picker afterwards.
  * `Esc` — back to the main view.

**`Ctrl+I` — comment inspector.**  Floats a pane
showing the contiguous `#` / `//` / `/* */`
comment block immediately preceding the focused
field's key in `inkhaven.hjson`.  Reads directly
from the byte-range index built by Phase 2's
HJSON walker — no re-parsing.  Surfaces the
matching annotation (if any) in a second section
underneath.  Distinct from `Ctrl+H` help: the
help pane shows the *author's* (inkhaven)
docs for the field; the inspector shows the
*user's* notes (HJSON comments + sidecar
annotations).

(The proposal called this `Ctrl+B i`; the
standalone config TUI has no meta-prefix system,
so it became plain `Ctrl+I`.  Same intent.)

**`Ctrl+A` — annotation editor.**  Single-line
free-text input attached to the focused field's
path.  Stored in a flat HJSON sidecar
`<project>/.config-annotations.hjson` (keyed by
dotted path; canonical store separate from the
inkhaven.hjson file).  Empty input clears the
entry.  Atomic write; empty store deletes the
sidecar file to keep the project root clean.

  * Annotated fields paint a `+` chip beside their
    state chip in the tree pane (so a configured-
    AND-annotated leaf reads as `●+`, a staged-
    AND-annotated leaf as `✱+`).
  * Annotations persist across sessions — the
    inspector picks them up on next launch.

New modules:

  * `src/config_tui/backup.rs` — list /
    relative-time format / read / delete the
    `.config-backups/` directory.  Sorts
    newest-first via parsed timestamps from the
    filename (entries with unparseable names land
    at the bottom in filename order).
  * `src/config_tui/annotations.rs` —
    `BTreeMap<String, String>` sidecar with
    `load` / `save` / `get` / `set` plus
    test-only `new` / `iter` / `len`.

New modal variants in `App::Modal`:

  * `Rollback` + `RollbackDelete` + `RollbackPreview`
    (three states of the rollback workflow).
  * `Inspector` (comments + annotation).
  * `Annotate` (single-line text input).

429 tests pass (was 416, +13):

  * `backup`: 8 tests covering relative-time
    formatting (just-now, singular minute,
    yesterday, multi-day, old-absolute-fallback)
    + directory listing (skip unrelated files,
    sort newest-first, missing-dir handled).
  * `annotations`: 5 tests covering empty-store-
    on-missing-sidecar, round-trip save/load,
    empty/whitespace text clears the entry, and
    empty-store-save removes the file.

Zero build warnings.

Still deferred (one more commit):

  * **Providers map editor** (Q9).  The
    `LlmConfig.providers` HashMap needs a
    dedicated map widget — `a` add a named
    provider, `d` delete with confirm, `Enter`
    descend into the provider's fields.  Lands
    next.

### Phase 4 — known-map-path support for `llm.providers` (shipped)

The first half of the proposal's Q9 work: user-
added LLM providers in the live HJSON now show up
in the config tree as first-class entries, ready to
edit.  Previously they were dumped into the
"unknown sub-fields preserved as-is" footer of the
detail pane — visible but un-editable.

How it works:

  * A small hand-rolled registry
    `KNOWN_MAP_PATHS: &[&str]` lists every config
    path whose direct children use dynamic keys
    (`HashMap<String, T>` in the Rust schema).
    Phase 4 ships a single entry: `llm.providers`.
  * When the schema builder hits a known map
    path, any live-HJSON key that isn't in the
    defaults gets a first-class tree child built
    from the template of any existing default
    entry — recursively, so every leaf inside the
    new entry gets its own SchemaNode.  Those
    leaves are then editable via the Phase 2
    typed widgets, inspectable via Ctrl+I,
    annotatable via Ctrl+A, etc.
  * The whole user-added stanza is force-marked
    `ValueSource::Configured` (it came entirely
    from the live HJSON, never a default).
  * Typos inside a user-added entry — e.g. an
    `api_token` field when the template has
    `api_key_env` — still flow into the
    `unknowns` collection.  Map-shape recognition
    expands what counts as a *valid map entry*,
    not what counts as a *valid field name
    inside one*.
  * The detail pane labels known map paths
    "map of named entries · N entries" so the
    UX cue lands.

What's still in `Ctrl+B 0`-only territory (lands
next commit):

  * **Adding** a new provider name from the TUI
    (today: edit `inkhaven.hjson` directly in
    the main app, then re-open `inkhaven config`
    — the new provider will be in the tree).
  * **Deleting** a named provider (today: same).

433 tests pass (was 429, +4):

  * `schema::user_added_provider_appears_under_known_map_path`
  * `schema::user_added_provider_marked_configured`
  * `schema::unknown_path_outside_map_still_collected`
  * `schema::map_path_with_extra_field_in_entry_still_reports_unknown`

Zero build warnings.

### Phase 5 — map-entry a/d chords + enriched help (shipped)

**`a` add map entry** (focus must be a known map
stanza, currently `llm.providers`).  Opens a name
prompt; on commit, a new SchemaNode is spliced into
the tree using the first existing default entry as
the structural template.  The new entry is staged
(top-bar chip lights `N changed`); the user can
edit its fields like any other leaf before `Ctrl+S`.

**`d` delete map entry** (focus must be a direct
child of a known map stanza).  Opens a confirm
modal.  On commit, the entry is added to
`removed_map_entries` and renders strike-through in
the tree until save.  Esc reverts.

If `d` is fired on a *newly-added* (still-unsaved)
entry, the addition is dropped entirely — no save
round-trip needed.

**Save pipeline** learns two new `EditKind`s:

  * `AddMapEntry` — payload is the full stanza
    body; on save the surgical pipeline appends
    `name: { … }` just inside the parent map's
    close-brace.  Indentation is auto-detected from
    a sibling entry's leading whitespace, so the new
    entry visually lines up with its neighbours.
  * `DeleteMapEntry` — splices out the entire
    `name: { … }` block, including the key, any
    trailing comma, and one trailing newline, so the
    result stays tidy.

Both run *before* the existing splice / append
passes (deletions shrink the source, additions
extend; the index re-parses between passes so byte
offsets stay valid).  All previous edit kinds —
splice, append — keep their semantics intact.

**hjson_index** gains `key_range` on both
`LeafSpan` and `StanzaSpan` (the start of the
splice-out range for deletions; `None` for the
synthetic root stanza).

**Tree-pane chips** gain two new state markers:

  * `✚` — staged addition (paints green-bold)
  * `✗` — staged deletion (paints red strike-through)

Combined with the existing `●` / `✱` / `+` chips,
a row can read e.g. `✚+ ollama_remote` (newly-
added with an annotation attached).

**Enriched help pane** (the screenshot fix).  When
the user pressed `Ctrl+H` on an undocumented field,
the previous help body was just "No CONFIGURATION.md
row matched …" with no actionable info.

The new help layout has three sections — always
present:

  * **Structural** — path, type, default value,
    current value (when divergent from default),
    source (`default` / `configured (in HJSON)` /
    `unknown`).  This section is generated from
    the schema; never blank, never says "no docs".
  * **Notes** (when relevant) — map-context hints,
    staging state, attached annotation.
  * **Documentation** — the matching CONFIGURATION.md
    row body when present; otherwise a clear
    actionable instruction ("add a row to
    Documentation/CONFIGURATION.md keyed by this
    field's path to enrich this pane").

Section headers render in cyan-bold; key/value
lines bold the key for at-a-glance scanning.

436 tests pass (was 433, +3 save tests covering
`AddMapEntry` / `DeleteMapEntry` / mixed-save).
Zero build warnings.

The Config TUI design is now feature-complete
against the proposal — every Q1-Q11 item lives in
shipped code.  Future polish: theme preview,
build-time doc-comment extraction so
`Documentation/CONFIGURATION.md` doesn't need
manual rows for every field, richer pickers for
color / path / enum / list types.

### Phase 6 — polish (shipped across three commits)

**Phase 6A: build-time doc-comment extraction.**
The Config TUI's help pane now has docs for every
single field — not just the ones with
CONFIGURATION.md rows.  Fixes the "No
CONFIGURATION.md row matched `ai.diff_review_on_apply`"
gap from the screenshot report.

New build.rs sibling `config_help_extract.rs` parses
src/config.rs with `syn`, walks the type-graph
rooted at `Config`, and emits a `(path,
doc_comment)` table at `$OUT_DIR/config_help.rs`.
89 doc entries auto-extracted from the current
struct tree.  Handles `Option<T>` / `Vec<T>` /
`HashMap<K, V>` descents; honours
`#[serde(rename)]`; uses `<entry>` placeholder
segments for HashMap fields so map-entry paths like
`llm.providers.gemini.model` resolve via runtime
canonicalisation.

help.rs lookup: curated CONFIGURATION.md wins;
build-time doc-comment fallback; path-walk-up.
Result: every field shows actual struct-level docs
inline.

**Phase 6B: richer widgets.**  The Phase 2 widget
catalog (bool/int/float/string) gains four new
kinds:

  * **Color** — hex digits + `#` input; live RGB
    swatch + decomposition; validates `#RRGGBB`
    format; normalises to lowercase on commit.
  * **Path** — text input with live "✓ exists"
    / "○ does not exist (created on first use)"
    feedback.
  * **Enum** — ↑↓ cycler over a fixed-set
    metadata table.  Currently routes
    `typst_compile.engine`
    (`external`/`inprocess`) +
    `embeddings.model` (`MultilingualE5*` /
    `BGEM3`).
  * **List** — vertical Vec<String> editor.
    Browse mode: ↑↓ select, `a` add, `d` delete,
    `e` edit selected.  Inline add / edit
    sub-mode with single-line text input.
    `Ctrl+S` / Ctrl+Enter commits the list back
    to the schema.

schema.rs gains `ConfigType::{Color, Path, Enum,
StringList}` variants + `refined_type(path,
inferred)` that narrows generic JSON-derived
`String`s to Color / Path / Enum based on the
field's path suffix and a hand-rolled enum-
variant table.

**Phase 6C: theme preview pane.**  When editing a
`theme.*` color, the widget shows a context-
appropriate preview mock:

  * `*_fg` colors render sample text in the
    candidate colour against the editor
    background, with BOLD + DIM variants.
  * `*_bg` colors render sample text on the
    candidate background (using a neutral
    foreground so the contrast is visible).
  * `*_border` colors render a small bordered box
    using the candidate colour.

The widget reads its own `path` to pick which
mock to show — so editing `theme.modal_bg` shows
a bg mockup, `theme.tree_chapter_fg` shows fg
text, etc.

Edit-modal height auto-grows based on widget
kind: 16 rows for List, 14 for Color (preview
block), 12 for Enum, 9 for scalar widgets.

439 tests pass (was 433 at end of Phase 5, +6 in
Phase 6 spread across the three commits).  Zero
build warnings.

That's the Config TUI design's full v1 surface
shipped — every Q-resolution lives in code, every
field type has a dedicated picker, and the help
pane is always informative.

## Adjacent research — Prompts editor TUI

Design proposal under
[`PROPOSALS/PROMPTS_EDITOR_TUI.md`](../PROPOSALS/PROMPTS_EDITOR_TUI.md).

Headlines:

  * New CLI: `inkhaven prompts-editor
    --project-directory <dir>`.
  * Four-pane workbench: prompts list (left),
    prompt editor (centre, same chord set as the
    main inkhaven editor), AI response (right),
    AI prompt input (bottom).
  * Send-to-LLM action: Enter in the AI prompt
    sends `(system = editor body, user = input)`
    via the existing `spawn_chat_stream`
    plumbing — no new client.
  * Save semantics mirror the Config TUI: atomic
    HJSON write + timestamped backup in
    `.prompts-backups/`; per-prompt dirty
    tracking with red `✱` chips; confirmation
    modal lists every dirty entry before
    writing.
  * Reuses the Phase 2 surgical-write pipeline
    indirectly via the `config_tui::save`
    helpers (the prompts library is JSON-
    shaped enough that comment-loss-on-first-
    save isn't a concern — the file is mostly
    auto-generated).

Estimated 3.5–4.5 focused days across four
phases.  Targets 1.2.10 (or late 1.2.10 if the
config TUI shipping leaves room).

All four flagged open questions resolved 2026-
05-27:

  * Q1 Tab cycles three panes (Prompts list →
    Editor → AI prompt → Prompts list).  AI
    response pane is display-only.
  * Q2 LLM payload: render template (replace
    `{{selection}}` with the AI prompt input,
    `{{context}}` with empty) and send as a
    single user message; no system prompt.
    Mirrors how the same template behaves when
    invoked from the main TUI.
  * Q4 Auto-populate `prompts.hjson` with
    inkhaven's embedded defaults on first
    launch.
  * Q5 Single-shot per send — this is prompt
    *assessment*, not a chat session.  Multi-
    turn isn't planned.

Implementation ready against the four-phase plan.

### Phase 1 — prompts-editor read-only walk-through (shipped)

`inkhaven prompts-editor -p <dir>` (also accepts
`--project-directory`) now launches a standalone TUI
that:

  * Walks `<project>/prompts.hjson` via the existing
    `PromptLibrary::load` API (the same one the
    main TUI uses).  Missing file → auto-populates
    from the embedded `DEFAULT_PROMPTS` snippet
    that ships at `assets/default_prompts.hjson`,
    same source the main TUI's
    `reseed_prompt_examples` policy uses (Q4
    resolution).
  * Renders a four-pane layout: prompts list
    (22%), prompt editor (43%, read-only in Phase
    1), AI response (35%, display-only), AI prompt
    input (3-row bottom strip — inert in Phase 1).
    Status bar at the very bottom carries
    cursor-position + focus + chord hints.
  * Tab / Shift+Tab cycle focus across **three**
    editable panes (Q1 resolution): List → Editor
    → AI prompt → List.  The AI response pane is
    display-only and not in the Tab cycle.  Focus
    is surfaced via a coloured border + a magenta
    chip on the top bar + adaptive chord hints in
    the status line.
  * ↑↓ / PgUp / PgDn / Home / End navigate the
    prompts list (when focused) and scroll the
    editor pane's read-only view (when focused).
    Switching prompts resets editor scroll.
  * `Ctrl+H` / `?` opens a focus-aware help pane:
    the body changes depending on which pane is
    currently focused, so the chord summary is
    always relevant.  Editor-pane help also
    documents the `{{selection}}` / `{{context}}`
    template variables that Phase 3 will
    substitute in the send pipeline (Q10
    resolution).
  * First-launch banner: a centred overlay that
    summarises what loaded + what Phase 1 covers.
    Dismissed by any keypress.
  * Esc / Ctrl+Q exits.

No editing, no save, no AI send — strictly an
explorer for what's in the project's prompts
library.  Phase 2 plugs in `tui-textarea` and
the surgical-save pipeline; Phase 3 wires the AI
prompt input to `spawn_chat_stream`.

CLI plumbing matches the Config TUI's setup —
the new `Command::PromptsEditor` variant adds to
the `is_tui` predicate in `main.rs` so logs route
to `.inkhaven.log` instead of corrupting the TUI
back-buffer.

New module: `src/prompts_tui/` (mod.rs + app.rs).

445 tests pass (was 439, +6 new):
  * `focus_next_cycles_three_panes`
  * `focus_prev_reverses`
  * `embedded_defaults_parse_cleanly` (regression
    guard — if `assets/default_prompts.hjson`
    drifts to invalid HJSON, this catches it
    before the TUI tries to auto-populate)
  * `help_bodies_have_expected_headings`
  * `current_prompt_handles_empty_library`
  * `current_prompt_returns_indexed_entry`

Zero build warnings.

### Phase 2 — prompts-editor editing + save (shipped)

The Phase 1 read-only walk-through becomes a real
editor.  What landed:

  * **tui-textarea in the editor pane.**  Replaces
    the Phase 1 read-only Paragraph view with a
    full TextArea carrying the focused prompt's
    template body.  All the standard tui-textarea
    mechanics work: arrow movement, Home/End,
    PgUp/PgDn, Shift+arrows selection, Backspace
    / Del, plain typing.  Emacs-style Ctrl
    shortcuts are NOT forwarded so the global
    chords (Ctrl+S / Ctrl+Q / Ctrl+H) stay live.
  * **Stash on focus / cursor change.**  Switching
    prompts in the list pane (or Tab-ing away from
    the editor) commits the in-progress edit back
    to the library entry.  Switching back picks
    up where the user left off — no per-prompt
    "discard or save" prompt.
  * **`a` add prompt** (list focus).  Name-input
    modal; on commit a fresh
    `Prompt { name, description: "", template:
    "" }` lands in the library, the list re-sorts
    alphabetically, the cursor moves to the new
    entry, and focus jumps to the editor pane so
    the writer can start typing immediately.
    Validates name shape (identifier-ish: letter
    or `_` start, then letters / digits / `_` /
    `-`) and uniqueness.
  * **`d` delete prompt** (list focus).  Confirm
    modal.  Newly-staged entries get dropped
    entirely; existing entries get added to
    `removed_prompts` and render struck-through
    in the list until Ctrl+S.  A second `d` on
    a deleted entry revokes the deletion.
  * **Ctrl+S save flow.**  Confirmation modal
    lists every pending change partitioned into
    ✚ added / ✱ modified / ✗ removed buckets.
    y / Enter commits via atomic write (`.tmp`
    + rename); the post-save copy lands in
    `<project>/.prompts-backups/prompts_YYYYMMDD_HHMMSS.hjson`.
    After save, the library reloads from the
    written file, the baseline refreshes, and
    all three staging sets clear.
  * **Esc / Ctrl+Q with unsaved changes.**
    Discard-confirm modal pops first; y discards
    + quits, n returns to editing.  Matches the
    Config TUI's safety net.
  * **Top-bar chip + status hints.**  Red `N
    unsaved` chip beside the embedded-defaults
    chip.  Status-bar chord hints swap per-pane:
    list shows `a add · d delete · Ctrl+S save`;
    editor shows `type to edit`; AI prompt still
    shows the Phase 3 deferral.
  * **List chips.**  Per-row state markers:
    `✱` unsaved edit (red bold), `✚` newly-added
    (green bold), `✗` staged for deletion (red
    strike-through + DIM).

Serialisation: the save pipeline uses
`serde_hjson::to_string` to re-emit the library —
no surgical text rewrite.  Per the proposal §6,
`prompts.hjson` is a structured library file that
the user rarely hand-formats with custom comments,
so comment preservation isn't required.  (If a
user does annotate prompts.hjson by hand and saves
from this TUI, the comments are lost — flagged in
the proposal's risks table.)

449 tests pass (was 445, +4 new):
  * is_valid_prompt_name_accepts_typical_idents
  * is_valid_prompt_name_rejects_leading_digit_and_empty
  * save_summary_partitions_added_modified_removed
  * has_unsaved_covers_all_three_buckets_and_defaults_load

Zero build warnings.

Phase 3 (AI integration — wire the AI prompt input
to spawn_chat_stream, render the streaming response
in the AI pane, prompt history with Up/Down walk)
ready when you give the word.

### Phase 3 — prompts-editor AI integration (shipped)

The bottom strip becomes the live LLM workbench.
Type a test input, press Enter — the editor's
template renders against the input + streams the
response into the AI pane.

What landed:

  * **AI runtime built at launch.**  App::load
    reads `<project>/inkhaven.hjson`,
    AiClient::from_config, then resolves the
    default provider.  All three failures are
    non-fatal: the prompts editor still launches
    with the send path inert and a clear "LLM
    not configured" notice in the AI pane.
    Reuses the same AiClient / spawn_chat_stream
    plumbing the main TUI has used since 1.1 —
    no new client.

  * **Send pipeline (Q2).**
      1. Stash the in-progress editor edit.
      2. Render the editor body as a template:
         replace `{{selection}}` with the AI
         prompt input, `{{context}}` with empty.
      3. If no placeholders, append the input on
         a fresh paragraph so templates without
         a slot still have something to operate
         on.
      4. Spawn spawn_chat_stream with
         system_prompt = None and the rendered
         text as the user message.  Matches the
         main TUI's show-don't-tell + critique
         flows.
      5. Stream tokens through an unbounded
         channel into last_send.response — drained
         every event loop tick before the redraw.

  * **AI pane.**  Two-section layout — `▸ user`
    (the test input or "no input — template sent
    as-is") + `▸ assistant` (streaming text +
    Braille spinner while live, elapsed-time on
    completion).  Title chip surfaces `AI ·
    <provider> · <model> · streaming` / `1.2s`
    / `FAILED` based on state.  Errors prepend
    `⚠ ERROR: …` to the response.

  * **AI prompt input** (single-line TextInput):
      type to edit / Backspace+Delete / Left+Right
      cursor / Home+End / Ctrl+A+Ctrl+E readline
      shortcuts / Up+Down history walk / Enter
      sends / Ctrl+L clear / Ctrl+K clear input
      + history.

  * **In-session history.**  Each successful send
    pushes the input onto ai_history (deduped
    against the most-recent entry).  Up/Down
    walks; any keystroke that mutates the input
    clears the cursor.  Title chip shows
    `history N/M` while walking.

  * **Streaming UX.**  Event loop polls every
    80ms while a stream is live (vs 250ms when
    idle) so the Braille spinner animates and
    tokens flow into the pane promptly.

454 tests pass (was 449, +5 new):
  * render_template_substitutes_selection_placeholder
  * render_template_clears_context_placeholder
  * render_template_appends_when_no_placeholders
  * render_template_no_input_no_appends
  * render_template_no_input_substitutes_with_empty

Zero build warnings.

The prompts editor is now feature-complete against
the proposal's v1 scope.  Q1 / Q2 / Q4 / Q5 all
shipped; rollback picker (config-TUI-style Ctrl+R)
lands in a follow-up if it proves useful.

### Phase 4 — prompts-editor rollback + docs (shipped)

The proposal-mandated polish phase.  Three things
landed:

**`Ctrl+R` rollback picker.**  Lists every
`prompts_YYYYMMDD_HHMMSS.hjson` file under
`<project>/.prompts-backups/`, newest-first.  Each
row shows the absolute timestamp + a relative-time
chip ("just now" / "12 minutes ago" / "yesterday"
/ "4 days ago" / absolute beyond 14 days) + file
size.  Inside the picker:

  * `Enter` *stages* the backup into the working
    library — every leaf diff vs the live file
    becomes ✱ modified; entries in the backup
    but missing locally become ✚ added; entries
    locally but missing from the backup become
    ✗ staged for deletion.  Ctrl+S commits.
  * `v` preview the file's contents (scrollable
    full-pane view; Esc back to the picker).
  * `d` delete with confirm.
  * `Esc` back to the main view.

The first Ctrl+S after a rollback writes a fresh
backup of the pre-rollback state, so the safety
chain stays intact.

New module: `src/prompts_tui/backup.rs`.
Mirrors the shape of `src/config_tui/backup.rs`
(list / read / delete / relative_time) with the
prompts-specific PREFIX (`prompts_`) and
BACKUP_DIR (`.prompts-backups`).  Five unit
tests cover relative-time variants + listing
behaviour (sort, prefix filter, missing-dir
graceful).

**Tutorial 44 — `Documentation/Tutorials/44-prompts-editor.md`.**
Full workflow walkthrough: four-pane layout
diagram, mental model ("reviewer not executor"),
send pipeline step-by-step, chord set per pane,
save semantics, rollback, auto-populate-on-first-
launch, iteration loop.

**KEYBINDING.md section for `inkhaven prompts-editor`.**
Full chord reference + send-semantics callout +
save-chip palette + rollback chord set.

**Tutorials/README.md** indexes the new tutorial as
row 44.

460 tests pass (was 455, +5 backup tests):
  * relative_time_just_now
  * relative_time_minutes
  * relative_time_yesterday
  * list_sorts_newest_first_skips_unrelated
  * list_missing_dir_returns_empty

Zero build warnings.

The prompts-editor TUI is now fully feature-
complete against the proposal — Phases 1-4 all
shipped; Q1 / Q2 / Q4 / Q5 / Ctrl+G addition all
in code; rollback + tutorial + KEYBINDING.md
in docs.


