Chat attachments: bidirectional delivery and previews

How incoming user files and outgoing agent files appear in chat, and whether they can be opened. Four sections: current behavior, eight implementations, remaining work, and the target design. How attachments enter model context is a separate subject covered in Complete attachment handling design. This page references its conclusions where needed. Both documents share the marker grammar in openprogram/attachments.py .

1 Current behavior Four inbound paths What appears in a message Outbound delivery Byte-serving endpoints Status of three requirements 2 Eight implementations Four outbound designs Additional techniques 3 Remaining work 4 Target design Gaps and tradeoffs Implementation status

01 Our current implementation Current behavior

Attachment bytes, markers in message text, model input, and user-visible content are separate concerns. All four inbound paths follow the same rules: persist bytes once, leave a marker using the shared grammar, provide images or paths as appropriate to the model, and render chips with one parser.

1.1 Four inbound paths, one marker grammar

Source Byte storage Marker in message text Model input Chat display Web · image Paste / drag / picker Session workdir/attachments/ Same storage, deduplication, and limits [attachment: image-1.png (png, 20 KB) @ /abs/path] ImageContent Plus a path Thumbnail chip Click to view the original Web · document PDF / code / binary Session workdir/attachments/ sha256 deduplication, 32MB/64MB limits [attachment: name (pdf, 1531 KB, 500 pages) @ /abs/path] Path 4KB preview Clickable chip Rendered in an overlay @mention · entered path file-search / file-resolve Unchanged location Zero copy; use the absolute path only Same grammar as above Path Clickable chip Same parser Channel Telegram / Discord Slack; no WeChat files State directory/channels/<Channel>/ accounts/<Account>/attachments/ Separate directory, 20MB limit, no deduplication [attachment: photo.png (png, 20 KB) @ /abs/path] Same format_marker as the web Images as ImageContent Paths for other files Thumbnail chip Open on the web Speaker prefix Speaker prefix comes before the text; attachment markers follow. _one_line strips brackets to prevent forged prefixes.

Diagram details: Same persistence path, deduplication, and limits as documents. Plus a 4KB opening preview. Slack (WeChat has no attachments). Can be opened on the web. [display name (id)]. is prepended to the text; attachment markers are appended. They never overlap. _one_line removes square brackets, preventing forged additional prefixes.

All four inbound paths provide content to the model and leave the same clickable representation in chat. Markers are generated exclusively by attachments.format_marker ; chips are parsed exclusively by parseAttachments . A fifth inbound path must call this formatter instead of defining another grammar. Web images previously existed only as base64 in memory: the model could see them, but users could not reopen them. They now share document persistence (sha256 deduplication, 32MB/64MB limits, path insertion) while remaining ImageContent in model input. openprogram/attachments.py · openprogram/channels/_attachments.py:181 · apps/server/openprogram_server/_webui/ws_actions/chat.py · apps/web/components/chat/messages/user-attachments.tsx

1.2 One message, three sources, one result

Actual message rendering Drag a PDF into the web UI spec.pdf pdf · 1531 KB · 500 pages Please review this specification button opens the browser PDF reader in an overlay Paste a screenshot into the web UI image-1.png png · 20 KB What does this error mean? Thumbnail from /api/file-raw; click for original size Send an image through Telegram photo.png png · 20 KB [Zhang San (10086)] Look at this Keep speaker and message; hide marker The chip regex previouslymatched the path without capturing it. Its comment said, “the path is for the model, not the chip.” Capturing it is the entire change needed to make the chip clickable. The optimistic-message gap is also fixed:chat_ack echoes the stored text, so the newly sent attachment is clickable immediately without reloading.

Diagram details: Only the speaker prefix and message remain; the marker is hidden.

Three sources, one component.parseAttachments extracts markers from text; AttachmentChips renders chips; AttachmentPreview opens them using FileViewer the absolute-path mode. User messages, assistant messages, and the message rail share this implementation. apps/web/components/chat/messages/user-attachments.tsx · attachment-preview.tsx · apps/web/components/files/file-viewer.tsx · apps/web/lib/net/chat-stream.ts

1.3 Outbound: one tool, two delivery paths

