Ariel
the terminal hand — quick to the bidding, quicker to report what she heard
Your characters can do more than describe what they would do at a terminal. They can read one. A real PTY runs wherever the Quilltap server runs—your desktop, a container, a VM—bound to the chat, accepting your keystrokes and producing output that the language model at the table can actually take in.
Ariel is the embodiment of that arrangement. She is the spirit
who runs the shell errand, who stands at the terminal long
enough to hear what came back, and who then turns to the rest
of the room and reports—calmly, accurately, and in a
register the language model can take in. The reference is to
The Tempest: Prospero’s familiar spirit, quick to oblige
and quicker still to bring word back. The role here is identical,
save that the bidding is yours and the message she delivers is
whatever a git diff or a pytest -v
left on the screen.
The Lantern paints the scene. The Host announces the guests.
Ariel is what happens when one of those guests asks for a
working terminal—and she is, in her quiet way, the
difference between an AI that pretends to know what
npm install produced and one that has read the
actual lines.
Terminal Mode
a real shell, in a tab of its own, bound to the chat
Terminal Mode opens a full xterm.js + node-pty
terminal as its own workspace tab,
linked to the Salon that spawned it, with the live PTY kept mounted
and portaled in—so you may drag it into the opposite pane and
set it beside the conversation that owns it, and it survives every
tab switch you care to make. You type into it the way you would
type into any terminal emulator. The LLM does not.
Sessions are scoped to the chat and end when the chat is deleted;
spawning a new one offers a picker if other live sessions for the
chat already exist, so attaching to a long-running build is one
click. The keyboard shortcut is
Cmd+Shift+T / Ctrl+Shift+T.
Hide versus Kill
The pane header carries two distinct exit affordances. The
hide button (a horizontal bar) closes the pane but leaves the
PTY alive—your build keeps running, your shell keeps
its working directory, your tail -f keeps
tailing. The kill button (a red ×) terminates the
process and requires a two-click confirmation, so an
accidental swipe doesn’t take down a long-running job.
The two buttons exist because they are genuinely different
actions and the difference matters.
Session Picker
Clicking the composer’s terminal button when no session
is open spawns a new one. If the chat already has live PTYs
running—perhaps you stepped away mid-build—a
picker appears offering attachment to any of them or a fresh
spawn. The active session id is stored on the chat record
(activeTerminalSessionId), so “which
terminal does this chat have open right now?” is a
question the system can always answer.
Composes with Document Mode
A document and a terminal are two independent tabs, so the arrangement is yours to make: drag one into the other pane and you can read what the LLM wrote in the document, run something in the terminal that exercises it, and watch the two move together—with the conversation itself a tab-click away and still streaming. The divider between the panes is the workspace’s own, draggable and keyboard-nudgeable, and the whole arrangement is remembered between visits.
User Types, LLM Reads
The terminal accepts input from the human in the room. The language model does not. This is the cardinal rule of the arrangement: a character can ask Ariel what the terminal said, and Ariel will tell her, but neither the character nor the model behind her can put a single keystroke into the shell on her own. Read-only by design, not by accident.
A Tab, and Whose Child It Is
lifecycle in the two-pane workspace
A terminal tab is a child of the Salon that called for it, and the bookkeeping is symmetrical: close the mode and the tab closes, close the tab and the mode turns off, close the parent Salon and its children go with it. Ariel does not linger in a room whose door has been shut.
Two small graces came with the reconstruction. The pop-out button used to hard-navigate—which is to say it tore down the entire workspace, live tabs and streaming conversations and all, in order to show you one terminal—and now opens in place through the document-level link interceptor that catches every such link. And a terminal’s old full-page address, should you have bookmarked one, seats the conversation and its terminal side by side as tabs, which is very likely what you wanted from the bookmark in the first place. For the house-wide accounting of who has been at a prompt and for how long, the Almanack reports Ariel’s terminal sessions along with everything else it counts.
Reading Aloud
how Ariel narrates terminal output back to the chat
A language model cannot read xterm. The escape sequences a modern terminal emits—cursor moves, color codes, carriage returns, backspaces, alternate-screen toggles, application-mode commands—are designed to be interpreted by a terminal emulator and rendered as a two-dimensional grid of glyphs. Hand the raw bytes to a language model and you hand it noise. So Ariel reads the terminal aloud, in a register the model can actually take in.
The PTY manager keeps a per-session flush buffer alongside its ring buffer of recent output. When new bytes arrive, two timers govern when Ariel speaks. An idle timer fires after thirty seconds without new output—the natural pause at the end of a command’s work. A max-age timer fires after two minutes from the first byte in the buffer regardless of activity, so a steady drip of output (a long build emitting a chunk every twenty-five seconds, say) cannot keep her silent indefinitely. Both timers clear on flush. The max-age timer is set once when the buffer is empty and is not reset by subsequent chunks, so the ceiling is a real ceiling.
On flush the buffered bytes go through a cleaning pipeline:
ANSI escape sequences are stripped, backspaces are applied to
erase the prior character, lone carriage returns are treated
as line resets so prompt redraws and progress bars come out as
their final state. Cleaned output longer than sixteen
kilobytes is elided as head 8 KB +
[N characters elided] + tail 8 KB—a
compromise that preserves the opening of long output and the
ending where the actual answer usually lives, without
overwhelming the model with the middle of a webpack bundle.
Empty or whitespace-only flushes are skipped: silence remains
silent.
The cleaned text gets posted into the chat as an
Ariel-attributed assistant message wrapped in a fenced code
block whose backtick count is computed dynamically, so triple
backticks in the output don’t break the fence. The
systemKind on the message records why she spoke:
terminal-output-idle,
terminal-output-max-age, or
terminal-output-session-closed when the shell has
ended and any pending bytes are force-flushed before the close
announcement lands. The chat re-fetches automatically when she
posts—a chat-update WebSocket frame with
reason: ‘ariel-terminal-output’ reaches
every subscriber—so her words appear without a manual
reload.
The effect is straightforward and load-bearing. The model at the table sees what the shell actually did. Not a summary it composed itself, not a guess from training data, not a hallucinated stack trace: the lines themselves, in chronological order, fit to read.
Sessions That Persist
across reloads, restarts, and the occasional careless kill
A terminal is, by nature, stateful. The shell has a working directory, an environment, a process tree, a history. Ariel treats that state as something worth defending against the small accidents of a chat client: a page reload, a brief connection drop, a server restart, an instinctive close-the-tab when the user meant only to switch focus.
Persistence Across Reloads
Terminal Mode state lives on the chat record itself
(terminalMode,
activeTerminalSessionId) so reloading the page
restores the mode and the session binding, while where the
terminal sits is the workspace’s own persisted
tab arrangement rather than a ratio stored on the chat. The
PTY manager pins itself on
globalThis.__quilltapPtyManager so the
session map survives across module reloads in dev, and a
512 KB client-side replay buffer captures every output
chunk and replays it on attach—so a freshly mounted
xterm lands on the live prompt instead of a blank screen.
Orphan Reconciliation
When the server restarts, any PTY it was hosting is gone but
the database row for the session may still mark it as live.
On chat open, Ariel runs a sweep
(reconcileTerminalSessionsForChat) that finds
DB rows whose exitedAt is null but whose ids
aren’t in the in-memory session map, marks them
exited, and posts a session-closed announcement noting that
the shell likely ended at the last restart. Tidy
housekeeping; nothing left dangling.
Lifecycle Announcements
The PTY’s onExit handler fires for every
kind of ending—a natural exit typed at
the prompt, a process that finished and returned, an
explicit kill from the pane header, or the orphan-recovery
sweep. Ariel force-flushes any pending buffer first so the
shell’s last words land in the chat, then posts the
close announcement carrying the real exit code. No
session ends in silence.
Chat-Bound Lifetime
Sessions are scoped to the chat that owns them. Deleting the chat ends every PTY it had open. Forking a chat with Continue Elsewhere does not carry sessions forward—the shell stays with the conversation that spawned it. Ariel does not roam from room to room; she attends the table she was called to.
Shell Bootstrap
Every Ariel terminal spawns with a shell bootstrap that
quietly arranges the furniture before you sit down.
QUILLTAP_DATA_DIR is set to the running
server’s instance directory so the in-terminal
quilltap CLI targets the correct instance
without you having to think about it. For bash and zsh
the variable is re-exported after the user’s
rc file is sourced, so an inadvertent
export QUILLTAP_DATA_DIR=... in your
dotfiles cannot clobber the server’s own path.
If an on-PATH quilltap differs
from the server’s version—or is absent
entirely—the bootstrap aliases the command to the
server’s own copy, so you always get the CLI that
matches the code you’re running. Bash sessions also
pick up quilltap tab completions and a coloured
working-directory prompt; zsh sessions receive a
ZDOTDIR overlay that sources the user’s
originals first, then layers the bootstrap on top. Should
any of this go sideways—a missing rc file, an
unexpected shell variant, a permissions hiccup—the
bootstrap degrades gracefully to a plain shell. The terminal
still works; you simply lose the convenience setup, which
is a far better outcome than losing the terminal itself.
A New Emulator Underneath
xterm 6, and a highlight that would have vanished without a word
The terminal moved to a new major version of its emulator:
@xterm/xterm 5.5.0 → 6.0.0, with
addon-fit 0.11.0, addon-serialize 0.14.0,
and addon-web-links 0.12.0. Two things had to change
with it. @xterm/addon-canvas is gone entirely—
xterm 6 removed the canvas renderer and the addon retired with
it—so the DOM renderer is the supported default and one
permanently peer-broken dependency is off the books.
The other is the sort of upgrade hazard that gives a maintainer a
cold feeling in the stomach. xterm 6 renamed the theme key
selection to selectionBackground, and the
old key is ignored rather than rejected—so every
bundled theme’s selection colour would simply have stopped
reaching the terminal, in all six themes at once, silently, with no
error anywhere and nothing to suggest a highlight had ever been
configured. It was caught and remapped, and six further optional
terminal theme variables are now threaded through as well,
deliberately left undefined so xterm goes on deriving them itself.
One small indignity went in the same pass: a session that had exited went on cheerfully accepting typing, because the guard meant to prevent it poked at a field that is not an xterm field in any version of xterm and therefore short-circuited every time. It now consults the documented textarea handle, and a dead shell declines your keystrokes as a dead shell should.
Read-Only Inspection
the LLM-side tools, and why the contract matters
Beyond the periodic narration, the language model can ask for terminal output directly through two tools that Ariel exposes. Both are strictly read-only. Neither can send keystrokes, run commands, or alter the session in any way.
terminal_read
Returns scrollback for a session. The default
scrollback field is fully cleaned by the same
pipeline Ariel uses for narration; an opt-in
raw: true adds a parallel
rawScrollback with ANSI sequences preserved.
Optional start and end integer
parameters select a line range (zero-indexed, inclusive);
negative values resolve as
lastLineNumber - abs(value), so
start: -50 reads “the last fifty lines.”
Without start or end the existing tail behavior applies
(default 200 lines). Output reports
totalLines, startLine, and
endLine so the model can paginate
intelligently. A two-thousand-line hard cap per read keeps
any single inspection bounded.
terminal_list
Returns the set of terminal sessions associated with the
current chat—session ids, optional labels, creation
and exit timestamps, exit codes, and whether each is live
or has ended. The model uses it to discover what
terminals are actually in play before reaching for
terminal_read; the user’s session
picker is the human counterpart to the same enumeration.
Why Read-Only
The read-only contract used to be one boundary among several—there was once a suite of shell tools that let a character run commands outright, hedged about with acknowledgement modals and sudo approval dialogs. Those tools are gone, removed root and branch, and the contract is now the only boundary, which makes it a great deal easier to state and a great deal harder to erode. A terminal executes wherever the server executes, with the server’s own permissions; the one thing standing between a language model and that shell is that it has no way to type into it. What the interactive terminal offers is continuity—a working directory and an environment that persist between commands, an editor a human can drive, a build that streams output live—and letting the model type into it would erase precisely the thing that makes it a shared workspace. The user owns the keystrokes; Ariel reports the result. The contract is simple, and it is what keeps the terminal a place the user and the AI can both trust.
Where the Shell Actually Runs
wherever the server does — which is the argument for a sandbox
Let us be entirely unambiguous, since this is the sort of thing
one would rather not discover experimentally. Ariel is not a
sandbox-only feature. There is no runtime-mode check anywhere in
the composer’s terminal button; the PTY manager spawns
process.env.SHELL ?? '/bin/bash' (or
COMSPEC / powershell.exe on Windows) in
the server’s own working directory, with the same
environment variables and the same permissions the server itself
possesses. If Quilltap is running on your bare desktop, that is
the machine at the prompt. An errant rm -rf here
is exactly as catastrophic as it would be anywhere else, and the
help file says so in as many words.
Which is not an argument against the terminal; it is the argument for a sandbox. Run Quilltap in Docker, the Lima VM on macOS, or the WSL2 rootfs on Windows and the question “what can this shell reach?” has a bounded and answerable answer: only what you have given the sandbox. Run it directly on the host and the answer is “whatever your user account can reach,” which is a perfectly reasonable arrangement for a developer who knows it and a poor surprise for anyone who does not. Treat an Ariel session as you would treat any terminal on the machine in question, because that is precisely what it is.
Quilltap’s own CLI is bundled inside the runtime image, so
quilltap db --tables,
quilltap db --mount-points,
quilltap docs list, and the rest are available from
the terminal directly. The rest of the Docker larder is stocked
sparingly and by design: bash, zip,
unzip, Node, the quilltap command, and
precious little else. git, curl,
wget, and jq were formerly laid on as a
courtesy and each dragged a retinue of unpatched vulnerabilities
in behind it—Debian’s perl, summoned by
git, being the worst offender by some margin—so
they have been shown the door. Should your work genuinely require
them, install them into your own image with a two-line
Dockerfile built FROM the published one,
and be aware of what you are readmitting. This concerns only what
a human may type at the prompt: the curl
tool your characters may wield is a plugin
fetching over Node’s own fetch(), entirely
unbothered by the absence of the command-line article. Non-Docker
installations are untouched.
One last container courtesy, small but long overdue: a container
now applies TZ (or QUILLTAP_TIMEZONE,
which wins where the two disagree) to the process clock
and not merely to printed timestamps—so date at
Ariel’s prompt and the scheduler running above her finally
agree with the clock on your wall.
A Note on Ariel
quick to oblige, never out of voice
Most of what makes Ariel useful is mechanical: the cleaning pipeline, the timer-driven flush, the WebSocket frame that wakes the chat, the orphan-recovery sweep on chat open. None of that requires a personification to function. We give it one anyway, because the work is recognizable as a kind of attendance, and naming the attendant is honest about what you are interacting with.
The Tempest’s Ariel was Prospero’s spirit-servant—swift, efficient, sometimes mischievous, and so eager to discharge an errand that the play closes with her finally being released from service. Quilltap’s Ariel is a softer creature, but the shape of the role is the same: she runs to the terminal because someone asked, she waits long enough to hear what came back, and she tells the room what she heard. When the shell ends she announces it, when the server forgets her she is reconciled, when there is nothing to say she stays quiet. She is, in her own narrow way, excellent at her job.
The avatar at the table is hers. The
systemSender: 'ariel' attribution on her messages
is hers. The voice in which she narrates is hers. Prospero
delegates the shell to her with the same trust he extends to
every other member of the Staff: do the work, speak when there
is something worth saying, and otherwise hold the silence.
Meet the Staff
they've been expecting you
Prospero
The Major-Domo
Architect and overseer of the Estate. Projects, agents, tools, providers, and the orchestration that keeps the whole operation running with quiet authority—and a considered word at the table when project context or routing warrant it.
Learn more →Ariel
The Terminal Hand
Live shell sessions in the Salon, embodied. Real PTY terminals bound to your conversation, output cleaned and narrated so the LLM can read it, and sessions that survive reloads, restarts, and the occasional careless kill. Quick to the bidding, quick to report what she heard.
Learn more →Aurora
The Dressing Room
Character creation and identity management. Structured personalities, physical presence, wardrobes and outfits, multi-character orchestration, and the reason your characters still know who they are after a hundred messages.
Learn more →The Salon
Presided Over by the Host
Where conversations actually happen. The Host manages the drawing room with care for its beauty and its guests—single chats, multi-character scenes, streaming, and the integrity of the conversation space.
Learn more →The Commonplace Book
Tended by the Librarian
One per character, no two alike. Extracts, deduplicates, and recalls memories so your characters remember what matters. Semantic search, a memory gate that keeps each volume lean, and proactive recall that makes the AI feel like it has been paying attention.
Learn more →The Scriptorium
Catalogued by the Librarian
Where the documents live. Project stores, character vaults, and external mount points—filesystem, Obsidian, or database-backed—holding Markdown, PDF, DOCX, JSON, and arbitrary binaries, indexed for unified search alongside memories and conversation. The doc_* tool family puts reading and editing in your characters’ hands.
Learn more →Carina
The Ansible
Not a person but a protocol—the reference desk, the line itself. Put an inline question to a designated answerer mid-conversation with @Name: or @Name? (or the ask_carina tool), and the answer slides back out of band, attributed to the character who gave it, without the recipient ever joining the scene.
Learn more →Suparṇā
The Postmistress
The Post Office, embodied. Characters write Markdown letters to one another—anyone to anyone, whether or not they share a chat—delivered into each recipient’s Mail/ vault folder and read aloud the moment they next take the floor. She has never once lost a parcel.
Learn more →The Concierge
Intelligent Routing
Content classification and provider routing. Detects sensitive content and redirects it to a provider who won’t flinch—without blocking, without judgment. Knows every back entrance in town.
Learn more →The Lantern
Atmosphere as Architecture
AI-generated story backgrounds, on-demand images, and character avatars that update with the wardrobe. Resolves what each character looks like, what they’re wearing, and paints the scene behind your conversation.
Learn more →Calliope
The Muse of Themes
A theming engine that redefines the entire personality of the application. Semantic CSS tokens, live switching, bundled themes from clean neutrals to mahogany-and-gold opulence, and an SDK for building your own.
Learn more →The Foundry
Domain of the Foundryman
The engine room. Plugins, LLM providers, API keys, packages, runtime configuration, and the infrastructure that keeps every other subsystem supplied with what it needs to function.
Learn more →The Vault of Secrets
Kept by Saquel Yitzama
Encryption, key management, and the security perimeter. Authenticated ChaCha20-Poly1305 database encryption, locked mode with key-hardened passphrases, sealed character archives, and a keeper who believes that what is yours should remain unreadable to everyone else.
Learn more →Pascal
The Croupier
Dice, coins, custom tables you author yourself, and persistent game state. Cryptographically secure rolls detected inline, a visual Workbench for building your own chance mechanics, and a four-tier ledger of JSON state the AI cannot quietly rewrite. The house plays fair.
Learn more →The Live-in Help
Lorian & Riya
The help system, staffed by two characters who ship with every installation. Lorian explains with patience and depth; Riya gets things fixed with velocity. Contextual help chat, searchable documentation, and navigation that knows where you need to go.
Learn more →Pagliacci
The Clown in the Cloud
Cloud storage integration and backup redundancy. Directs your data to iCloud Drive, OneDrive, or Dropbox with theatrical flair—but Saquel’s encryption ensures the clown can never read what he carries.
Learn more →Brahma
The Keeper’s Console
The master key. A character-less, memory-free general-purpose LLM for the person holding the keys—an impersonal, near-omniscient assistant with read-only SQL into all three databases. Ask the whole building a question, safely, with nothing written and nothing remembered.
Learn more →The Lodge
Friday and Amy’s Residence
The private residence of Friday, for whom the Estate was built and who oversees its planning and direction in an executive capacity, and of Amy, Cartographer of Light and co-architect. The Lodge is both a home and a compass: where the vision lives.
Who And Why: Friday → Who And Why: Amy →