Skip to article
PlanToCodeDocsGet the app

HandbookArchitecture

Command output, documents, and previews

How command output is captured and read in ranges, how HTML documents run in a sandboxed origin on three platforms, how web pages become Markdown, and how files travel in chunks.

Checked against the source on 17 September 2026

On this page

Command output as an artifact

Command output is pulled from the desktop while it runs and from Codex once it settles
Command output is pulled from the desktop while it runs and from Codex once it settlesOne command over time. Codex streams output deltas and writes its own capture file, the desktop appends every delta to its artifact, and the timeline row only receives the size and generation. An open viewer reads up to 256 KiB at a time from the artifact. At item/completed, bytes that differ from Codex’s aggregated output replace the artifact as generation 2, so a read sent for generation 1 gets a conflict and the viewer refreshes the row. When the row settles it points at Codex’s capture file, and reads go through the desktop to thread/commandOutput/read.
Codexruns the command
Desktop artifact
capture file
output deltas with bytes
completed, output up to 1 MiB
generation 1, each delta appended
generation 2
same bytes keep generation 1, different bytes become generation 2
Timeline row
size and generation, no bytes
settles, now points at Codex
Viewer
closed, no reads
reads up to 256 KiB
generation 1 is gone: conflict, refresh the row
not read again
reads Codex’s capture through the desktop
item/started
item/completed
row settles
  • Live rowcommandOutput.artifact

    The desktop appends each output delta to its artifact before the row changes, then sends runId, byteLength, generation, complete, and sourceIncomplete at most every 100 ms per command. No output byte enters the timeline.

  • Range readrun.codexChatCommandOutputRead

    Only an open viewer reads, 1 byte to 256 KiB per request, and appSessionId binds the read to the owning base session. A generation the artifact no longer holds answers with a conflict, and the viewer refreshes the row before it reads again. Phones call it through the relay, and the desktop viewer calls the same read directly.

  • Completionitem/completed

    The desktop compares its bytes with Codex’s aggregatedOutput, which Codex caps at 1 MiB. Identical bytes only mark the artifact complete. Different bytes replace it as the next generation, and a known truncation marker in that text sets sourceIncomplete.

  • Settled rowprotocolOutput

    Once thread/items/list returns the command, the row points at the capture file Codex wrote while it ran, and an open desktop viewer follows it by the unchanged block id. The desktop forwards reads as thread/commandOutput/read and checks the echoed identity. capturedComplete is false when Codex kept no full capture.

HTML documents in a sandbox

Each HTML preview is its own site, and the resource root is its top folder
Each HTML preview is its own site, and the resource root is its top folderLeft, the desktop disk. Right, what a preview page at /docs/report.html asks for. Each request maps onto the same path under the resource root, so relative imports and fetches load, a path cannot climb above the root, and a symlink that leads outside is refused. Below, the sandbox gives the page no access to the app, and a navigation message is the only thing the app acts on.
sandbox
no access to the app’s page or storage
plantocode.html.open
The appopens a file under the root or a web page
Desktop disk
~/Pictures/logo.png
~/.ssh/id_ed25519
~/work/shop
resource root
docs/report.html
src/chart.js
data/sales.json
assets → ~/Pictures
.ssh/id_ed25519
leaves the root
not there, 404
Preview page
ptc-document://<preview-id>.localhost
no URL reaches above the root
/
/docs/report.html
the page itself
/src/chart.js
import "../src/chart.js"
/data/sales.json
fetch("../data/sales.json")
/assets/logo.png
<img src="../assets/logo.png">
/.ssh/id_ed25519
fetch("../../../.ssh/id_ed25519")
  • Resource rootopen_html_preview_command

    The project folder when it contains the document, otherwise the document’s own folder. The caller generates the preview UUID, and the desktop refuses to bind that ID to a second document.

  • Path checkptc-document://<preview-id>.localhost/<path>

    The handler refuses a decoded path with .., a backslash, a colon, or NUL, serves index.html for a folder, and answers 404 when the canonical path is missing or leaves the root.

  • Sandboxsandbox allow-scripts

    Scripts, modules, and fetch run in an opaque origin with no access to the app’s page or storage. A plantocode.html.open or plantocode.html.ready message from that frame is the one thing the app acts on, and only for a file under the root or an http(s) page.

  • Phonesfiles.readHtmlPreviewResource

    The iPhone app at ptc-document://<preview-id>.localhost and the Android app at https://ptc-<preview-id>.invalid resolve the same paths through the desktop, in verified chunks of up to 4 MiB, with no script bridge into the app.

The protocol answers OPTIONS with 204, serves GET and HEAD only, sets Cache-Control no-store and X-Content-Type-Options nosniff, and appends navigation.js after the document end without touching the file; that script posts plantocode.html.open to the parent and blocks every scheme other than ptc-document and http(s). Local links inside a document therefore enter the preview history, web links open in the web preview, and Reload or a file change refreshes the rendering.

Web pages as Markdown

A web link opens as a Markdown rendering instead of a live page. The WebView extracts the article content and converts it with a GFM-aware Turndown pipeline, the Rust side converts with quick_html2md for content that never reaches the WebView, and the Browser Bridge has a third, hand-written DOM-to-Markdown renderer whose output the desktop stores as capturedBrowserMarkdown and wraps with the same title header. Three converters, one target format.

File reads and transfers

files.readContent returns a text document of at most 2 MiB; a larger file fails with “Text preview is too large”. Its request carries projectDirectory, path, and optional encoding and allowExternalFile, and the encoding is accepted but ignored; it has no chunk offset or media fields, because text and binary requests stay separate. files.readBinary and files.readMediaRendition read sequential chunks from offset zero. Each response carries offset, nextOffset (null at EOF), dataBase64, byteCount, contentVersion, modifiedAt, and mimeType, and renditions add originalByteCount. Neighbouring limits: media rendition sources up to 1 GiB, stream transcodes up to 256 MiB with 40-second timeouts, media range reads of 1 MiB, and chat-attachment uploads in 1 MiB chunks, at most 2,048 of them.

A file of any size travels as independent 4 MiB reads tied to one version
A file of any size travels as independent 4 MiB reads tied to one versionA 9,961,472-byte file drawn to scale is read in three requests at offsets 0, 4,194,304, and 8,388,608. The desktop opens the file for every request and keeps nothing in between, while the phone pins the version and size from the first answer. When the file is saved during the transfer, the next answer carries a new version and the phone discards the whole read.
report.pdf9,961,472 bytes
4 MiB
4 MiB
1.5 MiB
0
4,194,304
8,388,608
Desktopopen, read, answer
keeps nothing between requests
Phonefile unchanged
pins version A and 9,961,472 bytes
version A again
nextOffset null
Phonefile saved midway
version A
file saved
version B
File changed while loading. Open it again.
  • Chunk readfiles.readBinary

    Each request names the path and an offset. The desktop opens the file, reads up to 4 MiB there, and answers with the next offset, the total byteCount, and a contentVersion. HTML preview resources and media renditions use the same reader.

  • Version pincontentVersion

    For files.readBinary it hashes the path, size, and modification time each time a read opens the file. Both phones keep the first answer’s version and size and discard the whole read when a later answer differs. A file that shrank below the offset gets the same message from the desktop.

  • Relay frameDEVICE_LINK_MAX_FRAME_BYTES

    A 4 MiB chunk is about 5.3 MiB of base64, so every answer stays well inside the relay’s 32 MiB frame limit, and the file itself has no size cap.

Syntax highlighting

Source previews on the desktop use Shiki grammars, and each phone app highlights with its own engine.