50: Quillins and the Developer Console - transcript

Download the MP3

Jessica: The QUILL Cast, episode fifty. I'm Jessica. Today we open the hood: Quillins, QUILL's extension system, and the developer console where you can script the editor yourself. We are fifty of fifty-four episodes in, with four to go.

Liam: I'm Liam. Recap of episode forty-nine first, since today stands on it. Last episode was braille production, the workflow that takes a document from a writable file to a physical embossed page. We covered BRF, BRL, and PEF, the page geometry as a measurement rather than a preference, the Read Layout Metrics pre-flight, the repair loop, Go to Longest Line, Go to Longest Page, Remove Trailing Spaces, and the way braille mode inherits the rest of the ecosystem. That episode ended with Jessica saying next up was the second-to-last of the post-app feature stories, which is what we are about to deliver.

Jessica: Frame for the next forty minutes. A Quillin is a plug-in, the name is QUILL's own, and the architecture leads with a safety decision worth understanding. Quillins run in a separate worker process. A crashed extension cannot take down your editor. Fault isolation is not a setting you forget to turn on, it is the floor plan. We will read the actual worker code in quill/core/quillins/host.py to prove it, not just assert it.

Liam: Two languages today, Python and JavaScript on a Node runtime, so both major scripting communities can build. We will look at one Python bundled example and one JavaScript one, both real, both in quill/quillins_bundled. We will also be honest about the parts of the previous short version of this script that drifted from the code, including the marketplace framing and the word counter claim.


Jessica: Before we get into the architecture, a short do this now beat. If you have QUILL running, open the Quillin Manager. The path is Preferences, Extensions, Quillin Manager. Don't change anything yet. Just read the list. Note the names, the categories on the right, and the row for Word Count (Node), because we are going to look at that one on disk later. Pause the audio, look, then come back. The seeing is part of the learning.


Liam: Welcome back. Now the architecture, the part the previous short version of this episode oversimplified. A Quillin is one directory on disk. Inside that directory is a manifest.json, an optional entry module, and any supporting files the Quillin needs. The manifest is the contract, and the contract is a published JSON schema at quill/core/schemas/extension.json. The loader, the linter, the build, the test suite, and the docs all read the same file. There is no negotiation. The schema is the authority.

Jessica: The schema's four required fields are schema, id, name, and version. Schema is the constant string quill.extension/1, the discriminator. Id is a reverse-DNS identifier you control, with a regex that is enforced in the schema. The com.quill namespace is reserved for first-party bundled Quillins, so you do not squat on it. Name is the display string. Version is a semantic version, MAJOR.MINOR.PATCH. Without all four, validation fails and the Quillin never loads.

Liam: Then the optional identity block, author, description, license, min_quill_version. Then capabilities, an array from a fixed enum of twenty-one strings. The categories field is a separate array from a different enum of eleven labels, and the Quillin Manager filters by them. Then the contribution surface inside contributes, the part that makes a Quillin feel like part of QUILL, with commands, menus, context_menu, hotkeys, abbreviations, smart_triggers, document_events, preferences, status_bar, schedule, file_types, snippet_gallery, transcription_providers, plus the sound pack and sound event contributions. That is a lot. The previous short version of this script said Quillins add commands, contribute menu items, supply AI prompts, react to document events, automate workflows. That is directionally right, but the surface is wider than those four items, and we owe you the list.

Jessica: What Quillins can do, restated in plain language now that the schema is on the table. Add commands, contribute menu items, hotkeys, context-menu items, and command palette entries. Supply AI prompts, AI skills, and transcription providers. React to document events, before save, after save, before close, after close, on creation, on open, on activation. Open files of certain extensions through a file-types subscription. Render preferences pages as data, status bar cells, scheduled background timers, sound packs, snippet gallery entries, and a Snippet Gallery you saw in episode twenty. Contribute typed smart triggers and abbreviations, the equals-name and the qsomething insert automation we covered in episode fifteen. So a Quillin can be a one-line snippet that inserts a date, a self-contained AI prompt library, a transcription provider, a scheduled status bar cell, or a complete preferences page. The space is large on purpose, and the schema enforces the shape.


Liam: Now the safety architecture, because this is the part the previous short version got the gist of but undersold. The Python entry module does not run inside the editor. Look at quill/core/quillins/host.py, line 326 onward. The method start on the ExtensionHost class calls subprocess.Popen with the current Python executable and the module quill.core.quillins.host_worker, passing the Quillin directory as an argument. Stdin, stdout, stderr, text mode, line-buffered. That is a real child process, with a real OS process boundary, talking to the parent over JSON messages.

