# v1.2.7 — Polish + plumbing

1.2.7 is a polish cycle on top of the timeline-heavy 1.2.6
release.  Two themes carry the branch:

1. **Timeline + ergonomics polish.**  The story timeline
   gained tree-style navigation, track collapse, span-aware
   event selection, grid stripes, a book-wide F12 critique,
   per-book session persistence, and a quick-event-from-
   selection chord that works in any pane.  Around it:
   paragraph undelete, browser-style visited-paragraph
   navigation, a recent-¶ picker, mouse-capture toggle for
   native terminal copy-paste, an external-change auto-reload
   watcher that detects when CLI / sed / git pull touched
   the open paragraph.
2. **Internal architecture rework.**  `src/tui/app.rs` —
   the editor's TUI brain — went from a 26,605-LOC monolith
   to a 13,939-LOC central dispatcher with 21 sibling /
   child modules hanging off it (text helpers, modal +
   state types, splash painters, render dispatcher with
   modal / pane painter sub-modules, 8 `impl App` domain
   splits).  Pure refactor — every commit byte-equivalent
   modulo visibility / paths, verified by a programmatic
   per-method diff after the dust settled.  255 → 260 tests
   pass; zero build warnings.

The branch carries 37 commits.  The bug fix list is
intentionally short — bug reports from real users surface
slowly, and this cycle's bandwidth went to ergonomics +
plumbing.

## Headline changes

### Timeline — six polish refinements

The 1.2.6 swim lanes shipped with single-Tab navigation
through tracks and no persistence.  1.2.7 layered:

- **Two-level Tab/Enter navigation** mirroring the tree pane.
  Tab cycles tracks; Enter expands a track into a text
  sub-row of its events; Tab inside the expansion cycles
  events in chronological order; Enter on an event opens
  the LinkPicker modal over its linked paragraphs.  Esc /
  Backspace pops back up.
- **Track collapse** with `Space`.  Collapsed tracks render
  as `▸ track-name · N events`; expanded ones as full swim
  lanes prefixed `▾`.
- **Span-aware event selection.**  `↑` / `↓` pick the
  previous / next event by start tick AND highlight every
  cell carrying that event id.  Viewport auto-pans so both
  `start_ticks` and `end_ticks` are visible — duration
  events read clearly even when zoomed in.
- **Grid stripes.**  Every Nth column gets a faint dotted
  backdrop so the eye has something to register against at
  wide zoom.  N follows the visible span (weeks at day
  zoom, years at month zoom).
- **`F12` full-book critique** from inside the timeline
  view.  Same payload as `Ctrl+Y` (book-wide health check)
  via a more direct chord.
- **Quick-event from selection.**  `Ctrl+V Shift+E` reads
  the editor selection, the tree paragraph title, or the
  timeline cursor — whatever's contextually present — and
  pre-fills the new-event prompt with it.  Two chords to
  drop a calendar event for the scene under the cursor.
- **Book-slug prefix** in the project overlay (`p` chord).
  Cross-book tracks now render as `aerin-saga/main` vs
  `khaal-arc/main` instead of two ambiguous rows called
  `main`.

Session persistence captures the per-book view state in
`.session.json`: collapsed tracks, expanded track, track
highlight, zoom, scroll tick, cursor tick.  Re-opening the
timeline for the same book restores all six.

See [`Tutorials/31-story-timeline.md`](../Tutorials/31-story-timeline.md)
"1.2.7 polish".

### Paragraph undelete (Ctrl+B U)

A single-slot kill-ring for paragraph deletions.  After a
Ctrl+D delete-confirm, `Ctrl+B U` restores the paragraph at
its original position, with body, tags, status,
linked-paragraph cross-refs, and event timing intact.  The
restored paragraph gets a fresh uuid; the status bar is
upfront that incoming wiki-links to the old uuid stay
broken.

The slot is single-paragraph only — branch deletes (chapter
/ subchapter / book) can't be undone and clear the slot
when they fire.  Session-local; restart starts empty.

