# Inkhaven 1.2.8 — Shell, config editor, surface sharpening

1.2.8 picks up the items 1.2.7 left on the table and adds two
significant new surfaces — the **embedded nushell pane**
(`Ctrl+Z o`) and the **in-TUI HJSON config editor**
(`Ctrl+B 0`).  Plus the Help book now renders as
markdown, `export-timeline` ships SVG / PNG renderers,
Scrivener imports pick up `<CustomMeta>` dates, mouse
wheel covers every modal, and a small mountain of
ergonomics refinements.

Two of the new surfaces deserve special mention:

- The **shell pane** runs nushell *in-process* via
  `nu_engine` + `nu_command` — no subprocess, no PTY.
  External commands get stdout + stderr captured; the
  TUI is protected from full-screen apps via a
  configurable blocklist plus a wall-clock watchdog.
  Tab autocomplete spans nu commands, `$PATH`
  executables, and filesystem paths.  Per-project SQLite
  history; readline-style line editing.
- The **HJSON editor** is the same `tui-textarea` the
  paragraph editor uses, but stripped of all
  per-paragraph chrome.  Save flags a *Restart required*
  overlay when the config actually changed (live-reload
  is intentionally not supported — frozen-at-startup is
  simpler and more predictable).

Read on for the full picture.

## Planned themes

### Timeline — the items 1.2.7 didn't ship

- **`inkhaven export timeline` (SVG / PNG / Typst).** The
  swim-lane view + the event picker are TUI-only today.
  An export subcommand renders the same view to a file so
  authors can attach the timeline to a query letter, a
  pitch doc, or a private wiki page. Format flags mirror
  `inkhaven export pdf` style. Tracks the
  `crate::tui::timeline_render` layout module so the file
  output and the screen output stay byte-for-byte aligned.
- **Cascade-impact AI when an event is re-timed.**
  `Ctrl+V Shift+I` (the edit-event prompt) currently
  rewrites timing without flagging that 6 linked paragraphs
  now refer to a date in the past, that a season-precision
  event now overlaps a war it didn't before, etc. New
  opt-in (config-gated) chord sends the before/after state
  plus every linked paragraph to the AI for a cascade
  audit. Probably reuses the existing health-critique
  payload builder.
- **Scrivener `<CustomMeta>` date import bridge.** The
  Scrivener importer (1.2.4) ignores `<CustomMeta>` fields
  even when they carry obvious dates. Map known field
  names (`Date`, `Story Date`, `Event Date`, …) onto
  events under a per-imported-book Timeline chapter. The
  mapping table itself wants to be HJSON-configurable so
  authors using non-English Scrivener templates can name
  their own field.

### AI polish

- **F11-on-paragraph "suggest event" variant.** When the
  cursor sits inside prose that names a date or a clearly
  scene-shaped moment, F11 (or Ctrl+F12 on macOS, since
  macOS grabs F11) gains a paragraph mode that proposes
  a draft event from the prose. User accepts → event
  lands in the book's Timeline chapter, pre-linked to the
  source paragraph. The default-on prompt template lives
  in `tui::app::timeline_suggest_default_prompt`.

### Persistence + UX follow-ups from 1.2.7

- **Navigation ring across restarts.** The 1.2.7 `Alt+←` /
  `Alt+→` history is in-memory only. Persisting it to
  `.session.json` requires careful pruning of stale uuids
  (paragraphs that no longer exist) but would survive a
  TUI restart cleanly. Optional; gated behind a HJSON knob.
- **Mouse-capture default in HJSON.** 1.2.7's
  `Ctrl+Shift+M` toggle is session-only and starts ON.
  Add an `editor.mouse_captured` knob so authors who
  prefer terminal-native select can opt out by default.

### Real-project polish (rolling)

Bug reports from real users land here. Currently empty.

## What's landed so far in 1.2.8

### Multi-slot kill-ring + Shift-letter chord fix

Widened `App.last_deleted: Option<Stash>` to
`App.kill_ring: VecDeque<Stash>` capped at 10.  New chord
`Ctrl+V Shift+U` opens a picker over the ring; `Ctrl+B U`
still pops the front (most-recent) without opening the
modal.  Branch deletes no longer clobber the ring — older
single-¶ entries remain valid recoveries.