Jessica: The protocol, in the small. The host sends a load message that names the Quillin entry module and the granted capabilities. The worker runs the entry module, the entry module calls register with its commands, the worker returns the registered command ids. When a user invokes a command, the host sends an invoke with the handler name. The handler may call back into the host, get text, set text, announce, prompt, read the clipboard, write a file, fetch a URL. Every one of those callbacks crosses the process boundary, and the host's ApiDispatcher checks the capability map before it does anything. Capability not granted returns a structured error. Consent-gated capability, fs.read, fs.write, net, runs the consent callback first, which is the Quillin Permission Request dialog you see when a Quillin asks for a sensitive action.

Liam: That capability map is exhaustive and lives in host.py as _METHOD_CAPABILITY. Every one of the twenty or so host-callable methods is wired to a capability, and a Quillin cannot call a method it did not declare. The fetch method additionally checks the net_allowed_hosts field in the manifest, so a Quillin that declares only one API endpoint cannot accidentally call another. A failed fetch with a host not in the allowlist returns a QuillinError, not a network call. The worker can ask. Only the host can act.

Jessica: The crash story, which is what makes fault isolation a property of the floor plan rather than a setting. The worker process is a normal child process. If it crashes, the parent reads EOF on the stdout readline and raises a QuillinError with the message worker process closed the connection. The Quillin Manager announces the error to the screen reader, the editor keeps running, and the rest of the Quillins are unaffected. If a handler raises, host.py's _pump_until_result catches it as a structured error message and returns it through the protocol, again with the editor untouched. If a Quillin event handler raises, main_frame_quillins.py's _run_quillin_event_handler_async wraps the call in a daemon thread and surfaces the error to the status bar via wx.CallAfter, never to the UI thread. The threading model itself is a load-bearing part of the safety story, and the source comments call this out: a daemon thread is required so a slow out-of-process worker never blocks the UI.


Liam: Two runtimes, Python and Node. The Python runtime is the default and uses QUILL's embedded Python worker. The Node runtime uses a separate node process speaking the same Quillin stdio protocol. The manifest chooses which one with the runtime field, which the schema's enum restricts to the strings python and node, and a separate allOf branch requires the main module's extension to match. Python main, .py. Node main, .js. Pick the wrong extension and the schema refuses the manifest.

Jessica: Look at quill/quillins_bundled/word-count-node. Its manifest is com.quill.bundled.word-count-node, name Word Count (Node), runtime node, main extension.js, capabilities editor.read, ui.announce, ui.command. The entry module is twenty lines of JavaScript. It reads a JSON request from stdin with a method name and a context object, runs the handler, writes a JSON response to stdout with the actions to take, announce, replace selection, set status. That is the entire protocol in one file. The handler counts words in the selection or the whole document and announces the result. No npm dependencies, no package, just the inline runtime shim. The README is honest about this. Published Node Quillins would depend on the @quill/api npm package rather than inlining the shim. This bundled example inlines the shim to stay self-contained.

Liam: And the Python runtime, the default. The bundled Quillin doc-guardian is a good example. Its manifest declares six document events, document.before_close, document.before_save, document.after_save, quillin.enabled, quillin.disabled, quill.shutdown. The entry module is extension.py, and the file defines Python functions matching those handler names. The Quillin declares capabilities document.events, editor.read, editor.write, ui.announce, settings.own.read, settings.own.write. No network. No filesystem. No clipboard. So the only consent dialogs the user ever sees from Document Guardian are the ones tied to the user actually changing a setting, not the Quillin running its core duties. That is the discipline of asking for what you use. The example to imitate is the smallest capability list that does the job.


Jessica: Now the honest correction we owe about the previous short version of this script. The previous version said QUILL ships several bundled ones, a word counter, writing-prompt packs, a document guardian, with the tone of a small handful. The current bundled set is seventeen directories under quill/quillins_bundled. We walked them in detail in episode forty-eight, so we will not re-enumerate them here, but the framing matters. Seventeen is a real surface, not a sample. Word Count, in its current form, ships as the Node example we just looked at, not as a separate Python word counter. Writing-prompts and writing-skills are manifest-declared prompt and skill libraries, not active AI agents. And Document Guardian is one of the seventeen. The short version's enumeration was impressionistic; this is the count.

Liam: The second correction, the bigger one. The previous version said third-party Quillin installation at large is deliberately gated while the sandboxing, signing, and review pipeline is finished, the roadmap has a hub and marketplace on it, and the project refuses to open the doors before the security model deserves it. That is true in spirit but the specifics are more precise. The security model is built, with capabilities, consent, out-of-process workers, the SEC-8 flag, the bundled-and-third-party split. The thing that is not finished is the review and signing pipeline for community submissions.

