mod clipboard;
mod complete;
pub mod diff;
mod editor;
pub mod highlight;
mod history;
mod input;
mod jsonl;
pub mod keys;
pub mod markdown;
pub mod pane;
pub mod panel;
mod pick;
mod render;
mod renderers;
mod screen;
pub mod status;
mod terminal;
mod theme;
mod transcript;
mod turns;
mod view;
pub mod wrap;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::common_conditions::any_match_filter;
use rig_ecs::stage::AgentLoop;
use rig_harness::prelude::launcher::BUILD_ORIGIN;
use rig_harness::prelude::{Answerer, AppCliExt, AppKeepsExt, Front, RigHome, ShownAgent};
pub use complete::{Candidate, Completion, CompletionKind, CompletionSource, CompletionTrigger};
pub use crossterm;
pub use editor::{Editor, MainEditor, MainInput};
pub use history::{HistorySystems, PromptHistory, Sent};
pub use input::{InputContext, InputSystems, KeyInput, PendingInput, TuiKeys};
pub use keys::{
AppKeyBindingsExt, InputAction, KeyBinding, KeyFocus, KeyPress, KeyPressed, KeyScope, Keys,
run_command,
};
pub use pane::{MainPane, MainTranscript, PaneKeys, TranscriptPane, TranscriptSource};
pub use panel::{
PanelCanvas, Placement, RedrawArea, RequestRedraw, TerminalSize, TuiPanel, TuiSystems,
};
pub use pick::{ChoiceAction, ChoiceKeys, InlineStrip, PickerInput};
pub use ratatui;
pub use render::{TranscriptLayout, TuiFrame};
pub use renderers::{
AppToolRenderersExt, AsideRenderer, AsideView, CallDecorator, CallState, ChildCall,
ContentBlock, ContentKind, ContentRenderer, ContentView, DecorateMessage, DecoratedMessage,
EntryRenderer, EntryView, MessageDecorator, MessageView, OUTPUT_INDENT, RenderAside,
RenderContent, RenderEntry, RenderToolCall, Role, ToolCallView, ToolRenderer, text_rows,
};
pub use screen::DefaultScreenPlugin;
pub use status::{AppStatus, Side, StatusItem, StatusItems, StatusLine, StatusSystems, Tone};
pub use terminal::{
CopyToClipboard, RunInTerminal, TerminalExit, TerminalFocus, TerminalWrite, TuiTestScreen,
};
pub use theme::Theme;
pub use transcript::PastEffects;
pub use view::{
DrawnLine, DrawnRow, DrawnTranscript, Highlight, ItemRow, LineSpan, MarkIn, Notices, RowWrap,
ScrollTo, ShowAgent, ShownNotice, TextMark, TranscriptItem, TranscriptSlot, TranscriptView,
};
pub const FRONT: &str = "tui";
#[derive(Default)]
pub struct TuiPlugin;
impl Plugin for TuiPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((TuiViewPlugin, DefaultScreenPlugin));
}
}
#[derive(Default)]
pub struct TuiViewPlugin;
impl Plugin for TuiViewPlugin {
fn build(&self, app: &mut App) {
status::add(app);
keys::add_built_in(app);
app.add_entry_renderer(BUILD_ORIGIN, build_failed);
app.keeps_files("tui");
app.init_resource::<render::FrameLayout>()
.init_resource::<TuiFrame>()
.init_resource::<Notices>()
.init_resource::<transcript::CallFacts>()
.init_resource::<TerminalSize>()
.init_resource::<Theme>()
.init_resource::<PromptHistory>()
.init_resource::<TerminalFocus>()
.init_resource::<PendingInput>()
.init_resource::<InputContext>()
.add_message::<RequestRedraw>()
.add_message::<RedrawArea>()
.add_message::<Sent>()
.add_message::<TerminalWrite>()
.add_message::<CopyToClipboard>()
.add_message::<PastEffects>()
.configure_sets(
TuiKeys,
(InputSystems::Filter, InputSystems::Dispatch).chain(),
)
.add_systems(PreUpdate, input::run_keys)
.add_systems(
TuiKeys,
input::clear_events
.in_set(InputSystems::Dispatch)
.run_if(not(resource_exists::<view::TuiView>))
.ambiguous_with(input::read_input),
)
.configure_sets(
PostUpdate,
(
TuiSystems::Prepare,
TuiSystems::Layout,
TuiSystems::Fit,
TuiSystems::Draw.run_if(render::frame_due),
TuiSystems::Render.run_if(render::frame_due),
)
.chain()
.after(StatusSystems::Edit),
)
.configure_sets(
Update,
(HistorySystems::Record, HistorySystems::Save).chain(),
);
app.add_front(Front::new(FRONT).on_terminal());
}
fn finish(&self, app: &mut App) {
if !app.runs_front(FRONT) {
return;
}
add_view(app);
app.add_systems(PreStartup, history::load)
.add_systems(Startup, terminal::open_terminal)
.add_systems(
Last,
terminal::keep_screen_on_reload.in_set(bevy_app::OnAppExitSystems),
);
}
}
pub fn add_view(app: &mut App) {
app.world_mut()
.spawn((Name::new("answerer:tui"), Answerer::default()));
app.add_message::<rig_harness::watch::FileChanges>()
.init_resource::<RigHome>()
.init_resource::<ShownAgent>()
.init_resource::<view::TuiView>()
.init_resource::<complete::FileIndex>()
.init_resource::<turns::Told>()
.register_required_components::<rig_ecs::agent::Partial, transcript::StreamingCalls>()
.add_observer(transcript::forget_streams)
.add_observer(clipboard::type_pasted)
.add_observer(render::redraw_on_remove)
.add_observer(render::forget_looks)
.add_systems(Startup, (view::load_notices, view::load_draft))
.add_systems(
PreUpdate,
(
(
input::receive_events.run_if(resource_exists::<input::TerminalInput>),
complete::receive_paths,
)
.chain()
.before(input::run_keys),
(pick::mark_highlighted, input::note_input_context)
.chain()
.after(input::run_keys),
),
)
.add_systems(
TuiKeys,
(
input::note_input_context.before(InputSystems::Filter),
input::read_input.in_set(InputSystems::Dispatch),
),
)
.add_systems(
Update,
(
(
pick::queue_choices,
pick::mark_highlighted,
pick::follow_previews,
keys::arm_inline_keys,
view::collect_notices,
)
.chain()
.after(AgentLoop),
view::recall_messages.after(AgentLoop),
history::record.in_set(HistorySystems::Record),
history::save.in_set(HistorySystems::Save),
),
)
.add_systems(
render::LayOut,
(
render::layout.run_if(terminal::has_screen.and_then(render::needs_redraw)),
pane::lay_out_panes.run_if(render::frame_due),
)
.chain(),
)
.add_systems(
PostUpdate,
(
terminal::run_in_terminal
.run_if(any_match_filter::<(With<RunInTerminal>, Without<TerminalExit>)>)
.before(TuiSystems::Layout),
transcript::note_effects.in_set(TuiSystems::Prepare),
turns::tell_state,
input::note_input_context.in_set(TuiSystems::Prepare),
(
pane::draw_panes,
editor::draw_editors,
status::draw_status_lines,
pick::draw_inline_strips,
)
.in_set(TuiSystems::Draw),
(
complete::follow_input,
transcript::follow_streams,
render::lay_out_frame,
)
.chain()
.in_set(TuiSystems::Layout),
render::lay_out_fitted
.after(TuiSystems::Fit)
.before(TuiSystems::Draw),
(
terminal::report_releases.run_if(resource_exists::<terminal::Tui>),
render::render,
)
.chain()
.in_set(TuiSystems::Render)
.run_if(terminal::has_screen),
),
)
.add_systems(
Last,
(
view::keep_draft,
(view::collect_notices, view::keep_notices)
.chain()
.after(rig_ecs::WriteLogs),
)
.in_set(bevy_app::OnAppExitSystems)
.run_if(on_message::<AppExit>),
)
.add_systems(
Last,
turns::restore_title
.run_if(on_message::<AppExit>)
.in_set(bevy_app::OnAppExitSystems),
)
.add_systems(
Last,
(
terminal::write_to_terminal.run_if(terminal::has_screen),
clipboard::copy_to_system.run_if(resource_exists::<terminal::Tui>),
)
.after(bevy_app::OnAppExitSystems),
)
.add_observer(keys::release_focus)
.add_observer(view::on_focus)
.add_observer(view::on_scroll_to)
.add_observer(transcript::note_agent_name)
.add_observer(turns::turn_started)
.add_observer(turns::turn_ended);
}
fn build_failed(entry: &EntryView<'_>) -> Vec<ratatui::text::Line<'static>> {
use ratatui::style::Style;
let mut lines = vec![ratatui::text::Line::styled(
"✗ build failed (noted for the agent)",
Style::new().red().bold(),
)];
lines.extend(text_rows(entry.text, Style::new().red().dim()));
lines
}
#[cfg(test)]
mod tests;