File available to the agent write / image_generate canvas / output from any script send_file(path) Resolve symlinks → check allowed roots Read size → register a marker No delivery; no channel knowledge Explain rejection to the model instead of silently failing Dispatcher appends markers when finishing [attachment: chart.png (png, 42 KB) @ /abs/path] final_text is the same string for every consumer: database storage, streaming result, and TurnResult Web assistant message Same chip and overlay as user attachments Native channel upload Extract → post_file → hide marker Failure / unsupported → explain The four platforms’ post_file implementations already existed (Telegram sendPhoto/sendDocument, Discord multipart, Slack three-step upload). Only the connection from the tool was missing. Capabilities follow channel declarations unsafe_in: in cli / tui / wechat / plan, the tool is absent from the tools array, so the model never discovers the inability to send only after calling it. Allowed roots arethe session workdir and explicitly bound project directories. The default projectdoes not count: its path is the user’s home directory, so accepting that binding would make all of home readable for outbound delivery. The sandbox (openprogram/sandbox/) governs shell commands. This tool runs as in-process Python;path validation is its entire boundary, so symlinks must be resolved before containment is checked.

Diagram details: Extract marker → post_file → omit marker from delivered text. Failure / unsupported → rewrite as a readable explanation.

Why use a tool instead of a MEDIA: instruction line in message text? A textual convention can leak to the user when extraction fails; channel inbound handling already exposed that failure mode. A tool call validates file existence and can report “the file no longer exists.” Merely mentioning a path is not interpreted as a delivery request. openprogram/programs/tools/interaction/send_file.py · openprogram/agent/dispatcher/__init__.py · openprogram/channels/_conversation.py:_deliver_outbound_files

1.4 Byte-serving endpoints: two routes with separate contracts

Two routes for browsers to retrieve bytes Project-relative path GET /files/raw?project_id&path Always reject absolute paths; preserve this invariant Serves the file panel and central file tabs FileViewer Images / PDF / Markdown / line-numbered code Binary files show a name, size, and download card Absolute path GET /api/file-raw?path&session_id Same headers as /files/raw: nosniff and CSP sandbox, including the PDF exemption GET /api/file-read?path=<Absolute> Text uses this route, with an additional binary flag FileViewer abs mode switches both endpoints readable_roots Project root State directory/sessions channels/*/accounts/*/attachments Explicitly bound project directories Glob down to attachments leaves, because credentials.json is a sibling Both directions share resolve_within First .resolve() resolve symlinks, then check containment. A symlink inside an allowed root pointing to ~/.ssh passes a name-based check; only checking its actual target rejects it. Outbound uses the narrower sendable_roots: only the session workdir and explicitly bound project directories, excluding the project root and channel directories.
Do not expand /files/raw. Its “always reject absolute paths” invariant should remain. Each route retains its own contract. The viewer implementation remains shared: FileViewer adds an abs argument to switch the two endpoints to absolute-path routes. Image, PDF, Markdown, line-numbered code, and download-card rendering still have one implementation. openprogram/attachments.py:readable_roots · apps/server/openprogram_server/_webui/routes/files/file_search.py · apps/server/openprogram_server/server.py · apps/web/components/files/file-viewer.tsx

1.5 Status of the three requirements

RequirementAvailable now?ImplementationRemaining gap
User-sent images
Open for viewing
Yes
Thumbnail and overlay
Images use document persistence (sha256 deduplication, 32MB/64MB limits, path insertion) while retaining ImageContent blocks. Pasted images without filenames receive an extension based on media type; the chip regex captures the path. Overlay only, no central tab; no multiresolution thumbnails for large images
Agent sends images
or files to users
Yes
Web and channel paths
send_file registers a marker; the dispatcher appends it to final_text; the web renders a chip and channels call the existing post_file; unsafe_in Capabilities declared per channel No caption parameter; no separate outbound allowlists for plugins and MCP tools
Readable files
Open directly for viewing
Yes
Images, PDF, Markdown, code, download cards
New /api/file-raw and absolute-path /api/file-read, FileViewer plus abs mode, reused in the overlay Code has no syntax highlighting (pre with line numbers is sufficient); no central-tab entry
An additional defect fixed: attachments arriving through Telegram or Discord displayed their [attachment: /abs (image/png, 20481 bytes)] line as ordinary text when the same session was opened on the web. Channels had their own grammar (bytes instead of KB, and no @ path), so neither chip regex matched. Both sides now share format_marker, producing a clickable thumbnail chip on the web.