Jessica: The Quillin Hub is real, at hub.quillforall.org, and the service code lives in quillin-hub outside the editor tree. The Hub accepts seven artifact families, not just Quillins, including dictionaries, themes, prompts, and skills, and runs an authoritative validator on each. The validator is python -m quill.tools.artifact_validate, and the Hub, the in-app submission check, and CI all run the same tool so an author never sees three different verdicts. Submissions are reviewed in the Hub's Submission Forge and published via a pull request to the Community-Access/quill repository, so review is transparent, attribution is preserved, and every change is auditable. The Hub does not store artifacts.

Liam: That is the right framing. A general-purpose marketplace with auto-update of third-party extensions is explicitly out of scope for now, and the project docs say so: a general-purpose marketplace and auto-update of third-party extensions is a later deliverable. The Hub is the community store and submission service today. Submission tooling ships, the GitHub-native publish path ships, the marketplace front-end does not. Treating those as the same thing, as the previous short version did, understates what is built and overstates what is open. The current state is capability when it is ready, the same line we have used for fifty episodes.

Jessica: And remember the standing guarantee, repeated because it never stops being load-bearing. Safe Mode, set with QUILL_SAFE_MODE equals 1 or the --safe-mode flag, disables AI, watch folders, and Quillin contributions. The Quillin Manager still opens and shows the bundled list, but no Quillin code runs. Whatever anyone's code does, your clean room is always one restart away. The flag is the escape hatch, the floor under all the excitement, and it appears again in episode fifty-four.


Liam: Now the playground. The QUILL Developer Console, the QDC. Two consoles in one window, Python and TypeScript. The window is reachable through Tools, Advanced, Developer Console, then either Open Python Console or Open TypeScript Console. The command palette equivalents are quill.console.openPython and quill.console.openTypeScript. The window is a non-modal frame, lazy-initialized, that opens the first time you ask for it and stays open and preserves its namespace until you close it.

Jessica: The Python console is the easier one to describe. You type Python, you press Enter, the result lands in the transcript, the result of the last expression is also spoken. The execution is synchronous on the UI thread, and the consent text you see on first open says so in plain language. A loop with no exit condition will freeze QUILL until it completes. Save your document before running document-wide commands. That is the deal. The console exposes a small API object called q, plus the facades q.selection, q.doc, q.editor, q.settings, q.profile, q.bookmarks, q.quillins, q.macros, plus q.begin_macro and q.end_macro, q.spell, q.diagnostics, and q.a11y, q.commands, q.focus, q.support, plus q.describe_command. You can run any registered command by id with q.run_command. The transcript is saved, copied, or cleared, the history is navigable with up and down arrow keys, and the help dialog lives behind F1.

Liam: The TypeScript console is the more interesting one to describe because it is the proof that the QDC is not bolted on. The console starts a Node subprocess, the worker code lives in quill/tools/ts_worker/worker.js, and a TypeScript declaration file at quill/tools/ts_worker/quill-console.d.ts describes the quill object available to your code. The execution path crosses the same process boundary the Quillins use, and the result is marshalled back to the UI thread with wx.CallAfter. The pattern is await quill.gotoLine(42). The console returns a Promise, awaits it, and the transcript sees the result. If Node is not on PATH, the console surfaces a TypeScriptConsoleError and tells you so in the transcript. The Help button notes this: requires Node.js on PATH.

Jessica: Accessibility of the console itself got real attention, and the previous short version of this script said so. The console is screen-reader-friendly output, keyboard-first layout, transcripts of your session, and that is true. The transcript is a multiline TextCtrl in a teletype font, named ConsoleTranscript, with names on every region so screen readers can navigate them. The Language selector is a wx.Choice named LanguageChoice, the input is ConsoleInput, the status is ConsoleStatusBar. The transcript speaks short output verbatim, up to about three lines or one hundred fifty characters, and announces the line count for longer output. The first time you open the Python console, a CallAfter announces Python console ready. Type q.help() for the scripting API reference. That is the first screen-reader cue, before you type a thing.

Liam: The keyboard contract, lifted from the ConsoleWindow source. Enter runs the command, Shift-Enter inserts a literal newline, Ctrl-Enter forces execution of a multi-line block, Up and Down navigate history but only when the caret is on the first or last line of the input, Ctrl-L clears the transcript, Ctrl-Shift-C copies the transcript, Ctrl-S saves the transcript, Escape closes the window and returns focus to the editor, F1 is help. The frame-level EVT_CHAR_HOOK so Esc, F1, Ctrl-L, Ctrl-Shift-C, and Ctrl-S work regardless of which control has focus. That contract is the difference between a console and a toy.

Jessica: Safety in the console mirrors the house rules. First-run warning, a confirmation culture around dangerous operations, and the document's undo stack still guards the document, console edits are edits, control-Z applies. The consent text is shown on first open and can be re-shown from settings. Python code runs synchronously on the UI thread by design, and the warning text is explicit about that. A long-running Python expression will block the editor, on purpose, because the alternative, hidden background execution, would be worse for trust. If you need a long-running job, the documented path is to wrap it in a background task via threading from inside your code, and to do it in a Quillin where the work happens out of process. Experiment freely. The escape hatches you have trusted for fifty episodes are all present.