See [`Tutorials/32-paragraph-undelete.md`](../Tutorials/32-paragraph-undelete.md).

### Visited-paragraph history (Alt+← / Alt+→ + Ctrl+V Shift+P)

Browser-style back / forward across every code path that
opens a paragraph: tree Enter, fuzzy picker, bookmark
picker, wiki-link follow, snapshot picker, similar-¶
picker, timeline Enter, undelete.

```
Alt+←           step backward through visited paragraphs
Alt+→           step forward (after stepping back)
Ctrl+V Shift+P  recent-paragraph picker (most-recent-first list,
                up to 32 entries)
```

Opening a NEW paragraph (anything not Alt+→) clears the
forward stack — same as a web browser.  Ring is in-memory
only; restart clears it.

See [`Tutorials/33-navigation-history.md`](../Tutorials/33-navigation-history.md).

### Mouse capture toggle (Ctrl+Shift+M)

`Ctrl+Shift+M` flips between two modes:

- **Capture ON** (the default): click-to-focus, scroll-wheel
  per-pane, in-TUI drag-select.
- **Capture OFF**: the terminal handles drag-select; the
  terminal's native copy keystroke (Cmd+C on macOS,
  Ctrl+Shift+C on Linux) copies the highlight to the
  system clipboard.  Useful for grabbing a snippet to paste
  elsewhere, or copying mid-stream AI output without
  routing through the Ctrl+C selection-mode chord-flow.

Session-only; defaults to ON every fresh launch.

### External-change auto-reload

Once per autosave tick (every keystroke + idle timer)
the editor checks whether the open paragraph's file changed
on disk.  Three behaviours:

- **mtime unchanged**: no-op.
- **mtime newer, buffer clean**: silent reload + status
  `↻ reloaded …`.  Cursor jumps to (0, 0) since the previous
  position may not survive an external rewrite.
- **mtime newer, buffer dirty**: red warning, buffer
  untouched: `⚠ <title> changed on disk while you have
  unsaved edits — Ctrl+S to overwrite the external change`.

Triggers: `inkhaven event add` (events live as paragraphs),
`sed -i`, `git pull`, another instance of the TUI, Bund
scripts that mutate paragraph files.  Cost: one
`std::fs::metadata().modified()` per tick — cheap.

See [`Tutorials/34-mouse-and-external-changes.md`](../Tutorials/34-mouse-and-external-changes.md).

### Snapshot dedupe

The F5 / autosave snapshot pipeline drops a new snapshot
when the buffer content matches the previous snapshot's
content.  Avoids growing the F6 picker with duplicate rows
when nothing meaningful changed (common during status
toggles, metadata-only edits, and the autosave loop firing
on a clean buffer).

The dedupe is content-based — annotation differences alone
don't create a new snapshot if the body is byte-identical.

### F8 from any pane

The typst-diagnostics list modal previously needed editor
focus.  In 1.2.7 it works from tree, AI, and search panes
too — opens against the most-recently-active paragraph's
cached diagnostics.

### today widget gets colour

The status-bar "today: N/M words" widget now colours by
goal progress:

- red `<25%` of goal
- yellow `25–75%`
- green `≥75%`
- bold-green when goal-hit / exceeded

Visual nudge without taking up more status-bar real estate.

## Internal — architecture refactor

Not user-visible, but worth surfacing for anyone
contributing or following the source: `src/tui/app.rs` went
from 26,605 LOC to 13,939 LOC over 22 mechanical-extraction
commits.  21 new modules now sit alongside or under `app.rs`:

