Handoff — 2026-09-15 — ACP agent windows, and the jsonrpc extraction
State
Done and working end to end. Branch feature/acp in both turbo-core and turbo-go, created locally, not pushed and not committed.
The feature was driven against the user's own stack — docker agent v1.139.0 talking to a llama.cpp serving JetBrains Mellum2 — by running the real turbo-go binary in a pty. What was seen: Alt-A opens a menu listing the agent from .turbo-go/acp.toml; the window opens and its rule says Enter sends once the handshake finishes; a prompt gets a streamed reply; asking for a shell command raises the permission dialog with the agent's own three options; answering it runs the tool and folds its output onto the tool line; and a ```go fence comes back coloured span by span (keywords, identifiers, strings and punctuation each their own colour, read off the wire).
Both suites green. Both quality gates PASS. Docs EN+FR done. .memory/ updated in both repositories.
In flight
Nothing.
Next steps
- Read the diff. It is large: two new packages in turbo-core, a refactor of
lsp, and theappwiring. - Use it with your hands. Everything so far was driven from a script. See "never touched by a human" below.
- Commit and publish, in this order — turbo-core first, because turbo-go cannot build against an unreleased library from a clean clone:
- turbo-core: commit, tag, release (
01-release.tag.sh,02-release.publish.sh) - turbo-go:
go get codeberg.org/turbo-editors/turbo-core@vX.Y.Z, thenGOWORK=off make check— the proof it builds without the workspace - turbo-go: commit, tag, release
- turbo-core: commit, tag, release (
- Roll out to the other editors. Each needs exactly one thing: an
acp.toml.tmplbeside its other starter files, andAgents:in itsprofile.Templates. Nothing else.turbo-rust,turbo-python,turbo-moonbit,turbo-golo,turbo-js,turbo-spectrum.
Watch out for
- Every write to an agent must leave the caller's goroutine.
Session.Cancel,Permission.AnswerandPermission.Cancelall write on a goroutine of their own. Writing blocks until the agent reads, and an agent that has stopped reading would otherwise freeze the editor on the very keystroke meant to get away from it. This was found by a test hanging, not by reasoning — twice, in the same session. If you add another call made from the event loop, do the same. net.Pipeis synchronous, and that is what makes these tests honest. A write blocks until somebody reads. Any test helper that reads on demand deadlocks against a reply made from the test goroutine; the peer injsonrpc_test.goandfakeagent_test.goreads continuously into a buffered channel for exactly this reason. Do not "simplify" it back.t.Fatalfoutside the test goroutine hangs the run instead of failing it. Cost two debugging rounds here. The peers return errors as values.Painter.Subtakes absolute coordinates; every drawing call takes local ones. The first version ofview_draw.godrew at absolute coordinates and the input box simply did not appear, with no error anywhere. The memory warned about this and it still happened — check it againstfiletree/view_draw.gowhen adding any widget.- A ui.Result is an outcome, not a button index. The permission dialog has one button per option the agent offered, so which one was pressed is recorded into a captured pointer.
ResultOKcannot say which of four. - Never back a file up with
cphere. The corruption below destroyedacp/view_events.goand the backup taken seconds earlier withcp, in one command; the file had to be rewritten from scratch. Usecat a > b. - The sandbox's filesystem corrupts
cpfor some inodes. A file reads correctly throughcat,git,gofmtandgo build, and comes out ofcpas the right number of NUL bytes. It is sticky to the inode; recreating the file at a fresh one fixes it, content unchanged.release_test.gocopies the module withcp -r, so it fails spuriously after new files are written. It is not a code defect and git stores the right bytes — verified by comparing the on-disk SHA against the blob's. Ifrelease_test.gofails withunexpected NUL in input, this is why. - The permission request arrives before the
tool_callupdate.Transcript.addToolCalltherefore has to tolerate an id it has already seen. Recorded from the real agent; the specification does not say so. contentis two shapes under one name — an object on a message chunk, an array on a tool call. It is kept asjson.RawMessageon purpose. Decoding it eagerly breaks only once an agent uses a tool, which is late.
Never touched by a human
Everything below was verified by driving the binary through a pty from a script, which is not the same as using it:
- the mouse anywhere in an agent window — the wheel, clicking between the panes
Tabbetween the transcript and the box on a real keyboard- resizing the window, or the terminal, while a turn is running
- two agent windows side by side under
Window ▸ Tile fs/read_text_file/fs/write_text_filefrom a real agent —docker agentnever calls them, because its filesystem toolset is server-side- a conversation long enough to matter: the transcript is re-wrapped every frame and nothing has been profiled
Open questions
- Should the input box keep a history? Up-arrow on an empty box currently scrolls the conversation. Nobody has asked for one yet.
- Is
agent.*in the themes worth it? The decision was to reuse the syntax styles, which costs a theme the ability to make thoughts quiet without making comments quiet. Only use will say.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|