# Script Language Syntax

This document describes the script language implemented in Paragrapher as it currently exists in code.

It is a small, case-sensitive language with BASIC-like statements and block/document objects.

## Document Structure

The document is a linear sequence of **blocks**. Each block represents a piece of content in the manuscript.

### Block Types

- **TEXT** — Prose/fiction content (the main story text)
- **SUMMARY** — Summary of upcoming story events (for continuity)
- **LORE** — World-building notes (characters, settings, rules)
- **OUTLINE** — Chapter/outline notes

### Block Properties

Every block has:

- **text** — the current draft content (string)
- **type** — one of TEXT, SUMMARY, LORE, OUTLINE (constant)
- **label** — an optional short label (e.g., "Chapter 1") stored as `block.label`
- **variations** — a collection of alternative drafts for the same content
- **variationCount** — how many variations exist
- **liveIndex** — which variation is currently selected (1-based, or 0 for latest)

### Variations

Each block can have multiple **variations** — different versions of the same content. The variation shown in the reader is the **live variation**. You can:

- Add new variations (`block.addVariation(text)`)
- Switch which is live (`block.setLive(index)`)
- Get a specific variation (`block.getVariation(index)`)

The `block.text` property always returns the live variation's content.

### Iterating Blocks

Use `for block in document` to loop through all blocks in order. The blocks collection is `document.blocks` and is zero-indexed.

Strings can be written in two forms:

- normal quoted strings: `"Hello\nWorld"`
- triple-quoted multiline strings:

```txt
let prompt = """
You are a fiction writer.
Write vivid prose.
Return only the prose.
"""
```

## Case Sensitivity

The language is case-sensitive.

Examples:

- `for` is a keyword
- `FOR` is not
- `TEXT`, `SUMMARY`, `LORE`, `OUTLINE` are built-in constants
- `text`, `summary`, `lore` are not the same thing
- `document` is a built-in global
- `DOCUMENT` is not

## Comments

Line comments start with `//`.

Example:

```txt
// this is a comment
let x = 10
```

## Statements

Supported statements:

- `let`
- assignment
- `print`
- `stop`
- `break`
- `continue`
- `return`
- `for ... in`
- `for ... = ... to ... [step ...]`
- `while ... wend`
- `if ... then ... [else ...] endif`
- `sub ... end sub`

### `let`

```txt
let name = "value"
let block.label = "Chapter"
let block.variations[0].text = "New text"
```

### Assignment

```txt
name = "value"
block.text = "Updated"
let arr = []
let nums = [1, 2, 3]
```

Array literals are supported:

- `[]` creates an empty array
- `[1, 2, 3]` creates an array with three items
- `["a", "b"]` creates a string array

### `print`

```txt
print "Hello"
print block.text
```

### `stop`

Stops the script immediately without reporting an error.

```txt
if document.selectedBlock == null then
    print "No block selected."
    stop
endif
```

### `break`

Exits the nearest enclosing `for` or `while` loop immediately.

```txt
for block in document
    if block.type == SUMMARY then
        break
    endif
next block
```

Using `break` outside a loop is a runtime error.

### `continue`

Skips the rest of the current iteration of the nearest enclosing `for` or `while` loop and proceeds to the next iteration.

```txt
for block in document
    if block.type <> TEXT then
        continue
    endif
    print block.text
next block
```

Using `continue` outside a loop is a runtime error.

### `return`

Returns a value from the current subroutine immediately.

```txt
sub chapterTitle(text)
    let linesArr = lines(text)
    if len(linesArr) == 0 then
        return "Untitled Chapter"
    endif
    return trim(linesArr[0])
end sub
```

Using `return` outside a subroutine is a runtime error.

### `sub ... end sub`

Defines a top-level subroutine. Subroutines are called like normal functions.

```txt
sub chapterTitle(text)
    let linesArr = lines(text)
    if len(linesArr) == 0 then
        return "Untitled Chapter"
    endif
    return trim(linesArr[0])
end sub

let title = chapterTitle(block.text)
```

Rules:

- subroutines must be declared at the top level
- nested subroutines are not supported
- variables remain global; assigning inside a subroutine updates the same script-wide variable space
- if a subroutine reaches `end sub` without `return`, it returns `null`