The same commit fixes a pre-existing chord matcher bug:
`Ctrl+V Shift+P` collapsed onto `Ctrl+V p` (so the recent-¶
picker never actually opened) on terminals without the
kitty disambiguation protocol.  Uppercase letters arriving
without the explicit SHIFT modifier flag are now treated
as implicit-Shift; `Ctrl+V Shift+*` chords route to their
intended actions across all terminals.

### Surface polish

- **Active-LLM chip in AI pane title.**  Always shows
  `· llm=<provider>` so `Ctrl+B L` swap is visible without
  opening `Ctrl+B I`.  In-flight provider fragment suppresses
  redundancy when running on the bound default.
- **F1 query history** — Up/Down inside the Help-query
  input recalls / steps forward through previous queries.
  Session-only (intentional — keeps stale "what did 1.2.5
  do for X" out of long-running ring tails).
- **Tag autocomplete** — `Tab` inside the `A` (add-new-tag)
  prompt completes to the first existing project tag whose
  name starts with the typed prefix.

### Embedded nushell pane

`Ctrl+Z o` opens a floating shell inside the TUI window.
Runs **real nushell** in-process via `nu_engine` +
`nu_command` — the full default command set (`ls`, `where`,
`str`, `path`, `into`, pipelines, env mutations) without
shelling out.  Output of any command can be selected
(`Ctrl+Z h` enters selection mode, ↑↓ walks turns) and
either `c` copied to clipboard or `i` inserted into the
editor wrapped as a typst `#raw(block: true, …)` block
(template HJSON-configurable).

```
Ctrl+Z o    open / close (engine + history preserved)
Ctrl+Z O    open fresh (drop cached engine + ring)
Ctrl+Z h    inside the pane, toggle selection mode
PgUp/PgDn   scroll the turn buffer (10 lines/press)
Shift+Home  jump to top of buffer
Shift+End   jump back to newest output
```

External command output is **captured**, not inherited —
`^/bin/ls -l`, `^git status`, … all route their stdout +
stderr through a `Stack::push_redirection(Pipe, Pipe)` guard
so the bytes land inside the pane instead of escaping into
the ratatui alternate-screen surface.  Stderr is **merged
into stdout** (the `Pipe`-on-both contract), so a failing
`^/bin/ls /nonexistent` surfaces its `No such file or
directory` message in the pane like a real terminal —
including when the external exits non-zero, because the
captured ChildProcess has `ignore_error=true` flipped on
it before draining so the bytes survive the exit-code
check.  Per-turn output is capped at `shell.max_output_lines`
lines (default 1000) so a single `git log` doesn't bloat
the buffer; excess tail is replaced with a `… (N more lines
truncated)` marker.

Layout polish: short turn buffers (a single `ls` in a
fresh session) now anchor to the **bottom** of the pane
just above the prompt, instead of floating at the top of
a 60-row body with a huge empty gap between output and
prompt.  Long buffers that exceed the body height
continue to scroll normally.  When NOT scrolled, the
render also clamps the visible window's top to the
start of the most-recent turn — so after a `help
commands` (truncated to 1000 lines) followed by `ls`,
the `ls` output appears alone at the bottom instead of
sharing the screen with the tail of the help output.
PgUp brings the older turns back into view.

Typing `exit` or `quit` at the shell prompt closes the
pane (terminal muscle memory) rather than forwarding to
nu's `exit` built-in, which would call
`std::process::exit()` and take the whole editor down
with it.  Any trailing args are ignored — `exit 1`,
`quit`, `exit anything` all close the pane.

**Readline-style line editing.**  The shell prompt now
honours the chord set familiar to bash / zsh users:
`Ctrl+A` / `Ctrl+E` for start / end of line; `Ctrl+U` /
`Ctrl+K` for kill-to-start / kill-to-end; `Ctrl+W` to
kill the previous word; `Ctrl+Left` / `Ctrl+Right` and
`Alt+B` / `Alt+F` for word-wise motion; `Alt+Backspace`
for kill-word-back; `Ctrl+L` to clear the scrollback
(engine + history preserved); `Ctrl+D` to clear input
(or close the pane when input is empty).  Hyphens stay
word-chars so `--short` counts as one logical token.

**Colored prompt.**  Replaced the bare `$ ` prefix with
`[ <cwd> > ` — `[` in white, `<cwd>` in blue (`$HOME`
abbreviated to `~`, tracking `cd`), `>` in red.  Input
after the `>` keeps the existing syntax-highlight pass
from nu's flat-shape tokeniser.

**Ctrl+B H in-pane help.**  A 40-line overlay describes
line-editing chords, pane chords, sample nu commands,
and a one-paragraph orientation about what the
embedded shell is and isn't.  Any key dismisses the
overlay; pane state is preserved unchanged underneath.

**Tab autocomplete.**  Tab in the pane prompt completes
the token under the cursor.  In command position (start
of input or after `|` / `;` / `(`) it matches nu's
declared command set (all built-ins + any `def`s the
user has made this session) **plus** executables on
`$PATH`.  In argument position (and for any
command-position token containing `/`) it matches
filesystem entries under the engine's current working
directory; directories come back with a trailing `/`
so a second Tab can walk into them.  Single match →
splice into input + trailing space; multiple → splice
the longest common prefix and list candidates on the
status bar (`shell: 5 matches — string  str  ...`).

**Full-screen TUI safety net** (three layers):

- *Blocklist.*  A pre-spawn check against
  `shell.blocked_externals` refuses commands whose
  basename matches a known full-screen app — `vim`,
  `nvim`, `less`, `top`, `htop`, `tmux`, `mc`, `fzf`,
  `ssh`, `gdb`, `psql`, `sudo`, … (default ~45 entries,
  fully HJSON-configurable).  Case-insensitive,
  basename-matched, so `^vim`, `^/usr/bin/vim`, `^VIM`
  all hit.  The user gets a clear "blocked: `vim` is a
  full-screen TUI app and would corrupt the editor's
  alt-screen surface" message and the engine is left
  untouched.
- *Null stdin.*  The embedded engine sets
  `EngineState::is_mcp = true`, which causes nu's
  external runner to spawn children with
  `Stdio::null()` for stdin instead of inheriting our
  TTY.  Apps that respect `isatty(STDIN_FILENO)`
  (well-behaved REPLs, password prompts, most
  cooperative TUIs) fail-fast.  Doesn't stop apps that
  open `/dev/tty` directly — that's what the blocklist
  is for.
- *Wall-clock watchdog.*  Every eval runs in a worker
  thread; the main thread polls with
  `shell.external_timeout_secs` (default 30s).  On
  timeout, nu's interrupt signal fires; if the worker
  responds within a 2-second grace window, the engine
  is recovered cleanly.  If the worker stays wedged
  (vim blocking on `/dev/tty` despite signal), the
  engine is **rebuilt from scratch** — env vars / `def`
  declarations / `cd`-mutated `$env.PWD` are lost, but
  the TUI stays responsive.  The user sees a status
  message explaining what happened and how to add the
  offending binary to the blocklist.

Per-project SQLite history at
`<project>/.inkhaven/shell_history.db` — survives restart.
Engine state (env vars, defs) survives modal close + reopen.
Configurable via HJSON: `shell.enabled` (default true),
`shell.max_buffered_turns` (default 50),
`shell.max_output_lines` (default 1000),
`shell.insert_template` (default typst raw block).

Out of scope (deliberate): long-running TTY apps (`vim`,
`top`, `less`), reedline integration, plugins.  This is for
one-shot pipelines whose output you want to capture or
inspect — not a full terminal emulator.  Use a separate
terminal for interactive workflows.

Binary size: +18M (103→121M end-to-end across the seven
shell-phase commits), of which ~13M is rusqlite's bundled
SQLite and ~5M is nu's core + standard command set.  LTO
+ strip aggressively trim nu's built-ins we don't reach
via runtime dispatch.  See
[`Tutorials/35-embedded-shell.md`](../Tutorials/35-embedded-shell.md).

### Ctrl+Q confirmation

New HJSON knob `editor.confirm_quit` (default `false`).
When `true`, `Ctrl+Q` opens a yellow `Confirm quit`
modal instead of quitting immediately; `Y` / `Enter`
confirm, `N` / `Esc` cancel.  The existing autosave-on-
quit behaviour still runs after confirmation.

Default stays `false` so muscle memory doesn't break for
users who like the immediate-quit semantics.  Helpful
when terminal software-flow-control or accidental
chord-mashing keeps killing the editor.

`Ctrl+Q` inside an already-open modal bypasses the
confirmation — that path was the unconditional escape
hatch and stays so.

### Markdown in the Help book

The Help system book is read-only documentation, so its
paragraphs now render as **fully-rendered markdown**
(via the same pulldown-cmark pipeline the AI pane uses
for streamed assistant turns) — not just colored source.
Heading prefixes (`#`), emphasis delimiters (`**`, `*`,
`_`), bullet markers, and link `[brackets](and-urls)`
are stripped or styled rather than shown verbatim,
giving a document-viewer feel.

New paragraphs created under the Help book also default
to `content_type = "markdown"` and a `# Title` template
on disk.  Detection is by walking the parent chain for
the `SYSTEM_TAG_HELP` system tag — the tag is sticky
across renames, so renaming the Help book doesn't break
either the rendered-view swap or the default content-type
assignment.

Source-level markdown syntax highlighting (the
`markdown_highlight` lexer added alongside the rendered
view) stays available for any future paragraph that's
markdown-typed but NOT read-only — `# heading` shows as
bold function-colour, fenced code in string-colour,
links separated into text + URL spans, etc.  It's not
on any path by default outside the Help book.

**Scope is deliberately limited to the Help book.**
Every other book — user books, Notes, Research, Prompts,
Places, Characters, Artefacts, Typst, Scripts — keeps
the existing Typst default + Typst source view.
Authors don't get markdown silently applied to their
fiction.

### Mouse wheel everywhere

Wheel scroll now covers the panes and modals that were
previously key-only:

* **AI pane** — wheel up shows older chat turns, mirroring
  the `PgUp` keybinding (3 lines per tick).
* **OS Shell pane modal** — wheel adjusts the turn-buffer
  `scroll` field, same direction as `PgUp` / `PgDn`.
* **HJSON editor modal** — wheel moves the textarea cursor
  up / down 3 lines, preserving any in-progress selection.
* **Kill-ring picker / Fuzzy paragraph picker** — wheel
  walks the entry cursor (3 per tick, clamped to the list
  bounds).

Tree + Editor wheel behaviour is unchanged.  Other modals
(pickers without a meaningful scroll axis, the floating
preview modals) ignore the wheel silently.

### Edit project HJSON from inside inkhaven

**`Ctrl+B 0`** opens `<project>/inkhaven.hjson` in a
full-screen modal editor.  No paragraph chrome —
stripped of typst diagnostics, lexicon hits, the
gutter-diff overlay, match highlights, and the
correction-baseline view.  Just a textarea + HJSON
syntax highlighting (reusing the existing
`hjson_highlight` lexer that already powers `.hjson`
file editing in the main editor).

`Ctrl+S` writes the buffer to disk.  When the saved
bytes differ from the bytes loaded on open, a
centered **"Restart required"** overlay pops up: the
running editor is still using the OLD config until
the user actually relaunches inkhaven.  Any key
dismisses the overlay; the modal stays open so the
user can continue editing.  Subsequent saves of
unchanged bytes don't re-fire the overlay (baseline
updates on each save).

