inkhaven 1.2.21

Inkhaven — TUI literary work editor for Typst books
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
# 3 — The editor

The editor pane is where you spend most of your time. This tutorial
covers movement, selection, find/replace, snapshots, split-edit, and
the visual feedback the editor gives you about save / dirty / read-only
state.

The keyboard is the primary interface. Everything works without the
mouse, but click and scroll work too (see
[`../KEYBINDING.md`](../KEYBINDING.md) §0).

## Opening a paragraph

In the Tree pane, navigate to a paragraph row and press `Enter`. Focus
moves to the Editor pane and the paragraph's `.typ` file loads. The
border colour reflects state:

- **Green** — saved (clean)
- **Yellow** — modified (dirty)
- **Teal** — read-only (Help subtree)
- **Plain border** — pane is unfocused

The pane title shows the paragraph's title, the dirty / read-only
chip, and the live `L<row> C<col>` cursor read-out (in sky-blue by
default — `theme.editor_position_fg`):

```
Editor — Opening Scene · L4 C18
```

## Movement

| Key | Action |
| --- | ------ |
| arrows | Move cursor one cell |
| `Ctrl+Left` / `Ctrl+Right` | Move one word |
| `Home` / `End` | Start / end of line |
| `Ctrl+Home` / `Ctrl+End` | Top / bottom of buffer |
| `PageUp` / `PageDown` | One viewport |
| `Shift+arrows` | Extend linear selection |

Mouse:

- Left-click positions the cursor where you click (gutter clicks are
  ignored).
- Scroll wheel scrolls by 3 lines.

## Selection

Two selection modes:

### Linear selection

Hold Shift while moving the cursor. Standard text-editor behaviour.
The selection range is highlighted with `REVERSED` background. Cut /
copy / paste operate on this selection.

### Vertical block selection

For multi-line column edits. The flow:

1. `Alt+arrow` enters block-select mode and starts an anchor at the
   current cursor. The selection forms a rectangle from the anchor
   to the cursor.
2. Keep moving with `Alt+arrows` to grow the rectangle.
3. `Alt+C` copies the rectangle to the system clipboard as a
   multi-line string (one row per line). Anchor cleared.
4. `Esc` cancels without copying.

Rectangular **paste** is not yet supported in this release — block
mode is copy-only.

## Clipboard

Inkhaven uses the system clipboard via `arboard`. Behaviour fallbacks
gracefully if no clipboard is available (e.g. headless SSH session)
— operations still work using tui-textarea's internal yank buffer.

| Key | Action |
| --- | ------ |
| `Ctrl+C` | Copy selection to system clipboard |
| `Ctrl+K` | Cut selection |
| `Ctrl+P` | Paste at cursor |
| `Ctrl+A` | Select all |

(`Ctrl+V` would conflict with terminal paste bindings on many setups
— `Ctrl+P` is the explicit Inkhaven paste.)

## Edits

| Key | Action |
| --- | ------ |
| `Ctrl+U` | Undo |
| `Ctrl+Y` | Redo |
| `Ctrl+D` | Delete current line |
| `Ctrl+E` | Delete from cursor to end of line |
| `Ctrl+W` | Delete from cursor to start of line |
| `Ctrl+Backspace` | Delete previous word |

`Ctrl+Z` is **intentionally unbound** (it conflicts with the shell's
job-control SIGTSTP on many setups). Use `Ctrl+U` for undo.

## Save

`Ctrl+S` writes the `.typ` file to disk, updates bdslib metadata,
re-embeds the content, and clears the dirty flag. The status bar
reports:

```
saved books/sample-novel/01-chapter-one/01-opening.typ (412 words, re-embedded)
```

Three save triggers:

1. **Explicit**`Ctrl+S`.
2. **Idle autosave** — after `editor.autosave_seconds` of typing
   inactivity (default 5 s). Disabled when a grammar-correction
   highlight is active.
3. **Implicit on focus / paragraph switch** — autosaves a dirty
   paragraph when you switch panes or open another paragraph.