### `for ... in`

```txt
for block in document
    print block.text
next block
```

You can also iterate arrays and collections exposed by script objects, for example:

```txt
for v in block.variations
    print v.text
next v
```

```txt
for part in split("a,b,c", ",")
    print part
next part
```

### `for ... = ... to ... [step ...]`

```txt
for i = 1 to 10
    print i
next i
```

```txt
for i = 10 to 1 step -1
    print i
next i
```

### `while`

```txt
while i < 10
    print i
    i = i + 1
wend
```

### `if`

```txt
if block.type == SUMMARY then
    print "summary block"
else
    print "not summary"
endif
```

## Expressions

Supported expression features:

- numbers
- strings
- booleans: `true`, `false`
- identifiers
- function calls
- member access
- method calls
- index access
- parentheses

### Literals

Numbers:

```txt
1
3.14
```

Strings:

```txt
"hello"
"line\nbreak"
```

Supported escapes in strings:

- `\"`
- `\\`
- `\n`
- `\t`
- `\r`

Booleans:

```txt
true
false
```

Null:

```txt
null
```

### Operators

Arithmetic:

- `+`
- `-`
- `*`
- `/`

String concatenation:

- `+` (preferred — works for numbers and strings alike)
- `&` (also supported, BASIC-style)

Comparison:

- `==` (equality)
- `<>` or `!=` (not equal)
- `<`
- `<=`
- `>`
- `>=`

Note:

- `=` is assignment, not equality comparison
- using `=` inside an expression is a parse error; use `==` instead

Logical:

- `and`
- `or`
- `not`

### Operator Precedence

Highest to lowest:

1. postfix access/calls/indexing
2. unary `-`, `not`
3. `*`, `/`
4. `+`, `-`, `&`
5. comparisons
6. `and`
7. `or`

## Postfix Syntax

### Member access

```txt
block.text
block.label
document.blocks
```

### Method call

```txt
block.addVariation("new rewrite")
document.insertBlockAfter(block, SUMMARY, "summary text")
llm.call("prompt")
```

### Index access

```txt
document.blocks[0]
block.variations[1]
```

## Built-in Globals

### `document`

Built-in global object representing the current document.

### `llm`

Built-in global object for LLM calls.

### `input`

Built-in global object for runtime script forms.

Call `input.begin(title)` first, then add controls, then call `input.show()`.

Example:

```txt
input.begin("Rewrite Block")
input.addText("name", "Chapter name", "Chapter")
input.addMultiLine("prompt", "Prompt", "Tighten this scene.", 5)
input.addSelector("mode", "Mode", "overwrite;new variation", "new variation")
input.addCheckbox("include_lore", "Include lore", true)

if not input.show() then
    stop
endif

print input.getText("name")
print input.getText("mode")
print str(input.getBool("include_lore"))
```

Properties (read/write):

- `llm.model` — model name; uses the app default if empty
- `llm.temperature`
- `llm.systemPrompt`

Methods:

- `llm.addUser(text)` — append a user message
- `llm.addAssistant(text)` — append an assistant message
- `llm.clearMessages()` — clear the message list (keeps systemPrompt/model/temperature)
- `llm.reset()` — clear messages and reset all properties to defaults
- `llm.call()` — execute with the current message list
- `llm.call(prompt)` — append `prompt` as a user message then execute
- `llm.callBudgeted(prompt, contextWindowTokens, reserveOutputTokens)` — append `prompt` as a user message, trim oldest prior messages as needed to fit the estimated input budget, reserve output tokens via `max_tokens`, then execute
- `llm.summarize(block, includeLore?)` — build and execute the full summary request for the given summary block using the app's prompt-flow settings; returns the result text
- `llm.applyLive(block, instruction, addNew?)` — read the block's active variation text, send `instruction + "\n\n" + text` to the LLM, then write the result back; `addNew=false` (default) overwrites the active variation in-place, `addNew=true` adds the result as a new variation; returns `true` on success

`llm.call(...)`, `llm.callBudgeted(...)`, `llm.summarize(...)`, and `llm.applyLive(...)` execute non-streaming requests using the app's configured base URL, API key, and include-model setting.