`Esc` closes the modal.  When the buffer has unsaved
edits, the status line warns; data on disk is
untouched.

All standard `tui-textarea` editing chords work —
arrows, Home/End, PgUp/PgDn, Shift+arrows for
selection, `Ctrl+Z` undo, `Ctrl+Y` redo, the readline
kill/yank set — matching the paragraph editor.

### Plumbing

- **`editor.mouse_captured` HJSON knob** — initial mouse-
  capture state on launch (default `true` = unchanged
  behaviour).  `false` lets the terminal's native drag-
  select + clipboard copy work without pressing
  `Ctrl+Shift+M` first.
- **`inkhaven export-timeline`** subcommand — three
  formats now ship: `typst` (text listing, default),
  `svg` (vector swim-lane render with date axis, instant
  circles, duration bars, dashed orphan markers), `png`
  (the same SVG rasterised through `resvg` + `tiny-skia`).
  Optional `--track <name>` filter; canvas is sized
  ~1200 × (30 + 25 + 40·track_count + 24) px and scales
  with the number of tracks.
- **Scrivener `<CustomMeta>` date import.**  The binder
  parser now reads `<CustomMetaDataSettings>` registries
  + per-item `<CustomMetaData>` values.  When
  `timeline.enabled = true`, fields matching
  `scrivener.date_fields` (default `["Date", "Story
  Date", "Event Date"]`, case-insensitive) parse against
  the project's calendar; successful parses attach
  `EventData` to the imported paragraph.  Bad values
  become non-fatal warnings on the import report.

