View Buffer¶
The view modules render a compact user-facing projection of the authoritative
gptel data buffer. mevedel-view.el owns the mode, zones, and session
coordination. mevedel-view-composer.el owns the editable composer,
submission hooks, root dispatch, and send/fork coordination.
mevedel-view-input-files.el owns local file drops and clipboard-image input.
mevedel-pending-inputs.el owns steering, queued follow-ups, automatic
delivery, and the Pending Inputs cockpit.
mevedel-surface-mode, derived from text-mode, supplies shared ephemeral
buffer behavior for non-transcript surfaces. Save-state enforcement clears
the modified flag only after a modification; ordinary cursor movement does
not force a mode-line redisplay, which can misplace the cursor on hidden
Markdown at a wrapped line boundary.
mevedel-view-agent.el owns agent transcript inspection, live agent status
rows, and targeted handle refresh. mevedel-view-execution.el owns the
background-work panel. mevedel-view-interaction.el owns interaction
descriptor registration, ordering, callback overlays, and redraw.
mevedel-view-control-transfer.el owns cooperative transfer polling,
presentation, commands, and view registration.
On view closure, mevedel-view-control-transfer-stop-polling stops the timer
and prevents rearming while retaining the root registration needed for journal
sealing. Full transfer teardown removes the registrations after sealing.
mevedel-view-disclosure.el owns source-backed disclosure identity, state,
and expand/collapse actions. It also locates contiguous section bounds for
source disclosures, hook context, user-input folds, and turns using each
caller's identity property; equal but distinct source objects remain separate. mevedel-view-render.el owns transcript
projection, source mapping, and live transcript navigation.
mevedel-view-segments.el owns archived segment buffers, switching, and
ephemeral projection state.
mevedel-view-stream.el owns request progress and streaming redraw scheduling;
mevedel-gptel-stream-bridge.el owns private gptel stream compatibility.
mevedel-view-animation.el prepares bounded time-based status frames;
mevedel-view-native.el optionally presents those samples through independent
Wayland surfaces, owning setup, placement, fallback and teardown;
mevedel-present.el makes PGTK frames present only what changed;
mevedel-native.el builds and loads the native module both use;
mevedel-view-power.el shares local battery observations and computes the
effective animation ceiling. The stream owner decides when visible frames
and semantic progress metadata need updating; these modules do not own the
authoritative transcript.
mevedel-side-conversation.el owns transient
/btw conversations. The data buffer remains the model-visible transcript;
C-c C-z closes the ephemeral side and returns its resources to the parent.
Buffer Roles¶
- Data buffer: org-mode gptel buffer. Holds
mevedel--session,mevedel--workspace, the canonical mixed chat/directive transcript, tool results, hidden render-data blocks, and persisted gptel metadata. - View buffer:
mevedel-view-mode. Holdsmevedel--data-buffer, compact Markdown-rendered turns, status and interaction zones, and the input zone. - Agent transcript view: rendered read-only projection of a sub-agent transcript. Resident retained agents use their live conversation buffer whether running or idle; cold and historical agents use the saved transcript file.
- Directive inspector: explicit read-only projection of one workspace directive record for durable access after compaction, archive, or source loss. It replaces the currently displayed view and never owns a composer, streaming target, or interaction registry.
An open running-agent transcript view follows the main view's update cadence:
streamed text uses mevedel-view-stream-render-delay (a batch flushed by the
stream bridge renders in the flush's own wakeup), tool boundaries use
mevedel-view-tool-boundary-render-delay, and terminal settlement renders
immediately. It shows running tool calls the same way as the main view,
but not the main foreground-request spinner; the transcript header already
shows that the agent is running. Live updates keep the window at the bottom
only when it was already there. If the reader has scrolled upward, rendering
preserves point and window start until they return to the bottom.
Agent transcript views reuse the main view's transcript renderer, incremental rendering, and stream scheduling. The agent-specific layer only fans live agent events out to open transcript views and applies their read-only chrome; it does not maintain a parallel rendering implementation. The live agent data buffer keeps its existing parent-view binding, so opening an inspection view does not redirect parent status and interaction UI. Stream and tool callbacks temporarily bind that pointer to the inspection view. The observer module declares the pointer as a special variable itself, so this routing also works when compiled before the session structs have been loaded.
Agent transcript views are observation-only. Permission, Ask, plan, and other actionable interactions remain exclusively in the parent view. A transcript header may report that an agent is blocked, but the transcript view never duplicates interaction controls or owns their callbacks.
Each parent view owns at most one agent transcript inspection window. Opening a different agent transcript replaces the current inspection view; live refresh does not add multi-view window management. Transcript buffer identities include the owning session, so equal canonical agent paths in different sessions never reuse or repurpose one another's inspection view.
The default is a right-side window. Set mevedel-agent-view-display-action to
(display-buffer-same-window) for same-window inspection; this does not change
the observation-only contract. Close an existing inspector before changing its
placement. The header's ← Parent action and q close inspection and return
to the original parent view, including when inspecting another agent from an
already-open inspector. The parent's draft, point and scroll position survive.
A parent window that showed its composer gets back the composer's screen row
from before the inspection opened: the side window narrowed it, and closing
the side window widens it again, so its text rewraps into fewer rows. Without
this, the composer of a 25-row window with wrapped paragraphs rose from the
last row to row 18 above blank space. The row is restored only to windows that
still show the composer with point in it just before the inspector closes
(reflow from the close does not qualify a window); a parent window the user
scrolled or moved into earlier history meanwhile keeps its start and point.
If the observed agent settles while its transcript view is open, the view
renders the final content immediately and updates its header in place. It
keeps the retained data buffer rather than swapping source buffers mid-display.
Closing the inspection view preserves data buffers owned by retained agent
records so a later SendMessage can continue them; parent-session teardown
kills every retained conversation buffer from the session registry, whether
or not it has an open inspection view. Reopening reuses a resident retained
buffer and otherwise resolves the saved transcript.
The live transcript header updates on the same stream and tool events as the body. It reflects running or blocked state, the agent's effective provider, model and known effort, tool-call count, and elapsed time. A narrow window keeps the Parent action, agent identity and state first, truncates ancestor paths before the leaf name, and omits optional metadata that does not fit. The full path, session identity and model details remain available on the path's hover text. The session label also appears at the end when space permits. Elapsed time has no independent ticking timer; it advances when another live event refreshes the view. Terminal status updates immediately.
The parent Started row and expanded active-agent roster show compact
model:effort labels. Provider details belong in their expanded disclosures;
the collapsed roster remains counts and state only. Unknown effort is omitted,
not inferred from the parent. Summary-preparation metadata stays separate and
explicitly labeled summary. A Started row is a historical snapshot with
At launch details; provider acknowledgement can subsequently change the
effective configuration shown in the live header and roster. Idle and cold
inspection uses the retained last-served configuration when available. Metadata
lookups do not open buffers or read files, and roster collection stays linear.
Live transcript rendering is an observer side effect and cannot alter agent execution or the parent view. If a refresh fails, the last good projection remains visible, mevedel emits a warning, and a later event or terminal settlement may retry.
Opening a live transcript performs the synchronous full render needed to attach the stream tail. A settled retained or saved transcript opens with scheduled canonical projection, keeping the previous display until preparation completes. Resuming a session, rewinding it, starting a new segment, taking or following control, rolling back a failed directive request, and refreshing Source after a Session Fork schedule the same projection rather than blocking on a complete rebuild. Subsequent live events use the main incremental renderer, except that retained-agent metadata replacements fully rerender because delete-and-insert invalidates their source endpoints. Missing or stale source anchors use the same full-rerender correctness path rather than introducing a second recovery strategy.
Opening a view while a tool is already in flight does not reconstruct a running row from activity history. The full render shows the authoritative live transcript as it stands; subsequent tool events draw running calls normally, and terminal settlement renders the final state.
Live updates preserve the main view's source-backed disclosure state. An incremental update or full-rerender fallback must not collapse response, reasoning, tool, or audit sections that the reader expanded. When a tool run grows past the grouping threshold beneath a reader, its member rows remain visible and the group stays expanded after the reader leaves. This also applies before any result arrives: running rows retain point by call key, then by transcript tool id on settlement, and remembered group expansion carries over to the settled group. Grouped tool and reasoning rows use their own transcript source identity, so expansion and the reader's cursor survive a row moving out of a group when a late audit requires individual presentation. Rendered source ranges use data-buffer markers so a length-changing update to one tool's hidden render data cannot retarget an adjacent tool disclosure. Single-tool progress refreshes preserve that marker-backed range, so expanding the refreshed row still reads the same tool after another tool's metadata grows. Expanding and collapsing a tool also retains its tool-use identity, so targeted refreshes can still find the row and preserve its disclosure choice.
Agent transcript views open only through explicit user action on an agent handle or status surface. Agent start, progress, and blocked events never create or focus an inspection window automatically.
Agent status refreshes replace only the handle. Adjacent audit disclosures, including delivered system reminders, retain their rows and fold state. Standalone audits after deliveries keep their audit identity during activity grouping: provider bookkeeping and agent mailbox receipts stay hidden, and user-facing hook records keep their own disclosures rather than becoming generic tool rows.
The view is reconstructable from the data buffer. Avoid storing durable conversation state only in view overlays or text properties.
Agent terminal results and Bash completions remain distinct. Expanding an agent notice reveals its final answer, and the agent path opens its transcript. A yielded execution keeps its compact completion breadcrumb at the original command row, identifying the command and terminal outcome and linking to its output. It is the only completion line: the transcript's own delivery of that completion renders nothing. A transcript receiving another owner's completion, such as a parent receiving a child's, shows an expandable notice from the mailbox record instead. A command that finished before yielding has no breadcrumb. Model-facing mailbox payloads and agent settlement are unchanged. Targeted tool-progress refreshes replace only the tool row and its nested calls, leaving separate completion and hook-audit rows untouched.
Directive requests render in the ordinary session view as first-class turns. The directive header carries id, action, turn, and an exclusion badge; the submitted prompt is folded, while responses, tool blocks, permission prompts, Ask, agents, tasks, and progress use the existing renderer and interaction zones. Settled directive turns older than the newest chronological turn fold to one-line summaries by default. A newest directive turn remains expanded so its response stays visible; explicit fold state wins. Every summary expands back to the actual turn, which is never replaced by a compact event row.
The shared composer has either chat scope or an explicit directive id/action
scope. Entering through Discuss, Continue discussion, Discuss result, Request
changes, Retry, or Implement this stashes the chat draft and shows a compact
directive header, a distinct prompt prefix, and modeline state. The header names
the next scoped action; its secondary line shows chat isolation, the effective
permission mode, Plan paused when applicable, and C-c C-k for Back to chat.
Both lines are truncated to the narrowest window showing the view, so the scoped
prompt is always the same four rows; resizing or displaying the view in a
window of another width refits them before redisplay.
Directive scope is sticky across sends and exits only through Back to chat;
leaving restores the chat draft, and resume always starts in chat scope. Queued
inputs retain the scope in which they were accepted. Status, agent, task, and
interaction redraws preserve the active scoped draft and point exactly,
including a multiline draft whose first editable character is >.
Permission cards in the interaction zone start folded; TAB re-renders the
queue head with its details, which also preserves the draft and point. The
card tags its Keys row, its command or expression, and its two detail blocks
with zone anchors, so point on the Keys row stays on it when TAB, c, n,
p or g redraws text above it; restoring by offset had moved point from the
Keys row into the middle of the expanded command. The
permissions manual owns what a folded card must show.
Directive prompt construction remains independent of this visible chronology. A follow-up uses only durable discussion turns for the current authored request; Implement this adds the complete matching discussion, Discuss result can target one selected attempt, Request changes uses fresh directive context and the immediately preceding successful attempt, and Retry uses the preceding failure or abort. Submitted subdirectives disappear only after success, while failed and aborted attempts leave them editable in source.
Render flow¶
flowchart TD
A[Data buffer transcript] --> B[Parse turns and metadata]
B --> C[Render history region]
S[Session and live execution state] --> D[Render status zone]
Q[Pending interaction descriptors] --> E[Render interaction zone]
C --> F[Preserve composer text and point]
D --> F
E --> F
F --> G[View buffer]
G --> H[User submits composer]
H --> A
The transcript supplies history; session state and interaction descriptors supply the surrounding controls. Each redraw preserves the user-owned composer.
While any mouse tracking is active, projection writes coalesce until
the gesture's command finishes, even if release selects another buffer. This
includes commands such as Evil's that read the release inside track-mouse,
as well as built-in drag tracking that spans press and release commands. Requeued
releases handed back to the command loop keep the hold until their command runs.
Mouse events contain numeric character positions; rewriting text above them
between press and release otherwise places point at the old offset. Semantic
elapsed ticks use the same hold; decorative frames can still update without
changing character positions. A two-second one-shot deadline drains held writes
if release is lost outside Emacs, so a stale gesture cannot freeze the view.
Expiry identifies the built-in drag by its terminal start event and single-command
tracking by its initiating command event, so a later gesture can hold writes again.
This adds no polling timer or permanent command hook.
History-boundary recovery stops at the input prompt. Copying a rendered response
and yanking it into a draft can retain transcript source/type properties, but
those properties do not make composer text part of history.
Targeted tool-row refreshes also stop at that boundary: a copied tool row in a
draft is not a live result, even after the original row moves into a group.
Refreshing it would add spurious composer undo entries despite restoring the
draft text.
Informational recovery notices offer a Dismiss control. It removes only that notice, not the failure's retained agent result or transcript. Blocking issues have no dismissal control. A successful retained-agent retry clears that agent's previous notice; refused retries and other agents' notices remain. These changes preserve an active composer draft, including multiline input.
Full rerenders parse the data buffer through
mevedel-transcript-segments, after skipping gptel-org leading
metadata and any leading compaction summary. mevedel-view.el owns the
surrounding view coordination, while mevedel-view-render.el owns turn
grouping and projection and mevedel-view-disclosure.el owns source-backed
fold state and actions. Transcript span classification, tool block recovery,
and mailbox, reminder, hook-context, render-data, prompt, and ignored-range recognition live in
mevedel-transcript.el so persistence and compaction use the same structural
view of the buffer. Container payloads are opaque to that scan: an inline-skill
render-data block carries the prepared root prompt and attachment names and
bodies, reasoning and mailbox bodies are model- and agent-authored, and a prompt
drawer holds the user's directive, so a control marker starting inside one
stays data and never splits the enclosing span. Tool ranges and render-data /
hook-audit ranges are exempt because they
carry their own proof -- validation against the raw tool property runs or a
trust hash -- and backends really do nest a tool call inside a reasoning block.
This exemption never applies to markers inside an authorized serialized
render-data payload. The codec's complete Lisp boundaries make that payload
opaque during control-line indexing, tool-boundary recovery and prompt-depth
scanning, including when metadata shares a tool-property run. A prematurely
split saved metadata run is recovered only with a matching surrounding tool
owner; marker-shaped prose alone does not gain authority. Full, live and batched
projections therefore disclose the original output as one call without leaking
its outer metadata. A structural scan reuses each decoded block across its
phases rather than copying or repeatedly parsing large payloads.
Independently, no generated control block ever lands inside gptel's streamed
response run, so markup found there is prose the model quoted and renders as
prose. What remains unguarded is markup that opens a run: a response or user
prompt whose very first line is <system-reminder> still reads as structure.
Hidden audit record grammar and attachment spans live in mevedel-transcript-audit.el; the view consumes
those spans without reparsing the wire format.
Attributed shared questions keep the authored question visible and place the
generated snapshot and attachment links in a Shared context disclosure,
collapsed by default. Its title includes the item, scope, and revision. Expansion
survives the transition from the immediate echo to a full render. Only the exact
generated suffix of an attributed question is folded; edited or mismatched
prompts remain fully visible. This is presentation only: the data buffer and
model input retain the complete snapshot.
Reasoning summaries and expanded bodies retain source trust properties until
the audit parser removes hidden records, including provider tool-history
records between nested tool calls. Removal precedes reasoning-cache lookup
so quoted audit markup stays visible even when its text matches a trusted record.
Streaming chunks, tool boundaries, and explicit rerender requests share one buffer-local render scheduler. Requests in the same pending window collapse into one refresh; a full request upgrades an incremental request instead of starting a second timer. Status and interaction zones remain independent of transcript parsing. Reconciliation leaves an unchanged managed fragment in place, and animation changes only a registered label or tool-indicator display property without rewriting the textual progress row. Elapsed text is refreshed at most once a second; status and agent changes use their existing event paths. Progress spacing also reconciles when the preceding content changes. Managed zone boundaries advance past history inserted immediately before them, keeping status, interaction, and progress overlays outside the transcript.
Scheduled full refreshes of settled history leave the current projection in
place while callbacks restore properties, scan the full canonical source with
retained parser context, group and annotate turns, and prepare response Markdown
and tool entries. A complete plan is checked against the source's modification
tick before publication; changing the source cancels and restarts the job.
Each projection resolves Markdown mode and grammar availability once; a batch
retains that snapshot through preparation and publication. A later projection
checks again, so installing missing grammars does not require restarting Emacs.
Each callback runs one pure preparation step under while-no-input. Pending
input postpones that step; input arriving during it unwinds at an Emacs quit
checkpoint, retains the same job, and reschedules without synchronous fallback.
An explicit quit cancels the job instead of retrying a cancelled fontifier.
The old projection stays displayed. Source-property restoration and all
publication stay outside this boundary and retain per-view mutation ownership.
Markdown fontification sees each whole response and normally yields between
line-aligned regions. An input abort keeps its private buffer and retries the
interrupted region, including a very long line; only complete output is promoted.
Tool entries are prepared in small groups; the job owns and releases
their private buffers and caches. Preparation uses the same merged tool/audit
source spans as publication and commits its cursor after each normalized entry.
Completed batch Markdown enters the bounded per-view response cache, so a later
batch-only projection can reuse it; unfinished output never enters that cache.
Historical tool cache keys also track the live source identity and revision when
it differs from the archived transcript, so execution completion cannot leave
archived output displaying a stale running state. Full projections still replay
the turn renderers and rebuild their view-position and interaction state; neither
final turn text nor intermediate turn-preparation plans are cached. The design
rejection of final-text caching and measured rejection of preparation plans are in
ADR 0119.
Once ready, the refresh
renders every turn a displaying window shows, the turns needed to restore
point, selection, and window anchors, and all chat prompt turns immediately (so
moving onto pending responses still has an accurate prompt and click target).
Offscreen turn placeholders are then replaced one per timer callback; a reader
never sees a placeholder replaced in front of them. Pending-history placeholders
do not count as later rendered turns when deciding whether to collapse the
newest directive. Each callback
captures the current draft and reader state, so typing between callbacks is
preserved. Source text or property changes retire the old plan. Callbacks pause
while the view is unattended or its transport is busy, and view teardown cancels
them. Ordinary manual turn folds retain source identity across rebuilds, as do
expanded tool disclosures. A competing projection writer completes the history
before resolving its own source positions; failed batch insertion rolls back
and falls back to synchronous projection. Explicit zero-delay refreshes, direct
full-render calls, and in-flight streaming reconciliation retain synchronous
projection. Scheduled projection can defer large tool parsing as described below.
Scanner lists and phase/cursor updates commit together, and normalized tools
commit per entry, so interrupted preparation restarts without partial publication
or a completed-projection cache sweep. This is cooperative, not a maximum input
delay guarantee: source-property restoration, Markdown mode setup and buffer
handoff, publication of an individual turn, non-quitting C primitives (including
large copies or decodes), short state/cache commits, and GC remain atomic.
Synchronous rendering and in-flight reconciliation are not input-interruptible.
Transcript mutations additionally share per-view ownership. Full, incremental, and terminal projection, agent-handle refreshes, and disclosure actions cannot write the same projection recursively. Nested requests coalesce and run after the active writer unwinds; terminal intent prevents an older live update from reviving streaming state. Idle settlement remains immediate. Source and turn replacement invalidate obsolete queued projection work, while mandatory terminal release remains ordered before replacement state. Failed writers release ownership without starving pending terminal cleanup. Status and interaction redraws requested during projection use the same queue: their descriptors and pause state update immediately, but text and callback overlays materialize after the active writer finishes. Idle chrome-only redraws remain immediate and do not force a pending history batch to complete. Redisplay stays inhibited until the writer and its queued work finish, so fontification cannot expose the temporarily deleted projection. Failed disclosure expansion rolls back its text replacement and retains the old row for a retry.
Queued agent refreshes carry agent identity and rediscover current handles; they do not apply live transcript offsets to archived segments. Queued disclosure actions retain source identity and the requested expanded/collapsed state, not an old view position or a second blind toggle. Internal full-render restoration composes within the current owner. These operations preserve composer text and point, source-backed reader anchors, and adjacent disclosures. Tool replacement includes only child rows within the owning transcript source span, never a neighboring independent agent delivery with the same row type. Agent transcript inspection uses the same projection ownership rather than a second renderer.
Scheduled transcript flushes and history batches, and live tool-row refreshes
are attention-gated (mevedel-view--unattended-p). When every window
showing the view sits on an invisible or iconified frame, or on an unfocused
graphical frame, the row refresh does nothing and a scheduled
render keeps its pending kind without running. Skipped tool-row refreshes
retain each changed tool-use ID once; focus return reads the latest progress
or terminal state and refreshes those rows. An incremental render also drains
these row updates, including executions before its live tail. A full render
subsumes them. A turn that settles, stops or begins projects only its own
range, so it keeps the pending IDs for their own refresh, and a pending full
render, such as a Goal checkpoint's before its continuation turn, still runs;
view teardown discards them.
Focus returning to a frame
(after-focus-change-function) or a window redisplaying the buffer
(window-buffer-change-functions) reschedules the pending render. A view
shown in no window is unattended too; one on a terminal frame is attended,
since focus is unknowable there, and so is an undisplayed view in batch Emacs,
which keeps test behavior unchanged. While unattended, a render request only
records its kind: it arms no timer that would wake the editor merely to defer
again. Child frames use their top-level ancestor's focus
state. The rendering measurements are recorded in
ADR 0119.
Animation checks whether each registered indicator's buffer span overlaps the
visible buffer range of an attended window. Both focus and range eligibility
must hold in the same window. A partially visible span remains eligible even
when its start is above the viewport. These checks reuse the completed
redisplay's window-start and window-end; they never request glyph layout.
Horizontal clipping and partial pixel scrolling deliberately do not suspend an
indicator whose buffer span still overlaps that range. A clipped indicator may
therefore receive decorative updates until its row leaves the viewport. This
bounded extra work avoids expensive pixel-position queries and visibility
checks from inside redisplay for ordinary text animation. The native presenter
also validates pixel placement at semantic boundaries: it covers only a fully
visible, single-line span with matching font geometry. If any window showing
part of the label cannot host it, ordinary animation continues for that target.
A local pre-redisplay guard hides stale native pixels after text, text
property (decorative frames aside), overlay geometry or paint, invisibility,
wrapping, geometry, pixel scrolling, opacity or cursor/selection changes,
and replaces a surface the module closed itself
(its frame unmapped or changed scale, or drawing failed). There is no
per-frame Lisp callback and no horizontal/pixel-scroll primitive observer.
The elapsed suffix uses the same range check and retains its once-per-second semantic refresh independently of decorative motion. Focus, window and normal buffer scrolling changes reevaluate scheduling. A scroll hook and a just-inserted label or tool row both see the previous redisplay's range, so they reevaluate once after the next redisplay instead; without that a new request's indicator stayed frozen, elapsed time included, until the next focus or scroll event. A span still outside the window after that check waits for scrolling or a window resize, which rechecks the same way. A target's start marker advances past text inserted at it, so a reply streaming in just above the progress row stays outside the label's span. Theme and display-frame changes repaint eligible labels at their displayed or frozen phase; labels outside the visible buffer range wait until they return. Frozen glyphs recheck display fallbacks on these events without restarting decorative motion. The transcript's separate buffer-wide attention gate remains unchanged.
The view-owned one-shot timer uses the Emacs UI host's top-level timer list: TRAMP's temporary timer binding neither discards a rearm nor hides an existing timer from ownership checks and cleanup. Stopping a view inside that binding removes its timer before the outer list is restored. The buffer-local window-change hook reevaluates a view when its window switches buffers; a shared window-state hook catches deletion of its last window. Both release power monitoring even if zero-fps tool-only progress has no animation or elapsed timer, and the shared hooks are removed after the last eligible view unsubscribes. Each one-shot callback rearms from the present rather than queuing overdue repeats after a stall. Static or frozen indicators require no decorative timer, although a visible active request can still update elapsed text once a second without advancing a frozen indicator. Freezing keeps the last displayed sample, except that a color shimmer frozen mid-sweep moves to the end of its sweep and a color breathe frozen mid-fade to its normal foreground, so a pause never holds a faded band. Their glyph fallbacks retain the displayed phase, including after the window disappears. The kept sample includes an existing tool row's glyph across option changes and running-row redraws, incremental projection, and full rerenders of the same transcript (the projection carries the displayed samples to the live render that redraws the running rows); a scheduling-only rearm does not sample the next clock phase. A subsequent resume clears the old freeze latch without restarting the underlying phase. Waiting for input freezes active elapsed time and holds every indicator still; answering resumes motion. Progress/status ownership, the stream-render delay, and the authoritative data buffer do not change with animation settings.
The foreground request label supports shimmer, breathe, bounce, dots,
ellipsis, braille, ascii, and static (shimmer by default). Running
tool rows use shimmer, braille, ascii, dots, or static (shimmer by
default), independently of the request label. Shimmer is cadenced: 0.6 seconds
after a request starts, and then every four seconds, a cosine band at least
six columns wide (a tenth of the label each side, minimum three) fades toward the background as it sweeps the label in one
second. Text outside the band and between sweeps keeps its normal foreground;
between sweeps the view schedules no frames. Tool rows
shimmer their tool name ("Bash", not its arguments), and a running group its
N running count, in the same frames as the label, so they add no wakeups of
their own. The sweep fades the span's own face (the tool-name or metadata
face, recorded as mevedel-view-tool-animation-face), so a name keeps the
color it has once settled; banks stay one per label. Glyph styles animate the row's marker instead, and static keeps
the running ●. Color styles use 64 theme-derived shades;
breathe and bounce use a continuous 3.6-second cycle sampled at at most 8 fps.
Breathe starts at normal foreground, fades toward the background, and returns;
bounce moves a faded band over otherwise normal text. Prepared frames are
cached with a bounded animated prefix so long labels remain readable. Color
resolution honors the view's buffer-local face remapping, also while the
frame's selected window shows another buffer, so indicators blend into views
styled by Solaire or buffer faces. A native surface paints the background
redisplay draws at the label's start: overlay faces for every window or for
that window (such as hl-line), then text faces, then the remapped default.
Native placement runs after global redisplay hooks, so per-window hl-line
has restored its highlight after a status-row replacement before the surface
samples its background.
Translucent frames (alpha or alpha-background below opaque) keep ordinary
text animation, since an opaque box would show there. Prepared banks distinguish each view's remapping specs. A live view reserves a prepared bank for every registered tool label, including
a running group's count, plus the main label (with a four-bank minimum). This working
set is bounded by the grouping threshold, beyond which running calls share one
group row, and retained independently of the
shared six-bank reuse cache, so other views cannot evict active frames. The
scheduler prepares every eligible label at semantic boundaries. Color and
portable glyph rows may coexist and each keeps its own cadence. Frozen rows
adopt their destination display on visibility changes; a changed running count
updates its text while retaining its displayed phase. Theme changes clear both caches. If colors
cannot be resolved on a display, color styles fall back to a glyph indicator;
when Braille is unavailable on its target display, the indicator uses ASCII.
Terminal palettes
with fewer than 256 colors use the glyph fallback even when their face colors
resolve: their coarse shade mapping cannot support a smooth color animation,
so the view also uses glyph rather than color-rate timer wakeups. When either dots
glyph is unavailable, dots uses a same-width ASCII pattern at the same speed.
An undisplayed status or a new running row starts with a portable frame
until its display is known. If the same indicator is visible in multiple display
frames, color styles use a portable glyph rather than a palette prepared for
only one frame; configured glyph styles retain their cadence (with ASCII
substitution only where a target display lacks the glyphs). Glyph animations
keep their natural cadence (240 ms for braille/ascii, 480 ms for dots and 960 ms for ellipsis); raising the frame-rate ceiling does not accelerate them.
A color style falling back to a glyph also uses that glyph cadence, not
a needless color-rate timer.
Changes to styles, colors, and labels invalidate affected prepared frames;
theme changes and edits to the spinner, its inherited faces, or the default
face (including Customize) clear the color cache and repaint visible labels
at their current or frozen phase without a periodic timer. Theme changes also
recheck the Braille and dots fallbacks of frozen main and running-tool glyphs;
a visibility resume retries dots support if the display font changed without
a theme event. Each running call retains its own last displayed phase while
motion is disabled, including when the main label is absent or static. Hidden
labels repaint when they become visible again. Applying animation settings
through Customize refreshes live views without restarting a request. Moving a
paused view to another display frame also repaints its frozen color sample for the
destination palette (or the portable multi-frame fallback) on visibility
resume, without starting a decorative timer or discarding reusable color banks.
The normal mevedel-view-spinner-framerate ceiling defaults to 30 fps.
With ordinary text animation, each decorative frame is a redisplay, and the
measured pgtk build presents its whole surface. Native presentation avoids that
path: GLib timers draw prepared Pango samples into small Wayland shared-memory
surfaces. Breathe/bounce then use the configured ceiling rather than the ordinary
8-fps compromise. Glyph cadence and shimmer sweep/rest timing remain unchanged.
Equal adjacent samples share an interval. Native callbacks never call Emacs APIs.
mevedel-view-spinner-power-policy defaults to auto: external power uses
that ceiling; battery/backup power or unknown/stale readings use the lower of
it and mevedel-view-spinner-battery-framerate (default 15). full always
uses the normal ceiling; save always uses the battery ceiling. Battery 0
freezes decorative motion for both indicator types, while the global
mevedel-view-spinner-animate switch disables all motion without suppressing
semantic status or elapsed updates. The policy reads the Emacs UI host, not a
remote workspace. One shared battery.el observer consumes existing battery
notifications without enabling battery mode; when subscribed automatic views
are visible it queries at most once per 60 seconds. Loading views and restoring
browser sessions do not load battery.el or initialize its D-Bus backend:
that optional initialization belongs to the first deferred power poll and
uses the same conservative fallback on failure as later queries. The shared
fallback timer
is installed on the UI host's top-level timer list even when TRAMP temporarily
binds that list away, and final unsubscription removes it from that list before
the binding is restored. A stale timer callback cannot replace its successor.
No frame update queries power. Automatic power
transitions follow a notification immediately, or are normally detected
within that fallback interval. Backend failures and unsupported/unknown
readings use the conservative saving ceiling. An unknown notification after
the previous external-power sample expires also rearms the actual view timer
immediately, rather than waiting for elapsed-metadata maintenance. Explicit
full is useful on desktops whose power source cannot be determined. Lower
frame rates reduce
scheduled animation work, not necessarily battery drain proportionally.
The view enables cheaper PGTK presentation when its mode starts on a PGTK
frame (mevedel-present-ensure), once per session and for all frames. Stock
PGTK queues a whole-widget redraw for every frame at the end of each redisplay,
and GTK 3's shared-memory path then allocates and backfills a whole-window
buffer per frame while the compositor holds the last one. The native module
(native/mevedel-present.c) creates a GL paint context for each frame's
toplevel, so GDK presents through EGL, and redirects the Emacs executable's GOT
entries for its drawing calls (cairo_fill, cairo_fill_preserve,
cairo_stroke, cairo_paint, cairo_mask, cairo_show_glyphs) to record
their clipped extents on each frame surface. Emacs's gtk_widget_queue_draw
becomes a redraw of that region, or nothing. A frame counts as shown only once
its draw handler painted its newest surface; before that (a resize before the
flip) redraws stay whole, and so do they while a copy of the shown surface made
with cairo_surface_create_similar lives: the visible bell draws on such a copy
and records no damage. Emacs's calls to g_main_context_query, _pending and
_dispatch do nothing on threads other than the main thread: a Lisp thread
waiting in sleep-for used to dispatch GTK painting and crash in GDK. A GL
context can be current on one thread only, so each dispatch and each
gtk_main_iteration releases it: a Lisp thread that shows a frame runs GTK's
loop itself while it waits for the map event. Moving an animation surface
redraws one pixel of its frame, enough for the parent commit that applies the
position. The module refuses, with a
reason kept in mevedel-present--state, on displays other than Wayland,
architectures other than x86_64 and aarch64, and executables that do not
import the calls it requires or that import a drawing call it does not track
(cairo_show_text, cairo_paint_with_alpha and the like). While it is active,
text breathe/bounce use the power-aware ceiling instead of 8 fps.
test/test-mevedel-present-wayland.el checks the protocol on a headless nested
KWin with both the default renderer and software EGL: no fresh shared-memory
buffers during steady presentation, no commits for redisplays that drew
nothing, and painting while a Lisp thread waits. Software EGL uses reusable
shared-memory buffers rather than dmabufs, so the fixture exercises a reversible
text change before measurement to populate the swap buffers; an unchanged
redisplay cannot do so.
The native surface tests also close real GTK displays and reconnect with fresh Wayland bindings. Stock Emacs 31 PGTK leaves a closed display's descriptor in its input wait set and can leave a frame scale timer behind when deleting a terminal with live frames. The regression therefore deletes frames first and reconnects immediately; direct close followed by an event-loop wait requires fixing those Emacs teardown bugs. Native display teardown itself releases its surfaces and registry bindings.
The native module (mevedel-native.el) requires Linux, Emacs module support,
cc, pkg-config, and Emacs/GTK 3/Wayland development headers. Its rendering
features require PGTK/Wayland; its timed condition wait for
telemetry sampling also works without a graphical frame.
It compiles every native/*.c into one library once, at the first view,
eligible animation or telemetry sampling window, and caches it by source/build identity under
mevedel-user-dir/native/. The build looks for emacs-module.h beside the
running Emacs (its prefix's include directory or its build tree) before the
compiler's default path, and does not treat warnings as errors, so newer headers
cannot disable the module. mevedel-view-native-enabled disables the
presenter; unsupported displays or build/placement failures retain ordinary
text animation. A downloaded prebuilt module that matches its pin is loaded
first, then a cached local build, then a new local build. mevedel-native-install downloads the
prebuilt module for the current source revision and architecture from the
project's GitHub release native-HASH (HASH is the first 16 hex digits of the
SHA-256 of native/*.c concatenated in byte order) and loads it only when
its SHA-256 matches native/prebuilt.eld. The sources share no header, so
package recipes that list native/*.c build it. CI
(.github/workflows/native.yml) builds the x86_64 and aarch64 modules on
Ubuntu 22.04 against its older emacs-module.h, so they load into any newer
Emacs with glibc 2.34 or later, and records their checksums there. A failure is
reported once per session in the echo area, naming the next step; the internal
mevedel-native--load-state retains its reason, and setting it to nil
retries. Installing after a failed build allows presentation to retry on the
next view. Installing while a module is already loaded leaves that module
active until Emacs exits, preserving its surfaces and presentation hooks.
Labels that contain characters XML
cannot carry, such as control characters from tool arguments, change width once
escaped, so they keep the ordinary renderer. An error or a refused surface
hands only that label back to text animation, closing its own surfaces;
retrying reopened the surfaces before it on every semantic update. A refused
surface is tried again once its font, colors or geometry change; a label whose
preparation signaled stays with text until it changes or animation stops. A
row not yet laid out (reporting no height) is not placed, rather than refused.
The presenter matches the frame's opaque window ID against live GTK toplevels;
it never dereferences that ID. Native ownership includes three bounded pixel
buffers per surface, Pango layouts, timers and parent lifecycle handlers. An
empty input region leaves mouse events with Emacs. Hidden/unfocused/destroyed
parents release native work. Cursor/selection overlap, child frames, clipping,
wrapping and incompatible font metrics retain the ordinary text renderer.
Creation and movement keep the child surface synchronized until a parent frame
callback confirms that its position has been committed. Only then do native
frames run independently, preventing a brief image at the initial (0, 0). A
move while that callback is pending waits for the commit carrying the new
position instead, so frames never run free at the old one. When a Wayland
display closes, the module closes its surfaces and releases that display's
globals; a later display binds its own. test/test-mevedel-view-native-wayland.el
exercises the module on a headless nested KWin where one is installed.
Projection writers inhibit redisplay while deleting and reconstructing rows. Native synchronization coalesces to their latest intent at the parent redisplay boundary, retaining unchanged surfaces even when markers are replaced. Releasing every surface still waits for that boundary, but marks the view's windows for redisplay, because pre-redisplay hooks run only for windows being redisplayed. The stream owner keeps metadata cadence, power policy and the last presented phase for freezes; the native presenter owns pixels and placement. Stopping the view releases surfaces, timers, observers, pending parent callbacks and pending presentation.
Before rendering a restored transcript, mevedel-transcript-restore.el
recovers gptel bounds and normalizes their text properties through that same
canonical transcript grammar. Successful whole-buffer normalization is reused
until text or properties change; narrowing alone does not invalidate it.
Restoration does not maintain a second parser.
Inner transcript disclosures use a two-space left inset for their headers and
a four-space left inset for expanded bodies. This includes mailbox, tool,
reasoning, prompt, system-reminder, hook-audit, hook-context, and
completed-agent disclosures. Nested audit details and mailbox payload gutters
may indent further to express their hierarchy. Ordinary response prose and
whole-turn headers or folds remain flush-left. Body insets are display-only,
including on wrapped continuation lines, so copied disclosure content retains
its authoritative text without presentation padding.
Thinking keeps its muted marker and summary face, including when manually
collapsed. Nested row toggles retain the complete parent inset immediately;
their header and body indentation do not wait for a later transcript redraw.
Expanded ordinary tool text aligns with its parent's text label. View mode
disables adaptive-wrap-prefix-mode and visual-wrap-prefix-mode after major-mode
hooks: their automatic fill-prefix calculation would overwrite the renderer's
continuation insets. This is local to the view, not a change to other text buffers.
Delivered-message gutters are display-only too; continuation text aligns after
the gutter instead of crossing it, and copying preserves the original payload.
Collapsed delivery cards hide the header separator and retain the final payload
newline as their visible terminator. This gives the following row its own display
line and inset, without adding a blank row or changing payload ownership.
Display decoration keeps invisibility rear-nonsticky, including after a card
joins an activity group, so clicking past its header stays on that row rather
than moving point onto the following row.
Hook-context expansion restores through the same renderer as interactive toggles,
so redraws retain one heading and the same body inset.
Restoring an expanded system-reminder audit uses its ordinary disclosure renderer:
one summary header and the same audit face survive both full and live redraws.
Agent paths in launch handles use link highlighting. Delivery headers use
mevedel-view-mailbox-header, a keyword face with no added bold weight.
Background deliveries show subject-first one-line notices: ● Agent /root/name
finished, ● Background command "Run tests" completed (exit code 0),
● Monitor event: tick 1 · test ticker, or ● Monitor "test ticker" stopped ·
2 events. Each Monitor event is a separate record and row in source order.
RET expands the full record; collapsed notices have no line-count hint.
Execution labels prefer the model-written description, falling back to the
truncated command. A command's own completion appears once, as its breadcrumb
worded like these notices with a [Show result] link; its delivery adds no
second line. Command notices and breadcrumbs share the terminal
vocabulary completed, failed, stopped, or timed out; an agent result uses
finished, failed, interrupted, or incomplete. Stops
are neutral, not errors. Missing agent outcomes never imply success. These
labels use structured envelope attributes, not body prose or the current agent
registry, so saved transcripts reconstruct them without live execution state.
Accepted reviewer allow-once decisions add one subdued Auto-approved · once
hint to the affected tool header, with a bounded explanation on hover. The
canonical :permission-approvals records are display-only: they do not authorize
anything, and ordinary rules, human approval, and historical calls without
recorded provenance do not acquire a hint. Status remains independent; an
approved call can subsequently fail. Pipeline admission binds pending rows to
durable tool-use IDs before approval, so identical concurrent calls cannot
exchange hints when they settle out of order. The same header handles settled,
restored, and nested rows. A direct ToolCall displays its child's approval;
compound calls never aggregate child approvals onto the enclosing ToolCall.
Original Bash rows retain approval across execution yield and progress, while
polling calls do not inherit that approval. Browser tool presentations carry
the same bounded tooltip and row-local hint, preserving disclosure choices.
Background notice bodies always start collapsed. Ordinary message bodies start
collapsed above mevedel-view-mailbox-collapse-line-threshold.
Long user prompts fold to a one-line summary — the truncated first line plus
a hidden line count such as Please analyze this trace... (+83 lines) —
once they exceed mevedel-view-user-input-collapse-line-threshold (default
15, 0 disables). The fold expands in place. The full text travels in a text
property rather than being re-read from the data buffer, so the send-path
echo folds identically before the turn has data-buffer coordinates; only
the source-backed fold keeps its state across full rerenders. Prompts
containing org block markers stay unfolded so their block decorations
remain visible.
An expanded inline $skill prompt keeps the invocation as the visible user
text and discloses the prepared prompt in a collapsed Prompt row. The
required-attachment system reminders leading that prompt get their own
collapsed row above it, labelled from the render-data record's attachment
names (2 attached skills (artifact, artifact-design)), so the prompt drawer
holds the prepared body alone and no <system-reminder> markup renders as
prose.
A Skill tool row does the same for a model-side invocation. A tool segment
carries its own proof of structure, so the reminder scan does not descend into
it and reminders welded into a tool result would render as prose. The Skill
tool's render data therefore carries the bare skill body and the attachment
names, and the row shows that body under a header naming the dependencies.
Turn and activity rules¶
An assistant turn closes with one rule. Once the request summary is known the
rule carries it, as in ── Worked for 2m 37s ─────: the label uses
mevedel-view-separator, and a strike-through on its extended newline
(mevedel-view-request-summary-rule) continues it to the window edge, so split
and narrow windows need no width calculation. One blank row separates it from
the next heading. A turn without a summary line, such as a running turn,
closes with the plain overlined mevedel-view-turn-rule row after a blank line;
that empty row leaves the same space above the next heading. A user turn that
renders nothing visible, such as a harness-started background-event wake,
contributes no rows, so the space after a closing rule does not depend on
which kind of turn follows.
Inside a turn, activity hangs below the prose that announced it, separated by a
blank line only. Prose that follows activity starts with a quiet rule: two
undecorated columns, then mevedel-view-activity-rule, whose overline is
blended from the active theme at about half the strength of shadow, so it
stays lighter than the turn's closing rule. Each section therefore reads as
intent followed by its actions. Activity is any non-prose row of the same
turn, including thinking, reminders, and mailbox cards, so grouping or
expanding rows never adds or moves a rule. The rule is decided when non-empty
prose arrives: the activity above it is already rendered, so it needs no
lookahead, and it belongs to the prose's live render unit, so retained
activity is never rewritten. The search stops at turn-closing chrome, so a
directive response without its own heading never takes a rule from the user
turn before it. Activity that ends a turn is closed by the turn's rule.
Runs of more than mevedel-view-tool-group-collapse-threshold (default 3)
plain tool rows fold into one grouped activity row such as
Searched 5 patterns, read 1 file, ran 5 commands; tools
without a verb mapping — MCP tools included — appear as NAME ×N. A summary
too long for the narrowest window showing the view ends in …, leaving room for
its running and failed counts, so the folded row stays one screen line as they
come and go. The
expanded group reuses the compound-tool nested-row machinery: each tool call
is a tool-child row in chronological
order with its own collapse state, and collapsing the group takes its rows
with it. Collapse removes only rows whose source the group's source range
contains, so that range spans every member, including a call that ends after
its split-out completion breadcrumb. Nested compound calls retain each descendant's row identity and
nesting depth: toggling one removes its descendants with it while preserving
sibling rows, so repeated expansion never accumulates copies.
Ordinary agent messages inside an activity run join the same group as
independently expandable cards; received N messages counts them separately
from tools. Background notices, including agent results, remain separate rows
and split surrounding activity groups; the transcript's own completion delivery
renders nothing and splits nothing. The original yielded Bash row retains
its linked completion breadcrumb. Breadcrumbs can fold into activity groups as counted completions,
such as 3 commands completed, 1 failed, and show linked rows when expanded.
A newly formed group stays open when one
of its rows is already open, or when point or a window's point is on one of its
rows, settled or running, as the run folds; the group then remembers itself as
open, so it does not fold later when the reader moves on. Grouping therefore
does not hide or move text the reader is inspecting. An explicit group fold wins over
its children's states. Rows that demand individual presentation — agent
handles, compound tools, rows carrying hook audits, rows their renderer wants expanded or
compact, and coalesced rows — never fold into a group; they split the run
around themselves. Substantive thinking also splits surrounding tool runs:
it renders as a standalone Thinking... disclosure, expanded by default,
so collapsing either neighboring group never hides it. Explicit manual
collapse and expansion survive streaming chunks, tool completion, settlement,
full redraws and revisiting an archived segment. A distinct reasoning block
gets its own default. Empty blocks and signature-only provider metadata produce
no blank cards and do not split a tool run. Tools nested in reasoning retain
their own rows in source order; their payloads never enter the thinking body.
The provider's reasoning type and stored transcript remain unchanged, even
when a thought contains user-facing recommendations or progress updates.
An unfinished
activity run remains mutable across streaming events so later calls can join
its group.
Tool calls the model started but whose results have not reached the
transcript are drawn in the same grammar, as running calls of the turn's
trailing activity run. They count toward the run's grouping threshold
together with its settled calls, so a parallel batch has the shape it will
keep: above the threshold one group row such as
● Read 9 files, searched 1 pattern · 2 running, otherwise one
● Read: path row per call. When the batch settles, the next render draws
✓ in place of ● and drops the running count; nothing else moves. Calls
after assistant prose, a reminder, or a row that cannot fold start their own
run rather than joining the earlier one, inside the turn where they will
settle. A call the pipeline reports finished keeps its place until its
result reaches the transcript (for a gptel batch, after the whole batch):
its row shows ✓, or × for a failure, and it leaves the running count,
so one early finisher never reshapes the run. A folded running group cannot
show those rows, so its header adds · N failed: TOOL naming the failed tools;
once no call is left
running, a failure takes the group's marker as ×. The ID-less post-tool hook
that precedes the result retires such a finished call first and never a
bound sibling still running. A response that opens with tool
calls has no turn in the transcript until the first result lands, so the
live render draws its Assistant heading with the running calls; the row
therefore does not move down when the heading would otherwise appear. The
interim turn also ends with the settled turn's blank line and rule, so the
progress row below does not move either. The labelled closing rule that
replaces the plain rule when the request ends is one row taller.
Directive requests keep their own heading instead. An expanded running group lists its
running calls after its settled children; they are not expandable, have no
transcript source, and key no disclosure state. A fold or expansion of a
running group survives its growth like any group's. Folding or unfolding a
section of the live turn while calls run redraws the live turn after the
toggle, because a toggle rebuilds its section from the transcript alone.
While the view shows an archived segment, the live turn is not on display:
toggles there never redraw it, and a live update does nothing.
Running calls are drawn
only by the live render: a gptel batch runs every pre-tool hook and later
every post-tool hook at once, and each batch shares one boundary render. A
full projection draws settled history and leaves running calls to the live
turn's next boundary render. Stopping a turn without settling it removes
running rows and an interim heading. An abort renders nothing afterwards, so
the stop redraws the live turn once from the transcript: a group counts only
calls that reached it, and one formed only by running calls returns to plain
rows. If that redraw fails, the running decorations are still removed and a
running group's settled marker is restored in place.
Failed calls stay in their run, so a call failing never reshapes it. A folded
group containing one takes the red × marker in the error face and ends its
summary with · N failed: Grep, the same text its running form showed: each
failed tool is named once, and a long list of names ends in … so the row stays
one line. The failed row inside the group carries its own ×. Warning rows stay groupable; a group containing
a warning carries !, and the warning stays visible on its nested row.
Failed tool rows, including nested calls inside compound tools, start collapsed.
Only the marker uses warning or error highlighting; the tool name, argument,
metadata, and sandbox summary text keep their normal faces. An error takes
precedence over an accompanying sandbox warning. Explicit expansion survives
redraws. A note-class sandbox disclosure stays with its nested row without
marking the group. The same rules apply to native, wrapped/MCP, and generic
tools. A valid empty search is successful; a search that failed to run shows
an error instead of a zero-match count. Bash rows show command, status and
elapsed time; execution IDs, output counts, working directory and routine exit
facts belong in expanded details. Their output disclosure starts closed for
running and completed commands. A still-running command carries the running
marker ● rather than ✓. The original row owns all progress and output,
including output returned by a hidden, successful empty-input WriteStdin poll.
Input and stop interactions remain visible and link to the original execution;
their result links resolve the execution ID across the current transcript and
readable older segments, including nested ToolCall Bash children. Failed control
operations do not disappear. A command's failed exit is attributed
to the command instead of to its successful input or poll. A terminal event
of an idle transcript projects its durable breadcrumb into an open current view
immediately, without waiting for a full rerender. During a running turn the
breadcrumb is recorded at the turn's response position, and the live tail's
next incremental render shows it in place, folded into its activity group when
one forms. Views opened later reconstruct it from the
transcript. Source-backed disclosure choices and reader/composer positions
survive progress and completion. Folding a turn retains its projected
breadcrumb for retry deduplication. Deduplication against older segments reads
each archive once per view and rereads it only when its published hash, staged
write, or file size and modification time change. Show result first unfolds the turn holding
the source-backed Bash row. A separate execution-history disclosure
preserves inspectable polling and delivery records.
The same rules apply to execution tools nested within ToolCall. A direct
ToolCall Bash shows its child's latest sandbox disclosure on the outer row;
its expanded history action uses that child's execution ID, not the outer
ToolCall ID. Completion lookup follows the receiving transcript's own history:
root session segments or numbered agent compaction archives. A missing earlier
archive does not hide available later terminal evidence. If the source row is
gone, the result link opens retained output read-only instead of navigating
into the parent's segments. An empty retained output is shown as an explicit
no-output result; only absent evidence reports an unavailable result.
The read-only fallback shows material sandbox disclosures and marks incomplete
preview output when the complete output artifact is unavailable. A readable
artifact capped by the execution output limit remains marked as truncated. A process
signal is labeled as signaled rather than as a requested stop in the Bash row
and completion breadcrumb; the guest projection reports it as a failure without
calling its signal number an exit code.
An agent result link opens a surviving source row in its own read-only
compaction-archive view, with Latest returning to the current agent transcript;
agent archive navigation does not enter the parent session's segments.
On reload, nested Bash calls whose process is gone show a lost state and error
marker rather than inheriting the ToolCall's earlier successful call status.
Expanding a group rebuilds its rows from the folded run alone:
mevedel-transcript-segments expands its end bound to the
containing property run, so the segment beginning where the run ended is
dropped instead of being summarized as one more reasoning occurrence.
After an interactive ApplyPatch review settles, the applied patch row opens
expanded on a preview of its first two changes with an … N more changes
tail (the rendering's :preview-body); collapsing and re-expanding shows
the complete diff. Unreviewed edits/full-auto applications stay collapsed.
Zones¶
The view buffer is split into vertically ordered regions. The data buffer remains the model-visible source of truth; view zones are display and interaction chrome around that transcript.
+--------------------------------------------------------------+
| Header / mode line chrome |
+--------------------------------------------------------------+
| History region |
| Rendered user turns, assistant turns, tool summaries, |
| inline agent/tool handles, and any in-flight live tail. |
+------------------------- status marker ----------------------+
| Status zone |
| Active child confinement, tasks, and aggregate agent rows. |
+---------------------- interaction marker --------------------+
| Interaction zone |
| Permission prompts, plan approvals, Ask, |
| pending input, approvals, and preview controls. |
+--------------------------------------------------------------+
| Permanent composer rail |
| Mode badge, pending-action shortcut, and live turn activity.|
+-------------------------- input marker ----------------------+
| Input zone |
| Read-only input prompt, then editable composer body. |
+--------------------------------------------------------------+
Terminology:
- History region: rendered transcript above
mevedel-view--status-marker. Running tool calls such as● Read: pathare part of the in-flight turn's history, not status-zone content. Empty-inputWriteStdinpolls with a nonempty execution ID do not add running rows or a separate pre-tool spinner, including a ToolCall consisting solely of one literal poll. Invalid arguments and compound or ambiguous ToolCall scripts keep their running row. The request progress row remains visible, and a failed poll control appears in the settled transcript. - Status zone: session status chrome between
mevedel-view--status-markerandmevedel-view--interaction-marker. Task, live-execution, and aggregate-agent rows appear here. The agent roster lists every active agent, even when its launch handle also appears earlier in the transcript. Opening an agent transcript or folding history does not change roster membership. - Interaction zone: user-action chrome between
mevedel-view--interaction-markerand the request progress row; it is for pending input and controls that require user response. - Composer rail: the permanent, fragment-backed line directly above the
input prompt, with no intervening blank line. It shows the mode badge at
rest, pending-action counts with an actionable
C-c C-jhint, and foreground activity while a request runs. It does not start an animation timer while idle. The rail fits the narrowest window showing the view: activity yields space before the mode and pending-action hint. It is not part of the history, status, or interaction zones; its internal zone name remainsprogress. A window resize refits the rail fromwindow-size-change-functions, which run before windows redisplay. Scrolling never rewrites it:window-scroll-functionsrun inside a window's redisplay, which then restores point to its earlier buffer position, so text changed above the composer there would move the cursor within or out of the draft. A label that went stale while offscreen waits for the next once-a-second refresh. Its elapsed value measures active request work, excluding time spent awaiting an Ask answer, permission decision, Plan approval, ApplyPatch review decision, or direct request input. During those waits it readsWaiting for inputand every indicator holds still. Queued Pending Inputs and an armed session fork do not pause active elapsed time. - Input zone: the read-only prompt prefix plus the editable composer.
Composer refers only to the editable unsent input body.
The mode badge (
[edits],[Plan · edits],[Editing …]) starts the rail above a>prompt. The badge and draft start at fixed columns in every mode, Ask included, so switching modes never moves the draft. Explicit and soft-wrapped continuation lines are indented by the width of the prompt's last line (two columns, or four after a directive's◆ >), so the draft forms one aligned column. The indent is a display-onlyline-prefix/wrap-prefixon the composer overlay, recomputed whenever the prompt is rebuilt; the draft text, submissions, and kills never contain it. An empty final line uses the same overlay's display-only suffix to align its cursor: Emacs does not applyline-prefixat end of buffer. Typing there removes that suffix.
Only composer edits are undoable. Renders above the composer record their own undo entries, which Emacs never trims between commands: a busy view once held 24,000 entries and several megabytes, mostly text-property changes, and undoing one would rewrite read-only transcript. Asynchronous redraws, zone reconciliation, scheduled render flushes, and every command start therefore reduce the view's undo list to composer entries, shifted past the text renders inserted or removed above the input marker. An entry that cannot be placed ends the history there. Transcript inspection views and generated data buffers keep no undo history. Rebuilding the prompt is also chrome: undoing a draft never restores an older permission badge or prompt prefix inside the composer.
Zone changes and re-renders preserve one active surface per window: point in a surviving visited interaction takes priority, then a visible composer, then the logical reading position. Anchors use body-relative pixels and native display geometry, including wrapping, remapped faces, inline images and partial vertical scrolling. Bounded fresh chrome measurements keep body coordinates and usable height consistent before redisplay, even when header or tab-line heights change. Logical positions are restored before the pixel anchor. A surviving composer anchor retains intentional blank space. A lost or truncated logical window start instead permits filling the last page, even if anchoring exposes longer history lines and grows the character tail. Visiting windows repair only new blank pixels created below the buffer, retaining their prior blank area. For unanchored windows, a smaller character tail also permits clamping a start later than the last page backward; unchanged or growing tails retain their reading position. Equal-length reflow alone does not trigger that fallback.
Spinner start, update, and stop leave logical-position restoration to zone reconciliation. They do not restore raw saved markers over it: a selection inside the permanent progress rail survives status replacement, while a draft and its reading position in another window remain intact. The exception is a rail redraw nested inside an owned projection, such as a status update made while a full render is fontifying. There point is the projection's insertion cursor, not a reader position, so the rail keeps it exactly and the enclosing projection restores reader state when it finishes. Treating that cursor as a composer offset would move it past the new rail and append the rest of the history below the chrome.
Draft editing captures blank pixels before the first mutation of a command and
repairs only new blank space after the complete command. Ordinary self-insertion
without overwrite or auto-fill skips capture. Short plain deletions whose line
fits every window also skip viewport capture; wrapping, remapping and display
properties take the exact path. Other deletions (including multiline
delete-selection) and insertion-first commands retain it. Pre/post-command hooks
bound the transaction explicitly and finalize the originating view even when a
command switches buffers. Commandless mutations repair immediately despite a
stale this-command value. History/snapshot replacement and sending a folded draft are
single transactions, not independent insertion/deletion viewport states. Only
an actual increase in blank pixels triggers backward traversal. Height-only
window growth infers the old blank area from the height delta when the buffer
and width are unchanged; width changes
and newly displayed buffers are not inferred. Text scaling captures actual blank
pixels around text-scale-mode, including programmatic calls, without adding
work to ordinary commands or redisplay. Both repairs retain pre-existing blank
space.
A submission that starts in the composer captures the draft it forwards, so a
draft typed while UserPromptSubmit, skill preparation, or a slash command
runs asynchronously survives the send it started, together with its mention
bindings, dropped-file grants, and point within the draft. Acceptance clears
the composer only while it still holds the captured draft. A submission that
captured no draft clears unconditionally: a drained pending input already
required an empty composer, and a buffer with no composer has none to protect.
The interaction-zone painter in mevedel-view-interaction.el renders
descriptor bodies as interaction fragments. Descriptor overlays may still
span those fragments as callback handles for prompt settlement and preview
cleanup; they are not independent renderers. Register controls with
mevedel-view--interaction-register; do not direct-insert ad hoc UI near the
composer. Registering or rebuilding an interaction must not auto-focus the
prompt or move point out of the composer.
Cards stack in arrival order, the newest farthest from the composer, so an
arriving card moves only the transcript above it (ADR 0126). The pending-input
card stays pinned next to the composer. A descriptor re-registered under the
same id, as rebuilds do, keeps its place; one that closed and returns arrives
anew.
C-c C-j (or the rail's pending-action text) explicitly visits the pending
interaction nearest the composer, the one waiting longest, and repeated
presses cycle outward. It remembers the invoking window's draft offset;
settlement returns there only while point remains in that interaction.
Use :body-properties-owned only when the producer supplies complete per-span
read-only and stickiness properties, as ApplyPatch does for inline feedback.
A descriptor whose UI holds live editable state supplies :body as a
function returning fresh text and recreates its buffer markers in
:after-render; a registration-time snapshot would be redrawn verbatim by
foreign rebuilds (the control-transfer poll, queue events) and destroy what
the user typed. mevedel-view--interaction-rebuild drops and re-registers
queue-backed descriptors without rendering the intermediate states: one
final render reconciles the zone, and a descriptor re-registered under the
same id reuses its overlay object, so an unchanged rebuild is a no-op that
leaves zone text, point, and held overlay references untouched.
Unregistering a descriptor, or a rebuild that drops one, closes that
interaction. When a root view's last pending interaction closes while its
session is idle, the zone offers the session's held idle work again: queued
follow-ups first, then a background-event turn, otherwise an active Goal's
continuation. The choice is made
after the closing command, so a permission sibling rendered by the same
settlement still holds the work, and each offered path rechecks its own gates.
After an expired permission queue the session waits for the user; the zone
shows one non-interactive, non-pausing permission-absence notice for that
hold, and ending the hold by submitting a message closes it and re-offers idle
work like any other closed interaction.
Interaction keybindings are active only when point is on the interaction text;
composer input must never settle or cycle interaction prompts.
C-c C-j explicitly visits pending questions and approvals, revealing a hidden
Plan approval when necessary; repeating it cycles the cards. A visit from the
composer remembers that window's draft offset. Settlement returns there only
if the window is still inside the visited card. A reader who navigates away,
and other windows showing the same view, are not pulled back.
Portable sessions also render cooperative lease-transfer controls in this
zone; mevedel-view-control-transfer.el supplies their descriptors while the
generic interaction owner places them.
The owner sees the requester label with Grant and Keep actions; a granted
transfer remains quiescing until current requests, executions, prompts, pending
inputs, and publication work drain, then the final save releases the owner to
read-only. A read-only client sees Take control: r acquires an available or
expired lease, or requests a live owner's lease. Its composer remains untouched
during acquisition and while the polling timer waits for the named successor
fence to clear.
The interaction separator is virtual chrome. Task rows, aggregate agent status rows, interaction bodies, and request progress are view-owned UI chrome; they do not belong to the model-visible transcript. The input prompt starts with a read-only blank separator line so status, interaction, and request-progress rows stay visually distinct from the composer.
Directive Turns And Inspector¶
The source actions and mevedel-list-directives resolve the topmost workspace
directive record, bind or resume its execution session, and display that
session's ordinary MevView, by default in a directive frame anchored at the
directive. Starting an action appends a directive turn at the
chronological tip; it never opens another live rendering surface. Full request,
response, tool, and interaction content remains visible there while provider
prompt projection keeps it outside ordinary-chat context.
Follow-up actions put the shared composer into directive scope. Prompt preview shows the complete next isolated request, including discussion, requested changes, retry guidance, or selected-attempt context. Attempt actions can open the reusable patch viewer or invoke the session's ordinary Rewind impact and confirmation flow through the attempt's exact turn checkpoint; neither path creates another history owner.
The explicit read-only directive inspector renders the current request, lifecycle and anchor state, implementation attempts, and discussion turns from the workspace record. It is the durable access path after compaction, source loss, or archive. Opening it replaces the current displayed view rather than splitting beside MevView. It owns no composer, streaming, or interaction callbacks; View patch, Reattach, Rewind before..., Archive, and scope-entering actions dispatch to the record or execution session. Its activity entries fold by default as an overview, and each rendered row resolves back to its own durable entry by activity kind and settlement sequence together, because Rewind acts on whichever entry the row resolved to. Source overlays expose the Activity action only after the directive owns a planning, discussion, or implementation turn.
Plan-before-implementation is configured per top-level directive through the
source overlay or inspector Settings menu. The main action menu stays compact:
Settings contains the Plan toggle and model/effort selector, whose label becomes
planning model/effort only while Plan is on. An enabled presentation shows a
compact PLAN: ON hint. Planning and approval derive Planning, Plan Ready, and
Plan Accepted presentation states without replacing the directive's underlying
lifecycle. A cancelled proposal remains a draft and exposes Continue Plan,
which restores the isolated directive composer scope.
Directive Frame¶
The directive frame is a floating child frame anchored at a directive's source position that displays that directive's bound execution-session view. It shows the real view buffer, so permissions, Ask, patch review, streaming, and the composer work in it unmodified and no second renderer exists. See ADR 0106 for why an overlay cannot host these interactions.
mevedel-show-chat-buffer selects frame, window, or nil for directive
dispatch. Child frames need a graphical display before Emacs 31, so the frame
falls back to an ordinary window wherever they are unavailable; every behavior
except the floating geometry is identical in that fallback.
An explicit action opens the frame with focus. A request dispatch opens it without focus, because a request the user just started must not move point. Scope-entering actions enter the directive composer scope before displaying, so composer input in the frame becomes a follow-up for that directive; frame teardown leaves that scope again. Show answer positions point on the rendered answer instead, and deliberately does not enter composer scope.
The frame opens below the directive's last line, or flush above its first line when there is no room below, so the directive stays readable beside its conversation. Only when neither side has room does the frame take the larger side and overlap the directive. Placement uses the frame's actual height, so a frame fitted shorter than its maximum stays against its directive. Width stays a fraction of the parent frame, so in a split layout the frame can extend over a neighbouring window.
At most one directive frame exists at a time, and it is dismissed explicitly
rather than on directive settlement. The frame is anchored to its directive: it
tracks the directive's screen position as the source buffer scrolls, hides once
the directive leaves the window, and returns when it scrolls back, so the
directive and its frame scroll as one thing. Tracking runs from
window-scroll-functions and window-configuration-change-hook in the source
buffer, and repositions only on an actual change, because setting a frame
position from a redisplay hook triggers redisplay again.
Displaying the same directive in its already open frame reuses it without
rebuilding, so an action that both enters composer scope and dispatches a
request does not recreate the frame between the two steps. Displaying another
directive in the shared view retargets the frame's identity, source anchor,
follow hooks, filter, and close restoration before display. Teardown runs from
delete-frame-functions, so dismissing the frame, deleting it with ordinary
frame commands, or exiting Emacs all restore point and leave the composer
scope.
The frame may filter the displayed transcript to its own directive's turns.
Filtering marks the turns of every other directive and of ordinary chat with an
invisible text property; content before the first turn, such as the header,
is never hidden. The invisibility spec that implements this is buffer-local
rather than window-local, so filtering is skipped whenever the view buffer is
also displayed outside the frame, and the frame shows the full transcript
instead. Rendering re-applies the filter, so streamed turns stay hidden.
The frame binds only a filter toggle and a dismiss command, both on C-c
prefixes. Single-letter bindings are impossible because the view buffer holds an
editable composer, and C-g keeps its view meaning of aborting the request. The
directive scope hint line advertises both keys while the frame is showing.
Frame chrome¶
The frame carries its own chrome through window parameters, not buffer-local settings, because the view buffer is shared with the main view: the main view keeps its mode line and its full-width status strip. In the frame the mode line is suppressed, fringes are zero, and the header line is a condensed variant leading with directive identity, then composer scope, a filter marker, request state, model, and tool count. Session facts the parent already shows — session name, workspace root, execution target, preset — are deliberately absent, because this header has a fraction of the width.
The border is painted by setting a background on internal-border and
child-frame-border for that frame. A border width alone draws nothing: without
an explicit background the border takes the default background and is invisible.
mevedel-directive-frame-border and mevedel-directive-frame-border-inactive
distinguish whether the frame holds focus, which is how a dispatch-opened frame
that deliberately did not take focus reads as unfocused.
Frame height fits its content between mevedel-directive-frame-min-height and
mevedel-directive-frame-height, refitted on the view's throttled render
cadence rather than per streamed token. Only height is fitted; width stays as
computed from the parent frame at open, because fitting both dimensions sizes
the frame to the longest unwrapped line in the transcript and readily exceeds
the parent's width.
When the directive's source buffer is displayed in more than one window, the frame anchors to the window the user is in, preferring the selected window, then any window on the selected frame. It stays with that window afterwards, so scrolling a second window showing the same buffer neither moves the frame nor hides it.
Buffer display from the frame¶
While the frame is showing, the view buffer redirects display-buffer to the
parent frame. The frame's root window is dedicated and unsplittable and the
frame is a few lines tall, so a transient menu, a cockpit surface, a followed
file link, or the patch buffer is unusable inside it. Redirecting at the display
layer rather than per-command covers every such surface, without per-command redirects.
The redirect hands input focus to the parent along with the buffer. Callers such
as pop-to-buffer select the window the redirect returns; without moving focus
too, the selected window and the focused frame would disagree and typing would
still reach the directive frame. The redirect also re-wraps the incoming alist
as a display action, since display-buffer takes (FUNCTIONS . ALIST) and
reads a bare alist's first entry as an action function.
Frame teardown runs in two phases. delete-frame-functions hands input focus
back to the parent before the frame is deleted, because deleting a focused child
frame without moving focus first leaves no frame focused and Emacs stops
responding to the keyboard. after-delete-frame-functions then restores point
and focus again once the frame is actually gone: the window system reports focus
back asynchronously, so until Emacs processes that event the parent draws no
cursor, and it would otherwise reappear only when the next key arrives. The
second phase forces one redisplay to settle it.
The view buffer disables display-line-numbers. A transcript has no line
numbers worth counting, and they cost four columns of an already narrow frame.
Status Strip And Cockpit Routing¶
The view buffer tab line is mevedel-owned chrome. It shows session
orientation on the left as SESSION WORKSPACE-ROOT and operational
state on the right as MODE · REQUEST-STATE · MODEL · TOOL-COUNT.
When a user or directive prompt has scrolled above a window's top edge,
a separate header line below the status strip shows a one-line clickable prompt
preview. The project path and session name remain in the status strip. The
row stays reserved (empty when the original prompt header is visible), so
scrolling never shifts the transcript vertically. The preview
follows the exchange at the top of each window independently, not the buffer
point or the most recent submission. A visible prompt header does not pin
itself; scrolling to a different exchange changes the preview. The preview
is at most mevedel-view-sticky-prompt-max-width columns (80 by default; nil
uses the window width), so a long prompt does not run across a wide window. In
tight windows, it truncates to its own row's width, without taking
space from the operational controls. Its stored text is capped at 512 characters,
including an ellipsis, after filtering and whitespace normalization. This bounds
the width measurement performed on every header-line evaluation. Clicking it reveals the original prompt,
Finding the prompt above the top edge is a binary search over the view's prompt
header runs, collected once per text change: a scroll step can evaluate the
header line several times, and a backward property walk cost 0.1-0.3 ms each.
The cache follows buffer-chars-modified-tick, not the modification tick:
fontifying newly visible text while scrolling changes only properties and
would otherwise rebuild it on almost every step.
expanding a folded turn or input when necessary. It is view-only:
synthetic/model-only context is excluded, and archived segments use only their
own visible turns.
In the live segment after compaction, the row can also show the originating
prompt from an earlier segment when no retained prompt governs the window.
When compaction copies multiple prompts into the live tail, content before
their first header uses the preceding archived prompt, not the last copied
prompt. A local header anywhere in the visible window suppresses the archived
preview; once it scrolls above the window, its own local preview takes over.
Finding whether a local header is on screen checks graphical pixel visibility
(or fixed-height rows on a terminal), so it runs
only when an archived prompt exists, and its answer is kept per window until
the window's start, scrolling or size, the buffer, its invisibility spec, or the text's
layout changes: wrapping, text scale, line spacing, line numbers, tab width,
or the frame font.
The graphical measurement temporarily uses an empty, static header row:
Emacs's pixel query evaluates header formats itself, so evaluating the prompt
header there would recursively repeat the same query before its result is cached.
Header lines are evaluated on every redisplay, including the many caused by
timers and streamed output that change nothing on screen.
Clicking that preview opens the archived segment read-only at the original
prompt; [Latest] returns to the live transcript. Fresh segments started by
/clear do not carry a prompt across the boundary. The archived prompt is
view-only: it is not inserted into the compacted model context.
The session name is shortened or omitted in tight layouts so a long name
cannot hide the operational controls.
The workspace root uses Emacs path abbreviation normally, truncates to
the final directory when space is tight, and disappears before the
right-side state is dropped. Clickable parts route to session cockpit
surfaces such as top, mode, model, and tools. The request state is
plain status text. Goal status/turns and the model name are separate links:
the model always opens Model, even when a Goal is active or completed.
The view must not copy or proxy gptel's clickable
data-buffer header line; gptel-owned header controls stay in the raw
data buffer. The request state is settling while a lost turn's deferred
terminal work still owns the session. The tab line is evaluated on every
redisplay, so the strip is cached by its raw inputs: session name, root,
target identity, publication and lease state, permission and Plan mode,
directive scope, request state, model, Goal status and turns, preset, tool
count, width, display type and selected window. Each redisplay reads them
without entering the transcript buffer and formats labels only when one
changes. Entering an Org transcript buffer swaps its many buffer-local
variables; the five lookups that did so cost 26 µs per redisplay, and
reading the bindings directly costs under 2 µs with no string allocation.
The session cockpit is the normal control surface from the view. It resolves
the live view/data pair once and routes each action to the owner buffer. The
explicit g gptel menu cockpit row is the advanced bridge into gptel's menu
from the paired data buffer. One bridge restores at a time: the pending
view, its data buffer, and the window state to return to are single, because
transient-post-exit-hook runs in an arbitrary buffer and cannot find them
buffer-locally. Opening the bridge from a second view therefore hands the
first view its windows back before taking that state over, so an abandoned
bridge is not silently discarded along with the window state it was still
waiting to restore.
Its header is one identity line — session, permission mode, request state,
workspace root, and execution target — followed by a warning-face alert line
only when session state is off-nominal: an unready target, an unavailable
sandbox, a lease that is neither owned nor local, or a pending publication.
The complete target and durability state is the i Session info panel, so
nominal state costs the cockpit no lines. Commands are grouped as
Conversation, History, Configure, Workspace (Executions, Worktree, Artifacts,
Memory), Collaborate (S Share…, n New shared item, Control) and Extend
(Tools, Skills, Plugins). A frame at least 170 columns wide shows the six
groups side by side; from 100 columns they form two rows of three, and
narrower frames three rows of two. Data view, Session info, Usage, gptel menu
and Help share one row beneath them, split in two below 72 columns. Every
layout binds the same keys. Row values longer than 24 columns end in an
ellipsis; their own surface shows them whole. Nominal values such as none,
writable, not shared or an empty count are dimmed, so warnings and
meaningful counts stand out.
S Share… opens the Share surface, the menu form of /collab. Its header
shows whether this session and the workspace lobby are shared. It shows this
session's links or starts sharing it with /collab's disclosure, stops this
session's share only, reports status, and starts, stops or rotates the lobby.
Rotating asks first, because every earlier lobby link stops working. Links
appear only in the share panel with its QR code.
Below, Active everywhere lists up to nine shares of this Emacs, this
session's and workspace's first: session rooms with their distinct guests,
running lobbies with their tabs, and lobbies recorded to restart that are not
running. A digit opens that share's surface: o shows its session, s its
links, x stops it, and for a recorded lobby R restarts it and x forgets
it. Each action re-checks the share, so one that stopped meanwhile is refused
rather than started again. a Choose any share… offers the complete list,
including shares beyond the nine quick-access rows, with full names and
workspace paths. X Stop all… asks once, with the counts, before stopping
every share and lobby.
The top row's shared · N guests gains · +N more when shares exist
elsewhere. Like the rows, it is read from memory and the small lobby record:
opening the menu never loads collaboration, connects, or reads credentials.
Information panels use read-only sections with theme-inheriting headings,
aligned labels, and visual wrapping. RET on a heading folds its body; TAB
and S-TAB visit headings and links, n/p move between sections, and q
returns to the owner. Long supporting records can start folded; their complete
text remains available. g refreshes reports with a live refresh source.
U Usage in the cockpit and /usage share one disposable report per
originating session. The report opens with a loading state and retrieves the
current backend's account-wide quotas on opening or g, without polling. It
shows the provider, available account/plan details and last successful retrieval
time, and explains that activity outside mevedel counts toward these quotas.
Missing fields are unavailable, never inferred as zero. A failed refresh retains
previous successful data only with a STALE label and its original timestamp.
Changing backends clears that data. Refresh supersedes any pending retrieval;
q or killing the buffer cancels it, and late callbacks cannot change the report.
The composer, transcript, Goal state, and model accounting stay untouched.
Codex OAuth uses gptel's token restoration, account selection and authentication
header. A stale token starts mevedel's asynchronous renewal instead of a request;
the report then asks for a refresh once renewal finishes, or for
gptel-openai-oauth-login when no renewal is possible. Quota retrieval never
enters gptel's synchronous login flow. The quota HTTP request to
https://chatgpt.com/backend-api/wham/usage is asynchronous with a 30-second
timeout. Primary, secondary and additional quota buckets use the HTTP schema's
returned window durations, utilization and reset times; plan and credits appear
when available. Known utilization also has a 20-cell progress bar beneath each
window; unavailable utilization has no bar. Errors omit credentials and raw
response bodies.
Claude Code uses a fresh isolated ACP inspection conversation with no retained
coding history, tools, or MCP servers. Its launch enables native slash commands
while retaining the launcher's environment and external-settings isolation.
Startup retains session command advertisements even before session creation
completes. Inspection sends exact /usage only after usage is advertised;
missing support or the existing startup timeout fails without prompting.
The inspection response has a separate 30-second timeout. Only recognized native
subscription headers and quota sections are retained; temporary-session cost,
token and behavioral summaries are omitted. Unrecognized output is an error.
Native formatting is preserved; omitted fields, including extra-usage credits in
the inspected adapter, are unavailable. There is no direct SDK/OAuth fallback.
Dedicated inspectors retain their own action keys, including journal retry and discard, rewind file diffs, and sharing-tier cycling.
Memory details use a section index beside the reader, or above it in narrow
windows. n/p switch the selected section. Selection and source-relative reading
position survive refresh for the same item. Closing the inspector closes both
panes. Opening details retains table columns, sorting, selection and actions.
These buffers are disposable presentation state; they do not alter the composer
or the underlying source records.
Its History group owns mutation of the transcript:
f/Farm a Conversation Fork or Worktree Fork at the settled assistant response under point.Rconfirms a true in-place Rewind to that response.Bswitches conversation variants at that response.
N opens the Navigate submenu, whose entries all stay open so repeated motion
needs one menu. It only inspects, and never changes session state:
[/]project the previous or next persisted session segment in the same view.gopens the segment picker, including missing or unreadable archived segments.copens the chat picker: the runs of segments/clearseparates, by title.n/pmove through rendered displays.C-n/C-pmove through user queries.TABtoggles the section at point.
Those are the keys the view buffer's own keymap binds, so the cockpit teaches one vocabulary rather than two.
When the session has earlier chats, a read-only line below the live header opens the chat picker. Only its visible label is clickable and highlighted on hover; its trailing line break and blank separator remain protected chrome, not part of the hover target. Insertion at the label's leading boundary is protected too, without making the live composer read-only.
An archived segment is a read-only projection, not a second live session.
Its banner shows the segment number and a clickable [Latest] action. The
live composer draft is hidden and preserved. Live requests continue updating
status, interactions, and progress, but transcript redraws do not replace the
archived projection. Send, follow-up, Compact, Review, Verify, and slash
commands require returning to the latest segment. Fork and Rewind still target
the settled assistant response at point; arming a Fork temporarily reveals the
composer, while cancelling hides it and stays on the archived segment.
mevedel-view-segments.el owns this ephemeral inspection lifecycle and consumes
the segment descriptors and verified bytes from mevedel-session-artifacts.el;
it does not implement another storage or transcript parser.
Arming a Fork adds a temporary interaction row naming the selected assistant turn and Fork type, and dims the rendered turns after that response, which the child will not inherit. The row's render hook reapplies the dim after redraws. It focuses the existing composer, and cancellation removes the row and the dim while preserving the draft. The next accepted child prompt publishes a new session. Conversation Fork discloses that current files may be newer than its conversation and remain shared; Worktree Fork discloses its linked worktree and best-effort historical-file restoration. Provenance is derived from durable child state and delivered as a sparse reminder retained in the transcript; the one-time worktree restore report rides the pending reminder FIFO onto the child's first request. Both surface in the view through the hidden injection record's grouped reminder row.
At a shared fork point, the assistant header renders a variant button in both expanded and folded states. One alternative opens directly; several open a Source-first chooser with identity, working-directory, sharing, branch, recovery, and latest-prompt context. Switching uses ordinary session restore, rerenders source-backed history, and positions the target at the exact stable fork point. Each view retains its own composer draft and working directory.
Rendering the buttons reuses the process's last live session enumeration rather than re-listing the workspace on every redraw — a live listing costs several target round trips per persisted session. Any live enumeration (the session picker, resume, fork creation, or activating a variant button) refreshes what the decoration shows, so a variant created by another client appears after the next such action rather than instantly.
Ephemeral /btw Side Conversations¶
/btw [PROMPT] opens one multi-turn side conversation owned by the current
root session. It may be invoked while the root response is streaming. The side
receives an invocation-time copy of the effective post-compaction context and
request configuration. Additional gptel text and media context is materialized
at that point rather than retaining live files, buffers, or overlays. Fresh
@file, @ref, and @mcp mentions are still resolved for each accepted side
prompt. For an active root turn, the copy ends after the accepted user prompt
plus any complete assistant/tool material; partial text and unfinished tool
calls are omitted. A hidden model-visible reminder marks that parent turn
incomplete and reference-only. Later root activity is never synchronized or
merged. Synchronous and callback-style gptel context formatters are both
materialized before the side accepts input.
The inherited context stays in the side data buffer for gptel but is below the
view's projection boundary, so the side opens with only its origin header and
new turns visible. The side has its own transient session, request lifecycle,
permission queue, stream rendering, and composer. It has no session/input
history, persistence, compaction, queued follow-ups, slash commands, skills,
Goal/Plan/task state, or ordinary session hooks. C-c RET sends only while the
side is idle and C-c C-k aborts the current side response without closing the
conversation. Aborting appends the same structural incomplete boundary so a
follow-up cannot mistake partial prose for a settled answer. Closing the side
discards it; closing the parent also closes its side. Neither operation rolls
back already approved workspace effects.
A parent owns at most one side. Bare /btw focuses it; /btw PROMPT submits
only when its composer is empty and no side response is active. Refused inline
delivery preserves both composers and their points. Side redraws use the same
draft-preserving view machinery as root redraws. /btw requires an accepted
parent prompt and is available only from the live root chat or Plan composer;
directive, historical, agent, and side scopes cannot create one.
The status zone shows one background work panel for everything that runs
beside the conversation: active agents, yielded Bash commands, and Monitors.
A finished item stays in place for ten seconds
(mevedel-view-execution-grace-seconds) as an inert ✓ Completed (or
✓ Finished for an agent) line, so a glance shows what just ended; its
transcript notice remains the durable record. The glyph and state label use
the theme's success or error face for a known successful or failed outcome;
explicit stops use the warning face, and unknown outcomes stay subdued until
terminal facts arrive. Successful command outcomes include expected no-match,
different, and false results, not just exit zero. Names and metadata stay dim.
Point in any focused window showing the panel protects finished lines from
removal. Mouse-only protection lasts at most 60 seconds per observed visit;
polling, motion within the panel and status updates do not renew it. Observed
exit, panel disappearance or frame focus loss ends that visit. Both protections
require a visible frame whose top-level frame has focus; terminal frames may
also protect rows when their window system cannot report focus. Unfocused
frames do not retain rows merely because a pointer is parked on them.
While protected, the panel also keeps exactly the rows it showed, so the row
being aimed at cannot shift: new work, including a new child agent, is only
counted in the rule and [Show all N]. In-place updates still apply.
Losing protection, including mouse-lease expiry, restarts the ordinary
ten-second grace period. A timer runs only while finished lines are held, to
notice protection changes and grace periods ending; the layout then recomputes
in one step. Thus a parked mouse alone cannot retain old completions forever.
Collapsing a retained finished inspector leaves its completed row with a fresh
ten-second grace period. Time spent expanded does not count, and an explicitly
expanded execution inspector stays open independently of passive protection.
Every row, finished line, preview line and rule fits one screen row of the
narrowest window showing the view (mevedel-view-zone-width, leaving one
column for a fringeless window's continuation glyph). A row's name gives way
first, down to twenty columns, then its metadata is cut. The panel sits above
the anchored composer, so a wrapped row changed the panel's height whenever
its text changed length. At 100 columns, a long agent row occupied two rows
and shrank the panel by one when it became a one-row finished line. Expanded
controls pack whole buttons into explicit rows, shortening a label only when
it cannot fit alone; all actions stay reachable. This may take an extra row
in a narrow window rather than wrapping a button label. Completion reserves
the same control-row budget: the execution's Stop slot becomes inert space,
and a held agent's controls become a finished notice padded to the live row
count. Model detail lines fit identically in live and held states. When that
width changes, the zone runs mevedel-view-zone-width-change-functions
once with the view current. The status zone re-renders from it, so a split
window does not keep stale wrapped rules until an unrelated redraw changes
their height. The interaction zone rebuilds from it too, re-fitting its
separator and queue-backed cards, and pending Ask forms lay themselves out
again for the new width. An Ask form wraps to the smallest text capacity of
the windows showing the view: each window contributes its body width, less the
continuation column when that window has no right fringe (a terminal, or
overflow-newline-into-fringe off), so a wider fringed window never grants a
narrower fringeless one its last column. With no window the selected window's
width, fringeless, applies.
─── background work · 1 blocked · 2 agents · 1 command · 1 monitor [-] ──
▸ Blocked agent /root/reviewer · 1m 05s · waiting for permission
▸ Running agent /root/counter · 35s · opus:medium
▸ Running bash Run test suite · 14m 07s
▸ Watching monitor Server log errors · 2m 10s · 3 events · until 08:40
[Manage]
The rule counts blocked agents (in a warning face), agents, commands and
monitors, and ends with the panel's [-]/[+] collapse toggle; a collapsed
panel keeps only the rule. Rows share one grammar: state, kind, name, then
metadata. Rows are in stable start order, newest at the top: new work appears at
the edge farthest from the composer, and the composer is anchored on its screen
row (see Composer anchoring), so rows already shown do not
move. State changes, including blocking, update a row in place; blocked agents
are marked by the warning face and the rule's count, not by reordering. An agent
and its descendants form one unit ordered by the top-level agent, children
indented under their parent in path order. Equal unit start times put agent
trees before executions, then sort lexicographically by agent root path or by
execution owner path and ID, ascending. These identity tiebreaks do not change
when a row finishes or is retained. A Bash row shows the
model-written description when present, else the command; a row owned by an
agent names that owner. The footer progress line shows only the root turn's
own activity and elapsed time; it never repeats these counts.
Background rows show elapsed wall time with the same compact whole-second
format as inline tool rows (14m 07s, or 1h 02m). Agents measure their current
invocation rather than their retained lifetime. The existing view metadata
timer updates only visible, attended live rows, including quiet Monitors and
agents while the root is idle or awaiting input. It uses cached row facts and
re-fits only changed rows, without enumerating work, rebuilding status or
redrawing the transcript. Hidden/collapsed rows do not keep that timer awake.
Finished rows and retained completed inspectors keep their final duration
instead of continuing to count during inspection or the removal grace period.
Click, RET, or TAB anywhere on a row, including its indentation or line end,
expands it in place; activating it again collapses it. TAB in an expanded
item's details also toggles that item, never one of its action buttons. Only
TAB on the header or the header's explicit [-]/[+] control toggles the
whole panel. TAB on the footer or blank panel space does nothing.
A first primary click on a background-work row or control in an unselected Emacs
window only selects that window and places point. Clicking again in the
selected window activates it; no timed double-click is required. Dragging in
the selected window retains normal text selection.
Only one row is expanded at a time. An expanded agent row shows its provider,
model and effort with Open transcript, Show in conversation (the Agent
call that started it), and Interrupt, which interrupts the agent's current
turn and keeps it retained. When an expanded agent finishes, its row becomes a
finished line and its details stay as inert dimmed lines of the same height,
with Agent finished. in place of the actions, until the row's grace period
ends. Before this, the panel lost four rows under a pointer aimed at
[Interrupt]. An expanded command or Monitor row shows a bounded output
preview.
The preview is not another transcript or process owner: it projects execution
snapshots and passive progress/terminal events without consuming model output.
It is always a header and exactly three lines: the last three output lines,
each cut to one screen row, with blank lines when there are fewer. An open
inspector therefore keeps one height from no output through completion, and an
output-limit warning joins the header instead of adding a line. Previously the
preview grew from one to three lines, wrapping at 100 columns, as output
arrived, and the selected row moved up four rows during one command. The
preview labels its output bounds and distinguishes no output yet from
unavailable output. An expanded execution stays visible, in its place among
the rows, after completion until collapsed or replaced, so settlement does not
close or move something the user is reading.
The expanded execution has explicit Full output, Show in conversation, and live-only Stop actions. Full output opens a read-only inspection buffer; Show in conversation reveals the original tool row, opening the owning agent's transcript when necessary. Settled agent views finish their scheduled projection before locating the row, so an idle agent is as navigable as a running one. The captured tool ID includes nested ToolCall children.
The panel lists up to five rows plus the expanded row when needed; retained
finished rows also stay visible throughout their grace period. Its last
row holds the explicit controls: [Show all N] (then [Show fewer]) when rows
overflow, which expands the panel in place, and [Manage], which opens the
background-work cockpit. An explicit [Show fewer] immediately applies the
normal row limit even while point or the mouse remains on the panel; selected
inspectors and retained finished rows keep their existing exceptions. The
request survives deferred status redraws. Later passive updates protect the
reduced set of rows normally, without restarting finished-row grace periods.
Blank space does nothing. Monitor expiry is a fixed
timestamp, not a new countdown timer. Existing execution events and agent
state changes refresh the panel through managed-zone reconciliation,
preserving composer text, point, selection, and windows. Quiet progress that
changes no visible row or preview text updates the selected facts without
rebuilding the status area.
The background-work cockpit ([Manage], the main cockpit's Background work
entry, or /ps) lists active agents and every live managed execution of the
session, including foreground work. k stops an execution or interrupts an
agent's turn, C-c C-c interrupts either, i sends a line to a PTY, and o
opens an agent's transcript or an execution's artifact. Elapsed sorts by
number rather than text.
Live browser collaboration¶
The browser viewer projects a shared session through an encrypted relay. The Emacs host owns session state and tool execution; each browser owns its view and, when its bearer link permits it, submits typed input and interaction answers. See Browser collaboration for sharing, link tiers, remote controls, notifications, and connection recovery.
Artifact store¶
Artifacts live in the workspace artifact store,
<workspace>/.mevedel/artifacts/, owned by mevedel-artifact-store.el.
Each artifact has a directory whose name is its stable id. Authored files
(HTML mockups, Markdown, images and assets) stay in that directory. Host
bookkeeping lives separately under .state/ID/: metadata, saved versions,
HTML artifact discussions, and whiteboard/document state (see
shared editing). The protected
subtree keeps shell commands from creating or replacing bookkeeping, while
allowing ordinary file creation and atomic replacement in authored directories.
Discovery includes ids from both subtrees, so boards and documents remain
visible after cloning a Git repository that omits their empty authored directories.
.mevedel/artifacts/
ID/
<name> ; authored artifact file and optional assets
.state/ID/
meta.el ; kind, title, primary file, created, dedicated session
state.json ; a whiteboard's or document's state
versions/ ; numbered copies plus index.el
discussions.json ; an HTML artifact's discussions, each naming its answering session
Sessions do not own artifacts; they attach to them. A session persists only
the list of attached ids (:attached-artifacts in its sidecar), and Fork and
Save As carry that list, so a fork points at the same artifacts. Deleting a
session leaves its artifacts in place.
An artifact may have one dedicated session, its own conversation for work
started outside any chat: a discussion message from the lobby, the cockpit's
c, or Conversation in the browser. It is an ordinary session, created on
first use, attached to the artifact and saved at once, and named by the
artifact's title, or its id when the title is blank. The name follows the
title: renaming a whiteboard or document renames its conversation while it is
open in this Emacs, and opening the conversation renames it when the title
changed meanwhile; a rename the session refuses, such as one another client
holds, is reported and keeps the old name. The two name each other: meta.el
records the session's id and the
session records the artifact's (:dedicated-artifact). A saved session is
hidden from the session chooser and the lobby listing and kept from expiry
only while both agree; an open one goes by its own record, so a meta.el copied outside mevedel never makes another
artifact's conversation its own, and one whose artifact is gone is an
ordinary session again. It is never the default chat buffer for directives,
and a fork or Save As of it is an ordinary chat. Deleting the artifact deletes its
dedicated session too. One open in this Emacs is closed first, ending its
room, unless a turn is still running there, which refuses the deletion. One
that cannot be deleted yet, because another client holds it or journal
capture is pending, stays as an ordinary session and expires like one.
The model writes artifacts with ordinary ApplyPatch; there is no artifact
tool. After a settled ApplyPatch, mevedel-tool-patch-apply reports its
writes to the store:
- a write into a new id directory creates the artifact; the host writes its
meta.el, with the kind from the file type and the id as its title; - a turn that wrote an artifact's primary file records one version of it when the turn settles, as for whiteboards and documents, so the version before the turn survives its many small patches; a write outside a turn records one at once;
- any write attaches the writing session.
ApplyPatch matches hunks against current content, so a write planned on an
older copy fails and the model rereads; that is the store's concurrency
check, without a lease. Bash can write an artifact's files too, but records no
version until a supported save does; the sandbox keeps it out of .state/. Only the primary file, the
first written into the id directory, is versioned; other files there (assets)
change without versions.
Versions are capped per artifact by mevedel-artifact-store-max-versions
(default 20) and mevedel-artifact-store-max-version-bytes (default 64 MiB).
The oldest are dropped first; the latest always stays. Restoring a version
copies it back as a new version.
The browser's Versions list offers read-only View previews to every link tier. File versions use the artifact panel; whiteboards render as SVG and documents as HTML from their saved state, without restoring or changing the live editor. Previews use the same 16 MiB transfer cap and fetch throttle as current artifacts. Full and owner lobby links can create New whiteboard and New document entries in the store, then open their dedicated editor rooms.
The bundled artifact skill carries the conventions (start an artifact in a
new id directory, self-contained, keep it small) and resolves the store
directory at invocation. Whether the store is committed to Git is the
project's choice; mevedel adds no ignore rule for it.
In Emacs, the artifacts cockpit (cockpit A, or M-x mevedel-artifacts from
any project buffer) lists the store by title, with each artifact's kind, attachment to
the current session, version count, modification time and size, sorting
versions and sizes by their numbers; t toggles between all and attached
artifacts. Stable ids remain in the details report and identify rows even when
titles are identical. Row labels replace title newlines and tabs with spaces;
the stored title remains unchanged. It opens an artifact locally (o, e),
attaches it to the current session (a), views or restores a version (v),
duplicates it into an independent artifact (D), opens its dedicated session
(c), and deletes it with its versions, discussions and dedicated session (d).
n (also the session menu's n New shared item, or
M-x mevedel-artifacts-new-shared-item) creates a whiteboard or document; see
shared editing.
Whiteboards and documents are store artifacts too: o opens one in its editor
in the session's room, or without a session in its dedicated session's room
(see shared editing); s saves a version of one; d
deletes it through its editing queue (see
deleting). Every change reaches the live rooms
of the workspace. Everything except opening an item works with no
room and no relay. The session menu's Artifacts row shows how many artifacts
are attached to the session out of how many the project has, as
ATTACHED/IN-PROJECT.
See Browser artifact viewing for cards, on-demand transfer, sandboxed HTML, and supported formats.
Managed-zone chrome¶
mevedel-view-zone.el owns the four fixed fragment-backed regions of
view-owned chrome. Producers submit a complete desired fragment set for a
named zone. The module owns region identity, overlay lifetime, marker
choreography during mutation, uniform composer/point/window preservation,
reconciliation, stale-region recovery, collapse, and navigation. Producers
retain their domain text and actions.
Separator rules use a common width, four columns less than the narrowest window
displaying the view, with a ten-column minimum. A rule sized for the widest
window wrapped onto three rows in a narrower split. Labels are truncated when
necessary to leave room for the rule, including in narrow windows. If truncation
would hide the background-work panel's [+]/[-] control, it replaces the
rule's leading three dashes so the control remains clickable without changing
the width.
Composer anchoring¶
Chrome above the composer (status, interactions, progress) changes height while
the user types. Unless the window visits an interaction card, a window showing
the composer keeps its screen position across zone mutations and re-renders:
growth above it scrolls the
transcript up, and shrinkage reveals more transcript, as in a bottom-anchored
terminal. While everything from the window start through the end of the draft
still fits, the start is kept and free space below the composer is used first.
When one large burst overflows that free space, restoration chooses the earlier
of the saved composer viewport and the last full page. It consumes the remaining
space instead of leaving the composer high above an empty tail.
A window scrolled away from the composer keeps its own start, so reading history
is not disturbed. Native visibility and pixel traversal account for remapped
faces, images and partial vertical scrolling. A removed logical start instead
fills the last page; a visited card keeps its row unless shrink creates new blank
pixels. See ADR 0126 for the complete per-window policy.
View buffers set scroll-conservatively to 101 and scroll-margin to
0, so a draft growing past the window bottom scrolls one line instead of
recentering. Measured in a terminal Emacs: with the previous top anchoring, the
first status row added at the window bottom recentred the composer by thirteen
rows and every later change moved it by one; anchored, it stays on its row.
A fragment is keyed by managed region identity, namespace, and id. It may also carry priority, label/body text, keymap/help text, activation metadata, navigation metadata, and a collapse key. Whole-zone reconciliation sorts by descending priority and caller order. Unknown zone names and malformed descriptors are programming errors; stale disposable UI state is rebuilt. When rendered text and all its properties already match a fully owned region, with no stale namespace text outside it, reconciliation returns without capturing or restoring window state. Property-only updates and stale-region cleanup retain the normal mutation transaction.
Fragment metadata lives in mevedel-view-zone-* text properties. Those
properties are valid for view navigation, activation, collapse, and targeted
refresh decisions, but they are UI cache only. Durable conversation state
continues to live in the data buffer and session structures. The zone module
owns managed region overlays. Remaining interaction overlays are opaque
callback handles for permission, plan, Ask, and preview flows;
they are not parallel renderers.
Zone reconciliation keeps point, mark, window points and window starts inside
a fragment on the same content. A producer tags sub-parts of a fragment body
with a mevedel-view-zone-anchor text property whose value is equal across
redraws, such as a row key. A position in tagged text keeps its anchor and its
offset within that anchor's run, so text before it can change length without
moving it. If the anchor is gone, the position returns to the fragment start
rather than to an offset that now names other text. Untagged text keeps the
fragment offset. A window start keeps its screen-row distance below the
anchor or fragment start, measured with a bounded vertical-motion, so
re-wrapped text above it does not shift the view by a row. The
background-work panel tags its header, every row, each inspector's preview
and action line, and its controls. Before this, a cursor on one execution's
row landed on another execution's [Stop] when 35 characters of output
arrived in an inspector above it. mevedel-view-zone-position-anchor and
mevedel-view-zone-anchor-position expose the same identity to other redraw
paths. Full transcript redraws use that pair too, including screen-row offsets
for managed-zone window starts, rather than replacing semantic identities with
raw fragment offsets.
mevedel-interaction-prompt.el owns the common lifecycle for those opaque
handles: exactly-once settlement, request-local cancellation, buffer-kill
cleanup, weak request-registration bookkeeping, and standard prompt framing.
Ask, permission, plan, and preview code retain their domain-specific
descriptors and outcomes.
Arriving prompts leave point in the draft or history. C-c C-j explicitly visits
a pending prompt from the invoking window. Settlement restores that window's
saved composer position only if it is still visiting the prompt. Redraws preserve
the visited control and the draft, including multiline drafts; other windows keep
their own reading positions.
Current fragment namespaces:
status:tasks, liveexecutions, andagentsstatus-zone blocks. Task and aggregate-agent disclosure state is backed by fragment collapse state. Task panels toggle withTABorRET; mouse clicks only place point. Expanded task and agent panels start their rows immediately below the separator, without an intervening blank line. Long task titles are truncated in the compact rows; the truncation marker is not a disclosure control. Every task row and status note is cut to one screen row of the narrowest window showing the view, so the panel's line cap is a row cap. The cap follows the height of the shortest frame showing the view, not of its window: a popup split once changed it from 6 to 4 lines (15 to 10 expanded), and the panel re-flowed at the next unrelated status redraw.interaction: pending-input summaries and user controls plus a non-navigatable:separatorfragment. Ask, permission, plan, preview, and pending-input callers continue to use the descriptor registry.progress: the foregroundrequestprogress row between the interaction zone and input prompt.
Source-backed transcript turns, tool summaries, request-failure disclosures,
and agent transcript handles are intentionally outside this chrome-fragment
model even when they are clickable or collapsible. They are projections of
the authoritative data buffer and keep source-coordinate disclosure state.
Provider failures are expanded by default and preserve the complete provider
message and error payload for manual retry. Bounded sample retries across all
mevedel-admitted native gptel backends show transient root/agent status
(for example, a capacity failure with retry 3/10 in 4s)
without intermediate failure blocks. Failed provisional output is rolled back
before replay; earlier completed work and the composer draft remain intact.
One final ignored request summary persists :retry-errors and
:retry-exhausted alongside the original error payload. Its existing disclosure
shows bounded sanitized diagnostics in chronological attempt order (attempt,
HTTP status, type, code, message), and labels exhaustion in both the header and
body, including the number of failed attempts. Legacy failures without these
fields keep their ordinary rendering. See bounded sample recovery.
mevedel-view-render-live-update retains completed
semantic units in the view and reparses only the mutable source-backed tail.
Standalone reasoning disclosures and grouped tool activity runs are separate
units; substantive reasoning closes the preceding tool run. Response
prose advances at the last blank-line boundary outside fenced code. Terminal
settlement calls mevedel-view-render-settle for one exact whole-turn
reconciliation. A full rerender, a new turn, or stream cancellation invalidates
the retained tail. mevedel-view-stream.el schedules live updates and owns
request-progress state and the in-flight tool calls the live render draws;
mevedel-gptel-stream-bridge.el confines the private upstream advice. Fragment
updates should not bypass the data-buffer transcript.
Running tool calls are no fragment. The live render draws them from
mevedel-view--pending-tool-calls inside the mutable tail it re-renders
anyway, so they cost no separate reconciliation. They carry
mevedel-view-pending-tool-live and no transcript source: they are never
retained as a settled unit, preserved as transcript text, or matched by
heuristic line text. There is one entry per call, not per distinct tool:
gptel's tool-call hooks carry no call id, so identical parallel calls are told
apart by a serial paired with their name/argument fingerprint, which also
keys each row's animation phase across redraws. The fingerprint is the tool
name with the non-nil arguments as key-ordered pairs, compared structurally:
gptel's hooks see the model's argument plist while pipeline events carry
every specified argument in spec order, and both must identify one call.
It shares the argument values, so a large argument is neither copied nor
truncated into a false match.
When an owning tool or agent turn used materially non-default child access,
its source-backed row includes a durable Sandbox: line directly below the
normal header in collapsed, expanded, and compact-event forms. Additional
writes, network access, unrestricted or unavailable confinement, host /proc,
refusals, and no-start outcomes use short plain-language descriptions.
Default Bubblewrap/workspace-write/isolated/fresh-proc execution and additional
read-only mounts remain silent.
The line's class decides how loudly it reads.
mevedel-execution-telemetry-sandbox-summary-class returns warning only
when something the model asked for did not run — a refusal, or a child that
never started — and only that line's ! marker takes
mevedel-view-tool-warning. The text retains mevedel-view-tool-metadata.
Every other disclosure is a note: a boundary
wider than the strictest default is the boundary the session was configured or
granted, so network access, an additional write path, an escalation, or an
unavailable sandbox record what ran without claiming a fault. Notes take the
◇ marker and mevedel-view-tool-metadata.
The line is reconstructed from hidden transcript render-data rather than view-only state.
High-level zone markers still define layout order in mevedel-view.el.
mevedel-view-interaction.el turns the interaction marker into managed
fragments; producers do not own managed overlays, preservation wrappers, or
marker insertion choreography.
Redraw invariants¶
Redraw paths must treat the composer as user-owned text. Full rerenders, interaction rebuilds, status/task rows, spinner ticks, running tool rows, and targeted agent refreshes should preserve both composer text and point while suppressing modification hooks for view-owned changes. Attention gating is a redraw path like any other: a skipped tick or flush leaves buffer text, properties, and the composer byte-identical, and the resumed render runs through the same preserving wrappers.
mevedel-view--call-preserving-window-state restores point, mark, window points,
and window starts through semantic render anchors rather than raw buffer
positions: a composer position by its input offset, a managed-fragment
position by zone namespace, fragment id, and offset, and rendered transcript
text by its mevedel-view-source data start, section role, child discriminator,
and offset into the run. Disclosure keys supply the standalone role of grouped
rows and the identity of compound children sharing source coordinates. An
ordinal distinguishes runs within that same section identity; separators and
other section roles do not count toward it. This keeps a group summary distinct
from its first tool even when retained-tail projection changes the enclosing
source of a neighboring blank line. Content hashes and temporary in-flight
tokens are excluded so growth and settlement do not change reader identity. A
raw position saved across a delete-and-re-render lands in different content
whenever lengths shift. A transcript anchor whose section no longer exists, such
as a tool row folded into its group's row, resolves to the start of the
narrowest rendered run whose source contains it. Other anchors that cannot be
resolved fall back to the clamped raw position, but a position that was in the
transcript never resolves into the editable composer: it goes to the last
transcript character instead, so the next keystroke cannot edit the draft.
A caller that replaces one known span passes it as a region: markers follow
positions outside the span, advancing over text inserted at its end, and only
positions inside it take a semantic anchor whose runs are counted from the
span's start. Markers also stay correct if the redraw removes orphaned zone
text elsewhere. Targeted tool-row refreshes, request-rail
ticks and terminal breadcrumb insertion use this, so their reader preservation
is proportional to the changed span rather than to the transcript.
Restoring an existing mark updates its marker directly, preserving activation
state without running mark-activation hooks. A redraw must not schedule editor
mode transitions for the next user command.
Restoration also runs when a writer exits with an error. Managed-zone updates
retain fragment-relative selections inside the changed zone and use advancing
markers for positions in neighboring zones. Growing or removing status rows
therefore keeps the reader in the same permission prompt below them.
Region-based directive creation uses
mevedel-view-call-preserving-window-configuration around minibuffer input.
It restores the frame layout before reapplying chat anchors on acceptance,
cancellation, or error, so a saved window configuration cannot overwrite a
background reader's position after a redraw. Redraws during the prompt still
restore the reader immediately; the source window's original scroll is also
restored when input ends.
The allocation-heavy chokepoints run with garbage collection batched
(mevedel--with-gc-batched, a direct gc-cons-threshold and
gc-cons-percentage binding with no external GC-tuning dependency): scheduled
full and incremental transcript flushes, session save transactions, exclusive
transport sections, every tool-pipeline step chain, and the whole gptel
stream filter/cleanup advice. An unattended session otherwise runs at
whatever low threshold the user's idle GC tuning left behind, paying many
long collections inside a single redraw or settlement. Both GC criteria must
be satisfied: on a large heap the percentage term can exceed the batched
absolute threshold, so raising that threshold alone may leave collection
frequency unchanged.
Direct full projection does not add that binding; its collection cadence
depends on the caller's GC settings.
Batching moves a collection to just after its section; it does not make
collections rarer. While any root or agent request runs,
mevedel-gc-cons-threshold-while-busy (64 MB by default, nil to disable)
therefore keeps gc-cons-threshold at least that high, re-applied every
second and released 30 seconds after the last request ends: settlement's
journal publication and collection allocate as much as the turn did, and one
catch-up capture collected nine times in 1.6 s at the low threshold. gcmh lowers the threshold after
its idle collection and raises it only before the next command, so a
measured unattended request on a 107 MB heap collected 94 times in ten
minutes at about 160 ms each. Each hold carries a liveness check, so an
aborted or replaced request cannot keep the threshold raised, and a value
someone else set meanwhile is left in place. Save As, fork, and rewind hold
the same threshold for their transaction: a large Save As allocated 350 MB and
collected 21 times. A batched history render parked on an unattended view
(unfocused, or shown in no window) releases its hold until it resumes, so
a view hidden mid-render does not keep the threshold raised and the
maintenance timer waking. A batch Emacs is unaffected.
The busy threshold still let a collection land in the middle of typing: a
settled root refresh allocates about 40 MB, and on a long session's heap one
collection takes about 200 ms. While any hold exists and input arrived within
the last second, mevedel-gc-cons-threshold-while-typing (256 MB by default,
nil to disable) raises the floor further; the first command after a pause
raises it at once. A collection waits until allocation exceeds both
gc-cons-threshold and gc-cons-percentage of the live heap, so each floor is
applied to both: the percentage is the floor divided by the heap live after the
last collection. The threshold alone did not hold. gcmh resets it to its own
value in pre-command-hook before every command; once deferred allocation had
passed that value, the next allocation collected before any later hook could
raise it again, and scrolling during a request stalled for 130 ms every few
seconds. When input pauses, the busy floor returns and the pending
collection runs while nobody types. Scheduled history rebuilds hold the
threshold for their whole job and five seconds after it. In the compiled
replay, with a key every 10 ms, this removed the one 57-72 ms collection every
scheduled refresh had contained: median worst key delays fell from 125 to
43 ms for a warm root refresh and from 102 to 43 ms for a cold agent refresh.
A send that fails or is interrupted before the provider starts gets no terminal callback, so that boundary settles the turn itself: it keeps the committed user turn, records a retryable failure summary while the request still carries its elapsed time, stops the turn UI, and ends the request. A later render therefore never continues a dead turn.
The terminal response boundary releases the turn -- running tool rows, spinner timer, and both in-flight markers -- whether or not the work it guards succeeds. Everything fallible runs inside that guard: stopping the progress row, the zone mutations, the request summary, and the projection. A failure there warns and falls back to one debounced full rerender, so a terminal render bug cannot leave a live spinner timer, a stale in-flight anchor, or an error that skips the post-response observers that follow.
mevedel-view-rerender is the correctness fallback and is debounced for
bursty updates. Prefer narrower refresh paths when a stable source exists:
retained-agent metadata replacements refresh their source-backed handles and
status rows. Tool-use IDs and enclosing tool blocks recover current source
bounds when replacement moved an endpoint into deleted metadata or restored
properties split the metadata from its call. Missing or stale handles fall back to full
projection; a sandbox summary patched into a generic tool row without retained-agent metadata also
uses that fallback. Managed Bash progress and terminal events
identify their row by durable tool-use ID and replace only that row. If the row
is not visible yet, the stream scheduler coalesces one incremental recovery
render.
Full rerenders rebuild the zone markers from the header, skip leading compaction summaries, and re-anchor the in-flight assistant turn. Without a valid in-flight anchor, the next incremental render can erase freshly rendered history or duplicate a preserved live tail.
The reanchor also repairs mevedel-view--data-turn-start, the data-buffer
marker that bounds what incremental renders re-render. A whole-buffer
rewrite of the data buffer — compaction application, segment rotation —
goes through erase-buffer and collapses every marker to point-min; so the renderer repairs the marker after rebuilding the projection. When the projection found an
assistant turn to anchor to, the data marker moves to that turn's data
start; otherwise it parks at the data buffer's end, since everything before
it was just rendered as settled history.
Temporary buffers used only to fontify or render view text must suppress
user major-mode hooks and local variables. Use
mevedel-view--with-render-temp-buffer rather than raw
with-temp-buffer plus mode activation. The shared mode setup and Markdown
fontification buffer live in mevedel-view-fontify.el; response cache policy
and invalidation remain in mevedel-view-render.el. Generated transcript mode
setup also suppresses persistent Org element-cache loading; ordinary user Org
buffers keep their configuration.
Assistant responses and submitted user messages are highlighted as Markdown
in the view, including expanded user-input folds. The editable composer uses
the same highlighter during redisplay, keeping markup delimiters visible while
typing. Composer highlighting copies only faces and preserves draft text,
point, undo history, and mention bindings. The data
buffer remains org-mode for gptel state, tool parsing, and persistence,
but the user-facing projection does not convert assistant Markdown to org.
Markdown view text is fontified through markdown-ts-mode, which Emacs
31.1 ships, so mevedel needs no Markdown package. There is no
markdown-mode path: markdown-mode is a third-party package, and
markdown-ts-mode covers CommonMark and most of GFM, adds LaTeX, and
highlights fenced code blocks with the embedded language's own grammar.
Its two tree-sitter grammars (markdown and markdown-inline) are not
shipped with Emacs and must be compiled locally, which needs a C toolchain.
mevedel-view--markdown-fontify-mode returns nil until
treesit-language-available-p reports both, and then view text stays plain
unfontified Markdown - never a prompt, because markdown-ts-mode calls
treesit-ensure-installed, which offers to clone and compile, and a render
must never block on that. Loading the mode registers both grammars in
treesit-language-source-alist, so M-x markdown-ts-mode-install-parsers
has a source to build from; mevedel warns once, outside batch, when the
grammars are missing.
To avoid repeated mode setup during streaming redraws,
mevedel-view--markdown-fontify-target sets up one hidden buffer once and
only the content is swapped. Nested fontification, including calls from another
view or during mode setup, uses a separate temporary buffer so it cannot replace
the outer call's text. Temporary buffers are released on completion or failure.
Invalidating an active reusable buffer retires it immediately but lets its owner
finish reading before killing it. mevedel-view--fontify-as treats
markdown-mode as the tag meaning "this body is Markdown" and routes it
there; every other :body-mode is a real major mode and still gets a
throwaway temp buffer. Fontification appends a temporary newline so the last
line is highlighted even while typing without a line ending; the returned text
excludes that newline, preserving the draft and source positions.
Markup delimiters -- heading hashes, emphasis asterisks, code-span
backticks -- are hidden by default, so **bold** reads as bold alone. They
are only made invisible: markdown-ts-mode puts an invisible property on
them while fontifying, which rides into the view as an ordinary text
property, so the text the view holds and every position it maps back to
the data buffer are unchanged. Set
mevedel-view-hide-markdown-markup to nil to see the raw delimiters.
Because the property is applied at fontification time rather than at
display time, changing the setting drops the reusable buffer and the
response and tool rendering caches and re-renders every open view.
Markdown rendering adds small view-only affordances:
- completed fenced code blocks are rewritten in the view projection as
source panels: the data buffer keeps the raw Markdown fences, while
the view strips them, inserts a clickable
LANG ⧉label (snippet ⧉for unlabeled fences), adds vertical panel padding/background, and copies only the code body. A source panel adds no left inset of its own and inherits any inset from its containing disclosure; - incomplete streaming fences stay raw until the closing fence arrives;
- supported local image references render inline when Emacs can display
images; remote portable session-artifact images are decoded only from
resolver-verified committed publication bytes rather than owned staged
writes or the mutable fixed-path cache, while PID-lock sessions read their
authoritative fixed logical file. Click a preview or press
RETon it to open the image in an Emacs file buffer, using the same verified committed publication for remote session images even when newer staged writes exist.mevedel-view-inline-image-max-widthtakes a fixed pixel width (default 300) or a float in (0, 1] meaning a fraction of the displaying window's pixel width; ratio-sized images retain their path, ratio, and measured width and are re-scaled by the realignment job below; - canonical pipe tables (two or more consecutive
|...|rows outside fenced blocks and linkify-exempt text) are rendered bymevedel-view-table.elas aligned box-drawing rows after the link and path passes, so buttons and faces inside cells survive. Columns wider than 90% of the usable window width (window columns minus anyline-prefixorwrap-prefixinset) shrink proportionally toward their longest-word minima and wrap their cells. If those words cannot fit, they hard-wrap, but each column retains enough width for its widest indivisible glyph or composed cluster. An impossibly narrow window can therefore exceed the width target rather than misalign borders around a wide glyph. Graphical layout measures all text, including header and stripe faces, against the displaying window. Column units use the box-drawing glyph width, and padding uses exact pixel widths so proportional fonts, bold headers and mixed fonts keep their borders aligned. Terminal layout usesstring-width. Font metrics are scoped to one table render rather than retained across font changes. The rendered region retains the canonical Markdown source and the layout's window pixel width as text properties and carries the view's own source/read-only/turn properties across the rewrite; - rendered
@filementions, Markdown file links, and bare file paths are clickable open-file buttons, including:LINE,:L<line>,:#L<line>, comma-separated line lists, and#L<line>targets. A path inside the active remote session opens resolver-verified published bytes at its logical path; the disposable fixed-path cache is never used as evidence that the artifact exists. One projection resolves each distinct path once, and a bare path is considered for inline image display only when it has an image extension. - resource addresses, bare, in inline code, or as a Markdown link target, are
buttons. A click resolves them; a redraw never does.
shared://ID[/PART]opens the whiteboard or document in the session's room, likeoin the artifacts cockpit;agent://root/PATHandhistory://root/PATHopen that agent's transcript;work://andmemory://files open for editing; andartifact://,mevedel://andskill://files open read-only. An address with no backing file, such as a listing, reports that there is nothing to open.mcp://addresses stay text.
Markdown links, local images, paths, and fenced source-panel projection are
isolated in mevedel-view-markdown.el, deferred target path verification in
mevedel-view-path.el (remote paths wait for an idle transport, and the paths
pending on one target share one existence command); the table engine lives in
mevedel-view-table.el, adapted from agent-shell's renderer with
attribution.
Normal views first show pipe tables with their canonical source retained as text properties. The existing Markdown realignment timer formats one visible pending or width-stale table after 250 ms idle. Off-screen and folded tables wait until scrolling or unfolding makes them visible. A partially visible table is still rendered as a whole; one very large table can exceed the idle delay. There is no background queue sorted by cursor distance. Interactive visibility uses Emacs's current window end, reusing completed redisplay and accounting for variable-height rows. Changed window contents or scroll positions request an updated boundary; batch rendering, which has no glyph matrices, uses its line-motion approximation. That boundary costs about a millisecond of display simulation, and scrolling asks on every step. The check therefore first compares the window start with the end of the last unformatted table, cached per text change and width (table rendering, which can update a width in place, resets it): when no such table reaches the window, it answers without the window end. Tables in an unfinished streamed response stay raw even if that timer runs; terminal reconciliation replaces the live projection with a pending table, which the same idle timer then formats once. This avoids relaying out a table on each stream update or alternating between its raw and rendered forms.
mevedel-view-mode schedules that job from decoration, window size/buffer
changes, scrolling, and commands. A current image width and no visible stale
table require no timer. Each buffer owns one cancellable timer;
killing the buffer or changing its major mode cancels pending work. Pending
input postpones the callback. Consecutive tables receive separate idle passes,
with another 250 ms between them. The callback also realigns ratio-sized images.
The callback enters normal projection mutation ownership before locating work,
so queued work discovers current positions and the changed window's width when
it actually runs. A formatted table or resized image usually changes height:
the callback keeps each visible composer on its screen row, so the change
scrolls the transcript above it, and gives a window left past a shortened end
its last page back. Updates stay off the undo list and preserve the modified flag,
data buffer, composer draft, point, mark, other markers, overlays, and displayed
window anchors. Table replacement uses cell identity and unwrapped character
offsets instead of whole-table text diffing. This distinguishes repeated words
when wrapping changes, including markers held by callers' save-excursion.
Removed whitespace attaches to the next surviving character, or the cell end;
padding attaches to its cell edge. Separator interiors clamp before their next
junction. Row boundaries retain their logical row, clamping physical
continuation lines that disappear during reflow. Identical text only refreshes
display properties.
A buffer shown simultaneously in windows of different widths holds one layout:
the most recently realigned window wins. Staleness is keyed on window pixel
width, so a glyph-width-only change such as text-scale-adjust does not trigger
reflow until the window width changes.
Copying from a view is contract-bound to canonical Markdown:
mevedel-view-mode sets filter-buffer-substring-function so any
copied or killed region overlapping a pending or rendered table yields the table's
complete pipe-table source spliced into the surrounding text, never
box-drawing glyphs. The fenced code-block copy button still copies the
raw code body.
Audit disclosure formatting and toggling live in mevedel-view-audit.el;
mevedel-view-disclosure.el owns its shared source-backed toggle state, and
mevedel-view-render.el retains the surrounding turn projection. Each
tool-attached hook audit uses its own transcript span, so audits attached to
one tool retain independent collapse state across rerenders. Each audit is
drawn in that remembered state, so a streamed update never has to toggle it
afterward.
Read-tool syntax selection keeps a bounded per-buffer path cache. Its rule
snapshot includes copied regular expressions, so replacing or mutating
auto-mode-alist invalidates cached selections, including misses.
Tool-rendering caches are disposable UI caches, not just text caches.
A complete tool block followed by its merged hook audits is cached under the
whole span's key; only an incomplete or partially covered block is reparsed.
Cache keys must include session-side state that changes visible
headers/status — currently permission-queue origins and pending plan
approval — and collapsed-header cache entries should omit large bodies
so expansion can recompute body content when needed. The collapsed projection
also requests summary-only renderer work: Bash skips output cleanup and prefix
construction, and ToolCall skips returned-value formatting and nested-row
construction. Status and warning classification still run. Live expanded Bash
output, explicitly expanded disclosures, and browser projection retain their
complete bodies. This defers body formatting, not transcript or metadata decoding.
Agent registry
activity is deliberately excluded from the key: agent handle status
reaches a rendering only through render-data blocks patched into the
transcript text, which source-change tracking already invalidates, while
registry activity changes on every agent tick and would defeat the
cache exactly during agent runs. A new live-state dependency must
either ride a text patch or invalidate the affected tool-rendering entries at
its mutation point. Progress row refreshes invalidate overlapping source spans
only, preserving cached renderings of unrelated completed calls.
Complete tool keys use bounded, source-buffer-local character revisions rather
than copying and hashing payloads on every redraw. Before/after-change hooks
retain ranges preceding an edit and invalidate the affected suffix. A character
tick mismatch or missing observer discards all identities, including after edits
made with hooks inhibited. Partial spans depending on enclosing text are not
cached under their own range alone. Keys also inspect the source's gptel,
render-data, and audit property intervals directly, independently of character
revisions; unrelated fontification properties do not invalidate them. A matching
revision-and-provenance key skips payload copying, tool-call parsing, structural
recovery, and request-failure decoding. Restoring trust can
expose a request failure without changing text, so a text-only hit is insufficient.
Collapsed activity groups retain tool names with their cached headers rather
than reparsing hidden arguments and results to count them. A settled direct
ToolCall caches the recognized specialist's displayed name, so a direct Eval
groups as "evaluated 1 form" rather than as a generic ToolCall. This changes
presentation only: its canonical ToolCall child, provider/source identity,
envelope IDs and audits remain unchanged. Composed or live programs, malformed
audits and failed envelopes retain ToolCall labeling. Expanding a group, or
retaining an already expanded child, recovers its complete child data.
The tool-call reader uses offsets into the existing source string instead of
copying a large result merely to read its leading call form.
The render-data reader likewise reads within explicit string bounds instead of
copying and trimming the serialized payload. A cold complete tool reuses its
initial source copy during structural recovery; partial spans still recover the
enclosing block. These reduce first-arrival allocation without deferring decoding
or changing metadata validation and ownership checks.
Scheduled projections show a non-toggleable Tool: preparing result... row for
uncached collapsed tool spans of at least 256 Ki characters. The row indicates
pending presentation, not a successful tool outcome. Inside an activity run it
stays a member of the run's group, shown running, so publication does not
unfold or refold the group; an open group never parses the deferred span on the
main thread. One parser thread per view
processes admitted spans sequentially, waiting between the canonical parser's
stages. Checkpoints block on a condition variable released by the main-thread
preparation callback; worker waits never dispatch editor timers or process
sentinels. Registered renderers and view mutations run on the main
thread with current presentation context. Prepared payloads are temporary and are
released after publication or cancellation; collapsed caches retain summaries.
Each job validates its exact source range, character identity, trusted properties, session context and view lifetime. Unchanged source ticks can renew evicted cache identities; partial spans additionally require an unchanged whole-source tick. Source replacement, mode change and buffer death cancel owned work. Focus loss and busy transport pause preparation. Publication acquires projection ownership before validating and retiring a job; failures fall back without repeatedly admitting the same source. Active-turn results invalidate retained live units and refresh that turn. Historical results replace only their containing rendered turn while its source buffer, tick and render context still match. The local replacement reuses the turn-batch projector and preserves other pending turns; whole-turn folds retain the same context. A changed source, missing context or failed local projection uses the reader-preserving full-history fallback. Drafts, selection and disclosure state survive publication.
Explicit synchronous projections and expansion retain the ordinary parser path. Deferred preparation still decodes hidden metadata before displaying its final summary: this is not general payload-on-expansion storage. Native reader calls, string operations and shared-heap GC remain atomic costs, so staged preparation does not establish a maximum input latency.
Cache keys normalize marker positions to integers so
targeted agent refreshes and full renders share entries, and tool
block bounds are memoized per segment in a data-buffer-local table
keyed on buffer-modified-tick (property-only changes included, since
restored transcripts stamp gptel properties without character
changes). Distinct agent refreshes share one per-view queue. Each callback refreshes one
path using its current state, coalescing duplicate paths while allowing input
between rows. Killing or reinitializing the view cancels queued work; a failed
row does not strand other paths. Agent-source presence checks reuse the invocation-owned
render-data markers maintained by the live update path and never scan
the transcript.
Each structural scan first indexes candidate control-marker lines, so its existing parsers skip intervening payload text instead of repeatedly scanning it. The index selects possible positions only; the parsers retain their complete marker, nesting, boundary, and provenance checks. It expires with the scan. Structural ranges overlay the role segments in precedence order, advancing through their unchanged prefix as source positions increase and reusing untouched suffixes. A return to earlier source restarts the cursor; contained tool metadata retains the same classification. This avoids walking and copying the entire role list for each control range. Activity classification and insertion also share tool entries within that one activity render, keyed by source revision/range and session presentation state. Each caller owns its coalescing count; reuse does not carry mutable counts across callers or survive into another redraw.
Each full or live-turn projection also shares a lazy canonical tool-boundary
index and pure audit-decoding results. Boundary lookup uses binary search and
retains the existing structural-recovery fallback. A missing boundary limits
backward recovery to the gap after the preceding validated block, avoiding
repeated scans of unrelated completed history. The index is keyed by buffer, text or
property modification tick, and accessible range. Audit decoding caches valid
and invalid results by encoded text, while provenance and trust checks remain
in their callers. Both caches expire when the projection returns or fails;
nested projections receive their own caches. A scan for one record type first
decodes only the payload's leading base64 block: a record printed with its
:type first settles that type, so other payloads are skipped without being
copied or decoded, and any other head falls back to the full read. Audit-only
segment checks run over the data buffer region rather than a copy, and settle
visible text in the gaps before decoding any block.
mevedel-view-disclosure.el keys source-backed disclosure state from
data-buffer coordinates and stable source anchors, not view-buffer positions.
Rerenders should capture
and reapply collapse state, including temporary in-flight anchors that later
settle, so expanded tool/response sections do not collapse again during
live refreshes. Rebuilding the same transcript retains remembered states for
children hidden inside folded groups; an explicit transcript-source change
clears them. Only chosen states are remembered: a toggle, a state already
remembered, or a captured state that differs from its section type's default
(responses, turns and thinking open, other sections folded). Existing explicit
source-backed thinking choices still override the default; no persisted
transcript format changes. A section shown in its
default state is not remembered, so a group that once folded by default does
not later fold over a row the reader opened. Source anchors prevent unmatched states from applying to rewritten
content at the same position.
Normalized reasoning-block bounds remain marker-backed after both expansion
and collapse. Earlier background-result metadata can change length without
changing that reasoning block; its markers move with the source, allowing the
next redraw to rebase the remembered choice instead of folding it again.
Targeted tool-row refreshes likewise retain the row's complete marker-backed
source span when restored properties split off its hidden render metadata.
Before replacing that row, they capture its visible nested disclosure choices
at their current source positions. A background update therefore keeps both
the compound tool's children and their expanded/collapsed choices. Capturing
those choices is limited to the replaced row, not an additional transcript-wide
state scan.
mevedel-view-render-settle computes those keys with their durable
post-settle anchors (mevedel-view-disclosure--settling-p): it runs before
the stream clears the in-flight turn markers, and keys captured or stamped
under the temporary (in-flight) anchor there would be orphaned the moment
those markers clear. Empty-string tool ids — restored transcripts stamp
them where the live buffer had nil — never become key anchors, so a
property restore does not change a section's identity.
A user toggle deletes and re-inserts view text, which can drag the retained
live-tail view marker away from its data-buffer twin; mevedel-view-toggle-
section therefore invalidates the retained tail, sending the next update
down the non-retained path whose capture/restore preserves the toggle. The
live renderer records the complete final render unit before restoration.
Restoring an earlier disclosure may shift that unit without invalidating it.
Retention survives only when its original boundaries still enclose one complete,
uninterrupted source-property run; a split, merged, deleted or failed restoration
discards it. This avoids rebuilding earlier expanded thinking or tools on every
response chunk while preserving the full-render fallback for a rewritten tail.
A toggle that finds the in-flight marker inside the section re-anchors it at
the section start — never the end, which would leave everything above it stale
for the next incremental render.
Live-tail duplicate detection should compare literal lines while skipping volatile spinner/tool/agent rows. Avoid building one large regexp from streamed transcript text; long agent outputs can overflow Emacs regexp limits.
Layout examples¶
Idle session with no live status or queued controls:
main ~/project/ ask · idle · gpt-5.5 · 20 tools
[ask]
> draft starts here
Active request with a running tool call and pending input:
main ~/project/ ask · running · gpt-5.5 · 20 tools
You
Please inspect the view layout.
Assistant
I'll inspect the associated files.
● Read: mevedel-view.el
-- 1 pending input --------------------------------------------
Follow-ups
1. [+] Also check the docs.
Manage queue… (pauses delivery; C-c C-e)
Working... · 42s
[ask]
> editable composer draft
Active tasks, agents, and an interaction prompt:
main ~/project/ ask · running · gpt-5.5 · 20 tools
You
Implement the change.
Assistant
I'll work on the changes.
-- tasks -------------------------------------------------------
Main 1 open
- Run focused tests
Agent: verifier -- review spinner layout [running · 1 call]
─── background work · 1 agent [-] ──────────────────────────────
▸ Running agent /root/verifier · opus:high
[Manage]
-- 1 permission prompt pending --------------------------------
Allow Bash?
npx @emacs-eask/cli test ert test/test-mevedel-view.el
Waiting for input · 1m 08s
[Plan · ask]
>
Busy session showing every view-owned zone at once:
main ~/project/ ask · running · gpt-5.5 · 20 tools
You
Update the view docs and verify the spinner layout.
Assistant
I'll update the docs, run the focused checks, and ask before any risky action.
● Read: docs/view.md
● Grep: status zone
-- tasks -------------------------------------------------------
Main 2 open
- Update docs with zone mockups
- Run focused validation
Agent: explorer -- audit zone terminology [running · 3 calls]
Agent: verifier -- check spinner ordering [blocked · waiting]
─── background work · 1 blocked · 2 agents [-] ─────────────────
▸ Blocked agent /root/verifier · waiting for permission
▸ Running agent /root/explorer · opus:medium
[Manage]
-- 1 question · 1 permission · 2 pending inputs ---------------
1 Which validation should run next?
○ 1 Focused view tests
○ 2 Compile
○ 3 Full suite
○ c Custom input…
[ Submit ] 0 of 1 answered
n/p move 1-9 focus RET choose c custom C-c C-c submit q cancel
Permission request from /root/verifier
Allow Bash?
npx @emacs-eask/cli test ert test/test-mevedel-view.el
Steering
1. [+] Keep the request spinner pinned above the composer.
Follow-ups
1. [+] Also include a full mockup with agents and permissions.
Manage queue… (pauses delivery; C-c C-e)
Waiting for input · 2m 14s
[auto]
> I am drafting a follow-up while the request runs.
Input History¶
mevedel-view-history.el provides comint-style input history for the
view input zone. mevedel-view-composer.el owns the editable input boundary,
completion, prompt submission, and integration with that history ring. The
related input bindings are:
C-c RET: send while idle, or enqueue same-turn steering for the active ordinary root turnC-c TAB: send while idle, or enqueue a separate queued follow-up while the session is occupiedC-y: yank text or insert a clipboard imageC-c C-e: open the Pending Inputs cockpitC-c C-q: confirm and clear all pending inputM-p/M-n: previous / next inputM-r: search historyC-c C-l: browse historyC-c C-u: clear current inputC-a: beginning of input lineShift-TAB/<backtab>: cycleask,edits, andfull-autoC-<tab>in the composer: toggle Plan without changing the permission mode
These bindings apply only while point is in the editable composer.
History persists at the workspace level as
<workspace-root>/.mevedel/input-history.el, so new and resumed
sessions in the same project share prompt recall. When persistence is not
writable, the active ring remains available in memory. Rewind keeps the current
workspace ring and composer draft. The Lisp sidecar is printed with circle
syntax enabled so shared text-property objects remain readable. Transient read
failures leave the canonical sidecar in place for a later retry; only malformed
history is renamed aside.
The input zone installs slash command completion, $ skill completion,
and display-only skill argument hints. Root slash completion offers local
commands; root $ completion offers user-invocable skills. Both insert a
real space after a completed root name. Command argument completion is
available for commands with useful candidate sets, such as /mode and
/model. Skill hints are rendered
as a zero-width overlay at the end of the draft from argument-hint or
remaining arguments names. Their place and visibility depend only on the
draft, never on point, so moving the cursor never reflows the composer; a
motion command does no hint work at all. They are not buffer text and are
never sent to the model.
Root prompt producers share mevedel--insert-user-turn, which applies the
configured gptel separator and clears copied view, tool,
read-only, and gptel properties. Atomic mention bindings and live structural
provenance survive this cleanup; the transcript grammar restores internal
blocks' ignored properties. UI properties copied from the view must not become
model-visible transcript state. Callers retain request admission, response
markers, and view updates.
File Drag/Drop And Clipboard Images¶
Interactive view buffers install a buffer-local DND handler for local
file: URIs. Dropping regular files inserts visible @file mentions in
the composer; paths with whitespace or other token-breaking characters use
the braced @file:{...} form. Directory drops are ignored.
C-y in the composer first tries to save a clipboard image, using the
first available platform clipboard command, into
<workspace-root>/.mevedel/state/media/clipboard-YYYYmmdd-HHMMSS.png. When an
image is saved, the view inserts it as an @file mention instead of
yanking text. If no clipboard image is available, normal yank behavior
is used.
Each dropped file also records a pending exact-file grant on the session.
If the next send still contains an @file mention for that same expanded
path, the grant becomes an in-memory session-scoped Read grant for that
exact path. The grant does not create a directory rule, does not apply to
write tools, and is not persisted with the session. Clipboard image paste
uses the same pending-grant path.
Pending Input¶
In a managed root data buffer, gptel's M-RET menu action and prefix-0
gptel-send ask for steering text and submit it through the same preparation
and queue as the composer. The existing draft remains unchanged, even when it
equals the submitted text. Blank minibuffer input does nothing; edit or delete
pending entries through the Pending Inputs cockpit. These commands do not mark
raw transcript regions for later submission. A scoped directive composer or
side conversation cannot use this root steering route.
Retained-agent buffers refuse native gptel steering: use SendMessage
for their existing retained delivery path. Native gptel
reject-and-steer confirmation actions likewise direct managed sessions to
mevedel's permission feedback or composer. This keeps raw native steering out
of agent FSMs and assistant-output accumulation. Ordinary gptel buffers retain
their native commands.
Pending input is session-owned and has two independent FIFO categories.
C-c RET during an ordinary active root turn accepts same-turn steering:
preparation and UserPromptSubmit run immediately, then all steering already
present at the next model interaction boundary is inserted as durable user
transcript messages without creating extra turns. Steering submitted during
that injection waits for the following boundary. It never aborts the request.
Media mentions, such as a dropped or pasted image, steer too: the injected user
message carries the images in the provider's own format, from a temporary
context that never becomes the chat buffer's gptel-context. Root WaitAgent
uses the same steering path and wakes the wait at the next possible boundary
rather than creating a mailbox message.
A Claude Code turn takes steering the same way. It arrives with the next native tool batch, or as a further prompt of the same turn when the native prompt ends first. Steering with images stops the native prompt at its tool batch and continues the turn with a prompt carrying them. A native turn cannot wait at a held boundary, so steering still held by the cockpit or an unresolved interaction when it succeeds becomes the first follow-ups (Sessions).
A plain send refused because the workflow is occupied names the occupying
cause: a retained accepted-plan implementation hints
mevedel-retry-plan-implementation, a normal unfinished Goal hints
/goal resume, a budget-limited Goal hints /goal budget, a preconstruction
Goal handoff says to wait, and a pending plan proposal points at its approval.
Resuming a mutable session that holds an implementation retry record also
echoes the retry command; read-only inspection does not.
C-c TAB while the session is occupied accepts a queued follow-up. Each
follow-up later starts one normal user turn. Steering always has delivery
priority over follow-ups regardless of submission order; FIFO applies within
each category. While idle, both send keys perform an ordinary immediate send.
Slash commands cannot become pending input.
The interaction zone shows up to three compact previews per category. Click or
RET on an entry expands its input locally, with one entry open at a time.
Inputs already shown in full have no expansion control. An expanded row keeps
only its number, collapse control and optional directive scope in the header;
the input appears once in the body, not again as an abbreviated header preview.
Inspection does not pause delivery or clear a failure-review hold. The preview
follows the entry's stable identity through reordering and closes when the entry
leaves the visible summary; it never silently switches to a new entry occupying
the same position. Expanded text is plain, bounded to ten width-limited lines,
and explicitly labeled partial when more remains. Tabs and carriage returns
become spaces in that preview. Summaries use display-column limits, and both
collapsed and expanded previews process only bounded input prefixes rather than
rescanning an arbitrarily large queued paste on each asynchronous refresh. These
presentation limits never change the complete queued input.
Manage queue… or C-c C-e opens the Pending Inputs cockpit and pauses
automatic delivery; the link labels that consequence. Blank summary space is
inert. Use the cockpit to inspect the rest of a long input. The cockpit edits
one entry in the composer without losing an
existing draft, reorders entries within their category, converts entries
between steering and follow-up, marks and deletes selected entries, and clears
all pending input with C-c C-q. Saving an edit updates the entry in place;
cancelling restores the prior draft. Closing or killing the cockpit resumes
eligible delivery, and a kill cannot refuse the way q does, so an open entry
edit is cancelled -- its suspended draft returns to the composer -- rather than
a paused turn left parked. Opening any cockpit surface for another session
releases the surface the previous one held. Queue and recovery actions recheck
session mutation authority before
restoring reserved submission context or changing session state, so stale,
foreign, and quiescing surfaces fail closed.
After a failed turn or an interrupted queued dispatch, entries marked
Needs review require an explicit decision. Select f to convert failed
steering or requeue a failed follow-up at the follow-up tail, preserving its
attachments, guest attribution, and scope, or delete the entry. R clears the
failure pause only after both categories contain no unresolved failed entries;
closing the cockpit then resumes eligible delivery.
Permission, Ask, Plan, and other user-input overlays do not disable either queue. An unresolved interaction merely postpones steering injection and follow-up dispatch; closing the last one, including a child agent's card answered while the root session is idle, offers queued follow-ups again.
Background work also produces idle work. When a yielded root command ends on
its own, or a Monitor reports, its mailbox record wakes the
idle model (see terminal delivery in tools). Idle work runs in a
fixed order: queued follow-ups, then one background-event turn, then Goal
continuation; the turn each starts continues an active Goal when it settles
successfully, and a failed background-event turn does not wake again. A
background-event turn waits for an idle transport and then rechecks every
follow-up hold below, plus queued follow-ups and any pending permission, Ask,
or Plan interaction, so it never replaces a draft or interrupts a decision;
deleting the draft or closing the last interaction offers it again. The
failure pause holds a background-event turn only while queued steering or
follow-ups remain: once failed input is resolved, child results wake the idle
root even before R, and closing the cockpit offers them. Resuming
pending-input delivery, leaving directive planning, resolving readiness or
recovery holds, and successfully finishing manual compaction also offer held
follow-ups first, then background mail. Those release edges do not start a
Goal continuation when neither input remains. No live
root view means no wake. A hidden attribution record suppresses both the wake
heading and its trigger prompt in the view, including after rerender, archive,
and restore. The visible turn begins with its background notice lines. Its
model-facing prompt is fixed and says it is not a user message, and the
permission reviewer reads it as the latest user turn on those terms; the
reported output arrives as mailbox records, which never count as user input.
Automatic follow-up delivery also waits while the composer holds a draft, so a queued turn never replaces or clears text the user is still writing. The draft is the gate rather than a pause: the edit that leaves the composer empty or whitespace-only, whether by sending, killing, or deleting the draft, schedules delivery again. Delivery is likewise held by a running root request, a running prompt hook or skill preparation, the Pending Inputs cockpit or failed steering awaiting review, a pending Plan approval, directive planning (which admits only its own Plan input), an accepted-plan implementation retry or Goal handoff, and a paused, blocked, or budget-limited Goal that owns the next entry.
If a turn fails with undelivered steering, those entries
remain steering, become Needs review, and pause all automatic pending-input
delivery. The user must edit, delete, or recategorize the failed entries, then
resume delivery from the cockpit. Later follow-ups remain intact.
Entries retain atomically bound mention text and dropped-file grants; a grant remains active in memory only after its entry is delivered. It is not
persisted in the session sidecar.
Follow-ups run skill planning, mention expansion, and UserPromptSubmit only
when their own turn dispatches; an already prepared steering entry is not
prepared twice. Accepted input is added to ordinary workspace input history.
Agent Transcript Views¶
mevedel-view-agent.el owns transcript lookup and inspection views, live
rows and badges, and status/handle refresh. Transcript turn rendering remains
in mevedel-view-render.el.
Agent activity rows are projections of canonical tool and lifecycle events:
Started PATH, SendMessage: PATH,
InterruptAgent: PATH, and Waiting for agents. Settled waits render
WaitAgent: agents (OUTCOME); consecutive wait rows retain only the final outcome and show the
combined count. The view does not infer a second activity state from internal
storage identities or runtime tables.
An Agent with context="summary" first shows Preparing summary context....
After launch, its handle includes the summary provider/model/effort metadata
without copying summary content into the parent transcript. In the child view,
the persisted Task background block is a separate initially folded card
before the ordinary Agent Task turn.
Agent handles use TAB to expand or collapse their details. RET on the
visible agent path, or a mouse click, opens the transcript. Agent handles
and activity-row paths are clickable when a transcript entry is available.
SendMessage: PATH starts collapsed and expands to the exact sent message.
Its header reports message queued, started, or waiting for a slot from
the call's delivery render-data.
Resident retained agents show status/activity in the main view and open a
rendered read-only transcript view over their conversation buffer whether
running or idle. An open idle view begins live projection when a follow-up
turn starts. Cold and historical agents open from the saved transcript file
through mevedel-view-open-agent-transcript.
mevedel-transcript-restore.el restores only the gptel bounds/properties
needed for rendering and normalizes them through mevedel-transcript.el's
canonical grammar. Transcript views do not restore backend/tool objects or
become live agent buffers themselves.
When SubagentStart injects hook context, the parent transcript renders a
compact audit note on the Agent tool row, and the child transcript renders
the full hook-context disclosure on the child's initial prompt.
Hook Audit Display¶
Model-visible <hook-context> blocks are stripped out of the rendered
user message body so injected policy/context does not look like text the
user typed. When such context is present, the view shows a compact
disclosure:
◇ hook context added
Expanding it shows the contributing hook event names, known source, source file and plugin attribution, and injected context. When multiple hooks contribute context to the same prompt, the view renders one combined disclosure for that prompt, preserving contribution order in the expanded details. This keeps successful context injection quiet by default while still making it auditable in the transcript view.
If UserPromptSubmit blocks a root input, its context stays pending without a
visible user turn and joins the next accepted root input once. Context from
SessionStart(clear) behaves the same way. Automatic root compaction adds
SessionStart(compact) context to the already-rendered pending turn and the
rerendered transcript exposes it through the same disclosure; it does not run
the prompt hook again.
The renderer builds hook audit surfaces from visible hook audit records. Buffer directive discovery and fork-point classification scan trusted audit blocks directly, without copying the whole transcript. Range-limited scans omit partial records at either boundary and preserve the caller's narrowing. All audit readers share bounded pure decoding within the current buffer across projections, in addition to sharing decoding within one projection. Every scan still checks current provenance and positions. The memo retains at most 1024 encoded payloads totaling 4 MiB, and does not retain an individual encoded payload larger than 1 MiB. It does not grant trust to quoted or edited audit-looking text.
User and assistant turns share the same visibility filter: provider-history
boundaries and fork-point bookkeeping stay hidden, while system-reminder
disclosures retain their exact source spans. Consecutive generated reminder
blocks share one collapsed row, such as 2 system reminders; expanding it
shows their bodies in order. Responses and other intervening content keep
reminder groups separate. For context injection, it reads ordered
<hook-event name="..." ...> entries,
including optional source, file, and plugin attributes,
inside a <hook-context> block.
Tool input repair reuses the same hidden audit side channel. A committed
repair appears on the affected tool row as ◇ tool input repaired; a
tentative repair discarded because final validation failed appears as
◇ tool input repair abandoned. Expanding either disclosure shows only the
repair rule ID, argument-schema path, and before/after shape. Supplied and
repaired values never enter this metadata. Malformed records render the safe
tool input repair audit unavailable fallback without changing the tool
result. Async audit redraw follows the normal view invariant: composer text
and point, including multiline drafts beginning with >, are preserved.
Prompt rewrites from :updated-input use a separate compact disclosure
attached to the submitted user turn:
◇ hook changed prompt
Expanding it shows the hook event, any hook-provided message or reason, and the original and submitted prompt text:
◇ hook changed prompt
UserPromptSubmit
reason: normalized review request
Original prompt
review plz
Submitted prompt
Please review this file.
Tool calls blocked by PreToolUse or PermissionRequest stay visible as
normal tool attempts, with a short second line showing which hook blocked
the call and the hook-provided reason. Forced ask decisions are also
shown on the affected tool attempt. allow decisions are not rendered
unless they suppress a permission prompt that would otherwise have been
shown. A PreToolUse :updated-input rewrite is shown on the same tool
row as ◇ hook changed tool input; expanding it shows the event,
supporting message/reason, and original versus updated tool args.
PostToolUse and PostToolUseFailure context is rendered on the affected
tool result row, not the next user turn, because the hook modifies the
model-visible tool feedback. A post-tool :updated-result rewrite is
shown on the affected tool row as ◇ hook changed tool result; expanding
it shows original and updated model-visible result text.
Goal and Preset Cockpits¶
The session cockpit exposes two session-owned workflow surfaces. The Goal
surface shows the objective, status, turn count, and token accounting on one
line, and groups its keys as Lifecycle, Adjust, and Inspect. Its start, pause,
resume, and clear actions are enabled only at compatible lifecycle states. The
blocked reason, elapsed time, and accepted-plan reference are the i Goal
record info panel.
The Preset surface selects a preset buffer-locally in the owning data buffer.
Its header summarizes the preset name and how many tier and workload policies
resolve; i Model policy report opens the full table of resolved provider and
effort per tier and workload. A policy that fails to resolve is named on an
error-face alert line telling the user to fix the preset before dispatch, rather
than hiding inside the table. Presets remain configuration-as-code; the cockpit
does not author or rewrite them. The view status strip links to both surfaces.
Cockpit inspection and selection never rebuild the composer, so an active
multiline draft is retained.