`llm.callBudgeted(...)` keeps the system prompt and newest message, trims oldest prior messages first, and logs what it trimmed. `llm.summarize` and `llm.applyLive` use `llm.model` if set, otherwise the app default model. `llm.applyLive` uses `llm.systemPrompt` if set, otherwise the app's default system prompt. `includeLore` defaults to `true`.

Example:

```txt
llm.reset()
llm.systemPrompt = "You are a concise editor."
llm.addUser("Keep the tone but tighten the prose.")
let result = llm.call(block.text)
```

### `promptFlow`

Built-in global object that exposes the app's current prompt-flow settings and the same prompt-building helpers used by the agent runner.

Properties (read-only):

- `promptFlow.systemPrompt`
- `promptFlow.summarizerSystemPrompt`
- `promptFlow.summariesBridgePrompt`
- `promptFlow.previousParagraphsBridgePrompt`

- `promptFlow.rewritePrompt`
- `promptFlow.summaryPrompt`

Methods:

- `promptFlow.buildSystemPrompt(loreText)` — builds the full rewrite system prompt: `systemPrompt` + lore block (if non-empty)
- `promptFlow.buildSummarizerSystemPrompt(loreText)` — same but uses `summarizerSystemPrompt`
- `promptFlow.buildSummariesAssistantBlock(summariesText)` — returns the summaries text as-is (no wrapping)
- `promptFlow.buildPreviousParagraphsAssistantBlock(text)` — returns the text as-is (no wrapping)
- `promptFlow.buildFinalUserPrompt(promptText, sourceText)` — substitutes `sourceText` into `promptText` (replaces `{current_variation}`)
- `promptFlow.buildSummaryFinalUserPrompt(promptText, sourceText, summaryRangeText)` — same but also substitutes `{text_summary_range}`
- `promptFlow.wrapTag(tag, content)` — wraps `content` in `<tag>…</tag>`

## Built-in Constants

These are highlighted as constants:

- `TEXT`
- `SUMMARY`
- `LORE`
- `OUTLINE`

Runtime values:

- `TEXT` -> `"Text"`
- `SUMMARY` -> `"Summary"`
- `LORE` -> `"Lore"`
- `OUTLINE` -> `"Outline"`

These are intended for comparisons such as:

```txt
if block.type == SUMMARY then
    print block.text
endif
```

## Built-in Functions

These are global functions/builtins currently registered:

- `replace(s, from, to)`
- `replace_nocase(s, from, to)`
- `remove(s, sub)`
- `split(s, delim)`
- `join(arr, delim)`
- `removeEmpty(arr)`
- `append(arr, value)`
- `removeAt(arr, index)`
- `insertAt(arr, index, value)`
- `repeat(s, n)`
- `trim(s)`
- `ltrim(s)`
- `rtrim(s)`
- `len(x)`
- `upper(s)`
- `lower(s)`
- `titleCase(s)`
- `padLeft(s, n, ch)`
- `padRight(s, n, ch)`
- `contains(s, sub)`
- `indexOf(s, sub)`
- `lastIndexOf(s, sub)`
- `count(s, sub)`
- `contains_nocase(s, sub)`
- `startsWith(s, prefix)`
- `starts_with_nocase(s, prefix)`
- `endsWith(s, suffix)`
- `ends_with_nocase(s, suffix)`
- `before(s, sub)`
- `after(s, sub)`
- `beforeLast(s, sub)`
- `afterLast(s, sub)`
- `left(s, n)`
- `right(s, n)`
- `mid(s, start, len)`
- `lines(s)`
- `str(x)`
- `int(x)`
- `print(x)`
- `wordCount(s)`
- `sentenceCount(s)`
- `tokenEstimate(s)`
- `abs(n)`
- `max(a, b)`
- `min(a, b)`

### Function Details

#### `replace(s, from, to)`

Replaces all occurrences of `from` in `s`.

```txt
print replace("abc abc", "abc", "x")
```

#### `replace_nocase(s, from, to)`

Replaces all occurrences of `from` in `s`, case-insensitively.

```txt
print replace_nocase("Teh teh TEH", "teh", "the")
```

#### `remove(s, sub)`

Removes all occurrences of `sub` from `s`.

#### `split(s, delim)`

Returns an array.

```txt
let parts = split("a,b,c", ",")
print parts[0]
```