### Ergonomics chords

- **`Ctrl+V h`** — one-shot status-bar report of the open
  paragraph's tabs / trailing-whitespace lines / CR
  characters.  Visual editor overlay scheduled for 1.2.9.
- **`Ctrl+V Shift+S`** — print the cursor's full
  hierarchy path on the status bar (`Book ▸ Chapter ▸
  Subchapter ▸ Paragraph`).  Pane-aware.
- **F6 annotation filter** — `/` inside the snapshot
  picker enters filter-focus mode; typed characters
  narrow the visible list to snapshots whose annotation
  contains the substring (case-insensitive).  Esc exits
  filter mode (keeps query); D / V / Enter only fire
  outside filter mode.

### Persistence — already shipped pre-1.2.8 (doc fix only)

Tutorial 33 wrongly claimed the `Alt+←/→` visited-paragraph
ring was in-memory only.  It already persists to
`.session.json` (since `95a7f8c`), along with the
tree-cursor and opened-paragraph state — so "the TUI opens
where I left it" works across restarts.  Documentation
corrected; no code change.

## Still on the table

- **Cascade-impact AI on event re-time.**  Optional
  Ctrl+V Shift+I extension.
- **F11-on-paragraph "suggest event" variant.**
- **Visual editor overlay for `Ctrl+V h`.**  Tab + trailing-
  whitespace glyphs without breaking the column-bookkeeping
  used by match + lexicon highlights.