Liam: The layered defense is worth naming. Layer one, the manifest declares what the Quillin claims to need, and the host will not honor more. Layer two, every host-callable method is wired to a capability in the host's _METHOD_CAPABILITY map, so a Quillin cannot accidentally call a method it did not declare. Layer three, the consent-gated capabilities, fs.read, fs.write, net, plus settings.core.write, prompt the user via the Quillin Permission Request dialog before the action runs. Layer four, the worker is a separate OS process, so a buggy handler cannot read the editor's in-memory state or scribble on its widgets. Layer five, Safe Mode disables the whole machinery. Each layer is independent, and any one of them, working alone, prevents the worst outcomes. The combination is what the project means by trust.


Liam: The path from user to author, as the docs teach it, and as we will deepen in episode fifty-one next. Start in the console. Poke the API. See your document respond to a line of code. Grow a script that is actually useful. Wrap it in a manifest, with a real id, a version, a capability list that is the smallest set that does the job, a contribution surface that matches where the command belongs, and a README and LICENSE so the submission linter will pass. That is a Quillin, testable in the console environment before you ever share it. The distance from I use this editor to I extended this editor is shorter here than in anything else we have used in the course.

Jessica: Why this matters beyond hobbyists. Every specialized workflow in our community, the braille transcriber's ritual, the teacher's grading pass, the law office's document intake, the small-press author's submission formatting, is a Quillin someone could write. Extension systems are how software serves the niches its developers never imagined, and our community is nothing but underserved niches. The audit pass over bundled Quillins in episode forty-eight was a small proof. The Hub is the wider proof, in design and in trust, even with the marketplace front-end still future work. The console is the workshop.

Liam: The honest state, again, because the previous short version got the tone right but the precision wrong. The QDC is implemented and reachable in 0.5.0 under the Developer and Power Text and Full QUILL profiles. Python and TypeScript both ship, the q scripting API is wired, and the per-feature gating is profile-dependent. The Python console is off by default for Essential, Writer, and Reader profiles, the TypeScript console is off except in the Developer profile. The QDC is not for ordinary users. It is for developers, power users, and accessibility professionals who want to inspect and automate the running editor without rebuilding.

Jessica: One more architectural note for listeners who like the wiring. The console window is reachable because main_frame_devtools.py implements a ConsoleHost protocol that QuillScriptAPI can reach without importing wx. The same mixin hosts the TypeScript worker, the consent check, the diagnostic summary command, and the history load and save. The settings.dev_console_consent_accepted flag suppresses the consent dialog after the first time. The settings.console_typescript_timeout setting, default thirty seconds, bounds the TypeScript worker round-trip. The settings.console_enabled setting, default true, kills the consoles without uninstalling them. All three are reachable through the Preferences hub, and the help text explains the off-by-default profile gating in plain language.

Liam: And a quick note on the submission linter, because it is the practical front door. The tool is python -m quill.tools.quillin_lint, and it layers three independent checks. One, schema check against the published JSON Schema, run by a small executable subset the linter ships. Two, manifest contract validation through quill.core.quillins.validation.validate_manifest, the authority the loader actually enforces. Three, structure and capability hygiene, the entry module exists when declared, a README and a license are present, and every consent-gated capability is surfaced for deliberate reviewer scrutiny. The --strict flag, which is what CI uses, turns warnings into failures. A submission cannot land with unresolved advisories. That is the floor an author has to meet before the Hub's Submission Forge will accept the artifact, and the same tool runs in the editor through Tools, Quillins, Submit to Quillin Hub, so what you see locally is what the Hub will see.


Liam: Homework. One: open the Quillin Manager and read what is installed and enabled, know your own machine. Two: open the developer console, run one line, ask it for the current document's word count or text, and feel the API answer. The expression is q.doc and the line_count and char_count attributes are there for the asking. Three: open quill/quillins_bundled/word-count-node/extension.js in any text editor and read the protocol, twenty lines, no magic. Four: if you script at all, in any language, skim the Quillin authoring documentation at docs/quillins/quillins.md and let one idea form. No pressure. Ideas ferment.

Jessica: Next episode is the build. We change seats, and you are the author. We scaffold a real Quillin, Layer 1 snippet, then Layer 2 Python handler, and we read the produced files out loud so every line of the manifest is the line you wrote. Episode fifty-one is the workbench.

Liam: I'm Liam.

Jessica: I'm Jessica. The hood is open, and the worker is on the other side of the process boundary, which is exactly where it should be.

Back to all episodes