turbo-editors/turbo-gopublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-go.git
git clone ssh://git@rickub.com/turbo-editors/turbo-go.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

agent-windows.md · 114 lines · 13.8 KBmarkdown Blame HistoryRaw
📦 Turbo Go 3d7798b k33g 13h ago1# Agent windows
2
3This page is about why talking to an agent is shaped the way it is. For how to do it, see [How to talk to a coding agent](../how-to/talk-to-an-agent.md); for the exact keys and file format, [Agents and ACP](../reference/acp.md).
4
5## Why a protocol rather than a provider
6
7An editor that wanted to offer a chat window had two ways to get one. It could speak to model providers directly — an HTTP client per provider, a set of API keys to store, a tool-calling loop to write, and a new one of each every time somebody wants a provider the editor has never heard of. Or it could speak one protocol to whatever program the user already trusts to do that work.
8
9The [Agent Client Protocol](https://agentclientprotocol.com) is the second. The agent is a child process; the editor sends it prompts and draws what comes back. The editor holds no API key, knows no provider, and implements no tool-calling loop — and the same code talks to `docker agent` against a local llama.cpp, to a cloud agent, or to something you wrote this afternoon.
10
11It also means the editor is not the place a new model lands. Support for one is a line in *your* agent's configuration file, which is a file this editor does not read.
12
13## Why this lives in turbo-core
14
15Turbo Go is [a command, a profile and a scanner](architecture.md); everything else is the library every Turbo editor shares. An agent window is a window, a menu, a modal dialog and a turn of the event loop — all four of which belong to `turbo-core/app`. Building it here would have meant adding a general "let an editor add a window and a menu from outside" seam to the library and then using it exactly once.
16
17So the protocol client, the conversation model and the window are `turbo-core/acp`, beside `terminal` and `filetree`, which are the same shape. What Turbo Go contributes is the starter `acp.toml` it offers to write — the one part of this that is about Go projects. Turbo Rust and Turbo Python get agent windows by writing a starter file of their own, and nothing else.
18
19## Why a window, not a panel
20
21The same reasoning the [project tree](project-tree.md) settled. A docked panel would mean the desktop growing a notion of reserved edges, and `fitInto`, the grow modes, maximising, tiling and cascading all having to respect them — a change to the foundation of the interface for one widget. As an ordinary window an agent gets `F6`, `Alt`-digits, `[x]`, `[■]` and Tile for free.
22
23It also makes "several agents at once" fall out rather than being designed: two windows are two processes and two conversations, and Tile puts a fast local model beside a careful slow one. A panel would have had to grow tabs to do that.
24
25## Why one process per window, started when the window opens
26
27An agent is a conversation, and a conversation has a beginning. Starting the process with the window means the agent's working directory, its environment and its session all belong to that window, and closing it is an unambiguous end — the same bargain [terminal windows](terminal-windows.md) make, and for the same reason: what the window holds is a running process, not unsaved work, so closing it asks nothing.
28
29The alternative — one long-lived agent multiplexed across several windows — would have meant the editor deciding which window a `session/update` belonged to, and what to do with a window whose session had gone away while the process lived on. Two processes are cheaper than that bookkeeping.
30
31## Why the permission dialog is opened from the event loop, not from the message
32
33`session/request_permission` arrives on the connection's reading goroutine, and the answer comes from a dialog the user has to look at. The reply therefore cannot be made where the request is handled, and the dialog cannot be opened there either: everything that draws belongs to the main goroutine.
34
35So the request is *recorded*, and the event loop notices it on its next turn and opens the dialog. This is the fourth time this project has reached the same conclusion — [autosave](project-settings.md), the language server's re-announcement, and the terminal's redraws are the others — and the reason is always the same: `PostEvent` is allowed to drop what does not fit, so an event may cause a turn of the loop but must never be the only thing that carries a fact.
36
37That is why the JSON-RPC layer had to learn to answer a request *later*. It is also the whole of why `jsonrpc` was extracted out of `lsp`: a language server's questions can all be answered on the spot, and an agent's cannot.
38
39## Why the agent is offered the buffer rather than the file
40
41When the agent reads a file you have open and have not saved, it is given the text you can see, not the text on disk. The alternative is an agent that reviews the version you have just moved past, which is wrong precisely when you are most likely to be asking — you changed something and want to know about the change.
42
43The cost is that the agent sees text that no other tool can see, so an answer quoting a line number may not match what `go build` says. That is accepted: the same is already true of completion, which has answered from the buffer since the editor learnt to talk to `gopls`.
44
45Writes go the same way, into the buffer, marked modified. An agent that edits a file leaves the change in front of you, undoable with `Ctrl-Z` and unsaved until you press `F2`. An agent quietly rewriting a file under a window you have open would be the worst possible version of this feature.
46
47## Why the colours are the syntax classes, and not new theme keys
48
49The [project tree](project-tree.md) needed theme keys of its own, because it would otherwise have borrowed `list.selected`, a colour chosen against a *dialog* background, and drawn its selected row in the colour underneath it. Nothing like that is true here: an agent window's body is `window.body`, which is what the syntax classes are already chosen against and already tested against for contrast.
50
51So a speaker's name is drawn in the keyword style, a thought in the comment style, a tool call in the type style, and code in whatever its own scanner says. Eleven themes therefore colour agent windows correctly without being touched, and a theme somebody wrote last year does too.
52
53What is given up is expressiveness: a theme cannot make thoughts quiet without also making comments quiet, because they are the same key. If that turns out to matter in use, `agent.*` keys can be added later — the contrast rules and the completeness test are the cost, and they are worth paying only if somebody wants the distinction.
54
55## Why the transcript is a model the window merely draws
56
57The agent sends tokens: `"I"`, `" found"`, `" agent"`, `".yaml"`. A window that appended each one to a list of lines would be a window that could not reflow, could not tell prose from a fenced code block, and could not be tested without a live agent.
58
59So the conversation is a value — `acp.Transcript` — that coalesces chunks into entries, folds each `tool_call_update` onto the `tool_call` its id matches, and hands the window a list of blocks that are either prose or code-in-a-named-language. It knows nothing about a terminal, which is what lets it be tested by calling functions and comparing values, the same organising rule `buffer`, `lsp` and `syntax` follow.
60
61It is also what makes the drawing tests deterministic. The project has been bitten before by tests that asserted on a screen while a live process wrote to it, and that hid a real fault for a whole session; a window drawn from a fixed transcript cannot race anything.
62
63## What was deliberately left out
64
65- **Session resume.** `session/load` exists, and using it would mean deciding where conversations are stored, how long they are kept, and what happens when the project moved. That is a feature in its own right.
66- **Authentication.** An agent that needs a login is told to log in with its own CLI. Storing a credential is a responsibility this editor has so far avoided entirely, and one protocol method is not a good reason to start.
67- **The terminal capability.** An agent can already have a shell through its own toolsets, as `docker agent` does. Advertising `terminal` would mean the editor running commands on the agent's behalf and owning the output — the tools menu already does that, better, for commands *you* chose.
68- **Images in prompts.** The editor has text and files to send, and a terminal to draw in.
69
70## See also
71
72- [Architecture](architecture.md) — what is here and what is in the library
73- [Terminal windows](terminal-windows.md) — the other window holding a live process
74- [Project tree](project-tree.md) — where the window-not-a-panel argument was first made
75
76## Why copying goes to two clipboards
77
78"Copy this so I can use it elsewhere" usually means *elsewhere entirely* — another window, a browser, a message to a colleague. A clipboard that only worked inside this editor would answer the smaller half of the request, and the half you were least likely to be asking about.
79
80So a copy goes to both: the editor's own, which `Shift-Ins` pastes from, and the system's, reached by asking the terminal through OSC 52. Nothing verifies the second, because there is nothing to verify — the sequence has no reply, a terminal may refuse it for security, and some need it turned on. A message promising something that did not happen would be worse than one that stays quiet, so the status bar says only how many lines were copied, which is true either way.
81
82## Why copying with nothing selected copies a whole block
83
84The thing somebody wants out of a conversation is almost always a code block. Making them select it first — six keystrokes, or a drag they have to aim — is work the editor already has the information to do for them: it laid the conversation out, so it knows exactly where that block starts and ends.
85
86So the lines carry a **region**: one fenced code block, one passage of prose, one tool call's output. With nothing selected, `Ctrl-C` copies the region the cursor is on. A speaker's label and a tool call's heading are furniture and get regions of their own, which is what keeps `‣ Bob (llama.cpp)` out of a block pasted into a source file.
87
88That last part was not designed; it was found. The first version copied the label along with the code, and it was caught by copying from the real binary and reading the OSC 52 payload back off the wire.
89
90## Why the spinner is drawn from the clock
91
92An agent thinking for twenty seconds sends nothing at all, and a window that looked frozen would be indistinguishable from one that was. The spinner is the cheapest possible answer to "is this still working?".
93
94It is a function of the time — `Spinner(now)` — rather than a counter something increments. Nothing has to be reset when a turn begins, two windows thinking at once turn in step, and a test can assert on a frame without waiting for one, which is the same reason `editor.View` and `app.App` both take an injectable clock.
95
96Drawing from the clock means something else has to *cause* the redraw, so a session running a turn wakes the event loop at the spinner's own rate. That is allowed to be a ticker precisely because a dropped tick cannot strand anything: it asks for a turn of the loop and never carries a fact — the rule this project has now reached five times.
97
98The window's **title** deliberately does not animate. It is also what the window list and the `Alt`-digit menu show, and a name that changed eight times a second would make both of them flicker for no gain.
99
100## Why commands are a popup in the box, and not a menu
101
102An agent's commands arrive over the wire as a list — `available_commands_update` — and may change during the session. A menu built from them would have to be rebuilt on every update, would sit far from where the command is typed, and would still have to end by putting `/web ` into the box, because that is the only thing the protocol lets a client send: a command is a text prompt the agent recognises by its first word.
103
104So the list opens where the text is, on the character that starts a command, and closes when the word is complete. It is the same shape as the completion popup over a file, for the same reason: what you are choosing is what you are typing. Using `/` and `@` rather than keys of the editor's own is deliberate — they are the characters Zed uses, so an agent's own documentation is true here without a translation table.
105
106`Enter` has two meanings on the list, ordered by how finished the word is: it completes an unfinished one, and sends a finished one. The alternative — `Enter` always completes, a second `Enter` sends — costs a keystroke on every command and gains nothing, because a word that already reads exactly as a command has nothing left to complete.
107
108## Why a mention carries the file, when it can
109
110The protocol offers two ways to name a file in a prompt: a `resource_link`, which is a URI the agent fetches for itself, and an embedded `resource`, which is the URI *and the text*. The specification calls the second "the preferred way to include context", and the reason is the same one that makes `fs/read_text_file` answer from the buffer: the editor knows things about the file that the disk does not. An agent following a link to a file you have edited and not saved reads the version you have just moved past, which is wrong precisely when you are most likely to be asking.
111
112So the editor sends the text when the agent declared `promptCapabilities.embeddedContext`, read through the same path `fs/read_text_file` uses, and a link otherwise — never nothing. A file that cannot be read goes as a link too, so the agent is at least told which file was meant.
113
114The mention replaces the name in the text rather than travelling beside it. Sending `explain @main.go` as the text *and* an attachment would give the agent the name twice and leave it to match them; putting the block where the name was gives it the file where the sentence needs it. The conversation, on the other hand, keeps the line as typed: that is what you said, and the window is a record of the conversation, not of the wire.