If `delim` is empty, splits into characters.

#### `join(arr, delim)`

Joins array items into a string.

```txt
print join(split("a,b,c", ","), "-")
```

#### `removeEmpty(arr)`

Returns a new array with empty-string items removed.

#### `append(arr, value)`

Returns a new array with `value` appended to the end.

```txt
let arr = []
arr = append(arr, "x")
arr = append(arr, "y")
print len(arr)
```

#### `removeAt(arr, index)`

Returns a new array with the item at `index` removed.

If `index` is out of range, the array is returned unchanged.

```txt
let arr = split("a,b,c", ",")
arr = removeAt(arr, 1)
print join(arr, ",")
```

#### `insertAt(arr, index, value)`

Returns a new array with `value` inserted at `index`.

`index` is clamped into the valid range:

- less than `0` inserts at the beginning
- greater than the array length inserts at the end

```txt
let arr = split("a,c", ",")
arr = insertAt(arr, 1, "b")
print join(arr, ",")
```

#### `repeat(s, n)`

Repeats a string `n` times.

```txt
print repeat("-", 10)
```

#### `trim(s)`

Trims leading and trailing whitespace.

#### `ltrim(s)`

Trims leading whitespace only.

#### `rtrim(s)`

Trims trailing whitespace only.

#### `len(x)`

If `x` is an array, returns array length.
Otherwise returns string length.

#### `upper(s)`

Uppercases a string.

#### `lower(s)`

Lowercases a string.

#### `titleCase(s)`

Converts a string to title case.

Useful for labels and headings.

#### `padLeft(s, n, ch)`

Pads `s` on the left up to width `n` using the first character of `ch`.

#### `padRight(s, n, ch)`

Pads `s` on the right up to width `n` using the first character of `ch`.

#### `contains(s, sub)`

Returns boolean.

#### `indexOf(s, sub)` / `indexOf(arr, value)`

For strings, returns the zero-based index of `sub` in `s`, or `-1` if not found.

For arrays, returns the zero-based index of the first item equal to `value`, or `-1` if not found.

#### `lastIndexOf(s, sub)`

Returns the zero-based index of the last occurrence of `sub` in `s`, or `-1` if not found.

#### `count(s, sub)`

Counts non-overlapping occurrences of `sub` in `s`.

#### `contains_nocase(s, sub)`

Returns boolean, case-insensitively.

#### `startsWith(s, prefix)`

Returns boolean.

#### `starts_with_nocase(s, prefix)`

Returns boolean, case-insensitively.

#### `endsWith(s, suffix)`

Returns boolean.

#### `ends_with_nocase(s, suffix)`

Returns boolean, case-insensitively.

#### `before(s, sub)`

Returns the part of `s` before the first occurrence of `sub`, or an empty string if not found.

#### `after(s, sub)`

Returns the part of `s` after the first occurrence of `sub`, or an empty string if not found.

#### `beforeLast(s, sub)`

Returns the part of `s` before the last occurrence of `sub`, or an empty string if not found.

#### `afterLast(s, sub)`

Returns the part of `s` after the last occurrence of `sub`, or an empty string if not found.

#### `left(s, n)`

Returns the leftmost `n` characters.

#### `right(s, n)`

Returns the rightmost `n` characters.

#### `mid(s, start, len)`

Substring using 1-based start index, BASIC-style.

```txt
print mid("abcdef", 2, 3)   // "bcd"
```

#### `lines(s)`

Splits a string into an array of lines.

#### `str(x)`

Converts value to string.

#### `int(x)`

Converts value to integer.

#### `print(x)`

Builtin form returns the passed value.

The `print` statement is what actually logs script output.

#### `wordCount(s)`

Counts whitespace-separated words. Uses `iswspace` so tabs, newlines, and other Unicode whitespace all act as delimiters; runs of whitespace count as one gap.

#### `sentenceCount(s)`

Approximate sentence count.

It avoids some common false positives such as:

- `Mr.`
- `Dr.`
- `etc.`
- decimal numbers like `3.14`

#### `tokenEstimate(s)`

Estimates the BPE token count for English prose, approximating the cl100k tokenizer used by GPT-4 and similar models.

The heuristic currently works like this:

