- name
- ghostex-visuals
- description
- Ghostex visuals (gx visuals): charts, stat tiles, tables and text layouts in the Ghostex chat with a fenced ```visual block. Ghostex forms (gx form): questions for the reader as a form in the chat with a ```questions block. Ghostex pages (gx page): self-contained HTML pages (UI mockups, small interactive tools, questions that need a picture) published with `ghostex show`, which the chat shows as a card that opens the page. Use only when the user asks for $ghostex-visuals, a Ghostex page, form or visuals, or a gx page, gx form or gx visuals.
- disable-model-invocation
- true
# ghostex-visuals
Ghostex draws what you put in your reply natively in the chat, on desktop,
web and phone. Each kind has a name; the user uses these names, and so do
you when you talk to them ("I made a Ghostex page", "answer the Ghostex
form"):
| Name | Short | What you write | Older words for it |
| --- | --- | --- | --- |
| Ghostex visuals | gx visuals | a ```visual block: charts, graphs, stats, tables, text layouts | inline graphs, inline charts |
| Ghostex form | gx form | a ```questions block, or one-click answers for a quick pick | inline questions, inline q, question form |
| Ghostex page | gx page | a self-contained HTML page published with `ghostex show` | inline HTML, chat page, HTML page |
"Give me a gx page for this" means a Ghostex page, "ask me in a gx form" a
Ghostex form, "show it as gx visuals" a ```visual block.
- Prefer Ghostex visuals for charts, tables, stats and text layouts.
- Ask questions in a Ghostex form: the chat shows a ```questions block as a
form.
- Use a Ghostex page only for what a block cannot express: UI mockups, small
interactive tools, and questions the reader can only answer by seeing a
picture.
- Don't restate in prose what the visual already shows. Add only what it
doesn't: the conclusion, a caveat, the next step.
## Ghostex visuals: ```visual blocks
One JSON object in a fence whose info string is `visual`. The block is one of:
- **A Vega-Lite spec** at the top level: an object with `"mark"`. It may have
`"title"` and `"height"`.
- **A composition**: `{"title"?: "…", "rows": [part, …]}`, drawn top to
bottom. A part is one of the following, and every part may have `"title"`:
- `{"chart": <Vega-Lite spec>}`
- `{"stats": [{"label": "…", "value": "…", "note"?: "…", "tone"?: "good" | "bad" | "neutral"}]}`
(`value` may be a number)
- `{"table": {"columns": ["…", …], "rows": [["…", …], …]}}`, or rows as
objects keyed by column name (then `"columns"` is optional)
- `{"text": "…"}`
- `{"columns": [part, …]}`: parts side by side
- **A page**: `{"page": {"title": "…", "url": "…"}}`. Only ever paste this from
`ghostex show` output (below); never write one yourself.
### The Vega-Lite subset
- `mark`: `bar`, `line`, `area`, `point` (also `circle`, `square`), `arc` (pie;
`{"type": "arc", "innerRadius": 50}` for a donut). The object form
`{"type": "line", "point": true}` adds points to a line.
- Encoding channels: `x`, `y`, `color`, `theta`, `tooltip`, `xOffset` (grouped
bars).
- Field definition keys: `field`, `type` (`quantitative`, `nominal`,
`ordinal`, `temporal`), `aggregate` (`sum`, `mean`, `count`, `min`, `max`,
`median`), `title`, `sort` (`"ascending"`, `"descending"`, `null` for data
order, or an array of values), `axis` (`null` to hide it, or
`{"title": …}`), `scale` (`{"domain": [min, max], "zero": false}`), `stack`
(`null`/`false`, `true`, `"normalize"`).
- Data only as `"data": {"values": [ … ]}` with the rows inlined; `data.url`
isn't supported. Aggregate large data before writing the block.
- Not supported: `layer`, `facet`, `repeat`, `concat`, `transform`,
`params`/selections, `bin`, and the `rule`, `text`, `tick` and `rect` marks.
Compute bins, totals or moving averages into the rows yourself.
- The width always fits the chat column; `"height"` sets the chart height
(100 to 600).
### Check the block before replying
Write the JSON to a temporary file and draw it:
```bash
ghostex visual check /tmp/block.json # prints a PNG path, or names the problem
ghostex visual check /tmp/block.json --light # light theme
ghostex visual check /tmp/block.json --width 360 # phone width
```
Look at the PNG, fix what reads badly, then paste the same JSON into your
reply. If `ghostex visual` is an unknown command, this Ghostex is too old to
check: skip the check.
### Examples
Grouped bars:
```visual
{
"title": "Build time by platform",
"mark": "bar",
"height": 220,
"data": {"values": [
{"platform": "macOS", "build": "debug", "minutes": 4.2},
{"platform": "macOS", "build": "release", "minutes": 9.8},
{"platform": "Windows", "build": "debug", "minutes": 5.1},
{"platform": "Windows", "build": "release", "minutes": 12.4},
{"platform": "Linux", "build": "debug", "minutes": 3.6},
{"platform": "Linux", "build": "release", "minutes": 8.9}
]},
"encoding": {
"x": {"field": "platform", "type": "nominal", "sort": null, "axis": {"title": null}},
"xOffset": {"field": "build"},
"y": {"field": "minutes", "type": "quantitative", "title": "Minutes"},
"color": {"field": "build", "type": "nominal", "title": "Build"}
}
}
```
For stacked bars, drop `xOffset`; bars that share an `x` stack by `color`.
A line chart with two series:
```visual
{
"title": "Response time",
"mark": {"type": "line", "point": true},
"data": {"values": [
{"day": "2026-10-01", "series": "p50", "ms": 120},
{"day": "2026-10-02", "series": "p50", "ms": 118},
{"day": "2026-10-03", "series": "p50", "ms": 131},
{"day": "2026-10-01", "series": "p95", "ms": 340},
{"day": "2026-10-02", "series": "p95", "ms": 310},
{"day": "2026-10-03", "series": "p95", "ms": 365}
]},
"encoding": {
"x": {"field": "day", "type": "temporal", "title": "Day"},
"y": {"field": "ms", "type": "quantitative", "title": "Milliseconds"},
"color": {"field": "series", "type": "nominal", "title": "Percentile"},
"tooltip": [{"field": "day", "type": "temporal"}, {"field": "ms", "type": "quantitative"}]
}
}
```
Stats above a table:
```visual
{
"title": "Test run",
"rows": [
{"stats": [
{"label": "Passed", "value": 412, "tone": "good"},
{"label": "Failed", "value": 3, "note": "all in sync/", "tone": "bad"},
{"label": "Duration", "value": "2m 41s"}
]},
{"title": "Failures", "table": {
"columns": ["Test", "Error"],
"rows": [
["sync::resume_after_sleep", "timed out after 30s"],
["sync::merge_conflict", "expected 2 rows, got 3"],
["sync::large_file", "permission denied"]
]
}}
]
}
```
## Ghostex forms: ```questions blocks
When you need the reader's decisions, put the questions in a ```questions
block in your reply. The chat draws it as a form under your message, on
desktop, web and phone: every question at once, choices picked with a click,
a note box per question, and one Send button. Use it instead of asking in
prose or as a numbered list, and instead of your agent's own question tool
(AskUserQuestion and the like), which shows one question at a time. Only when
the reader has to see something to answer (a UI mockup, a layout, a diagram)
use a Ghostex page with questions instead (see "Ghostex pages with questions"
below).
First run `ghostex visual --help`: if it doesn't mention ```questions, this
Ghostex can't show the form, so ask in prose instead.
### One-click answers
One quick pick with 2 to 4 short answers and nothing to explain or note (a
yes/no, "merge it?", "delete the branch?") needs no form: write the answers
right after the question, one Markdown link per answer pointing at
`ghostex-answer:` and the question's tag. Only when `ghostex visual --help`
mentions `ghostex-answer:`; older versions show plain links.
```markdown
❓ **Needs you:**
- [PR #414](https://github.com/owner/repo/pull/414): Review and merge it? [Yes](ghostex-answer:Q901) [No](ghostex-answer:Q901) [Later](ghostex-answer:Q901)
- Temporary branch `fix/tab-drag`: Delete it? [Delete](ghostex-answer:Q902 "recommended") [Keep](ghostex-answer:Q902=keep)
```
- The link's text is the answer. `=value` sends a value other than the label;
the title `"recommended"` marks the one you recommend.
- The question is the line's words before its first answer link. Keep the
links on the question's own line, and keep it one line. Links on a line or
sub-bullet of their own take the nearest line above that names their tag
(else their parent bullet) as the question.
- The chat draws the answers as a small pill under the question. One tap sends
them back as a normal message in the same envelope a form's answers use,
keyed by the tag, with the question as its title:
`<ghostex-page-answers page="…" title="PR #414: Review and merge it?">{"Q901": "Yes"}</ghostex-page-answers>`.
The reader may change a sent answer: a change that reaches you after the
first arrives with `"Q901 (note)": "Changed from Yes"`.
- Tags count up with the forms' tags. The "?" pill counts an inline question
until it is answered, and a later ```questions block can withdraw its tag.
- Use a form instead for several related questions, multi-select, a text
answer, choices that need a line of detail, or anything the reader may want
to add a note to.
```questions
{
"title": "Settings redesign: layout and platforms",
"questions": [
{"tag": "Q1", "title": "Which layout should I build?",
"detail": "Why it matters, in a sentence or two.",
"choices": [
{"label": "Tabs", "detail": "One page per area.", "value": "tabs", "recommended": true},
{"label": "One long page", "value": "one page"}
]},
{"title": "Which platforms first?", "multiple": true, "choices": ["macOS", "Windows", "Phone"]},
{"title": "What should the new page be called?", "type": "text"}
]
}
```
- Always give the form a `title`: a short, specific name for what it asks,
such as "Settings redesign: layout and platforms". Ghostex lists every open
form and page by this name (the chat's "?" pill shows them under
Unanswered, For later and Answered), so a generic title ("Questions",
"Decisions", "A few questions") can't be told apart from the others.
`submit` renames the button ("Send answers").
- Each question: `title`; `detail`, a short explanation; `tag`, an optional
short label such as an issue id; `name`, the answer's key (the title when
there is none); `multiple` for several picks; `type: "text"` for a text box
instead of choices (`placeholder` sets its hint). At most 20 questions.
- Link what you ask about: when a question is about a PR, issue, file, page,
commit, thread or anything else the reader could open, link it in the
question itself, so they can check it before answering. Write a Markdown
link with the full URL (`[#407](https://github.com/owner/repo/pull/407)`)
or a file path link (`[kit.rs](/abs/path/kit.rs)`) in the `title` or
View on GitHub