bots-garden/sidekickspublic Fork
⑂ v1.0.0-macos
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/sidekicks.git
git clone ssh://git@rickub.com/bots-garden/sidekicks.git

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

Download ZIP
📦️ Create a release. 0a53b99 · on v1.0.0-macos · k33g · 2d ago
agents-and-events.md · 227 lines · 16.6 KBmarkdown
Blame HistoryOpen raw

Reference: agents, channels and events

Agent states

State Shown as Meaning
stopped stopped No process. The initial state of every agent.
starting starting… The process is launched and the ACP handshake (initialize) is in progress.
ready ready Running and idle.
busy busy Running and used by at least one conversation.
error error The start failed, or the process exited on its own (crash). The status carries the reason, followed by the agent's last stderr lines for a failed start.

outdated (shown as "restart to apply changes") is set on a running agent whose definition changed in the configuration; a restart applies it.

Transitions

From Event To
stopped, error start requested (▶ in the Agents panel, or first use) starting
starting handshake succeeds ready
starting handshake fails, protocol other than ACP v1, timeout (30 s) error
ready used busy
busy last user done ready
ready, busy stop requested (■), application quits, agent removed from the configuration stopped
ready unused for limits.stopIdleAgentsAfter (checked every 30 s) stopped
ready another agent must start and this one is the least recently used idle agent stopped
ready, busy the process exits on its own error (never restarted automatically)
starting stop requested stopped once the start ends

When limits.maxRunningAgents agents are running and all are busy, a start is refused with the message too many agents running: N agent(s) busy (…), the limit is N.

Stopping an agent closes its stdin, waits 5 seconds, then kills its process group (the agent and what it started, such as mm under scripts/agents.sh).

Events from Go to the interface

Event Payload Sent when
config:changed {path, valid, problems[]} The configuration file was saved and reloaded (valid or not).
agent:state {agent, state, error?} An agent changes state or its definition changes.
agent:log {agent, line} An agent writes a line on its standard error, or a sandbox command of the agent prints one (prefixed with sandbox: ).
files:dropped {target, files[]} Files are dropped from the system on an element marked data-file-drop-target; target is its id, files their absolute paths.

The Go names and types are in internal/events; the TypeScript constants and types are generated in frontend/bindings/events and frontend/bindings/…/internal/events.

Services called by the interface

Service Method Effect
AgentService List() Every agent with name, title, icon, description, state, error, outdated, runtime, canStopSandbox, canRemoveSandbox, in configuration order.
Start(name), Stop(name), Restart(name) As in the transitions above; an error is returned to the interface. A start first recreates a missing sandbox when runtime.lifecycle.create is set.
StopSandbox(name), RemoveSandbox(name) Stops the agent, then runs runtime.lifecycle.stop or remove (container or sandbox); the output goes to the agent's log.
Logs(name) The last 200 stderr lines of the agent.
ConfigService Current() path, source, created, valid, problems[], warnings[], theme, language, historyProblem.

Channels (milestones M2 and M3)

A channel holds a conversation between the user and one or several agents. A direct conversation is a channel with one member, no name, and direct set to its agent's name. Channels, members and finished messages are kept in the history of the configuration file (below); a streaming answer is kept once it ends.

Channels

Field Meaning
id sortable identifier
name, description 1 to 64 characters, unique whatever the case; empty for a direct conversation
direct the agent of a direct conversation; empty for a channel
members agent names, in the order they joined

Who answers a message

The message Answered by
/agent:command arguments (agent is a configured agent) that agent, which receives /command arguments
contains @name mentions of configured agents the mentioned agents, one after the other, in the order of the mentions; a mentioned agent that is not a member joins first
no mention the last agent who answered in the channel; with a single member, that member
no mention and nobody answered yet, several members nobody: system message no-recipient
any message in a direct conversation its agent; mentions are plain text there

A message starting with / (possibly after mentions) is a command: it is sent alone. Any other message is sent with the channel messages the agent has not seen yet (at most the last 20, each cut at 1500 characters). An agent answers one channel at a time; a channel waiting for it shows whose turn it is.

Joined files