- each whitespace-separated word counts as one base token
- trailing punctuation such as `.`, `,`, `!`, `?`, `;`, `:` adds extra tokens
- common contractions such as `n't`, `'re`, `'ve`, `'ll`, `'d`, `'m` add an extra token
- possessives such as `'s` or a trailing apostrophe add an extra token
- hyphens add extra tokens
- long word parts with 10 or more letters add an extra token
- the final total is scaled slightly upward (`× 1.026`) for residual BPE overhead

Returns an integer. It is an approximation, not an exact tokenizer implementation.

```txt
let tokens = tokenEstimate(block.text)
print "~" + str(tokens) + " tokens"
```

#### `abs(n)`

Absolute value.

#### `max(a, b)`

Returns the larger value.

#### `min(a, b)`

Returns the smaller value.

## Object Model

## `document`

Properties:

- `document.blockCount`
- `document.blocks`
- `document.selectedBlock` — the currently selected block, or `null` if nothing is selected; writable: assigning a block object selects it and scrolls it into view

Example:

```txt
document.selectedBlock = block
```

Methods:

- `document.getSummaries(block, maxCount?)`
- `document.getLastSummary(block)`
- `document.collectSummaryText(block)`
- `document.getLore(block, maxCount?)`
- `document.getPreviousBlocksText(block, n)`
- `document.getBlockIndex(block)`
- `document.insertBlockAfter(block, type, text)`
- `document.deleteBlock(block)`
- `document.deleteBlocks(blockArray)`
- `document.deleteBlocksAfter(block)`
- `document.saveIncremental(postfix?, filename?, overwrite?)`
- `document.setLive(index)`
- `document.collapse()`
- `document.splitParagraphs()`
- `document.addSummaryPoints(prefix?)`

### Block argument

All document methods that accept a block argument require a **block object** — not a numeric ID or index.

Pass `null` to mean "the entire document" (no upper bound cutoff).

```txt
// correct — pass the block object from iteration
for block in document
    let summaries = document.getSummaries(block)
next block

// correct — look up by 0-based position index, then pass the object
let b = document.blocks[9]
let summaries = document.getSummaries(b)

// correct — null means the whole document
let summaries = document.getSummaries(null)

// WRONG — integers are not accepted; stops the script with a runtime error
let summaries = document.getSummaries(10)
```

### `document.getSummaries(block, maxCount?)`

Returns the joined summary text up to, but not including, the given block.

- `maxCount` — optional; if greater than zero, only the last N summaries are returned. If omitted or zero, all summaries are returned.

Pass `null` as the block to collect from the entire document (subject to `maxCount`).

This mirrors the summary context assembly used by the normal rewrite path.

### `document.getLastSummary(block)`

Returns only the nearest previous summary text before the given block, or an empty string if there is none.

Pass `null` to get the last summary in the entire document.

### `document.collectSummaryText(block)`

Returns the text range that would be summarized for the given summary block:

- after the previous summary point
- up to, but not including, the given summary block
- only text blocks are included

Pass `null` to collect all text blocks from the last summary point to the end of the document.

### `document.getLore(block, maxCount?)`

Returns the joined lore text up to, but not including, the given block.

- `maxCount` — optional; if greater than zero, only the last N lore blocks are returned. If omitted or zero, all lore blocks are returned.

Pass `null` as the block to collect from the entire document (subject to `maxCount`).

This mirrors the lore context assembly used by the normal rewrite path.

### `document.getPreviousBlocksText(block, n)`

Returns the text of the `n` text blocks immediately preceding the given block, joined by blank lines.

Used to supply recent prior prose to the LLM as style and continuity context, mirroring the `includePreviousBlocks` option in the agent runner.

`n` must be greater than zero; returns an empty string if there are no prior text blocks or `n` is 0.

```txt
let prev = document.getPreviousBlocksText(block, 3)
if len(prev) > 0 then
    llm.addUser(promptFlow.previousParagraphsBridgePrompt)
    llm.addAssistant(promptFlow.buildPreviousParagraphsAssistantBlock(prev))
endif
```

### `document.getBlockIndex(block)`

Returns the zero-based position of `block` in `document.blocks`.

- returns `-1` if the block is `null` or no longer exists in the document