- **`export-timeline --width <px>`** for users who want a
  specific PNG resolution.  Currently the canvas is fixed
  at 1200 px wide; the SVG scales vector-wise but the PNG
  raster follows the SVG's intrinsic size.
- **`export-timeline` from inside the TUI** (chord, not just
  CLI).

## Smaller changes

Currently empty. Will fill in as work lands.

## Migration

No breaking changes planned. 1.2.8 is a polish cycle on top
of 1.2.7; existing projects upgrade transparently.

## Statistics

- 139 commits since 1.2.7
- 61 files changed (`+31,172 / −12,756` lines, source +
  docs combined)
- 312 tests passing (was 260 at 1.2.7 — +52 new tests
  across the embedded-shell, HJSON-editor, markdown
  highlight, mouse-wheel, blocklist, and watchdog
  surfaces)
- 2 new tutorials (36 — HJSON editor, 37 — Help book
  viewer); 35 updated as new chords landed
- Binary size: roughly +18 MB end-to-end across the
  shell phases (~13 MB rusqlite bundled SQLite +
  ~5 MB nu's core + standard command set, LTO + strip
  trims unreachable built-ins)

## What's next

1.3 — full-screen split-view ergonomics and the F4
split-edit workflow.  Same theme as the 1.2.6 / 1.2.7
"What's next" sections; the rework keeps slipping because
each polish cycle finds enough small items to fill the
branch.  1.3 is the line where it actually happens.

— inkhaven 1.2.8, released.