02 Other implementations Eight-way comparison

All eight repositories under references were examined. Missing capabilities are explicitly marked absent. Five let agents actively deliver files to users; three share this technique: write an instruction line in the reply and remove it in the delivery layer.

RepositoryInbound context handlingDisplay and openingAgent-initiated file deliveryReadable-file previews · cleanup
claude-code
TUI
Images are inline base64 blocks. Bytes are stored per ~/.claude/image-cache/<session>/ with 0600 permissions. Limits: 5MB and 2000px; sharp resizing; JPEG quality steps 80/60/40/20. Bridged web and mobile attachments inject paths for the read tool. utils/imageStore.ts:9-67 · utils/imageResizer.ts:256-334 · bridge/inboundAttachments.ts:2-11 [Image #N] Text chip; clickable OSC-8 hyperlink opens the system image viewer. Non-images show a summary line Read <Path> (N lines). No inline terminal image protocol components/ClickableImageRef.tsx:16-34 · messages/AttachmentMessage.tsx:131-158 Yes, dedicated tool parameter.SendUserMessage(formerly Brief), with attachments: string[], described as “photos, screenshots, diffs, logs, any file the user should see.” Uploads to the server return a file_uuid for mobile and web rendering; 30MB limit. Related tool SendUserFile appears only in references; its directory is absent from this dump tools/BriefTool/BriefTool.ts:25-30 · tools/BriefTool/upload.ts:32-166 · tools.ts:42-43 TUI Markdown rendering and syntax highlighting. PDFs become document blocks or per-page JPEGs through poppler. No built-in image viewer; OSC-8 opens the system viewer. Cleanup: each session start removes other sessions’ image caches; the in-memory path table is a 200-entry LRU. utils/imageStore.ts:129-150
codex-cli
TUI
Base64 data URL as input_image, wrapped in a <image name path> tag.Bytes remain in place without copying. 2048px limit and patch budget.sha1 content-addressed memory cache, 64MB LRU. Remote URLs are rejected protocol/src/local_media.rs:21-35 · utils/image/src/lib.rs:26-203 [Image #N] Label only, not clickable. Kitty and sixel exist in the repository but only render the mascot, never attachments tui/src/pets/image_protocol.rs:27-31 · tui/src/history_cell/messages.rs:95-96 No general-purpose tool. Image generation writes to disk and prints file://, with a prompt claiming “the image has already been shown to the user” ext/image-generation/src/artifact.rs:31-43 TUI Markdown and syntect highlighting. No image or PDF viewer.file_opener configuration rewrites file references into vscode:// URIs for the editor core/src/config/mod.rs:939-941
openclaw
Web + TUI
Two modes selected by capability. Models with vision receive base64 blocks; otherwise content goes to the media store and a receipt media://inbound/<id> is inserted into the text. Store permissions 0700; inbound and outbound subdirectories. Images 6MB, audio/video 16MB, documents 100MB. EXIF orientation normalization followed by JPEG re-encoding gateway/chat-attachments.ts:391-393 · media/store.ts:146-152 · media/constants.ts:1-4 Web actual thumbnails open in a new tab. Non-images use cards; audio has a native player, with “checking / unavailable” status badges. TUI has labels only ui/src/ui/chat/grouped-render.ts:822-830,1203-1290 Yes, two paths. A separate text line MEDIA:<path or URL>, taught through the system prompt; only allowlisted core tools may send local paths. Plugins and MCP tools are excluded to prevent induced reads of unauthorized files. A separate message tool accepts attachments/media/caption/asDocument and other parameters. Outbound files first enter the store’s outbound directory, then go to the channel agents/system-prompt.ts:424-426 · agents/pi-embedded-subscribe.tools.ts:280-300 · media/outbound-attachment.ts:5-30 Web Markdown-it with DOMPurify sanitization; native audio/video players. PDFs and documents are extracted as text for the model, not previewed for users. Cleanup: two-minute media TTL media/document-extractors.runtime.ts:14-45 · media/store.ts:24,188
opencode
Web + TUI
Base64 data URLs stored in the session SQLite part.data column, without disk files.@ Mentioned files actually run the read tool on submission; results are injected as two synthetic plain-text parts. 5MB and 2000px limits, Lanczos3 resizing, stepped JPEG quality. Deduplicated by URL session/prompt.ts:888-922,982-1002 · image/image.ts:10-136 Web image cards; click opens a lightbox dialog. Non-images are non-clickable file-icon cards.@ Mentions are character-offset highlights in the text, not cards. TUI shows a MIME badge and filename ui/src/components/message-part.tsx:1103-1164 · tui/src/routes/session/index.tsx:1350-1358 No. Tools may return attachments, but that goes into model context, not to the user. No channel adapters tool/tool.ts:52 Built into the web UI.GET /file/content returns text or base64 binary; the frontend renders images, audio, and SVG, plus file tabs and diff views. An “open in external application” menu opens projects, not attachments. No cleanup; data URLs are cascade-deleted with session rows server/routes/…/file.ts:62-100 · ui/src/components/file-media.tsx:31-70
hermes-agent
Primarily chat channels
Two routing modes selected per turn. Native mode inlines base64 and also adds a path hint, providing both to the model. Text mode first runs vision_analyze and prepends a description; the model never receives pixels. Documents receive sandbox paths only, with text files additionally inlined. Bytes use type-specific cache directories.No initial size limit; after server rejection, compress to 4MB agent/image_routing.py:298-379 · gateway/run.py:7794-7900 Telegram and Discord provide native rendering. Its CLI and TUI show one label line; the web uses an xterm.js terminal stream. None of these three are clickable cli.py:5649 · web/src/pages/ChatPage.tsx:19-23 Yes, the most complete of the eight. Text MEDIA:<path> instructions and a send_message tool are both supported. The security check validate_media_delivery_path: resolves symlinks before checking containment; files must be inside safe roots or an operational allowlist. System prompts declare capabilities per platform; CLI explicitly says “no attachment channel here; do not emit MEDIA tags.”![alt](url) is also automatically extracted and sent as a photo gateway/platforms/base.py:867-905,2262-2301 · agent/prompt_builder.py:420-597 No general viewer; CLI renders Markdown through rich. Inbound text documents are inlined into prompts instead of previewed. Cleanup: 24-hour TTL for image and document caches gateway/platforms/base.py:672-690,989-1005
pi-mono
TUI
@file parameter is inlined, and the model also receives an <file name> text marker.Clipboard paste supplies only a path: write a temporary file and insert its path as text into the editor for the model to read. 2000px and 4.5MB limits; PNG first, then stepped JPEG quality, running in a worker; users can disable it in settings cli/file-processor.ts:48-89 · modes/interactive/interactive-mode.ts:2442-2462 · utils/image-resize-core.ts:22-127 The only implementation of actual inline terminal images among the eight. Kitty and iTerm2 protocols; disabled under tmux and screen, with fallback to [Image: name [mime] 800x600]. However, only images in tool results are rendered; user-sent images are not echoed tui/src/terminal-image.ts:1-109,447-453 · modes/interactive/components/user-message.ts:11-34 No. Tools are only bash edit find grep ls read write; no channel adapters TUI Markdown, syntax highlighting, diffs, and inline terminal images. No PDF support. No cleanup for temporary clipboard images
pi-ai
Protocol layer
A read-only copy of the pi-mono package: twelve files, no UI or tools. Base64 images; image blocks are removed entirely if the model declares no image-input capability. Images in tool results are split into text and a synthetic user message providers/openai-completions.ts:561-570,657-714 No No No
weclaw
WeChat
The agent cannot see images at all. Inbound images are intercepted before reaching the agent: download, detect magic bytes, write as <timestamp>.<ext> with a sidecar, then reply “Saved: filename.” Voice is the exception, using WeChat’s transcription as the prompt. No size limit messaging/handler.go:288-294,726-783 No separate UI; WeChat is the client Yes, implicitly. No convention is needed: bare absolute-path lines or ![](url) in replies are extracted and uploaded, then the line is replaced in place with “Attachment sent: filename”. Failure appends “Attachment delivery failed.” Files must be under allowed roots, defaulting to ~/.weclaw/workspace, with an additional extension allowlist messaging/attachment.go:17-107 · messaging/handler.go:491-524 None. Markdown is stripped rather than rendered and sent to WeChat as plain text messaging/markdown.go:7-40

Four outbound designs: which to adopt

How the model expresses “send this file to the user” Tool parameter claude-code SendUserMessage( attachments: [path]) Explicit types prevent formatting mistakes File existence can be checked at the tool layer Sending a file consumes a tool call Speech and files are separate actions Instruction line in text openclaw · hermes MEDIA:/abs/path Separate line extracted by the delivery layer Send with speech; no extra round trip System prompt enables channel support Incorrect paths require security checks Partial extraction exposes instructions Implicit scanning weclaw Any bare absolute-path line or ![alt](url) Nothing for the model to learn; no prompt cost The line becomes “Attachment sent: x” Merely mentioning a path can trigger delivery Intent cannot be distinguished from description No codex-cli · opencode · pi-mono · pi-ai When the model wants to deliver a file, it can only paste a path in its reply. Codex image generation prints file:// and claims delivery, but the TUI only renders a text line. Our state when compared. The three text-instruction implementations use the same security check:paths must remain inside allowed roots, with symlinks resolved before containment checks. Openclaw adds another restriction: only allowlisted core tools may send local paths; plugins and MCP tools are excluded to prevent induced reads of unauthorized files.

Diagram details: Speaking and delivering files become separate actions. Same action as speaking; no additional round trip. Enable capabilities per channel in the system prompt. The model may write an incorrect path; a security check is required. Incomplete extraction leaks the instruction into user-visible text. file:// and claims it has been shown to the user,. This cell represents our position at the time of the comparison.

Additional techniques

Output as a page

Implemented independently by three projects

Openclaw materializes documents as /__openclaw__/canvas/documents/<id>/index.html, rendered inline in chat through [embed ref="cv_123"] ; its system prompt prohibits local paths in embeds. Codex asks the assistant to emit an ::codex-inline-vis{ instruction line; the TUI materializes the HTML fragment in a sandboxed viewer and replaces the line with a browser link, with a 2MB limit and strict CSP. Pi-mono’s /share exports the complete session as self-contained HTML, publishes a private gist, and returns a hosted viewer URL.

canvas-hostinline-vis/share
Safely serving bytes to the web

Short-lived signed tokens

Openclaw’s /__openclaw__/assistant-media?source=…&mediaTicket=… uses token format v1.<payload>.<signature>, with a five-minute TTL. This addresses browser access to bytes without granting arbitrary-path access, imposing stricter controls than allowed-root checks alone.

ticket TTL 5minroot guardsymlink resolve
Chips as editing units

Removing the placeholder cancels delivery

Claude-code treats [Image #N] as an indivisible unit for cursor and vim operations. An image is sent only while its placeholder remains in the text. Deleting the chip cancels the attachment without a separate remove button.

Cursor.ts:322vim/operators.ts:469
Other techniques: Hermes rewrites host cache paths to container paths so a sandboxed agent can open the same file. Its native mode inlines pixels and also includes a path hint, giving the model both. Codex has a view_image tool for loading local images into context. Pi-ai removes image blocks entirely when the model lacks declared image-input support—the fallback gap recorded in attachment-handling. Cleanup intervals differ: Openclaw two minutes, Hermes 24 hours, Claude-code removes other sessions’ caches at every session start.
An earlier description has been corrected: attachment-handling previously described Opencode as synthesizing a fake read tool call and result on submission. The actual code injects two plain-text parts marked synthetic, containing “Called the Read tool with the following input …” and the actual read result. The model receives role: "user" text, not tool_use and tool_result blocks. The three reasons for rejecting that approach remain; only the description was wrong. Both documents are corrected.

03 Planned improvements Next steps

All three requirements are implemented; section one describes their resulting behavior. Only deliberately deferred work and issues discovered during implementation remain here. None blocks the existing user paths.

Remaining itemBenefitWhy deferredWhen to implement
Central file tab
as a second opening option
Large files requiring extended reading can remain beside the chat without occupying an overlay Central tabs are keyed by (projectId, relative path) . Absolute-path tabs require changes to center-tabs-store and its consistency checker; the overlay already covers quick viewing without changes When someone needs to read an attachment for an extended period.turn-files-chips already uses fileTabId plus useCenterTabs; reuse that approach
send_file caption for Telegram sendPhoto can attach explanatory text to the image instead of sending two messages The marker grammar must match inbound markers and has no caption field. Reply text already accompanies the file, so repeating it provides no benefit When a platform specifically requires text attached to the file
Outbound tool allowlist Openclaw’s extra restriction: only core tools may send local paths, excluding plugins and MCP tools to prevent induced unauthorized reads Currently, send_file is the only outbound entry point and already checks allowed roots. Another layer would block nothing new When plugins or MCP tools can trigger outbound delivery themselves
Short-lived signed tokens Openclaw media endpoints use signed tokens with a five-minute TTL, stricter than allowed-root checks alone The web UI listens locally, already has a same-origin guard, and validates allowed roots at the route. Tokens add no protection under these assumptions As soon as the web UI is exposed beyond localhost; this is the first addition required
Code highlighting Opened .py attachments display more than a single color FileViewer with line-numbered pre is sufficient. docs_site pygments runs at build time and cannot directly serve runtime single-file rendering When users report a need
An issue discovered during implementation and already fixed: every session is automatically bound to the default project, whose path is the user’s home directory. Treating that binding as explicit would let sendable_roots read and deliver any file under home from any temporary session. Allowed roots therefore accept only non-default project bindings. This emerged from inspecting the real state directory; static code reading did not expose it, because project_for_session the name suggests explicit bindings only.

04 Target design Desired behavior

The requirement: any file, regardless of source channel, producer, or format, opens through the same viewer in every interface. Agent-produced and user-uploaded files differ only in their source field. The diagram is not constrained by the current implementation. Dashed elements are unavailable or too costly; reasons follow.

Source Single attachment record Consuming interfaces Web upload · paste · drag @mention · entered path Inbound channel attachments Agent-produced files One attachment record id · sha256 · name · MIME Byte location · page or line count Source (user/agent/channel) Session · message Only a reference remains in the text [attachment:<id>] Rename · deduplication · cross-turn references become queries Web message chips and thumbnails One component regardless of source Central tab · lightbox Same FileViewer Native channel file delivery Web-openable files reach Telegram Inline terminal images in TUI iTerm2 · kitty; file-card fallback elsewhere Also desirable, but explicitly deferred Attachment table in session_db; migrate historical messages Text markers already carry all required information; insufficient benefit for a schema change Connect the canvas tool to a live rendering panel canvas-panel exists but is not connected to the canvas tool Explicitly excluded Global content-addressed blob storage That violates per-turn workdir commits and session replayability. Accepted cost: large binaries remain permanently in git history. recorded in attachment-handling.

Diagram details: Anything openable on the web can be received on Telegram. It would violate the invariant that each session workdir is committed to git every turn and remains replayable. The cost is retaining large binaries permanently in git history, an acknowledged and accepted cost.

Relationship between sections three and four: after section three, the solid-line behavior in the diagram exists, represented by a text marker instead of a record. Section four replaces that marker with an ID without changing the remaining interfaces.

Four target capabilities and their gaps

Desired behaviorProblem solvedCurrent gapWhy deferred
One attachment record
References only in text
Rename, deduplication, cross-session references, and “which turns mentioned this file?” become queries instead of string manipulation All information is encoded in a text marker parsed by two regexes Requires session_db schema changes and historical-message migration. Markers currently carry all necessary information; wait until cross-session file references are a concrete requirement
One parser
Four interfaces
Web, central tabs, overlays, and terminals share the same rendering decision for a resource Already true on the web (parseAttachments plus FileViewer has one implementation); TUI has no attachment concept and cannot even paste images into the composer TUI work is independent and does not block the web. Only Pi-mono among the eight supports real inline terminal images, and it does not echo user-sent images. Claude-code and Codex only render an [Image #N] label. The practical benefit is lower than it initially appears
Full bidirectional symmetry User and agent files share a data model and UI, differing only in source Grammar, parser, chips, and viewer are already shared; only fallback paths remain asymmetric Partially implemented: when WeChat cannot receive a file, the marker becomes a Chinese explanation with a path, rather than leaking a raw marker or silently dropping it. Clickable links require an externally reachable web UI URL
Output can be a page
rather than only a file
The agent renders while writing, allowing users to watch a document develop without waiting for a saved file canvas already writes Markdown blocks with IDs, and canvas-panel.tsx exists, but the two are not connected The wire format is settled. canvas.py comments say to preserve it and add rendering when WebUI gains a richer canvas interface. Three of the eight projects independently implement this, demonstrating demand. It is the most valuable addition after section three

Canvas file content semantics

The canvas tool persists named Markdown blocks independently of the deferred rendering panel. For set and append, an explicitly supplied empty string is content; an omitted or null content argument requires an error without changing the file. set(content="") retains the named block with an empty body, so a subsequent get returns the empty string. Explicit content takes precedence over the body and text aliases, including when it is empty. The plain Python callable and the registered tool share these semantics and retain the existing block format and sandbox path checks. Acceptance uses registered set/get on an owned file and checks alias compatibility, missing-content non-mutation, and preservation of neighboring blocks.

Implementation status

Section one describes current repository behavior; section three lists deferred additions, and section four describes the unimplemented target design. Following project documentation conventions, unimplemented designs are stated positively in the body and identified here.

ItemStatusLocation
Empty-string canvas content semanticsIndependent reviews and combined verification pendingopenprogram/programs/tools/interaction/canvas.py
One marker grammar: format_marker and find_markers shared by four producersImplementedopenprogram/attachments.py
Allowed-root policy: readable_roots / sendable_roots / resolve_within (resolve symlinks first)Implementedopenprogram/attachments.py
Persist web images and write markers while retaining ImageContentImplementedapps/server/openprogram_server/_webui/ws_actions/chat.py:_persist_attachments
Infer extensions for unnamed pasted images from media typeImplementedapps/server/openprogram_server/_webui/ws_actions/chat.py:_attachment_name
Channel attachments use web grammar (previously chip regexes failed and markers appeared as text)Implementedchannels/_attachments.py:attachment_notes
Byte endpoints /api/file-raw and absolute-path /api/file-readImplementedapps/server/openprogram_server/_webui/routes/files/file_search.py
Capture chip paths, image thumbnails, and overlay openingImplementedapps/web/components/chat/messages/user-attachments.tsx · attachment-preview.tsx
chat_ack echoes stored text; newly sent attachments are immediately clickableImplementedapps/server/openprogram_server/_webui/ws_actions/chat.py · apps/web/lib/net/chat-stream.ts
send_file tool and dispatcher insertion into final_textImplementedprograms/tools/send_file/ · agent/dispatcher/__init__.py
Channel return delivery: extract markers, call post_file, rewrite failures as readable messagesImplementedchannels/_conversation.py:_deliver_outbound_files
Capabilities declared per channel (tool absent in cli / tui / wechat / plan)Implementedprograms/tools/send_file/send_file.py unsafe_in
post_file transport implementations for four platformsImplemented; now has callerschannels/_transport.py:186-211,777-780
FileViewer accepts absolute pathsImplemented (abs mode)apps/web/components/files/file-viewer.tsx
Central-tab alternative, caption, outbound tool allowlist, signed tokens, code highlightingNot implemented (section three)—
Attachment record table, TUI inline images, live canvas rendering panelNot implemented (section four)—
document modality and choose_delivery capability switchNot implemented; that column in attachment-handling’s decision matrix remains a designproviders/types.py:329
Division of responsibility with attachment-handling: that document explains how attachment content enters model context. This page explains its appearance in chat, whether it opens, and how agents return files. Both share one text-marker grammar, now implemented only in openprogram/attachments.py ; changing it changes behavior described by both documents.