This is useful when you want to remember a block object and then loop from that point onward with indexed access.

```txt
let startIndex = document.getBlockIndex(startBlock)
if startIndex < 0 then
    stop
endif

for i = startIndex to document.blockCount - 1
    let block = document.blocks[i]
    print block.text
next i
```

### `document.insertBlockAfter(block, type, text)`

- `block` — a block object (see note above). Pass `null` to insert at the end of the document.
- `type` should be `TEXT`, `SUMMARY`, `LORE`, or `OUTLINE`
- `text` is optional

Returns the newly inserted block object when successful.

Example:

```txt
let newBlock = document.insertBlockAfter(block, SUMMARY, "summary text")
```

### `document.deleteBlock(block)`

Deletes the referenced block. Accepts a block object only.

### `document.deleteBlocks(blockArray)`

Deletes all blocks in the given array of block objects.

- duplicate block references are ignored
- `null` entries are ignored
- blocks are deleted in reverse document order so the operation is safe for arrays collected during iteration

Returns the number of blocks actually deleted.

```txt
let doomed = []
for block in document
    if block.type == SUMMARY and len(trim(block.text)) == 0 then
        doomed = append(doomed, block)
    endif
next block

let n = document.deleteBlocks(doomed)
print "Deleted " + str(n) + " block(s)"
```

### `document.deleteBlocksAfter(block)`

Deletes all blocks strictly after the given block.

- pass a block object to keep that block and delete everything below it
- pass `null` to delete all blocks in the document

Returns the number of blocks actually deleted.

```txt
document.deleteBlocksAfter(null)
document.deleteBlocksAfter(draftAnchor)
```

### `document.saveIncremental(postfix?, filename?, overwrite?)`

Saves an incremental/backup copy of the document.

| Argument | Type | Default | Description |
|----------|------|---------|-------------|
| `postfix` | string | `""` → `"step"` | Suffix for auto-numbered file saved next to the document: `doc_step_1.pgr`, `doc_step_2.pgr`, … |
| `filename` | string | `""` | If non-empty, save to `Documents\Paragrapher\saves\<filename>` instead |
| `overwrite` | bool | `false` | Named save only — if `false`, skip silently when the file already exists; if `true`, overwrite |

Returns `true` on success, `false` on failure (e.g. document not yet saved to disk).

```txt
document.saveIncremental("draft")
document.saveIncremental("", "checkpoint.pgr")
document.saveIncremental("", "final.pgr", true)
```

### `document.setLive(index)`

Sets the live variation on every block to the given index.

- `0` → the latest (last) variation on each block
- `1` → variation at index 0, `2` → variation at index 1, etc. (1-based, same as agent recipe)

Each block is clamped to its own variation count, so blocks with fewer variations are not skipped.

Returns `true` if any block changed.

### `document.collapse()`

Removes all variations from every block except the live one. The live variation becomes the sole version.

Equivalent to the agent recipe `Collapse` step. Use before `splitParagraphs()` when the document has multiple variations.

Returns `true` if any block was changed.

### `document.splitParagraphs()`

Splits each text block into potentially multiple blocks using the same chunking algorithm applied on initial import.

Only text blocks with a single variation are split; blocks with multiple variations are left unchanged. Requires the document to be collapsed first.

Returns `true` if any block was split.

### `document.addSummaryPoints(prefix?)`

Inserts a summary-point block before every text block whose content starts with `prefix`. Defaults to `"Chapter"` if omitted.

Returns the number of summary-point blocks inserted.

Equivalent to the **Tools → Add Summary Points** menu command.

```txt
let n = document.addSummaryPoints("Chapter")
print "Inserted " + str(n) + " summary points"
```

## `input`

The `input` object builds a modal dialog at runtime using string ids for field lookup.

Typical flow:

1. `input.begin(title)`
2. `input.add...(...)`
3. `input.show()`
4. `input.getText(id)` / `input.getBool(id)`

Methods:

- `input.begin(title?)`
- `input.addInfo(text, uxHeight?)`
- `input.addText(id, label, defaultText?)`
- `input.addMultiLine(id, label, defaultText?, uxHeight?)`
- `input.addSelector(id, label, options, defaultValue?)`
- `input.addCheckbox(id, label, defaultValue?)`
- `input.show()`
- `input.getText(id)`
- `input.getBool(id)`

