1mod clipboard;
41mod complete;
42pub mod diff;
43mod editor;
44pub mod highlight;
45mod history;
46mod input;
47mod jsonl;
48pub mod keys;
49pub mod markdown;
50pub mod pane;
51pub mod panel;
52mod pick;
53mod render;
54mod renderers;
55mod screen;
56pub mod status;
57mod terminal;
58mod theme;
59mod transcript;
60mod turns;
61mod view;
62pub mod wrap;
63
64use bevy_app::prelude::*;
65use bevy_ecs::prelude::*;
66use bevy_ecs::schedule::common_conditions::any_match_filter;
67use rig_ecs::stage::AgentLoop;
68use rig_harness::prelude::launcher::BUILD_ORIGIN;
69use rig_harness::prelude::{Answerer, AppCliExt, AppKeepsExt, Front, RigHome, ShownAgent};
70
71pub use complete::{Candidate, Completion, CompletionKind, CompletionSource, CompletionTrigger};
72pub use crossterm;
73pub use editor::{Editor, MainEditor, MainInput};
74pub use history::{HistorySystems, PromptHistory, Sent};
75pub use input::{InputContext, InputSystems, KeyInput, PendingInput, TuiKeys};
76pub use keys::{
77 AppKeyBindingsExt, InputAction, KeyBinding, KeyFocus, KeyPress, KeyPressed, KeyScope, Keys,
78 run_command,
79};
80pub use pane::{MainPane, MainTranscript, PaneKeys, TranscriptPane, TranscriptSource};
81pub use panel::{
82 PanelCanvas, Placement, RedrawArea, RequestRedraw, TerminalSize, TuiPanel, TuiSystems,
83};
84pub use pick::{ChoiceAction, ChoiceKeys, InlineStrip, PickerInput};
85pub use ratatui;
86pub use render::{TranscriptLayout, TuiFrame};
87pub use renderers::{
88 AppToolRenderersExt, AsideRenderer, AsideView, CallDecorator, CallState, ChildCall,
89 ContentBlock, ContentKind, ContentRenderer, ContentView, DecorateMessage, DecoratedMessage,
90 EntryRenderer, EntryView, MessageDecorator, MessageView, OUTPUT_INDENT, RenderAside,
91 RenderContent, RenderEntry, RenderToolCall, Role, ToolCallView, ToolRenderer, text_rows,
92};
93pub use screen::DefaultScreenPlugin;
94pub use status::{AppStatus, Side, StatusItem, StatusItems, StatusLine, StatusSystems, Tone};
95pub use terminal::{
96 CopyToClipboard, RunInTerminal, TerminalExit, TerminalFocus, TerminalWrite, TuiTestScreen,
97};
98pub use theme::Theme;
99pub use transcript::PastEffects;
100pub use view::{
101 DrawnLine, DrawnRow, DrawnTranscript, Highlight, ItemRow, LineSpan, MarkIn, Notices, RowWrap,
102 ScrollTo, ShowAgent, ShownNotice, TextMark, TranscriptItem, TranscriptSlot, TranscriptView,
103};
104
105pub const FRONT: &str = "tui";
107
108#[derive(Default)]
112pub struct TuiPlugin;
113
114impl Plugin for TuiPlugin {
115 fn build(&self, app: &mut App) {
116 app.add_plugins((TuiViewPlugin, DefaultScreenPlugin));
117 }
118}
119
120#[derive(Default)]
125pub struct TuiViewPlugin;
126
127impl Plugin for TuiViewPlugin {
128 fn build(&self, app: &mut App) {
129 status::add(app);
132 keys::add_built_in(app);
134 app.add_entry_renderer(BUILD_ORIGIN, build_failed);
135 app.keeps_files("tui");
137 app.init_resource::<render::FrameLayout>()
138 .init_resource::<TuiFrame>()
139 .init_resource::<Notices>()
140 .init_resource::<transcript::CallFacts>()
141 .init_resource::<TerminalSize>()
142 .init_resource::<Theme>()
143 .init_resource::<PromptHistory>()
144 .init_resource::<TerminalFocus>()
145 .init_resource::<PendingInput>()
146 .init_resource::<InputContext>()
147 .add_message::<RequestRedraw>()
148 .add_message::<RedrawArea>()
149 .add_message::<Sent>()
150 .add_message::<TerminalWrite>()
151 .add_message::<CopyToClipboard>()
152 .add_message::<PastEffects>()
153 .configure_sets(
154 TuiKeys,
155 (InputSystems::Filter, InputSystems::Dispatch).chain(),
156 )
157 .add_systems(PreUpdate, input::run_keys)
158 .add_systems(
159 TuiKeys,
160 input::clear_events
161 .in_set(InputSystems::Dispatch)
162 .run_if(not(resource_exists::<view::TuiView>))
163 .ambiguous_with(input::read_input),
164 )
165 .configure_sets(
166 PostUpdate,
167 (
168 TuiSystems::Prepare,
169 TuiSystems::Layout,
170 TuiSystems::Fit,
171 TuiSystems::Draw.run_if(render::frame_due),
172 TuiSystems::Render.run_if(render::frame_due),
173 )
174 .chain()
175 .after(StatusSystems::Edit),
176 )
177 .configure_sets(
178 Update,
179 (HistorySystems::Record, HistorySystems::Save).chain(),
180 );
181 app.add_front(Front::new(FRONT).on_terminal());
182 }
183
184 fn finish(&self, app: &mut App) {
187 if !app.runs_front(FRONT) {
188 return;
189 }
190 add_view(app);
191 app.add_systems(PreStartup, history::load)
192 .add_systems(Startup, terminal::open_terminal)
193 .add_systems(
194 Last,
195 terminal::keep_screen_on_reload.in_set(bevy_app::OnAppExitSystems),
196 );
197 }
198}
199
200pub fn add_view(app: &mut App) {
207 app.world_mut()
208 .spawn((Name::new("answerer:tui"), Answerer::default()));
209 app.add_message::<rig_harness::watch::FileChanges>()
210 .init_resource::<RigHome>()
211 .init_resource::<ShownAgent>()
212 .init_resource::<view::TuiView>()
213 .init_resource::<complete::FileIndex>()
214 .init_resource::<turns::Told>()
215 .register_required_components::<rig_ecs::agent::Partial, transcript::StreamingCalls>()
216 .add_observer(transcript::forget_streams)
217 .add_observer(clipboard::type_pasted)
218 .add_observer(render::redraw_on_remove)
219 .add_observer(render::forget_looks)
220 .add_systems(Startup, (view::load_notices, view::load_draft))
221 .add_systems(
222 PreUpdate,
223 (
224 (
225 input::receive_events.run_if(resource_exists::<input::TerminalInput>),
226 complete::receive_paths,
227 )
228 .chain()
229 .before(input::run_keys),
230 (pick::mark_highlighted, input::note_input_context)
231 .chain()
232 .after(input::run_keys),
233 ),
234 )
235 .add_systems(
236 TuiKeys,
237 (
238 input::note_input_context.before(InputSystems::Filter),
239 input::read_input.in_set(InputSystems::Dispatch),
240 ),
241 )
242 .add_systems(
243 Update,
244 (
245 (
249 pick::queue_choices,
250 pick::mark_highlighted,
251 pick::follow_previews,
252 keys::arm_inline_keys,
253 view::collect_notices,
254 )
255 .chain()
256 .after(AgentLoop),
257 view::recall_messages.after(AgentLoop),
258 history::record.in_set(HistorySystems::Record),
259 history::save.in_set(HistorySystems::Save),
260 ),
261 )
262 .add_systems(
263 render::LayOut,
264 (
265 render::layout.run_if(terminal::has_screen.and_then(render::needs_redraw)),
266 pane::lay_out_panes.run_if(render::frame_due),
267 )
268 .chain(),
269 )
270 .add_systems(
271 PostUpdate,
272 (
273 terminal::run_in_terminal
274 .run_if(any_match_filter::<(With<RunInTerminal>, Without<TerminalExit>)>)
275 .before(TuiSystems::Layout),
276 transcript::note_effects.in_set(TuiSystems::Prepare),
277 turns::tell_state,
278 input::note_input_context.in_set(TuiSystems::Prepare),
279 (
280 pane::draw_panes,
281 editor::draw_editors,
282 status::draw_status_lines,
283 pick::draw_inline_strips,
284 )
285 .in_set(TuiSystems::Draw),
286 (
287 complete::follow_input,
288 transcript::follow_streams,
289 render::lay_out_frame,
290 )
291 .chain()
292 .in_set(TuiSystems::Layout),
293 render::lay_out_fitted
294 .after(TuiSystems::Fit)
295 .before(TuiSystems::Draw),
296 (
297 terminal::report_releases.run_if(resource_exists::<terminal::Tui>),
298 render::render,
299 )
300 .chain()
301 .in_set(TuiSystems::Render)
302 .run_if(terminal::has_screen),
303 ),
304 )
305 .add_systems(
306 Last,
307 (
308 view::keep_draft,
309 (view::collect_notices, view::keep_notices)
311 .chain()
312 .after(rig_ecs::WriteLogs),
313 )
314 .in_set(bevy_app::OnAppExitSystems)
315 .run_if(on_message::<AppExit>),
316 )
317 .add_systems(
320 Last,
321 turns::restore_title
322 .run_if(on_message::<AppExit>)
323 .in_set(bevy_app::OnAppExitSystems),
324 )
325 .add_systems(
326 Last,
327 (
328 terminal::write_to_terminal.run_if(terminal::has_screen),
329 clipboard::copy_to_system.run_if(resource_exists::<terminal::Tui>),
330 )
331 .after(bevy_app::OnAppExitSystems),
332 )
333 .add_observer(keys::release_focus)
334 .add_observer(view::on_focus)
335 .add_observer(view::on_scroll_to)
336 .add_observer(transcript::note_agent_name)
337 .add_observer(turns::turn_started)
338 .add_observer(turns::turn_ended);
339}
340
341fn build_failed(entry: &EntryView<'_>) -> Vec<ratatui::text::Line<'static>> {
343 use ratatui::style::Style;
344 let mut lines = vec![ratatui::text::Line::styled(
345 "✗ build failed (noted for the agent)",
346 Style::new().red().bold(),
347 )];
348 lines.extend(text_rows(entry.text, Style::new().red().dim()));
349 lines
350}
351
352#[cfg(test)]
353mod tests;