The title chip `[modified]` appears when dirty:

```
Editor — Opening Scene [modified] · L4 C18
```

## The visual overlays

The editor renders several overlays on top of plain Typst text. They
compose:

| Overlay | Source | Style |
| ------- | ------ | ----- |
| Syntax highlight | tree-sitter-typst | Per-token colours from `theme.syntax_*` |
| Lexicon (Places / Characters) | system books | cyan / yellow bold (see [LOCATIONS.md]../LOCATIONS.md) |
| Added-since-save | LCS diff vs. `saved_lines` | bold |
| Grammar-correction | LCS diff vs. correction baseline | `theme.grammar_change_fg` red |
| Find / replace match | regex hits | `theme.search_match_bg` |
| Current find hit | regex cursor | `theme.search_current_bg` |
| Current line | cursor row | `theme.current_line_bg` background |
| Selection | tui-textarea selection_range | REVERSED |

You can leave them all on; conflicts resolve in a deterministic order
so the most actionable cue wins (selection > search match > grammar
diff > lexicon > syntax).

## Focus mode (distraction-free)

`Ctrl+B W` hides every other pane — Tree, AI, Search, AI-prompt —
and gives the editor the full window. Useful for long drafting
sessions where the cross-pane chrome is more distraction than
help. Re-press to restore the four-pane layout. The chord is
internally still called "typewriter mode" in some log strings
and the HJSON serde key (`global.toggle_typewriter`); the chord
binding has been there since early 1.2 but the documentation
calls it "focus mode" from 1.2.9 onward because that's what it
actually does.

Mutually exclusive with `Ctrl+B K` (AI-fullscreen). Toggling
either turns the other off — there's no "AI + editor split with
everything else hidden" mode by design.

## Style warnings (filter words)

`Ctrl+B Shift+F` toggles an inline overlay that
underlines stylistically weak words in amber.  Today
the overlay flags **filter words** — intensifier
crutches (`just`, `really`, `very`), hedges (`seemed`,
`felt`, `appeared`), and generic placeholders
(`actually`, `basically`).  The writer's job is to
question whether each flagged word earns its place;
none of them are always-delete.

Built-in word lists ship for `english`, `russian`,
`french`, `german`, `spanish`.  The active list is
selected by the project's top-level `language` field —
no per-paragraph language switching.  Russian users
get a curated list of `очень / просто / именно /
довольно / казалось / выглядел / вдруг / возможно`
+ more.

Add your own:

```hjson
editor: {
  style_warnings: {
    enabled: true
    filter_words: {
      enabled: true
      extra_words: ["lifted", "shifted", "blinked"]
    }
  }
}
```

Words in `extra_words` apply on top of the language
default — case-insensitive, exact-word match (no
substring partials, so `lifted` won't flag
`shoplifted`).

The overlay composes with other editor highlights:
selection still reverses, search match still wins, the
filter underline persists underneath.  No performance
cost — the detector runs once per render frame on the
visible rows and the comparison is a `HashSet<String>`
lookup per Unicode-segmented word.

