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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
use shuvarie_config::ProviderConfig;
use shuvarie_db::StoredScroll;
#[derive(Debug, Clone)]
pub enum Command {
Ping,
ListModels {
provider_name: String,
},
AddProvider {
id: String,
config: ProviderConfig,
},
/// Register or overwrite a decision-model connection and make it the
/// active one. The two halves are written together —
/// `decision-providers { provider "<name>" { … } }` and
/// `decision { provider "<name>"; model "<model>" }` — because a decision
/// connection is unusable without a model. `connections.kdl` is the only
/// store: Selune has no decision-vendor registry, so `model` is free text
/// and nothing validates it until the first call.
SetDecisionProvider {
name: String,
api_key: Option<String>,
base_url: Option<String>,
model: String,
},
/// Sign in to an OAuth-backed provider (`chatgpt`, `copilot`) through the
/// device flow: the verification URL + user code surface as
/// [`crate::Event::AuthPrompt`], and completion or failure arrives as
/// [`crate::Event::AuthSuccess`] / [`crate::Event::AuthFailed`]. A cached
/// or pasted credential is verified in place; providers without OAuth
/// sign-in report a failure explaining that.
AuthProviderLogin {
name: String,
},
RemoveProvider {
name: String,
},
SetActiveProvider {
name: String,
},
SetActiveModel {
model: String,
},
/// Cycle the active model's reasoning-effort variant (Ctrl+T): forward
/// through the Selune catalog's variants, wrapping past the last back to
/// the unset default. A no-op when the active model is not in the catalog.
CycleVariant,
/// Set the active model's reasoning-effort variant directly
/// (`/variant [name]`). The TUI validates the value against the catalog
/// before sending; `None` selects the unset default.
SelectVariant {
variant: Option<String>,
},
/// Set the configured TUI theme (`/theme [name[:variant]]`): writes
/// `ui.theme` to the config file, `None` returning to the default
/// (built-in Faerun by the terminal's mode). The TUI validates the value
/// against the theme set before sending and reports the swap itself;
/// the core reports [`crate::Event::ConfigSaved`] once the file is
/// written.
SetUiTheme {
pref: Option<String>,
},
SaveConfig,
NewSession,
/// Start a user turn. `model` optionally overrides the streaming target
/// for this turn only: a `<provider_kind>/<model>` spec resolved to a
/// provider connection through the `default-providers` config (see
/// `resolve_model_override`); `None` streams on the active provider.
/// An unresolvable spec reports [`crate::Event::StreamError`]. A prompt
/// queued behind a busy agent (steered) keeps its override.
SendMessage {
content: String,
/// The composer's `@path` attachment directives for this turn:
/// relative paths resolve against the workspace root (`@`-prefix
/// stripped), absolute paths are user intent. The core resolves them
/// (sniffs, bounds images, converts documents) before the turn
/// starts; any failure reports [`crate::Event::StreamError`] and
/// aborts the send without persisting anything.
attachments: Vec<String>,
model: Option<String>,
},
/// Run a bash-mode (`!`-prefixed) command through the resolved shell.
/// Display-only: the output never persists or reaches the model.
RunBash {
command: String,
},
CancelStream,
ListSessions,
/// Fetch attachment blob bytes for the TUI's display. The reply is one
/// [`crate::Event::AttachmentMedia`] event whose items carry each hash's
/// bytes (or `None` when the store cannot serve it). Only sent from the
/// TUI; the request itself is read-only.
LoadAttachmentMedia {
hashes: Vec<String>,
},
/// Preview the composer's pending `@path` directives for the attach
/// strip: per-path existence/size/kind metadata only — no reads or
/// conversion (the authoritative preparation runs at send time), so the
/// handler answers directly instead of using the blocking pool. Reply:
/// [`crate::Event::DirectivesProbed`], matched by `token`. Read-only.
ProbeDirectives {
token: u64,
paths: Vec<String>,
},
/// Directory entries for the composer's `@` mention completion (reply:
/// [`crate::Event::PathCompletions`], matched by `token`). `query` is the
/// partial path typed after `@` — an optional directory part plus a name
/// prefix, resolved against the workspace root (absolute too). The
/// handler answers directly: one `read_dir`, no blocking pool.
RequestPathCompletions {
token: u64,
query: String,
},
LoadSession {
id: uuid::Uuid,
},
/// Persist the chat pane's last scroll position for a session, sent by
/// the TUI when it is about to leave the session (switch or quit).
SaveScroll {
id: uuid::Uuid,
scroll: StoredScroll,
},
DeleteSession {
id: uuid::Uuid,
},
/// Rename the active session. The core trims the title; an empty (or
/// unchanged) title is a no-op.
SetTitle {
title: String,
},
/// Draft the active session's title with the LLM configured under
/// `ui.title` `llm` (defaults: the active provider's catalog small model
/// and the built-in title prompt), from the active path's first user
/// prompt. Reports [`crate::Event::SessionTitleChanged`] on success and
/// [`crate::Event::SessionError`] when there is no session, no user
/// prompt, or no resolvable model. A manual rename that lands while the
/// call runs wins over the generated title.
GenTitle,
SearchHistory {
query: String,
},
AnswerQuestion {
id: u64,
/// `None` when the user dismissed the question.
answers: Option<Vec<Vec<String>>>,
},
/// The user's answer to a pending `ask` permission prompt (`id` from
/// [`crate::Event::PermissionRequested`]). `Allow` grants one call,
/// `AllowSession` also remembers the ask's scope for the rest of the
/// run, `AllowDirSession` remembers the path's whole directory tree
/// (path asks), and `Deny` cuts the turn. A missing id is a no-op.
PermissionDecide {
id: u64,
decision: crate::permissions::PermissionAnswer,
},
/// Fork the session: with `node: None`, walk to the active path's last
/// user prompt (`/undo`); otherwise the fork targets `node`. Turn nodes
/// fork *before* themselves — their parent becomes the tip and their
/// content is recalled into the input — while summary/system nodes walk
/// to themselves. With `after`, the targeted turn node is walked to
/// instead: it becomes the tip and nothing is recalled (the tree's tool
/// rows walk to their reply and reply rows walk to themselves this
/// way). When `summarize`, an LLM summary of the prefix before the fork
/// point is created first and the forked-away node is reparented under
/// it.
ForkSession {
node: Option<u64>,
summarize: bool,
/// Fork *after* the node's turn instead of before it: the node
/// itself becomes the tip and nothing is recalled into the input.
after: bool,
},
/// Delete a branch of the session tree: the node and all of its
/// descendants (must not contain the active leaf).
DeleteBranch {
node: u64,
},
/// Manually compact the active session (`/compact [instruction]`): run
/// the standard compaction flow over the active path (keep the recent
/// tail within `keep_recent_tokens`, summarize the head, splice the
/// summary in at the cut point, reparent the tail under it) and reload
/// the session, reporting [`crate::Event::SessionCompacted`].
/// `instruction` optionally focuses the summary. Refused with
/// [`crate::Event::SessionError`] while a turn is in flight, without an
/// active session/provider/model, or when there is no summarizable span.
CompactSession {
instruction: Option<String>,
},
/// Load the active session's tree for the `/tree` popup; reports
/// [`crate::Event::SessionTree`].
OpenTree,
/// Write the active session to a JSON file (`/export [path]`); `None`
/// picks `<session_id>-<timestamp>.json` in the working directory.
/// Reports [`crate::Event::SessionExported`] or
/// [`crate::Event::SessionError`].
ExportSession {
path: Option<std::path::PathBuf>,
},
/// Switch the active session's scene (`None` = the built-in default
/// scene). Refused with [`crate::Event::SceneError`] while a stream is
/// busy or the name does not resolve.
SwitchScene {
name: Option<String>,
},
Replay,
Reload,
/// Recall the most recently steered prompt into the input area. `stacked`
/// (Alt+Shift+Up) prepends the content to the existing text (separated by
/// two line feeds) instead of overwriting it.
RecallSteered {
stacked: bool,
},
LspStart {
name: String,
},
LspStop {
name: String,
},
LspRestart {
name: String,
},
LspList {
all: bool,
filter: Option<String>,
},
/// Snapshot the configured MCP servers' states and report
/// [`crate::Event::McpStatus`] (`/mcp`).
McpList,
/// Force a fresh connection to an MCP server, dropping any existing one.
/// Reports [`crate::Event::McpStatus`] on success; on failure
/// [`crate::Event::McpError`] plus a status snapshot in which the server
/// carries its failure detail.
McpReconnect {
name: String,
},
/// Fetch one registry's online source on demand: the built-in hosted
/// (Selune) registry or a custom `[registries]` one. A disabled registry
/// or one without an online source is reported as an error. Reports
/// [`Event::RegistryLoaded`] or [`Event::RegistryError`], tagged with the
/// registry id.
FetchRegistry {
/// The registry id (`selune` for the built-in one).
registry: String,
},
}