### `input.begin(title?)`

Clears the current form definition and sets the dialog title. If omitted, the title becomes `"Input"`.

### `input.addInfo(text, uxHeight?)`

Adds a read-only info area. `uxHeight` defaults to `2`.

### `input.addText(id, label, defaultText?)`

Adds a labeled single-line text field.

### `input.addMultiLine(id, label, defaultText?, uxHeight?)`

Adds a labeled multiline text field. `uxHeight` defaults to `5`.

### `input.addSelector(id, label, options, defaultValue?)`

Adds a one-line selector control with left/right buttons.

- `options` is a semicolon-separated string such as `"overwrite;new variation"`
- if `defaultValue` is omitted, the first option is selected

### `input.addCheckbox(id, label, defaultValue?)`

Adds a checkbox row.

### `input.show()`

Shows the dialog. Returns `true` when the user presses OK, `false` on Cancel.

### `input.getText(id)`

Returns the current string value for a text, multiline, or selector field.

Returns `null` if the id does not exist.

### `input.getBool(id)`

Returns the checkbox value for `id`.

## `document.blocks`

An indexable collection of block objects.

Properties:

- `document.blocks.count`
- `document.blocks.length`

Indexing:

```txt
let first = document.blocks[0]
```

## `block`

Blocks are yielded by:

- `for block in document`
- indexing into `document.blocks`

Properties:

- `block.type`
- `block.text`
- `block.label`
- `block.variationCount`
- `block.variations`
- `block.id`
- `block.liveIndex`

Writable properties:

- `block.text`
- `block.label`

### `block.type`

Returns one of:

- `"Text"`
- `"Summary"`
- `"Lore"`

Compare it against:

- `TEXT`
- `SUMMARY`
- `LORE`
- `OUTLINE`

### `block.text`

Gets/sets the currently selected draft text for the block.

### `block.label`

Gets/sets `breakBefore`.

### `block.variationCount`

Number of variations on the block.

### `block.variations`

Collection of variation objects.

### `block.id`

Internal numeric block id. Starts from 1, not guaranteed to be contiguous. Use this value when you need to have an unique number representing a block. Not the same as a positional index.

Methods:

- `block.addVariation(text)`
- `block.removeVariation(index)`
- `block.setLive(index)`
- `block.getVariation(index)`

### `block.addVariation(text)`

Adds a new variation using the given text. Important: The document will also set this variation as live.

### `block.removeVariation(index)`

Removes a variation by index.

### `block.liveIndex`

Returns the 1-based index of the currently live variation for this block (the variation returned by `block.text`).

### `block.setLive(index)`

Sets the live variation for this block only.

- `0` → the latest (last) variation on the block
- `1` → first variation, `2` → second variation, etc.

The requested variation is clamped to the block's variation count.

```txt
for block in document
    if block.variationCount > 1 then
        print block.id & " live=" & block.liveIndex
        block.setLive(1)
    endif
next block
```

### `block.getVariation(index)`

Returns a variation object using version-style indexing.

- `0` → the latest (last) variation on the block
- `1` → first variation, `2` → second variation, etc.

Returns `null` if the requested variation does not exist.

```txt
let first = block.getVariation(1)
let latest = block.getVariation(0)
if first != null then
    print first.text
endif
```

## `block.variations`

Indexable/iterable collection of variation objects.

Important:

- `block.variations[...]` uses normal array-style indexing and is `0`-based
- `block.getVariation(index)` and `block.setLive(index)` use version-style indexing and are `1`-based, with `0 = latest`

So:

- `block.variations[0]` = first variation
- `block.variations[1]` = second variation
- `block.getVariation(1)` = first variation
- `block.getVariation(2)` = second variation
- `block.getVariation(0)` = latest variation

Properties:

- `block.variations.count`
- `block.variations.length`

Indexing:

```txt
let v = block.variations[0]
```

Iteration:

```txt
for v in block.variations
    print v.text
next v
```

## `variation`

Properties:

- `variation.text`

Writable:

- `variation.text`

## Looping Examples

### Iterate all blocks

```txt
for block in document
    print block.text
next block
```

### Add label to summary blocks