```
src/tui/
  text_utils.rs           string / time / wrap helpers
  status_helpers.rs       Napkin → Ready ladder
  hjson_edit.rs           in-place HJSON rewrites
  diff_utils.rs           F6→V snapshot diff
  splash.rs               every centered "Please wait" overlay
  backup_ui.rs            maybe_auto_backup + run_manual_backup
  credits.rs              CARGO_PKG_* + embedded logo
  lexicon_build.rs        LexiconKind + build_lexicon
  timeline_state.rs       TimelineViewState + TimelineEvent + helpers
  inference.rs            AiMode, InferenceMode, Inference, …
  state.rs                Keymap, OpenedDoc, SplitView, BookStats, …
  modal.rs                Modal enum + 28-variant picker types
  app/
    render.rs             draw_modal dispatcher + render_template
    render/modals.rs      28 draw_*_modal painters
    render/panes.rs       14 pane painters + status / footer / search chrome
    timeline_impl.rs      33 timeline_* methods on App
    tag_impl.rs           13 tag picker methods
    snapshot_impl.rs      8 snapshot-flow methods
    backup_impl.rs        2 manual-backup wrapper methods
    tree_impl.rs          6 tree-pane navigation methods
    ai_impl.rs            18 AI / inference orchestration methods
    editor_impl.rs        27 editor buffer / save / load methods
```

Every commit on the branch is byte-equivalent modulo
visibility downgrades + path-prefix rewrites — verified
post-hoc by a programmatic per-method diff (561 method
bodies pre and post; exactly one differed, and that
difference was the `save_current` mtime restamp bug call-
out below).

The refactor changes nothing externally: same crate, same
binary entry point (`tui::run`), same API surface for the
scripting bridge.

## Fixes

- **Ctrl+S no longer jumps the cursor to (0, 0).**  Pre-
  existing bug since the 1.2.7 external-change auto-reload
  landed: `save_current` wrote the file but never restamped
  `doc.loaded_mtime`, so the next autosave tick saw the
  fresh mtime, considered it an external edit, and silently
  reloaded the buffer.  The reload path resets the cursor.
  Fixed by adding the same three-line restamp `save_doc`
  already had.  Surface symptom — every Ctrl+S snapped the
  cursor home — is gone.
- **`src/export/markdown.rs:166` duplicate-comparison.**
  `trimmed == ")" || trimmed == ")"` was a copy-paste
  artifact — the second operand identical to the first.
  Collapsed to the single comparison the runtime already
  evaluated.  No behaviour change; clippy was failing on
  the binary because of it.
- **5 environment-gated tests now run in CI.**  The
  story-view + typst-render + typst-inprocess end-to-end
  smoke tests previously carried `#[ignore]` because typst
  / resvg need OS-discoverable fonts.  CI installed
  `libfontconfig1-dev` (headers) but no fonts.  CI now
  installs `fonts-dejavu-core`; `#[ignore]` removed; doc
  comments updated.

## Migration

No breaking changes.  Existing projects upgrade
transparently:

- The kill-ring, navigation ring, and mouse-capture toggle
  are session-only and need no config.
- External-change auto-reload is always-on and has no knob.
  Cost is one syscall per tick, well below noise.
- Timeline session persistence reads / writes a new
  `timeline_views` field in `.session.json`.  Old
  `.session.json` files lacking the field load as an empty
  HashMap — fresh state on first open.
- The internal refactor is invisible to project / config /
  Bund script consumers.  Anyone depending on
  `crate::tui::app::TimelineEvent` should switch the import
  to `crate::tui::timeline_state::TimelineEvent` — that's
  the only public type that moved across module
  boundaries.

## Statistics

- 37 commits between 1.2.6 and 1.2.7.
- 260 tests passing, 0 failed, 0 ignored.
- `src/tui/app.rs`: 26,605 → 13,939 LOC (−12,667, ~47.6%
  smaller).
- 21 new tui modules.
- 4 new tutorials (32–34) plus a "1.2.7 polish" section on
  tutorial 31.

## What's next

1.2.8 picks up the still-pending 1.2.7-planned items:

- `inkhaven export timeline` to SVG / PNG / Typst.
- Cascade-impact AI on event re-time
  (Ctrl+V Shift+I opt-in).
- Scrivener `<CustomMeta>` date import bridge.
- F11-on-paragraph "suggest event" variant.

Plus whatever real-project polish surfaces in the first
weeks after 1.2.7 ships.

— inkhaven 1.2.7.
