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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
//! What every handler shares: the environment, the settings, the services, the app mode, and
//! the way out to the TUI.
use std::path::PathBuf;
use std::sync::Arc;
use argonauts_core::NoteLevel;
use argonauts_core::api::{Approvals, ConductorApi, StartRun};
use argonauts_core::app::{AppEvent, AppMode, UiCommand};
use argonauts_core::clock::Clock;
use argonauts_core::limits::{HealthBoard, PlanUsageBoard};
use argonauts_core::names::ChoiceId;
use argonauts_core::registry::ModelRegistry;
use argonauts_core::session_runner::RunnerDirectory;
use argonauts_core::settings::{SettingScope, Settings};
use argonauts_core::steps::StepRunner;
use tokio::sync::mpsc;
use super::conversation::TurnUpdate;
use super::runs::RunUpdate;
use crate::app_settings::AppSettings;
use crate::env::AppEnv;
use crate::setup::SetupUpdate;
use crate::trace::Trace;
pub(crate) struct AppServices {
pub conductor: Arc<dyn ConductorApi>,
/// `None` without a terminal or inside an agent, so nothing can be approved from here.
pub approvals: Option<Arc<dyn Approvals>>,
/// For the app's own sessions: conversation, the estimate, and the narrator.
pub workers: Arc<dyn RunnerDirectory>,
pub steps: Arc<dyn StepRunner>,
pub registry: Arc<dyn ModelRegistry>,
pub plan_usage: Arc<dyn PlanUsageBoard>,
pub health: Arc<dyn HealthBoard>,
pub clock: Arc<dyn Clock>,
}
/// Every `AppEvent` goes out through here, so the trace sees each one.
#[derive(Clone)]
pub(crate) struct Outbox {
events: mpsc::UnboundedSender<AppEvent>,
trace: Arc<Trace>,
}
impl Outbox {
pub(crate) fn new(events: mpsc::UnboundedSender<AppEvent>, trace: Arc<Trace>) -> Self {
Self { events, trace }
}
/// Never blocks. After the screen closed, events have nowhere to go and are dropped.
pub(crate) fn send(&self, event: AppEvent) {
self.trace.event(&event);
let _ = self.events.send(event);
}
pub(crate) fn trace(&self) -> &Trace {
&self.trace
}
}
/// Messages from the tasks the handlers start: conversation turns, run streams, setup probes, and
/// model list refreshes.
pub(crate) enum Internal {
Conversation(TurnUpdate),
Run(RunUpdate),
Setup(SetupUpdate),
/// A mod file changed, or a mod's command printed something new.
Mods,
/// A `!` command printed or ended.
Shell(super::shell::ShellUpdate),
/// The CLIs' model lists were read again.
ModelsRefreshed,
/// One CLI's or endpoint's model list was read again.
ModelSourceListed {
cli: String,
},
/// While `/model`'s picker is open: time to ask the local servers again.
ModelsTick,
/// What this machine gives models and what the local ones take, or nothing where it
/// couldn't be read.
MemoryRead(Option<Box<super::settings::memory_facts::Reading>>),
/// The workflow file `/workflow edit` watches was saved.
WorkflowSaved {
name: String,
},
/// The check of the goal the main agent works toward alone came back.
Goal(super::conversation::GoalChecked),
/// A CLI's plan usage was read live, so the bottom line shows the new reading.
PlanUsageRead,
/// The update of a CLI a run waits on has ended.
CliUpdated(Box<super::needed_update::Updated>),
/// What GitHub says of the pull requests Argonauts Code follows.
Followed(Box<crate::issue::follow::Looked>),
/// The deeper Behaviors map's update ended, so the map on screen is sent again: the
/// deeper one when the tab started it.
DeepMapMade {
deeper: bool,
},
}
/// What one part of the controller asks of another. The loop applies them in order.
#[derive(Debug)]
pub(crate) enum Effect {
/// Start this run and attach to it. A CLI it would use that must be updated is asked about
/// first, then the detected checks are offered, once.
StartRun(Box<StartRun>),
/// The main agent changed: its session carries on, or another CLI starts from what carries
/// over.
FreshConversation,
/// A workflow of yours was written or removed, so the `/` menu lists the commands again.
CommandsChanged,
/// Settings in effect changed: the narrator hears of it, and a new header and bottom line
/// go out.
SettingsApplied { keys: Vec<String> },
/// Ask where to save a setting, as an argument like `/effort high` does.
AskScope(Box<ScopeRequest>),
/// Save these `(key, TOML value)` settings where you picked, through the same confirming
/// and trust steps as `/config`.
SaveSettings {
pairs: Vec<(String, String)>,
scope: SettingScope,
},
/// Save these settings where you picked on a setup's question. The pick confirmed them, so a
/// safety key among them asks nothing more.
SaveConfirmedSettings {
pairs: Vec<(String, String)>,
scope: SettingScope,
},
/// Send a new header and bottom line.
Status,
/// Open the guided setup again (`/setup`).
OpenSetup,
/// The guided setup finished or was set aside.
SetupDone,
/// Input to handle as if you had just sent it.
Input(Box<UiCommand>),
/// Input typed while a conversation turn ran, handled now as if you had just sent it. It hears
/// of the `!` commands up to `typed_after`, the last that had ended when you typed it.
Queued {
command: Box<UiCommand>,
typed_after: u32,
},
/// Close the screen. Runs keep going.
Exit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ScopeRequest {
pub key: String,
/// TOML text.
pub value: String,
/// The settings command whose default scope applies, such as `fallback`.
pub command: String,
}
pub(crate) struct Ctx {
pub env: Arc<AppEnv>,
pub settings: AppSettings,
pub services: AppServices,
pub mode: AppMode,
pub out: Outbox,
pub internal: mpsc::UnboundedSender<Internal>,
next_choice: u32,
/// Where `/cd` asked the app to open next, read once the screen has closed.
reopen: Reopen,
/// What the screen was last told to call each CLI and model.
pub names: super::names::NamesSent,
/// The runs' agents' sessions as their journals were last read, for `/resume`.
pub agent_sessions: Arc<super::conversation::SessionCache>,
/// Holds the computer's sleep off while anything works, and for `/awake`.
pub awake: crate::keep_awake::KeepAwake,
/// What the Status tab knows of the CLIs: Doctor's check, and who each is signed in as.
pub cli_facts: Arc<super::status_tab::CliFacts>,
/// The deeper Behaviors map's update, while one runs.
pub deep: crate::behaviors::deep::tab::DeepWork,
}
/// Shared with whoever started the app, which outlives the controller.
pub(crate) type Reopen = Arc<std::sync::Mutex<Option<PathBuf>>>;
impl Ctx {
pub(crate) fn new(
env: Arc<AppEnv>,
settings: AppSettings,
services: AppServices,
out: Outbox,
internal: mpsc::UnboundedSender<Internal>,
) -> Self {
Self {
env,
settings,
services,
mode: AppMode::Idle,
out,
internal,
next_choice: 0,
reopen: Reopen::default(),
names: super::names::NamesSent::default(),
agent_sessions: Arc::default(),
// The app's own start gives it this computer's way to hold sleep off.
awake: crate::keep_awake::KeepAwake::never(),
cli_facts: Arc::default(),
deep: Default::default(),
}
}
/// The app opens again in `repo` once this screen closes.
pub(crate) fn reopen_in(&self, repo: PathBuf) {
*self
.reopen
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(repo);
}
pub(crate) fn reopen(&self) -> Reopen {
Arc::clone(&self.reopen)
}
pub(crate) fn settings(&self) -> &Settings {
self.settings.settings()
}
pub(crate) fn send(&self, event: AppEvent) {
self.out.send(event);
}
pub(crate) fn notice(&self, level: NoteLevel, text: impl Into<String>) {
self.send(AppEvent::Notice {
level,
text: text.into(),
});
}
/// The answer to a key that can't do anything now, for the notice line.
pub(crate) fn hint(&self, text: impl Into<String>) {
self.send(AppEvent::Hint { text: text.into() });
}
/// Every error a handler returns reaches you as a notice, naming files in the repo by
/// their path from it.
pub(crate) fn error(&self, err: &anyhow::Error) {
self.notice(NoteLevel::Error, self.env.shown_in(&format!("{err:#}")));
}
/// Sends `Mode` only when it changed.
pub(crate) fn set_mode(&mut self, mode: AppMode) {
if self.mode != mode {
// An answer or a run you watch is under way, so the computer stays awake.
let working = matches!(
mode,
AppMode::Answering { .. } | AppMode::Working { .. } | AppMode::Pausing { .. }
);
let settings = self.settings().awake.clone();
self.awake.working(working, &settings);
self.mode = mode.clone();
self.send(AppEvent::Mode { mode });
}
}
pub(crate) fn next_choice_id(&mut self) -> ChoiceId {
self.next_choice += 1;
ChoiceId::numbered(self.next_choice)
}
}