use crate::AppModel;
use crate::cli_outbound::Cli;
use crate::nav::convs::{ConvRow, age_label};
use crate::nav::tabs::Tab;
use crate::nav::ws_key;
use crate::theme;
use super::ShellState;
pub fn top_bar(
ui: &mut egui::Ui,
model: &mut AppModel,
state: &mut ShellState,
lernie: &Cli,
bl: &Cli,
) {
ui.horizontal(|ui| {
theme::wordmark(ui);
ui.separator();
let total = model.strip_total();
if total > 0 {
ui.colored_label(theme::BRAZEN, format!("⚑ {total} need attention"));
} else {
ui.weak("⚑ nothing stirs");
}
if ui.button("⏭ next").clicked() {
model.jump_next_attention();
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let bar = model.tab_bar();
overflow_menu(ui, model, &bar);
if ui
.button("+")
.on_hover_text("new workspace (a fresh sphere wall)")
.clicked()
{
let inputs = model.new_workspace_inputs();
super::start_pane::run_prepare(model, &mut state.start, lernie, bl, inputs);
}
for tab in bar.tabs.iter().rev() {
workspace_tab(ui, model, tab);
}
});
});
}
fn workspace_tab(ui: &mut egui::Ui, model: &mut AppModel, tab: &Tab) {
let badge = if tab.attention > 0 {
format!("⚑{} ", tab.attention)
} else {
String::new()
};
let suffix = if tab.replay { " ▶" } else { "" };
let label = ui.selectable_label(tab.selected, format!("{badge}{}{suffix}", tab.name));
if label.clicked() {
model.focus_workspace(&tab.ws);
}
if tab.pinned {
label.context_menu(|ui| {
if ui.button("unpin").clicked() {
model.toggle_pin(&ws_key(&tab.ws));
ui.close_menu();
}
});
}
}
fn overflow_menu(ui: &mut egui::Ui, model: &mut AppModel, bar: &crate::nav::tabs::TabBar) {
if bar.overflow.is_empty() {
return;
}
let attention = bar.overflow_attention();
let title = if attention > 0 {
format!("⋯ ⚑{attention}")
} else {
"⋯".to_owned()
};
ui.menu_button(title, |ui| {
for entry in &bar.overflow {
ui.horizontal(|ui| {
let kind = if entry.replay { "replay" } else { "foreign" };
let badge = if entry.attention > 0 {
format!(" ⚑{}", entry.attention)
} else {
String::new()
};
if ui
.selectable_label(entry.selected, format!("{} · {kind}{badge}", entry.name))
.clicked()
{
model.focus_workspace(&entry.ws);
ui.close_menu();
}
if ui.small_button("★").on_hover_text("pin as a tab").clicked() {
model.toggle_pin(&ws_key(&entry.ws));
}
});
}
});
}
pub fn side_panel(
ui: &mut egui::Ui,
model: &mut AppModel,
state: &mut ShellState,
lernie: &Cli,
bl: &Cli,
bz: &Cli,
) {
conversations(ui, model, state);
ui.separator();
balls_section(ui, model, state, lernie, bl);
if ui
.selectable_label(state.config.active, "⚙ Config")
.clicked()
{
state.config.active = !state.config.active;
}
ui.separator();
super::toolchain::pane(ui, model, &mut state.login, bz);
}
fn conversations(ui: &mut egui::Ui, model: &mut AppModel, state: &mut ShellState) {
let Some(ws) = model.focused_workspace().map(std::path::Path::to_path_buf) else {
ui.weak("no workspace yet — say the word");
return;
};
if ui.button("+ conversation").clicked() {
model.focus_workspace(&ws);
state.focus_composer = true;
}
ui.horizontal(|ui| {
ui.selectable_value(&mut state.group_by_ball, false, "recent");
ui.selectable_value(&mut state.group_by_ball, true, "by ball");
});
if model.conversations(now_unix()).is_empty() {
ui.weak("no conversations yet");
return;
}
let selected_root = selected_conversation_root(model);
egui::ScrollArea::vertical().show(ui, |ui| {
if state.group_by_ball {
for group in model.conversation_groups(now_unix()) {
super::conv_ball::group_header(ui, &group);
for row in &group.convs {
conversation_row(ui, model, &ws, row, selected_root.as_deref());
}
}
} else {
for row in &model.conversations(now_unix()) {
conversation_row(ui, model, &ws, row, selected_root.as_deref());
}
}
});
}
fn selected_conversation_root(model: &AppModel) -> Option<String> {
let agent = model.focused_agent()?.agent_id.clone();
let tree = model.focused_tree()?;
crate::nav::convs::root_of(&tree.agents, &agent)
}
fn conversation_row(
ui: &mut egui::Ui,
model: &mut AppModel,
ws: &std::path::Path,
row: &ConvRow,
selected_root: Option<&str>,
) {
ui.horizontal(|ui| {
let (glyph, color) = theme::state_badge(row.state);
let badge_color = if row.streaming {
let time = ui.ctx().input(|i| i.time);
ui.ctx().request_repaint_after(theme::PULSE_REPAINT_DELAY);
theme::pulse(time)
} else {
color
};
ui.colored_label(badge_color, glyph);
if row.uncertain {
ui.colored_label(theme::SIGIL, "?");
}
if row.attention > 0 {
ui.colored_label(theme::BRAZEN, format!("⚑{}", row.attention));
}
let selected = selected_root == Some(row.root_id.as_str());
let title = if row.preview.is_empty() {
row.root_id.clone()
} else {
row.preview.clone()
};
if ui.selectable_label(selected, title).clicked() {
model.focus_agent(ws, &row.root_id);
}
ui.weak(age_label(row.age_secs));
if row.has_children() {
ui.weak(format!("({})", row.members));
}
if let Some(ball) = &row.ball {
super::conv_ball::row_badge(ui, ball);
}
});
}
fn balls_section(
ui: &mut egui::Ui,
model: &mut AppModel,
state: &mut ShellState,
lernie: &Cli,
bl: &Cli,
) {
let collapsed = model.is_collapsed("balls");
let arrow = if collapsed { "▶" } else { "▼" };
if ui
.selectable_label(false, format!("{arrow} balls"))
.clicked()
{
model.set_collapsed("balls", !collapsed);
}
if collapsed {
return;
}
ui.indent("balls", |ui| {
let mut show_internal = model.show_internal();
if ui.checkbox(&mut show_internal, "internal clones").changed() {
model.set_show_internal(show_internal);
}
super::start_pane::startable(ui, model, &mut state.start, lernie, bl);
if let Some(ws) = model.focused_workspace().map(std::path::Path::to_path_buf) {
for ball in model.ws_balls(&ws) {
match &ball.badge {
Some(b) => ui.weak(format!("{} · {b}", ball.id)),
None => ui.weak(&ball.id),
};
}
}
});
}
fn now_unix() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX))
}