Agent Attachments and Image Previews¶
Overview¶
SASE treats files produced by agents as first-class completion artifacts. When a
successful agent adds or modifies a supported image or video file, the completion path
records the media in done.json and appends it to the notification file list after the
standard chat and diff artifacts, and after any generated Markdown PDFs. When a
successful agent adds or modifies up to 10 Markdown files, core SASE renders PDF
artifacts and attaches those PDFs to the same completion notification. Explicit
artifacts saved with sase artifact create are appended after generated media when the
agent completion notification is sent. Notification plugins can then deliver those files
from Notification.files without re-scanning the workspace.
ACE is SASE's terminal UI. It has two image surfaces: lightweight in-panel previews for
notification and file-panel attachments, and the separate a artifact viewer for
opening completed agent artifacts.
ACE can also surface media files referenced in saved prompt artifacts (raw_xprompt.md
and *_prompt.md) even when the media itself was not part of the agent's git diff. For
current successful runs, those prompt-referenced media files are persisted alongside the
other default generated-media artifacts — as byte copies or as byte-free version-control
references, per VCS-Backed Artifact Files — so the
Agents-tab artifact picker can still open them after a workspace is cleaned up. Legacy
runs without persisted default artifacts fall back to prompt-file discovery at view
time. Prompt-referenced media are not notification delivery attachments unless they also
appear in done.json.image_paths, appear in done.json.video_paths, or were saved
explicitly with sase artifact create.
Supported image extensions are:
.png.jpg.jpeg.webp.gif
Supported video extensions are:
.mp4.m4v.mov.webm
Generated Media Attachment Contract¶
Generated-image discovery runs when an agent finalizes successfully. This contract
covers images added to done.json.image_paths and completion notifications.
Generated-video discovery uses the same algorithm and writes videos to
done.json.video_paths. The collector checks candidate paths in stable order:
- tracked files changed relative to
HEAD - untracked files in the agent workspace
- files named by the saved proposal or commit diff
- files touched by the latest commit when the agent committed or opened a PR
Only existing files with supported media extensions are kept. Paths are resolved to absolute paths so outbound notification processes can attach them even when they run outside the agent workspace. Duplicates are removed while preserving order. Generated images are appended after any already-attached chat, diff, or generated PDF files; generated videos are appended after generated images.
The same lists are persisted as image_paths and video_paths in the agent's
done.json. Agent metadata consumers should read those fields instead of trying to
infer generated media from arbitrary notification files. GIFs remain in image_paths
for compatibility with image-preview consumers; downstream notification plugins can
still choose an animation-specific transport based on the .gif suffix.
Source: src/sase/axe/image_attachments.py
Prompt-Referenced Media Default Artifacts¶
Default artifact persistence also scans the saved prompt files in the agent artifacts directory:
raw_xprompt.md- every sibling
*_prompt.mdfile
Any path-like token ending in a common image suffix or supported video suffix is
resolved as an absolute, home-relative, or workspace-relative path. Existing files are
added after done.json.image_paths and done.json.video_paths, duplicates are removed,
and the file does not need to appear in the agent's git diff. These candidates carry
origin mentioned into the capture policy, so a mentioned repo file the run neither
authored nor can reproduce from version control gets no row at all. Images and GIFs are
added as image artifacts. Prompt-referenced videos are added as ordinary file
artifacts; ACE detects the video suffix at view time and opens them with the video
preview path.
This is useful when a prompt asks an agent to inspect or transform an existing screenshot, mockup, reference image, or reference video and the resulting run should keep that source media one keypress away in ACE.
Prompt-referenced media are ACE artifact-list entries, not notification delivery
attachments. Current runs persist them to the global artifact index during finalization;
legacy runs can still synthesize them from prompt artifacts when ACE loads the row.
Downstream notification plugins should continue to use done.json.image_paths and
done.json.video_paths for the generated-media notification contract.
Source: src/sase/core/artifact_file_defaults.py
Prompt Artifact Staging and Archive¶
Launch-time prompt preprocessing also stages every resolvable prompt reference so a later commit can publish a durable prompt document. This staging is workspace-local and lives under:
<workspace>/.sase/artifacts/
The directory has three responsibilities:
home/holds readable working copies for home-directory@pathreferences. This replaces the old.sase/home/location.pool/<sha12>-<basename>holds immutable content-addressed copies of external file bytes. Thesha12prefix is the first twelve hexadecimal characters of the file's SHA-256 digest, and the basename is sanitized for a single path component. Published prompt archives copy these pooled bytes to the agents sidecar'sfiles/objects/sha256/<hex-prefix>/<sha256>object store.prompt-artifacts.jsonlrecords one manifest row per staged reference;prompt-artifacts.lockserializes concurrent writers.
Clean tracked files inside a known repository are recorded as VCS-backed rows instead of
copied into pool/. External files are hashed and pooled unless they exceed
artifacts.capture.max_file_size_bytes; oversized files are still hashed and recorded
with a skip reason, but their bytes are not copied. Locator-only references such as
@agent:, @patch:, and @stitch: get manifest rows without file bytes.
When sase commit publishes the canonical prompt archive, it reads the manifest rows
for that run, copies pooled files to the agents sidecar's content-addressed object
store, and writes the prompt to prompts/<YYYYMM>/<name>.md. The body is the prompt
text selected for publication. The prompt's ARTIFACTS header section lists exactly the
@... references made clickable in the body. VCS-backed rows link to hosted source
blobs at the recorded revision, while copied external files link to
../../files/objects/sha256/<hex-prefix>/<sha256>.
The local pool is a cache for publication, not the permanent archive.
artifacts.capture.pool_max_bytes controls when SASE opportunistically garbage-collects
pool files whose manifest rows all belong to terminal runs that have already published
their prompt archive. Manifest rows are retained so validation can still explain what
happened.
If an older workspace still has .sase/home/, leave it in place until no live agent can
be using it. sase doctor reports it as workspace.legacy_artifact_home and tells you
the exact directory to remove after that check.
Sources:
src/sase/core/prompt_artifact_staging.pysrc/sase/agents_sync/prompt_archive/publish.pysrc/sase/agents_sync/prompt_archive/validation.pysrc/sase/doctor/checks_workspace.py
VCS-Backed Artifact Files¶
Automatic capture at agent finalization means authorship, and it never copies what
version control already stores. Every candidate discovered by generated-media and
prompt-referenced-media discovery is classified by
src/sase/core/artifact_capture_policy.py before anything is written.
The decision matrix¶
origin distinguishes candidates the run changed (done.json.image_paths /
video_paths, which come from git diff HEAD, untracked files, and the run's own
commit) from candidates merely mentioned in a saved prompt file. The first matching
rule wins:
| # | Condition | Outcome | Reason slug |
|---|---|---|---|
| 1 | The exact content is reproducible from a durable commit | reference — byte-free row with vcs_repo/vcs_sha/vcs_relpath |
vcs_reproducible |
| 2 | Authored by this run: inside the agent's artifacts directory, origin == changed, or mtime at or after the run start |
store — copy bytes |
artifacts_dir, changed, run_window |
| 3 | Mentioned only, and outside every known repo working tree | store — the user-supplied input case |
mentioned_external |
| 4 | Otherwise (mentioned only, inside a known repo, not reproducible) | skip — no row is written |
mentioned_repo |
Three invariants make this safe:
- No silent substitution. A
referencerow is written only after the candidate's bytes have been reproduced from<vcs_sha>:<vcs_relpath>and the reproduction's SHA-256 verified equal to the candidate's. A tracked file with uncommitted edits therefore fails rule 1 and is stored instead. - Durability.
vcs_shais reachable from a remote-tracking ref at capture time. An unpushed local commit does not qualify, because the numbered workspace holding it is reset on next open. - Fail-safe. Any git failure, timeout, unknown repo, or ambiguity downgrades a
would-be
referencetostore(reasonvcs_probe_failed). Capture can never lose bytes.
Explicit artifacts created with sase artifact create never enter this matrix and
always store their own bytes.
Finalization prints one summary line beside the other [artifacts] output:
[artifacts] default capture: stored=3 referenced=12 skipped=1 declared=2 cap_fired=false
cap_fired reports whether artifacts.capture.max_stored_per_agent was reached; once
it is, remaining store candidates become skip with reason capture_cap. Reference
rows cost no bytes, are not counted, and are never capped. declared counts
auto-discovered candidates omitted because the agent already registered their source
with sase artifact create. See configuration.md for
the artifacts.capture block.
The record fields¶
A VCS-backed row carries path: null plus three provenance fields:
| Field | Meaning |
|---|---|
vcs_repo |
Repo inventory name (record.name) the content belongs to. |
vcs_sha |
A durable commit that held the exact content at capture time. |
vcs_relpath |
Path of the file relative to that repo's toplevel. |
sha256, size_bytes, and mime_type are recorded exactly as they are for byte-backed
rows, so a byte-free row loses no integrity metadata. Index rows are written at schema
version 2; the reader accepts versions 1 and 2.
Materialization and the vcs-cache directory¶
Reference resolution stays pure — it never shells out to git — so read-only callers
(sase lsp, @-completion, TUI hover) stay cheap. A VCS-backed row resolves with
status vcs_backed and locator <vcs_repo>@<vcs_sha>:<vcs_relpath> and no
resolved_path. Callers that need bytes materialize explicitly through
src/sase/core/artifact_file_vcs.py's materialize_artifact_file(), which is the
single Python entry point:
sase artifact pathprints the materialized cache path, andsase artifact opencontinues into the usual viewer.@file:references in a prompt expand to a materialized path; a failure fails the launch loudly rather than handing an agent a dangling path.- The ACE Files pane renders a
PROVENANCEsection instead of a stored path and materializes off the UI thread. sase artifact showandsase artifact listneed no materialization;showreportsstored_path_status: vcs-backed (<locator>).
Materialized content lands in a content-keyed cache:
~/.sase/artifacts/vcs-cache/<sha256[:2]>/<sha256><suffix>
Lookup order is cache hit (re-hashed before it is trusted), then
git cat-file blob <vcs_sha>:<vcs_relpath> in each known checkout of the repo in turn,
then a bounded walk of at most artifacts.capture.max_history_scan durable commits
touching the path — which recovers content whose recorded sha was squash-rewritten or
pruned. The cache is transient: deleting it costs only re-materialization.
Diagnosing an unresolvable reference¶
If sase artifact path on a VCS-backed row exits 1, the content could not be reproduced
from any known checkout. Work through it in this order:
sase artifact show <ref>— read the locator to get the repo, commit, and relpath.sase repo list— confirm thevcs_reponame still exists in the inventory and has at least one existing clone. An unknown repo name leaves the resolver with no checkout to try.- In a checkout of that repo,
git fetchand retry. Resolution measures reachability from remote-tracking refs, so a stale clone is the common cause. git cat-file blob <vcs_sha>:<vcs_relpath>— if the object is gone (a rewritten or pruned commit), the bounded history walk is the fallback; raiseartifacts.capture.max_history_scanif the content is deeper in history than the current bound.sase artifact doctor -v— audits every VCS-backed row and lists the unresolvable ones underUnresolvable VCS references, so a systemic problem shows up as a bucket rather than one failing command.
A failure always names the repo, commit, path, and digest. SASE never substitutes different bytes and never returns an empty file.
Sources:
src/sase/core/artifact_capture_policy.pysrc/sase/core/artifact_file_vcs.pysrc/sase/core/artifact_file_defaults.py
Consumption Ledger¶
When a launch prompt expands @ artifact references in rewrite mode, SASE records the
references that the agent was actually handed. The ledger is append-only JSONL at:
~/.sase/artifacts/consumption.jsonl
Writes use a sibling lock file:
~/.sase/artifacts/consumption.lock
Each line is a schema-versioned envelope:
{
"schema_version": 1,
"consumption": {
"id": "3f0a91c2d4e5",
"timestamp": "2026-07-30T14:02:11.481293+00:00",
"ref": "file:default:52895d68931185056fd0e49f",
"ref_kind": "file",
"fragment": null,
"role": "image",
"artifact_id": "default:52895d68931185056fd0e49f",
"resolved_path": "/home/user/.sase/artifacts/agents/sase/20260730134501/image.png",
"resolution_status": "exact",
"agent_name": "sase-b8.2",
"agent_source": "SASE_AGENT_NAME",
"artifacts_dir": "/home/user/.sase/projects/gh_sase-org__sase/artifacts/ace-run/202607/30/20260730134501",
"project": "gh_sase-org__sase"
}
}
ref is the fragment-free canonical reference and is the join key used by
sase artifact show and sase artifact list --unused. A prompt reference such as
@file:default:<digest>#L1-L5 records ref: file:default:<digest> and stores the
discarded anchor as fragment: L1-L5, so all fragments of the same artifact aggregate
together. artifact_id is populated only for file: references; non-file references
such as plan:, bead:, agent:, patch:, stitch:, and research: leave it null
but are still recorded and summarized by show.
The v1 role vocabulary is deliberately small:
| Role | Derivation |
|---|---|
report |
Document and historical chat: references; file: references whose resolved path is Markdown, plain text, or PDF |
image |
Visual media by suffix, including images and videos such as PNG, JPEG, GIF, SVG, WebP, MP4, MOV, and WebM |
source |
Code, data, entity/revision references (including historical aliases), unknown suffixes, and other non-report, non-visual references |
test-result |
Reserved for future writers; no v1 code emits it |
Grouping videos under image is intentional: in this ledger the role means visual
media, and keeping the v1 vocabulary to four values leaves later lineage work additive.
SASE logs every reference that successfully expands in rewrite mode. It does not log
validation-only checks, failed expansion passes, sase artifact open,
sase artifact path, ACE browsing, or LSP completion. Within one expansion pass
duplicate references collapse to one event by canonical ref; later launches, retries,
and workflow steps append new events because they are separate consumptions.
sase artifact show <reference> reads the ledger for any resolvable reference and adds
consumption_count, consumed_by_agents, consuming_agents, and last_consumed_at to
the pretty report. JSON output adds an additive consumption object with the full
summary, or null when the reference has never been consumed.
sase artifact list --unused filters artifact files to rows with no recorded
file:<id> consumption; the filter is applied before --limit, so -u -l 50 asks for
50 unused artifacts, not for unused rows among the newest 50.
Every canonical, fragment-free file: key in the ledger is a hard lifecycle protection.
The shared protection collector unions those consumed IDs with IDs found in persistent
ProjectSpec, plan, bead, and research references before sase artifact stats projects
the default policy, sase artifact prune removes rows, sase artifact reclaim converts
stored rows to VCS-backed identities, or opt-in automatic retention runs after agent
finalization. Overlap is deduplicated, while stats reports referenced, consumed,
overlap, and total counts separately. A missing ledger is an empty optional source; if a
present ledger cannot be queried, reporting surfaces show it as unavailable and every
destructive apply or automatic enforcement pass refuses to change artifacts.
Sources:
src/sase/core/artifact_consumption.pysrc/sase/core/artifact_consumption_query.pysrc/sase/artifact_ref_prompt.pysrc/sase/artifact_cli/show.pysrc/sase/artifact_cli/listing.pysrc/sase/core/artifact_file_protection.pysrc/sase/artifact_cli/stats.pysrc/sase/artifact_cli/prune.pysrc/sase/artifact_cli/reclaim.pysrc/sase/axe/run_agent_exec_finalize.py
Store Lifecycle¶
The artifact-file store is measured, drained, and bounded through one deliberately staged progression: report → dry run → opt-in retention. Nothing in it removes anything by surprise, and nothing it removes is gone immediately.
| Stage | Command | Writes |
|---|---|---|
| Report | sase artifact stats |
Never |
| Reclaim (lossless) | sase artifact reclaim |
Only with -a/--apply |
| Prune (lossy) | sase artifact prune |
Only with -a/--apply |
| Undo / finalize | sase artifact trash {list,restore,purge} |
restore and purge write; purge is the only hard delete |
| Ongoing | artifacts.retention |
Only when explicitly enabled |
The protection contract¶
These rules hold on every surface above — manual and automatic, dry run and apply:
- Declaration is permanent. A row created by
sase artifact create(explicit=true) is never removed, converted, or rewritten by any lifecycle command. - Referenced artifacts are protected. An artifact ID appearing in a ProjectSpec,
plan, bead, bead page, or research document is excluded, in both its bare
default:/explicit:andfile:-prefixed forms. - Consumed artifacts are protected. Any canonical, fragment-free
file:ID in the consumption ledger is excluded. - The newest generation always survives. Whatever the predicates say, the newest
capture of every
(project, label)pair is never selected. A label's history can shrink to one; it cannot vanish. - Unreadable protection sources block removal, never permit it. If a required source
cannot be read,
--applyrefuses with the source named and the automatic pass skips entirely. The dry run still renders and reports the gap. - Nothing hard-deletes except
trash purge. Every other removal is restorable.
Report first: sase artifact stats¶
stats is read-only. It panels totals split explicit / automatic / VCS-backed, the
observed window and growth rate, per-kind and per-project and top-agent groups (with the
truncated remainder shown rather than hidden), duplicate-digest redundancy, per-label
generation projections, an upper bound on reclaimable rows, protection counts
(referenced, consumed, overlap, total, plus any unavailable source as a warning), trash
occupancy, and — last — exactly what the configured default policy would select.
-j/--json emits the same content as one envelope.
Read the reclaimable figure as an upper bound, not a result: it counts automatic
byte-backed rows whose source_path lives inside their workspace_dir. Reproducibility
is only ever established by the digest verification reclaim does.
Reclaim before pruning¶
sase artifact reclaim is lossless, so run it first: it keeps every row, label, and
provenance field and deletes only a copy of content version control already stores. For
each eligible automatic row it resolves the owning repo and relative path from
workspace_dir + source_path (mapping a sase/repos/... prefix to the matching
sidecar, linked, or external repo), walks that path's durable remote-tracking history
within -d/--max-history-scan commits (default 100), and converts the row only when a
blob's SHA-256 equals the row's recorded digest. Because the dry run compares digests
rather than materializing content, it touches nothing at all — not even vcs-cache/.
Reclaim changes a row's ID. A reference row's ID derives from its VCS identity
rather than its stored path, so the converted row gets a new ID; the old row is removed
and the new one written. Both IDs appear in the plan's OLD REF / NEW REF columns and in
each Reclaimed file:<old> -> file:<new> apply line, so anyone holding the old ID can
follow it. Preserving the old ID is not an option — it would collide with the ID a
re-capture of the same content computes, and idempotent capture depends on that. This is
also why reclaim honors the reference and consumption protections: changing an ID that
something already names would break the reference.
Rows reclaim cannot verify are never touched. They are reported in the Rows Left Untouched panel, grouped by a stable reason:
| Reason | What to do |
|---|---|
explicit, referenced |
Working as intended; the row is protected. |
already_vcs_backed |
Nothing to do; the row is already byte-free. |
missing_sha256, missing_size |
Run sase artifact doctor -f to backfill enrichment fields, then retry. |
missing_workspace_dir, missing_source_path |
The row predates provenance capture; only pruning can address it. |
source_outside_workspace |
The file was never inside a repo checkout, so no relpath is derivable. |
unknown_project, unknown_repo, inventory_unavailable |
Check sase repo list; the repo the row names is not in the inventory. |
missing_checkout |
No live clone of that repo exists right now. Open one and retry. |
vcs_probe_failed |
A git call failed or timed out; the row is left alone by design. |
digest_not_found |
The exact content is not in durable history within the scan bound. Raise -d, or accept that the bytes are genuinely unique and let pruning decide. |
Prune what remains¶
sase artifact prune is the lossy step, and the only one that removes information. It
plans from the same protection set with -g/--keep-generations (defaulting to
artifacts.retention.keep_per_label), -b/--before, -k/--kind, -m/--min-size,
-p/--project, and -l/--limit, renders the identical table in both modes, and changes
nothing without -a/--apply. Selecting a byte-free row is legitimate and reclaims no
bytes — it removes index noise — so the plan counts byte-backed and byte-free selections
separately and only ever sums byte-backed rows into reclaimable bytes.
The trash¶
Every removal — from prune, from reclaim, and from automatic retention — moves the
stored bytes and the complete index row into ~/.sase/artifacts/trash/. One entry is
one directory containing entry.json (schema version, entry ID, artifact ID,
trashed_at, reason, size_bytes, stored filename, and the verbatim original index
row) plus the moved payload file when the row had bytes. Bytes are moved before the
index row is dropped, so an interrupted batch leaves a restorable entry rather than an
orphaned row.
sase artifact trash # newest-first listing, grace-period flag per entry
sase artifact trash restore <entry-or-ref> # payload back in place, index row re-inserted
sase artifact trash purge # permanent, and only past the grace period
sase artifact trash purge -a # permanent, ignoring the grace period
artifacts.retention.trash_grace_days (default 14) is the cutoff purge honors and the
one trash list marks entries against. Until a purge runs, trashed bytes still occupy
disk: a full reclaim pass moves its recovered bytes into the trash, so du does not
drop at apply time. reclaim --apply says this outright in its summary.
Ongoing enforcement¶
artifacts.retention is disabled by default and removes nothing until enabled. Once
enabled, one bounded pass runs after automatic capture at each agent finalization: it
plans with the configured policy plus the protection scan, trashes what it selects, and
purges trash entries past the grace period, printing one [artifacts] retention: line
with rows trashed, bytes reclaimed, and entries purged. The pass is wrapped defensively
and never fails a run; when a protection source is unavailable it prints
[artifacts] retention skipped: and touches nothing. See
configuration for the fields.
Sources:
src/sase/artifact_cli/stats.pysrc/sase/artifact_cli/prune.pysrc/sase/artifact_cli/reclaim.pysrc/sase/artifact_cli/trash.pysrc/sase/core/artifact_file_retention.pysrc/sase/core/artifact_file_reclaim.pysrc/sase/core/artifact_file_trash.pysrc/sase/core/artifact_file_protection.pysrc/sase/axe/run_agent_exec_finalize.py
Markdown PDF Attachment Contract¶
Markdown discovery runs on successful agent finalization with the same candidate
ordering as image discovery. Supported source extensions are .md and .markdown.
Sources are resolved to existing workspace files, generated run artifacts are excluded,
and duplicates are removed before rendering. If more than 10 Markdown sources remain
after filtering, SASE skips PDF rendering for that run and adds a
completion-notification note instead of rendering a large attachment set.
Core SASE renders discovered Markdown sources into the current agent artifacts directory:
<artifacts_dir>/markdown_pdfs/<sanitized-relative-source-path>.pdf
<artifacts_dir>/markdown_pdfs/index.json
Rendering is best-effort. Missing Pandoc/PDF-engine tools or conversion errors do not
fail the agent run; failed sources are omitted. Successful PDF paths are persisted as
markdown_pdf_paths in done.json, and index.json records source_path to
pdf_path mappings for diagnostics. When the 10-source limit is exceeded,
done.json.markdown_pdf_paths is empty and the source count is carried through
completion handling for the user-facing skip note.
While PDFs are being prepared, the runner writes workflow_state.json.pdf_status plus a
compact activity label. ACE loads the activity during refresh and shows messages such
as Preparing PDFs from Markdown..., PDF 2/4 <path>, or PDFs done 3/4 (1 skipped)
only in the prompt/detail header's labeled Activity: field. This status is transient
finalization state; the durable output remains done.json.markdown_pdf_paths and
markdown_pdfs/index.json.
Markdown PDFs use a built-in small-screen layout by default: a narrow portrait page,
small margins, larger readable body text, and wrapping-friendly CSS for code blocks,
tables, links, and other long content. The preferred wkhtmltopdf path receives both
the default stylesheet and explicit page/margin options; LaTeX fallbacks receive the
same page size, margin, font size, and line-height defaults through Pandoc variables.
When a discovered Markdown source starts with usable, non-empty YAML frontmatter, the
rendered PDF replaces the raw metadata block with a styled Properties card; the
original Markdown file is not changed. Labels and property ordering use the same helpers
as ACE's plan-detail presentation, while the PDF renders nested mappings and sequences
as indented lines. HTML-sensitive property text is escaped. On the preferred
wkhtmltopdf path, Pandoc's document-title metadata uses the frontmatter title value
converted to text, or the source filename stem when title is absent. Empty, malformed,
or absent frontmatter leaves the render input unchanged, and a preprocessing failure
falls back to rendering the original Markdown. Dedicated launch-preview PDFs opt out of
this transformation in both the highlighted and generic-fallback passes, preserving
prompt frontmatter.
Completion notifications attach generated Markdown PDFs after the saved chat and diff
files, before image attachments. The Agents tab file panel also loads
markdown_pdf_paths alongside plan and image files for completed agents.
Sources:
src/sase/attachments/markdown_pdf.pysrc/sase/axe/run_agent_exec.py
Explicit Artifact Contract¶
Agents can save a generated file explicitly with:
sase artifact create [-k <kind>] [-l <label>] [-m] -p <path>
sase artifact-file create remains a compatibility alias for the same command.
-k/--kind is one of chat, plan, image, markdown, pdf, or file, and
defaults to a kind inferred from the file extension. -l/--label sets the display label
and defaults to the source file name. -m/--move removes the source after storing it
instead of retaining the default workspace copy; it is intended for scratch files, and
using it on a tracked file leaves a deletion in the working tree. -p/--path is
required.
On success the command prints four lines:
id: explicit:<hash>
source: /absolute/path/to/report.md
path: /home/<user>/.sase/artifacts/agents/<project>/<timestamp>/report-<digest>.md
ref: file:explicit:<hash>
The source: line records where the artifact came from, path: names the stored
snapshot, and ref: is the copyable name to hand to a user or another agent. By default
the source remains in place. Later source edits do not propagate to the stored snapshot;
run create again to register a fresh one.
Every new index row also records sha256 (the full digest of the stored file),
size_bytes, and mime_type. All three are optional at index schema version 1, so rows
written before they existed simply carry null; sase artifact doctor reports those
gaps, sase artifact doctor -f backfills them for every row whose stored file is still
present, and sase artifact doctor -v re-hashes live stored files to verify the
recorded digests. The reader accepts index schema versions 1 and 2 and the writer
preserves rows with any other schema version verbatim, so a mixed-age fleet cannot lose
rows on rewrite.
The CLI command is intended for agent processes: it requires SASE_AGENT=1 and
SASE_ARTIFACTS_DIR so SASE knows which run owns the artifact, and it exits non-zero
with an explanatory message when either is missing or the source path is not a file. It
copies the source file into persistent SASE artifact storage, records an association
with the current agent, and lets ACE show the artifact even after the agent is dismissed
and later revived. During completion notification delivery, SASE appends existing
explicit artifact files after chat, diff, generated Markdown PDFs, generated image
attachments, and generated video attachments. Duplicate stored paths and artifacts whose
source is already attached are ignored, missing files are skipped, and explicit-artifact
index failures do not fail the completion path.
Sources:
src/sase/artifact_cli/create.pysrc/sase/core/artifact_file_explicit.py
Notification Delivery¶
Core SASE stores generated PDFs, generated images, generated videos, and explicit
artifact attachments in the existing Notification.files list. There is no separate
notification schema field for typed attachments yet. This keeps the contract compatible
with existing notification storage and lets downstream plugins decide how to render each
file:
- Telegram integrations can send static images as photos, GIFs as animations, videos as videos, and keep markdown/diff files as documents.
- Google Chat integrations can upload image files directly into the completion thread.
- The ACE notification modal can still open attached files in
$EDITORwitheand cycle them withCtrl+N/Ctrl+P.
See notifications.md for the notification model and modal
keybindings.
ACE Artifact Viewer¶
The Agents tab exposes completed agent artifacts through the a key. When artifacts
exist, ACE opens the artifact panel for selection. Chat transcripts, plan files,
generated Markdown PDFs, generated images, generated videos, prompt-referenced media,
and explicit artifacts created with
sase artifact create [-k <kind>] [-l <label>] [-m] -p <path> all use the same list.
Generated videos are stored as ordinary file artifacts, but the picker labels
supported video suffixes as [video] and the viewer opens them with terminal video
playback. The panel is shown even for a single artifact so users can confirm the
artifact label, kind, and path before opening it.
The selected agent's prompt/detail header also includes non-chat artifacts in the
plan-adjacent SASE CONTEXT ARTIFACTS lane. The complete lane order is PLAN,
BEAD, ARTIFACTS, MEMORY, SKILLS, then WORKSPACES; within ARTIFACTS,
Commits, Deltas, and Artifacts remain in that order when present. Paths are shown
relative to the agent workspace when possible, home-relative when appropriate, and with
hint numbers when hint mode is active.
The panel supports one-key selectors, j/k navigation, m to mark rows, Enter to
open the marked set or highlighted row, y to copy highlighted Markdown contents, Y
to copy the highlighted artifact path, and A to open every artifact in list order.
Copied paths are workspace-relative when possible and fall back to home-relative paths.
When multiple artifacts are opened together, the terminal viewer adds n/p navigation
between artifacts in addition to page navigation.
When ACE is running inside tmux, the artifact viewer launches in a right-side tmux pane
and the Agents list collapses while the pane is live. Press l from the Agents tab to
focus the tracked artifact pane, or press a again to close it. Row-changing navigation
is guarded while the pane is open so the TUI does not drift to a different agent than
the viewer. Outside tmux, ACE suspends and opens the viewer in the current terminal
pane. The viewer chooses its mode from the artifact kind and file extension: supported
images are displayed directly, supported videos play with mpv, PDFs are converted to PNG
pages, and Markdown is rendered to PDF before paging. The page loop uses j/k to move
between pages, wrapping at the first and last page, n/p to move between artifacts in
a sequence, r to refresh or replay the current artifact, and q to close the viewer.
Only one plan artifact is listed for each agent. If run metadata contains both an archived plan path and an SDD tale path, committed plans prefer the SDD path; uncommitted plans prefer the archived path unless only the SDD path is available.
Viewer dependencies are intentionally outside the agent completion path. kitten is
required for image/PDF/Markdown terminal display, mpv is required for terminal video
playback, pdftoppm is required for PDF/Markdown paging, and Markdown rendering also
needs pandoc plus one supported PDF engine. If a dependency is missing, ACE shows a
warning instead of failing the TUI or changing the stored artifact list.
Source: src/sase/ace/tui/graphics/viewer.py
Video Preview¶
ACE plays .mp4, .m4v, .mov, and .webm artifacts in the same artifact viewer used
for images and PDFs. Inside tmux, selecting a video opens the tracked right-side
artifact pane; outside tmux, ACE suspends and plays in the current terminal. Playback
uses mpv --vo=kitty by default, bounded to the same cell area used for image
artifacts.
While mpv is running, mpv owns playback keys: space pauses or resumes, arrow keys
seek, m toggles mute, and q stops playback. After playback exits, the artifact
viewer footer returns with the usual navigation keys: r replays, n/p move through
a multi-artifact sequence, z toggles tmux zoom when available, <tab> focuses the
SASE TUI from a tmux artifact pane, and q closes the viewer.
Videos are muted by default because SASE often runs on a remote host or inside tmux
where the server audio device is not useful. Configure playback under
ace.artifact_viewer.video:
ace:
artifact_viewer:
video:
audio: false
loop: false
vo: "kitty"
extra_mpv_args: []
Set audio: true to start unmuted, loop: true to pass --loop-file=inf, vo to use
another mpv video output such as tct, and extra_mpv_args to append additional mpv
flags after SASE defaults. SASE launches mpv with --no-config so user mpv profiles
cannot break the curated terminal preview; put viewer-specific customization in the SASE
config instead.
ACE Image Preview Foundation¶
The notification modal and Agents tab file panel route supported image extensions through the preview layer before attempting text decoding.
The internal preview layer renders PNG, JPEG, WebP, and GIF attachments as a portable Rich cell preview. It uses Pillow to decode the first image frame, apply EXIF orientation, fit it inside the visible panel bounds, composite transparency onto a dark background, and apply a mild preview-only sharpen pass after resizing. Each terminal cell samples a 2 x 2 pixel block and chooses the closest Unicode block mask with foreground and background colors, which preserves more edges, diagonals, UI details, and text-like shapes than a fixed half-block sampler.
No Kitty, iTerm2, Sixel, or other terminal image protocol support is required. The renderer only emits colored Unicode text through Rich/Textual, so it works the same way in ordinary terminals, multiplexed sessions, SSH sessions, and environments with no image protocol support. Preview quality depends on the visible pane size and terminal color depth: larger panes provide more sampled cells, and truecolor terminals preserve colors better than 256-color terminals.
ACE checks only terminal color depth from the environment. When COLORTERM=truecolor,
COLORTERM=24bit, or a truecolor marker in TERM is present, previews use 24-bit
color; otherwise they use 256-color approximations. Missing files, unsupported
extensions, decode errors, missing Pillow, and images above the renderer guardrails show
a concise text fallback with the file path, byte size when available, and the relevant
editor or artifact action. Use e in notifications, E on the Agents tab, or the a
artifact viewer whenever full-fidelity viewing is needed.
Source: src/sase/ace/tui/graphics/