A $path at the start of a word joins the file at path of an agent's folder (issue #53): in a direct conversation, its agent's; in a channel, the first agent mentioned with @, otherwise the first member's. A $ inside code, after another character (5$), escaped (\$) or followed by no letter ($5) is text; punctuation ending a sentence is not part of the path. A path ends at the first space.

Each agent that answers receives the files with the message (not with a slash command), as ACP content blocks chosen from what it announced:

File promptCapabilities Sent as
image (image/*) image image block (base64), with its URI
text, 1 MB at most embeddedContext resource block with the text
any other text or image — resource_link: URI file:// + the path as the agent sees it, name, type, size
neither text nor image — not sent: attachment-unavailable

A file is read when the agent's turn comes (5 MB at most); one that cannot be read gives attachment-unavailable, and the agent answers without it.

Messages

Field Meaning
id, channel, createdAt identity
agent the author of an agent message, or the agent a system message is about
role user, agent or system
parts text (Markdown), thought (reasoning), tool (a tool call), plan (plan entries)
done false while an agent message streams
queued a user message waiting for the current answers
stopReason how an agent message ended: end_turn, cancelled, max_tokens, max_turn_requests, refusal
error why an agent message ended without a stop reason (the agent's own explanation), or cancelled for a queued message dropped by Stop
code system messages: agent-joined, agent-left, no-recipient, agent-unavailable (could not start or reach it; error says why), attachment-unavailable (a joined file was not sent to agent; error names it and says why)
attachments user messages: the files joined with $path, as {agent, path} (the agent whose folder holds it). Only the reference is kept, never the content.

A tool call has id, title, kind (read, edit, delete, move, search, execute, think, fetch, other), status (pending, in_progress, completed, failed), input (compact JSON), output, locations, diffs (path, oldText, newText, newFile).

Events

Event Payload Sent when
channels:changed {channels[]} a channel is created, renamed or deleted, or its members change
message:updated {channel, message} a message is added, or finished
message:delta {channel, messageId, index, kind, text?, part?} text is appended to the part at index (text, thought), or part replaces it (tool, plan)
turn:changed {channel, busy, queued, answering} the channel starts or stops answering, or another agent's turn begins; queued messages wait
permission:requested {id, channel, agent, messageId, toolCall, options[], rejections} an agent asks to run a tool; rejections counts the refusals already given in this turn
permission:resolved {id, channel} the request was answered, or cancelled by Stop
usage:changed {channel, agent, usage: {used, size}} an agent reported its context window use (size is 0 when unknown)
commands:changed {agent, commands[]} an agent announced its slash commands (name, description, hint)

ChannelService

Method Effect
List() Every channel and direct conversation, oldest first.
Create(name, description) Creates a channel. A taken or empty name is an error.
Rename(id, name, description) Renames a channel (not a direct conversation).
Delete(id) Stops its answers, then deletes the channel and its whole history.
AddMember(id, agent), RemoveMember(id, agent) Invites or removes an agent; the channel shows agent-joined or agent-left.
Direct(agent) The direct conversation with agent, created the first time.
Send(id, text) Adds a user message and returns its id; the agents that answer are started if needed; the message is queued if the channel is answering. Empty text and unknown channels are errors. Each $path joins a file of an agent's folder (see Joined files); more than 10 is an error.
Cancel(id) Stops the current answer (session/cancel), the agents still to answer and the queued messages; answers pending permission requests "cancelled".
AnswerPermission(id, optionId) Answers a pending request with one of its options.
Conversation(id) channel, messages (the last 100), more, busy, queued, answering, usage{agent: …}, permissions.
Older(id, beforeId) {messages, more}: the 100 messages before beforeId.
Commands(agent) The commands the agent announced last, kept from one run to the next; empty if it never announced any.
Prepare(id) Starts the members of the channel that never announced their commands and opens their session for it (no model is loaded); returns once the sessions are open, the commands arrive as commands:changed. A member answering elsewhere is skipped. The session serves the member's first answer, which is still told the channel's past.

History

Location <user config dir>/sidekicks/data/<16 hex characters>/: one Pebble database per configuration file (the hash of its absolute path). <user config dir> is ~/Library/Application Support on macOS, $XDG_CONFIG_HOME or ~/.config on Linux.
Kept channels, members (with each agent's ACP session id and the last message it saw), finished messages, the last commands of each agent
Not kept pending permission requests, queued messages, an answer interrupted by quitting
Unavailable if the database cannot be opened (another Sidekicks uses the same file), the history lives in memory until Sidekicks quits, and ConfigService.Current().historyProblem says why
After a restart an agent that supports session/load resumes its session; otherwise its first prompt in a channel carries the last messages of that channel

Workspaces (milestone M4)

An agent's workspace is its workspace folder, reached according to runtime.type.

Runtime Files Changes followed Terminal Deletion
local directly file system notifications (not inside .git, node_modules), half a second after the last change $SHELL -l (else bash, else sh) in a pseudo-terminal to the system trash (~/.Trash, or the freedesktop.org trash); for good if the trash is on another disk
container docker exec in workspacePath (default: the same path as workspace) dates and sizes compared every 2 s docker exec -it: bash if present, else sh for good
sbx sbx exec in workspacePath (default: the same path as workspace, where sbx mounts the host folder) dates and sizes compared every 2 s sbx exec -it: bash if present, else sh for good

Paths are relative to the folder and use /. A path leaving the folder (.., absolute, a symbolic link pointing out) is refused. The folder itself cannot be deleted.

Files

Field Meaning
name, path the entry's name, and its path from the folder
dir a folder
size, modTime size in bytes, last change

A file opened for editing has path, content, version; version identifies the content.

WorkspaceService

Method Effect
Info(agent) {location, problem}: where the folder is (local · /path, container name · /path), or why it cannot be shown.
List(agent, dir) The entries of dir ("" is the folder), folders first, then by name.
Read(agent, path) A text file of at most 1 MB; a binary or larger file is an error.
Save(agent, path, content, version) Writes the file if it still has version; otherwise {conflict: true} and nothing is written. An empty version overwrites. The file is written next to its place and moved over it.
Create(agent, dir, name) Creates an empty file; refused if the name exists or contains /.
Remove(agent, path) Deletes a file or a folder with its content (see the table above).
Attach(agent, hostPath) Joins a file of this computer to a message: returns its path in the agent's folder, to write after $. A file already in the folder is used where it is; any other is copied into attachments/ (a free name, spaces turned into -, 5 MB at most).
ChooseAttachment() Shows the native file dialog, from the home folder; returns the chosen path, or "".
Image(agent, path) An image of the folder (.png, .jpg, .gif, .webp, .svg…, 5 MB at most) as a data: URL, for the preview. Any other file is an error.
Watch(agent), Unwatch() Follows the changes of one agent's folder, announced by workspace:changed.

TerminalService

Method Effect
Open(agent, cols, rows) Starts a shell in the agent's folder; returns its id.
Write(id, data) Sends keystrokes.
Resize(id, cols, rows) Changes the size, in characters.
Close(id) Hangs up the shell and everything it started (its descendants' process groups), background jobs included, then kills what is left after a second; in a container, inside the container too.

Events

Event Payload Sent when
workspace:changed {agent} files changed in the folder being watched
terminal:output {id, data} the terminal printed something; grouped every 16 ms or 32 KB, never cutting a character
terminal:exit {id} the shell exited

Settings methods of ConfigService

Method Effect
Content() The text of the configuration file.
Validate(text) The problems and warnings of text, with line, column, message, warning, without saving.
Save(text) Writes text if it is valid; otherwise returns its problems and writes nothing.
Schema() The configuration JSON Schema.
Open(path) Stops the answers and every agent, closes the history, loads and watches path with its own history, remembers it.
Choose() Shows the native file dialog and opens the chosen file; returns "" when cancelled.
Recent() The configuration files opened from the application, most recent first (8 at most).
ReadSandboxEnv(path) Reads an sbx environment file for the assistant: name, workspace, sandboxFolder, createCommand, stopCommand, removeCommand (commands on one line). A file that is not one is an error.
ChooseSandboxEnv() Shows the native file dialog, from the home folder, for an sbx environment file; returns its path, or "" when cancelled.
ChooseAgentConfig() The same, for an agent's configuration file (YAML).
ReadCompose(path) Reads a Docker Compose file for the assistant: folder, and each service's name, container, createCommand, stopCommand, removeCommand. A file without services is an error.
ChooseCompose() The native file dialog, for a Compose file.
Reset(choice) The Danger zone: settings (default configuration, preferences forgotten), history (this configuration's), allHistory (every configuration's) or everything. Each thing reset is first kept as a dated copy; returns {copies[]}. The history is reopened empty and the declared channels created again; then config:changed is sent.

Stopping an agent

The agent's stdin is closed; after 5 s the agent and its children get SIGTERM (time for scripts/agents.sh to unload the model); after 3 more seconds, SIGKILL.

  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
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# Reference: agents, channels and events

## Agent states

| State | Shown as | Meaning |
| --- | --- | --- |
| `stopped` | stopped | No process. The initial state of every agent. |
| `starting` | starting… | The process is launched and the ACP handshake (`initialize`) is in progress. |
| `ready` | ready | Running and idle. |
| `busy` | busy | Running and used by at least one conversation. |
| `error` | error | The start failed, or the process exited on its own (crash). The status carries the reason, followed by the agent's last stderr lines for a failed start. |

`outdated` (shown as "restart to apply changes") is set on a running agent whose definition changed in the configuration; a restart applies it.

## Transitions

| From | Event | To |
| --- | --- | --- |
| `stopped`, `error` | start requested (▶ in the Agents panel, or first use) | `starting` |
| `starting` | handshake succeeds | `ready` |
| `starting` | handshake fails, protocol other than ACP v1, timeout (30 s) | `error` |
| `ready` | used | `busy` |
| `busy` | last user done | `ready` |
| `ready`, `busy` | stop requested (■), application quits, agent removed from the configuration | `stopped` |
| `ready` | unused for `limits.stopIdleAgentsAfter` (checked every 30 s) | `stopped` |
| `ready` | another agent must start and this one is the least recently used idle agent | `stopped` |
| `ready`, `busy` | the process exits on its own | `error` (never restarted automatically) |
| `starting` | stop requested | `stopped` once the start ends |

When `limits.maxRunningAgents` agents are running and all are `busy`, a start is refused with the message `too many agents running: N agent(s) busy (…), the limit is N`.

Stopping an agent closes its stdin, waits 5 seconds, then kills its process group (the agent and what it started, such as `mm` under `scripts/agents.sh`).

## Events from Go to the interface

| Event | Payload | Sent when |
| --- | --- | --- |
| `config:changed` | `{path, valid, problems[]}` | The configuration file was saved and reloaded (valid or not). |
| `agent:state` | `{agent, state, error?}` | An agent changes state or its definition changes. |
| `agent:log` | `{agent, line}` | An agent writes a line on its standard error, or a sandbox command of the agent prints one (prefixed with `sandbox: `). |
| `files:dropped` | `{target, files[]}` | Files are dropped from the system on an element marked `data-file-drop-target`; `target` is its `id`, `files` their absolute paths. |

The Go names and types are in `internal/events`; the TypeScript constants and types are generated in `frontend/bindings/events` and `frontend/bindings/…/internal/events`.

## Services called by the interface

| Service | Method | Effect |
| --- | --- | --- |
| `AgentService` | `List()` | Every agent with `name, title, icon, description, state, error, outdated, runtime, canStopSandbox, canRemoveSandbox`, in configuration order. |
| | `Start(name)`, `Stop(name)`, `Restart(name)` | As in the transitions above; an error is returned to the interface. A start first recreates a missing sandbox when `runtime.lifecycle.create` is set. |
| | `StopSandbox(name)`, `RemoveSandbox(name)` | Stops the agent, then runs `runtime.lifecycle.stop` or `remove` (container or sandbox); the output goes to the agent's log. |
| | `Logs(name)` | The last 200 stderr lines of the agent. |
| `ConfigService` | `Current()` | `path, source, created, valid, problems[], warnings[], theme, language, historyProblem`. |

## Channels (milestones M2 and M3)

A channel holds a conversation between the user and one or several agents. A direct conversation is a channel with one member, no name, and `direct` set to its agent's name. Channels, members and finished messages are kept in the history of the configuration file (below); a streaming answer is kept once it ends.

### Channels

| Field | Meaning |
| --- | --- |
| `id` | sortable identifier |
| `name`, `description` | 1 to 64 characters, unique whatever the case; empty for a direct conversation |
| `direct` | the agent of a direct conversation; empty for a channel |
| `members` | agent names, in the order they joined |

### Who answers a message

| The message | Answered by |
| --- | --- |
| `/agent:command arguments` (`agent` is a configured agent) | that agent, which receives `/command arguments` |
| contains `@name` mentions of configured agents | the mentioned agents, one after the other, in the order of the mentions; a mentioned agent that is not a member joins first |
| no mention | the last agent who answered in the channel; with a single member, that member |
| no mention and nobody answered yet, several members | nobody: system message `no-recipient` |
| any message in a direct conversation | its agent; mentions are plain text there |

A message starting with `/` (possibly after mentions) is a command: it is sent alone. Any other message is sent with the channel messages the agent has not seen yet (at most the last 20, each cut at 1500 characters). An agent answers one channel at a time; a channel waiting for it shows whose turn it is.

### Joined files

A `$path` at the start of a word joins the file at `path` of an agent's folder (issue #53): in a direct conversation, its agent's; in a channel, the first agent mentioned with `@`, otherwise the first member's. A `$` inside code, after another character (`5$`), escaped (`\$`) or followed by no letter (`$5`) is text; punctuation ending a sentence is not part of the path. A path ends at the first space.

Each agent that answers receives the files with the message (not with a slash command), as ACP content blocks chosen from what it announced:

| File | `promptCapabilities` | Sent as |
| --- | --- | --- |
| image (`image/*`) | `image` | `image` block (base64), with its URI |
| text, 1 MB at most | `embeddedContext` | `resource` block with the text |
| any other text or image | — | `resource_link`: URI `file://` + the path as the agent sees it, name, type, size |
| neither text nor image | — | not sent: `attachment-unavailable` |

A file is read when the agent's turn comes (5 MB at most); one that cannot be read gives `attachment-unavailable`, and the agent answers without it.

### Messages

| Field | Meaning |
| --- | --- |
| `id`, `channel`, `createdAt` | identity |
| `agent` | the author of an agent message, or the agent a system message is about |
| `role` | `user`, `agent` or `system` |
| `parts` | `text` (Markdown), `thought` (reasoning), `tool` (a tool call), `plan` (plan entries) |
| `done` | false while an agent message streams |
| `queued` | a user message waiting for the current answers |
| `stopReason` | how an agent message ended: `end_turn`, `cancelled`, `max_tokens`, `max_turn_requests`, `refusal` |
| `error` | why an agent message ended without a stop reason (the agent's own explanation), or `cancelled` for a queued message dropped by Stop |
| `code` | system messages: `agent-joined`, `agent-left`, `no-recipient`, `agent-unavailable` (could not start or reach it; `error` says why), `attachment-unavailable` (a joined file was not sent to `agent`; `error` names it and says why) |
| `attachments` | user messages: the files joined with `$path`, as `{agent, path}` (the agent whose folder holds it). Only the reference is kept, never the content. |

A tool call has `id, title, kind` (`read, edit, delete, move, search, execute, think, fetch, other`), `status` (`pending, in_progress, completed, failed`), `input` (compact JSON), `output`, `locations`, `diffs` (`path, oldText, newText, newFile`).

### Events

| Event | Payload | Sent when |
| --- | --- | --- |
| `channels:changed` | `{channels[]}` | a channel is created, renamed or deleted, or its members change |
| `message:updated` | `{channel, message}` | a message is added, or finished |
| `message:delta` | `{channel, messageId, index, kind, text?, part?}` | `text` is appended to the part at `index` (text, thought), or `part` replaces it (tool, plan) |
| `turn:changed` | `{channel, busy, queued, answering}` | the channel starts or stops answering, or another agent's turn begins; `queued` messages wait |
| `permission:requested` | `{id, channel, agent, messageId, toolCall, options[], rejections}` | an agent asks to run a tool; `rejections` counts the refusals already given in this turn |
| `permission:resolved` | `{id, channel}` | the request was answered, or cancelled by Stop |
| `usage:changed` | `{channel, agent, usage: {used, size}}` | an agent reported its context window use (`size` is 0 when unknown) |
| `commands:changed` | `{agent, commands[]}` | an agent announced its slash commands (`name, description, hint`) |

### `ChannelService`

| Method | Effect |
| --- | --- |
| `List()` | Every channel and direct conversation, oldest first. |
| `Create(name, description)` | Creates a channel. A taken or empty name is an error. |
| `Rename(id, name, description)` | Renames a channel (not a direct conversation). |
| `Delete(id)` | Stops its answers, then deletes the channel and its whole history. |
| `AddMember(id, agent)`, `RemoveMember(id, agent)` | Invites or removes an agent; the channel shows `agent-joined` or `agent-left`. |
| `Direct(agent)` | The direct conversation with `agent`, created the first time. |
| `Send(id, text)` | Adds a user message and returns its id; the agents that answer are started if needed; the message is queued if the channel is answering. Empty text and unknown channels are errors. Each `$path` joins a file of an agent's folder (see [Joined files](#joined-files)); more than 10 is an error. |
| `Cancel(id)` | Stops the current answer (`session/cancel`), the agents still to answer and the queued messages; answers pending permission requests "cancelled". |
| `AnswerPermission(id, optionId)` | Answers a pending request with one of its options. |
| `Conversation(id)` | `channel, messages` (the last 100), `more, busy, queued, answering, usage{agent: …}, permissions`. |
| `Older(id, beforeId)` | `{messages, more}`: the 100 messages before `beforeId`. |
| `Commands(agent)` | The commands the agent announced last, kept from one run to the next; empty if it never announced any. |
| `Prepare(id)` | Starts the members of the channel that never announced their commands and opens their session for it (no model is loaded); returns once the sessions are open, the commands arrive as `commands:changed`. A member answering elsewhere is skipped. The session serves the member's first answer, which is still told the channel's past. |

### History

| | |
| --- | --- |
| Location | `<user config dir>/sidekicks/data/<16 hex characters>/`: one Pebble database per configuration file (the hash of its absolute path). `<user config dir>` is `~/Library/Application Support` on macOS, `$XDG_CONFIG_HOME` or `~/.config` on Linux. |
| Kept | channels, members (with each agent's ACP session id and the last message it saw), finished messages, the last commands of each agent |
| Not kept | pending permission requests, queued messages, an answer interrupted by quitting |
| Unavailable | if the database cannot be opened (another Sidekicks uses the same file), the history lives in memory until Sidekicks quits, and `ConfigService.Current().historyProblem` says why |
| After a restart | an agent that supports `session/load` resumes its session; otherwise its first prompt in a channel carries the last messages of that channel |

## Workspaces (milestone M4)

An agent's workspace is its `workspace` folder, reached according to `runtime.type`.

| Runtime | Files | Changes followed | Terminal | Deletion |
| --- | --- | --- | --- | --- |
| `local` | directly | file system notifications (not inside `.git`, `node_modules`), half a second after the last change | `$SHELL -l` (else bash, else sh) in a pseudo-terminal | to the system trash (`~/.Trash`, or the freedesktop.org trash); for good if the trash is on another disk |
| `container` | `docker exec` in `workspacePath` (default: the same path as `workspace`) | dates and sizes compared every 2 s | `docker exec -it`: bash if present, else sh | for good |
| `sbx` | `sbx exec` in `workspacePath` (default: the same path as `workspace`, where sbx mounts the host folder) | dates and sizes compared every 2 s | `sbx exec -it`: bash if present, else sh | for good |

Paths are relative to the folder and use `/`. A path leaving the folder (`..`, absolute, a symbolic link pointing out) is refused. The folder itself cannot be deleted.

### Files

| Field | Meaning |
| --- | --- |
| `name`, `path` | the entry's name, and its path from the folder |
| `dir` | a folder |
| `size`, `modTime` | size in bytes, last change |

A file opened for editing has `path, content, version`; `version` identifies the content.

### `WorkspaceService`

| Method | Effect |
| --- | --- |
| `Info(agent)` | `{location, problem}`: where the folder is (`local · /path`, `container name · /path`), or why it cannot be shown. |
| `List(agent, dir)` | The entries of `dir` (`""` is the folder), folders first, then by name. |
| `Read(agent, path)` | A text file of at most 1 MB; a binary or larger file is an error. |
| `Save(agent, path, content, version)` | Writes the file if it still has `version`; otherwise `{conflict: true}` and nothing is written. An empty `version` overwrites. The file is written next to its place and moved over it. |
| `Create(agent, dir, name)` | Creates an empty file; refused if the name exists or contains `/`. |
| `Remove(agent, path)` | Deletes a file or a folder with its content (see the table above). |
| `Attach(agent, hostPath)` | Joins a file of this computer to a message: returns its path in the agent's folder, to write after `$`. A file already in the folder is used where it is; any other is copied into `attachments/` (a free name, spaces turned into `-`, 5 MB at most). |
| `ChooseAttachment()` | Shows the native file dialog, from the home folder; returns the chosen path, or `""`. |
| `Image(agent, path)` | An image of the folder (`.png`, `.jpg`, `.gif`, `.webp`, `.svg`…, 5 MB at most) as a `data:` URL, for the preview. Any other file is an error. |
| `Watch(agent)`, `Unwatch()` | Follows the changes of one agent's folder, announced by `workspace:changed`. |

### `TerminalService`

| Method | Effect |
| --- | --- |
| `Open(agent, cols, rows)` | Starts a shell in the agent's folder; returns its id. |
| `Write(id, data)` | Sends keystrokes. |
| `Resize(id, cols, rows)` | Changes the size, in characters. |
| `Close(id)` | Hangs up the shell and everything it started (its descendants' process groups), background jobs included, then kills what is left after a second; in a container, inside the container too. |

### Events

| Event | Payload | Sent when |
| --- | --- | --- |
| `workspace:changed` | `{agent}` | files changed in the folder being watched |
| `terminal:output` | `{id, data}` | the terminal printed something; grouped every 16 ms or 32 KB, never cutting a character |
| `terminal:exit` | `{id}` | the shell exited |

## Settings methods of `ConfigService`

| Method | Effect |
| --- | --- |
| `Content()` | The text of the configuration file. |
| `Validate(text)` | The problems and warnings of `text`, with `line, column, message, warning`, without saving. |
| `Save(text)` | Writes `text` if it is valid; otherwise returns its problems and writes nothing. |
| `Schema()` | The configuration JSON Schema. |
| `Open(path)` | Stops the answers and every agent, closes the history, loads and watches `path` with its own history, remembers it. |
| `Choose()` | Shows the native file dialog and opens the chosen file; returns `""` when cancelled. |
| `Recent()` | The configuration files opened from the application, most recent first (8 at most). |
| `ReadSandboxEnv(path)` | Reads an sbx environment file for the assistant: `name, workspace, sandboxFolder, createCommand, stopCommand, removeCommand` (commands on one line). A file that is not one is an error. |
| `ChooseSandboxEnv()` | Shows the native file dialog, from the home folder, for an sbx environment file; returns its path, or `""` when cancelled. |
| `ChooseAgentConfig()` | The same, for an agent's configuration file (YAML). |
| `ReadCompose(path)` | Reads a Docker Compose file for the assistant: `folder`, and each service's `name, container, createCommand, stopCommand, removeCommand`. A file without services is an error. |
| `ChooseCompose()` | The native file dialog, for a Compose file. |
| `Reset(choice)` | The Danger zone: `settings` (default configuration, preferences forgotten), `history` (this configuration's), `allHistory` (every configuration's) or `everything`. Each thing reset is first kept as a dated copy; returns `{copies[]}`. The history is reopened empty and the declared channels created again; then `config:changed` is sent. |

## Stopping an agent

The agent's stdin is closed; after 5 s the agent and its children get SIGTERM (time for `scripts/agents.sh` to unload the model); after 3 more seconds, SIGKILL.