```txt
for block in document
    if block.type == SUMMARY then
        block.label = "Summary"
    endif
next block
```

### Add a variation to text blocks

```txt
for block in document
    if block.type == TEXT then
        block.addVariation(block.text & " revised")
    endif
next block
```

### Insert summary before chapter blocks

```txt
for block in document
    if block.type == TEXT then
        if startsWith(block.text, "Chapter") then
            document.insertBlockAfter(block, SUMMARY, "")
        endif
    endif
next block
```

### Apply a plain instruction to every text block (simple)

`llm.applyLive` is the simplest way to run an instruction over text blocks without any prompt-flow wiring.

```txt
// Add a tightened variation to every text block
for block in document
    if block.type == TEXT then
        llm.applyLive(block, "Tighten this paragraph — cut filler words.", true)
    endif
next block
```

```txt
// Overwrite the live variation in-place
for block in document
    if block.type == TEXT then
        llm.applyLive(block, "Convert all dialogue tags to past tense.")
    endif
next block
```

### Rewrite all text blocks using the app prompt-flow settings (mirrors agent LLM loop)

```txt
for block in document
    if block.type == TEXT then
        let lore = document.getLore(block)
        let summaries = document.getSummaries(block)
        let prev = document.getPreviousBlocksText(block, 3)

        llm.reset()
        llm.systemPrompt = promptFlow.buildSystemPrompt(lore)

        if len(summaries) > 0 then
            llm.addUser(promptFlow.summariesBridgePrompt)
            llm.addAssistant(promptFlow.buildSummariesAssistantBlock(summaries))
        endif

        if len(prev) > 0 then
            llm.addUser(promptFlow.previousParagraphsBridgePrompt)
            llm.addAssistant(promptFlow.buildPreviousParagraphsAssistantBlock(prev))
        endif

        let result = llm.call(promptFlow.buildFinalUserPrompt(promptFlow.rewritePrompt, block.text))
        if len(result) > 0 then
            block.addVariation(result)
        endif
    endif
next block
```

### Summarize all summary blocks (simple)

`llm.summarize(block)` builds the full summary request using the app's prompt-flow settings, exactly as the agent runner and RightPane do, and returns the LLM result text. An optional second argument controls whether lore context is included (default `true`).

```txt
for block in document
    if block.type == SUMMARY then
        let result = llm.summarize(block)
        if len(result) > 0 then
            block.text = result
        endif
    endif
next block
```

### Summarize all summary blocks (manual alternative)

The equivalent written out step by step. Useful when you need to customise the system prompt, swap the model, or inspect intermediate values.

```txt
for block in document
    if block.type == SUMMARY then
        let lore = document.getLore(block)
        let rangeText = document.collectSummaryText(block)

        if len(rangeText) > 0 then
            llm.reset()
            llm.systemPrompt = promptFlow.buildSummarizerSystemPrompt(lore)

            let result = llm.call(promptFlow.buildSummaryFinalUserPrompt(promptFlow.summaryPrompt, "", rangeText))
            if len(result) > 0 then
                block.text = result
            endif
        endif
    endif
next block
```

## Notes and Limitations

- The language is currently case-sensitive.
- Newlines terminate statements.
- `if` requires `then`.
- `while` closes with `wend`.
- `for` closes with `next`.
- `stop` stops the script immediately without error.
- `break` exits the nearest loop.
- `continue` skips to the next iteration of the nearest loop.
- `for/in` iterates objects that support iteration.
- `for range` supports optional `step`.
- `next` may be written as bare `next` or `next variableName`.
- if a variable name is present after `next`, it must match the loop variable.
- `step 0` is an error.
- `llm.call(...)` executes real non-streaming LLM requests.

## Current Highlighting Categories

The editor currently highlights:

- keywords
- strings
- comments
- numbers
- builtins (global function names, `document`, `llm`, `promptFlow`)
  `input` is also highlighted as a built-in global
- constants (`TEXT`, `SUMMARY`, `LORE`, `OUTLINE`)
- methods (any `.name(...)` call — coral/red)

Current constants:

- `TEXT`
- `SUMMARY`
- `LORE`
- `OUTLINE`

Current built-in global highlighted names:

- `document`
- `llm`
- `promptFlow`
- `input`