Future detectors (show-don't-tell, sentence rhythm)
will share the same toggle.

### Repeated phrases

The repeated-phrase detector flags every occurrence of
any 4-word phrase (configurable via
`style_warnings.repeated_phrases.n`) that appears 3+
times in the open paragraph (configurable via
`threshold`).  Marker colour: soft magenta + underline.

Snowball stemming aligns inflections before comparison
— `she lifted her shoulders` matches `she was lifting
her shoulders`.  Closed-class words (`the`, `and`,
`и`, `в`) are filtered via a per-language stop list so
common connectives don't inflate the count.  Built-in
stop lists ship for the five supported languages;
override via `<lang>_stop_words`.

```hjson
editor: {
  style_warnings: {
    repeated_phrases: {
      enabled: true
      n: 4
      threshold: 3
      use_stemming: true
    }
  }
}
```

Useful for catching writer-crutch gestures and
favourite turns of phrase you didn't realise had
become reflexive.  Toggles with `Ctrl+B Shift+F`
alongside filter-words.

## Show-don't-tell (1.2.9+)

`Show, don't tell` — the writing principle that says
*"her knuckles whitened around the glass"* is better
than *"she was angry"*.  Inkhaven gives you two
layers for catching the second kind:

### Inline overlay (always-on)

Hooks under the same `Ctrl+B Shift+F` toggle as
filter-words + repeated-phrases.  Underlines (in soft
teal) three categories of telling:

  * **`was angry`-style 2-grams** — a linking verb
    (`be` / `seem` / `feel` / `appear` / `look` /
    `become` / `remain` / `grow` / `sound`) followed
    by an emotion adjective.
  * **Manner-of-emotion adverbs**`angrily`,
    `sadly`, `nervously`, `wearily`, … (the `-ly`
    adverbs that label the emotion outright).
  * **Cognition verbs**`realised`, `understood`,
    `knew`, `wondered`, `decided`, … (verbs that
    tell the reader what the character is thinking
    rather than letting it come through).

Stemmed, so `seemed` matches `seem`; `realises`
matches `realised`.  Won't flag `was running` or
`looks at the door` — the 2-gram requires both
halves to match (linking verb + known emotion).

### AI scan (`Ctrl+B Shift+T`)

Sends the open paragraph to the configured LLM with
a system prompt asking for telling passages and
suggested rewrites.  Response streams into the AI
pane — same plumbing as F12 critique.

When to use which:

  * Use the **inline overlay** while you're
    drafting.  It nudges you to swap `she was nervous`
    for something embodied as you type.
  * Use the **AI scan** during revision passes.
    It catches subtler telling — declarative
    statements, narration that explains rather than
    dramatises — and proposes concrete alternatives.

The AI prompt name is `show-dont-tell`; override it
via your project's prompts book or the global
prompts.hjson, same as the critique prompts.

## Sentence-rhythm gauge (1.2.9+)

`Ctrl+B Shift+H` opens a modal that quantifies the
rhythm of the open paragraph — useful for noticing
when your sentences have drifted into a monotone
drone and need a short one to break the pattern.

What it shows:

  * A **verdict** colour-coded by how varied your
    sentence lengths are: red MONOTONE, yellow
    STEADY, green VARIED, cyan CHOPPY.  The verdict
    is computed from the coefficient of variation
    (CV = stdev / mean) so it stays meaningful
    whether your average sentence is 8 words or 18.
  * The **numbers**: N sentences, mean word count,
    stdev, CV, min, max.
  * A **per-sentence bar chart** where each row is
    one sentence and the bar's length reflects its
    word count (capped at 40 chars for display).
    Tells you at a glance whether your paragraph
    looks like a flat plateau or like jagged peaks.
  * **Outlier callouts** — the three shortest and
    three longest sentences with line numbers + a
    preview, so you can jump to them and decide
    whether the extreme is intentional.

The split is intentionally simple: `.` / `!` / `?`
followed by whitespace, with common abbreviations
suppressed (Mr., Mrs., Dr., e.g., i.e., Ph.D., …)
and `...` treated as a pause rather than a
terminator.  Good enough for literary text — the
goal is a gauge, not a parser.

Inside the modal:

  * `` / `` / `PgUp` / `PgDn` / `Home` / `End`
    scroll the bar chart.
  * Any other key closes.

Mnemonic: `Shift+H` for *heartbeat*.

## POV / character chip (1.2.9+)

The status bar gains a small magenta chip showing
the characters present in the currently-open
paragraph.  Example:

```
[Editor]  POV: Anna  +Bob, Carol   • saved 2s ago …
```

The most-mentioned character wins the **POV slot**
(rationale: in third-person limited prose the
narrator's gaze inevitably centres on the POV
character).  Ties broken by who's named first in
the paragraph.  Up to three additional characters
trail behind as the supporting cast.

The chip is driven by the existing `characters`
lexicon — the paragraphs you've already nested
under the Characters book.  No separate tagging,
no POV annotation, no per-paragraph frontmatter
to keep current.

Config (`inkhaven.hjson`):

```hjson
editor: {
  pov_chip_enabled: true
}
```

Runtime toggle: `Ctrl+B Shift+P` flips the chip on
/ off without rewriting HJSON.  Session-local
override on top of the persisted setting.

Edge cases:

  * No characters mentioned → no chip; the status
    bar reverts to its non-1.2.9 layout.
  * No paragraph open → no chip.
  * First-person POV (the narrator is `I`, not in
    the lexicon) → chip surfaces the *other*
    prominent character, which is the contextually
    useful piece of information anyway.

## Concordance view (1.2.9+)

`Ctrl+B Shift+L` opens a project-wide concordance:
every distinct word in your manuscript, ranked by
how often it appears, with a few in-context excerpts
per row.

What it's good for:

  * Spotting overworked vocabulary you didn't realise
    you lean on (`somehow`, `slightly`, `glanced`).
  * Locating where a character / place is mentioned
    without re-running search across the book.
  * Auditing your prose's lexical fingerprint — the
    top of the list is your *real* voice, not the
    voice you imagine.

What the pipeline does for you:

  * Strips out stop-words (`the`, `and`, `и`, `в`)
    so the top is meaningful lexical content.
  * Groups inflections via Snowball stemming —
    `walk`, `walked`, `walking`, `walks` collapse to
    one row whose "variants" trailer shows the
    surface forms.
  * Filters single-character tokens and bare
    digit runs.
  * Honours the project's `language` field — the
    Russian / French / German / Spanish stop-word
    lists ship built-in.

Inside the modal:

  * Type to filter (case-insensitive substring
    match across headwords + variants).
  * `` / `` / `PgUp` / `PgDn` / `Home` / `End`
    navigate.
  * `Ctrl+S` toggles sort (count ↔ alphabetical).
    Plain `s` types into the filter.
  * `Esc` closes.

Bottom panel shows up to three KWIC excerpts for the
selected row — `«word»` marks the matched token in
each excerpt so it's visually obvious in monospace.

Performance: well under a second on a 100k-word
manuscript.  The build runs once at modal open;
filter + sort changes are instant after that.

## Read aloud (TTS)

`Ctrl+B S` (in the editor pane) speaks the open paragraph
through the host OS's text-to-speech engine.  Useful for
catching awkward phrasing that the eye glosses over —
reading prose aloud is the single best self-edit
technique that doesn't need another person.

The feature is **off by default**.  Enable it by adding
to `inkhaven.hjson`:

```hjson
editor: {
  tts: {
    enabled: true
    voice: "Milena"   // Russian female; the default
    speed: 1.0        // multiplier over the engine's "normal" rate
    greeting: ""      // spoken at startup; empty skips
    goodbye: ""       // spoken at shutdown; empty skips
  }
}
```

### Greeting + goodbye

When `enabled = true` and either field is non-empty,
inkhaven speaks the configured text at startup
(non-blocking, plays in parallel with the editor
coming up) and at shutdown (blocking, up to 5
seconds, so the shell doesn't truncate it).

Examples:

```hjson
greeting: "Welcome back"
goodbye:  "See you tomorrow"

# or, with voice: "Katya (Enhanced)":
greeting: "Доброе утро, Владимир"
goodbye:  "До скорого"
```

Keep the goodbye short — under five seconds of audio
— so quit doesn't feel slow.  The greeting can be
longer; it overlaps with the editor's startup work.

### Platform support

| Platform | Backend                 | Russian voices                                                      |
| -------- | ----------------------- | ------------------------------------------------------------------- |
| macOS    | AVFoundation            | Milena (default), Yuri.  Katya (Enhanced) for premium quality.       |
| Windows  | SAPI / WinRT            | Pavel (male), Irina (female).  Pre-installed on Windows 10+.        |
| Linux    | Speech Dispatcher       | Depends on the configured speechd backend (espeak-ng default = robotic; install RHVoice or piper for natural Russian). |

On macOS and Windows, voices ship with the OS — only a
one-time download via system settings is needed for
non-English languages.  On Linux you also need to
install the speech-dispatcher daemon (`apt install
speech-dispatcher` or distro equivalent) and may want
to swap its default backend for natural Russian
output.

### Voice selection

`tts.voice` is a case-insensitive substring match
against installed voice names.  `"Milena"` matches both
`Milena` and `Milena (Enhanced)` / `Milena (Premium)`;
the matcher prefers entries that also contain
`Enhanced` or `Premium`, so the premium variant is
picked automatically when installed.

Other voices: `"Yuri"`, `"Katya"`, `"Daniel"`, `"Karen"`,
etc.  Run **`inkhaven doctor --voices`** to print every
voice visible to the TTS engine on this machine —
name, language, gender, one per line.  Works on every
platform `tts-rs` supports.

### Playback UI

While speech is in progress, a `Read aloud` modal floats
over the editor showing:

- A spinner + the elapsed time
- The chosen voice
- The first 80 chars of the paragraph as a reminder of
  what's being read

Any key (Esc / Space / anything) stops playback
immediately.  The modal also closes automatically when
the paragraph finishes naturally.

### What gets read

The full paragraph body, with the leading typst heading
line (`= Title`) stripped — it's structural, not prose.
Empty / whitespace-only paragraphs surface a status
warning and don't open the modal.

### Save as audio file (`Ctrl+B Shift+R`)

Companion to the read-aloud chord.  Instead of speaking
through the speakers, writes the paragraph to an audio
file on disk — useful for sharing a draft chapter as
audio, building a podcast-style preview, or just having
the prose available for car listening.

Pressing `Ctrl+B Shift+R` (editor scope) opens a path
picker pre-filled with
`<project>/audio/<paragraph-slug>.aiff`.  Edit the path
if you want a different name or directory, then `Enter`
commits.  `Esc` cancels.

The same voice + speed as `Ctrl+B S` is used.  Output
format follows the file extension — `.aiff` (default
AIFF-C compressed), `.wav` (linear PCM), `.m4a` (AAC)
all work on macOS 13+.  The parent directory is
created if it doesn't exist.

Status bar reports the written file size + path on
success.  Failures (no disk space, permission denied,
voice unavailable) surface there too.

Only works on macOS for now — the underlying `say -o`
command is macOS-specific.  Non-macOS hosts see the
same "TTS is macOS-only in 1.2.9" modal as `Ctrl+B S`.

### Performance

The TTS engine is lazily initialised on the first
`Ctrl+B S` and cached for the rest of the session —
subsequent reads skip the init cost.  Engine init
failures (Linux without speech-dispatcher, etc.) are
cached too, so a missing-engine modal doesn't pay the
init cost on every keystroke.

## Find and replace (regex)

`Ctrl+F` opens the Find overlay:

```
┌── Find (regex) ─────────────────────────────────┐
│                                                 │
│  Search: │                                      │
│                                                 │
│  Enter find · Esc cancel                        │
└─────────────────────────────────────────────────┘
```

Type a pattern (Rust regex syntax — `(?i)` for case-insensitive,
`(?s)` for dotall, `\bword\b` for word boundaries), press Enter.
Every match in the buffer is painted; the cursor jumps to the first.

- `Ctrl+X` jumps to the next match (wraps at end).
- `Esc` clears the search overlay (back to plain editing).

For find-and-replace, press `Ctrl+R` instead:

```
┌── Find & Replace (regex) ───────────────────────┐
│                                                 │
│  > Search:  the\s+thunder│                      │
│            Replace: the storm                   │
│                                                 │
│  Enter run · Tab switch field · Esc cancel      │
└─────────────────────────────────────────────────┘
```

`Tab` switches between Search and Replace fields. Enter performs one
replacement at the current match; `Ctrl+R` again (while replace is
active) does **replace all** and dismisses. `Ctrl+X` advances to the
next match.

The regex is the standard Rust crate's syntax — see
[https://docs.rs/regex/latest/regex/](https://docs.rs/regex/latest/regex/)
for the full reference.

## Snapshots (F5 / F6)

Snapshots are versioned copies of a paragraph stored alongside it in
the database. They're separate from autosave — autosave just commits
the latest version; snapshots capture point-in-time bookmarks.

- `F5` creates a fresh snapshot of the current buffer. The status
  reports `snapshot N saved`.
- `F6` opens the snapshot history picker:

  ```
  ┌── Snapshots for `Opening Scene` ────────────────┐
  │  > 3   2026-05-19 14:32   412 words            │
  │    2   2026-05-19 14:05   401 words            │
  │    1   2026-05-19 13:48    98 words            │
  │                                                  │
  │  Enter to load · Esc to cancel                  │
  └──────────────────────────────────────────────────┘
  ```

  Arrow keys move the selection; Enter loads that snapshot into the
  buffer (marking it dirty so you can decide whether to keep it).

Snapshots live forever — Inkhaven never garbage-collects them.

## Split-edit mode (F4 / Ctrl+F4)

For when you want to see two versions of a paragraph side by side.
`F4` toggles split-edit; the editor pane splits horizontally:

- **Upper half** — your live editor (read-write).
- **Lower half** — a frozen snapshot of the buffer at the moment you
  pressed F4. Read-only. Dim text.

Scroll the lower pane independently with `Ctrl+H` (up) and `Ctrl+J`
(down). The live cursor in the upper half moves with normal keys.

When you're done:

- `F4` again closes split, dropping the snapshot.
- `Ctrl+F4` **accepts** — the lower pane's content replaces the live
  buffer. Useful for rolling back to the pre-edit version after a
  destructive change you decided you didn't want.

## File picker (F3) — load a file into the buffer

`F3` opens a file picker overlay rooted at your home directory.
Navigate with arrows; `→` expands a directory, `←` collapses (or
steps to the parent). `Enter` on a file **replaces the current buffer**
with that file's content. Mark dirty (not saved yet).

Useful for pulling drafts from `~/Drafts/` directly into Inkhaven.

For importing whole directory trees into the hierarchy, use the
Tree pane's F3 (different overlay context — creates new paragraphs)
or the `inkhaven import-help` CLI.

## Read-only mode (Help subtree)

If you open a paragraph that lives inside the **Help** system book,
the editor goes into read-only mode:

- Border is teal.
- Title carries `[read-only]`.
- All mutating keystrokes are intercepted with a `Help is read-only`
  status message.
- Navigation, copy, search, scroll, focus chords still work.
- `Ctrl+S` is a no-op (status: "Help is read-only — nothing to save").

To make the Help book editable, do not nest your prose under it. The
Help book is reserved for `inkhaven import-help` output and is
designed to feed F1 lookups.

## Cursor memory across paragraphs

When you switch between paragraphs, each one remembers where the
cursor was the last time you visited. This survives:

- Switching panes (Tab, Ctrl+1..5, etc.)
- Opening a different paragraph in the same session.
- A full Inkhaven restart (the data is persisted to
  `.session.json`).

So jumping to "the place I was editing this morning" is a one-Enter
operation.

## What you have learned

- Movement, selection, clipboard, edits, undo / redo all work as you
  would expect.
- Save state is visible in the border colour and the title chip.
- Find / replace uses regex; `Ctrl+X` advances.
- F5 / F6 are snapshot creation / picker.
- F4 toggles split-edit; Ctrl+F4 accepts the snapshot side.
- F3 loads an arbitrary file into the buffer.
- Help subtree paragraphs are read-only by design.
- Cursor position is per-paragraph and survives restarts.

## Next steps

- [`04-search-and-discovery.md`]04-search-and-discovery.md — finding
  prose with semantic search.
- [`05-ai-writing-assistant.md`]05-ai-writing-assistant.md — how the
  AI pane works.
- [`06-grammar-check.md`]06-grammar-check.md — F7 grammar workflow.