Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions crates/cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5337,6 +5337,87 @@ mod tests {
(app, dir, server)
}

async fn empty_app() -> (App, tempfile::TempDir, tokio::task::JoinHandle<()>) {
use tokio::net::UnixListener;
let dir = tempfile::tempdir().expect("tempdir");
let sock = dir.path().join("agentd.sock");
let listener = UnixListener::bind(&sock).expect("bind mock daemon");
let server = tokio::spawn(async move {
loop {
if listener.accept().await.is_err() {
break;
}
}
});
let client = Client::connect(&sock).await.expect("client connects");
let app = test_app(client, Vec::new());
(app, dir, server)
}

fn rendered_text(buffer: &ratatui::buffer::Buffer) -> String {
let mut text = String::new();
for y in 0..buffer.area.height {
for x in 0..buffer.area.width {
text.push_str(buffer.cell((x, y)).map(|c| c.symbol()).unwrap_or(" "));
}
text.push('\n');
}
text
}

#[tokio::test]
async fn empty_tui_renders_welcome_and_modeline_hint() {
let (mut app, _dir, server) = empty_app().await;
let backend = ratatui::backend::TestBackend::new(120, 36);
let mut terminal = ratatui::Terminal::new(backend).expect("terminal");

terminal
.draw(|f| crate::ui::render(f, &mut app))
.expect("draw");

let screen = rendered_text(terminal.backend().buffer());
assert!(
screen.contains("Welcome to agentd"),
"missing welcome:\n{screen}"
);
assert!(
screen.contains("C-x C-f"),
"missing create shortcut:\n{screen}"
);
assert!(
screen.contains("new: C-x C-f help: ? palette: C-x x"),
"missing modeline hint:\n{screen}"
);
server.abort();
}

#[tokio::test]
async fn help_modal_includes_getting_started_concepts() {
let (mut app, _dir, server) = empty_app().await;
app.help_visible = true;
let backend = ratatui::backend::TestBackend::new(120, 40);
let mut terminal = ratatui::Terminal::new(backend).expect("terminal");

terminal
.draw(|f| crate::ui::render(f, &mut app))
.expect("draw");

let screen = rendered_text(terminal.backend().buffer());
assert!(
screen.contains("getting started"),
"missing section:\n{screen}"
);
assert!(
screen.contains("A session is one live task"),
"missing session concept:\n{screen}"
);
assert!(
screen.contains("A harness is the runtime"),
"missing harness concept:\n{screen}"
);
server.abort();
}

/// A plain keystroke forwarded to the PTY must set
/// `skip_redraw_after_event` — its visible effect arrives later
/// as PTY output, so the immediate top-of-loop draw would be a
Expand Down
77 changes: 75 additions & 2 deletions crates/cli/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2179,6 +2179,10 @@ fn render_detail(f: &mut Frame, area: Rect, app: &mut App) {
f.render_widget(block, area);
clear_pane_side_borders(f, area, app);

if summary.is_none() && group.is_none() {
render_empty_session_state(f, inner, app);
return;
}
if let Some(g) = app.selected_group() {
render_group_overview(f, inner, app, g);
render_main_transition(f, inner, app);
Expand All @@ -2191,6 +2195,61 @@ fn render_detail(f: &mut Frame, area: Rect, app: &mut App) {
render_main_transition(f, inner, app);
}

fn render_empty_session_state(f: &mut Frame, area: Rect, app: &App) {
let lines = vec![
Line::from(Span::styled(
"Welcome to agentd",
Style::default()
.fg(app.theme.text)
.add_modifier(Modifier::BOLD),
)),
Line::raw(""),
Line::from(Span::styled(
"Start with a session. Sessions are the live terminals agentd tracks.",
Style::default().fg(app.theme.dim),
)),
Line::raw(""),
Line::from(vec![
Span::styled(" C-x C-f", Style::default().fg(app.theme.accent)),
Span::raw(" create a session"),
]),
Line::from(vec![
Span::styled(" C-x x", Style::default().fg(app.theme.accent)),
Span::raw(" open the command palette"),
]),
Line::from(vec![
Span::styled(" ?", Style::default().fg(app.theme.accent)),
Span::raw(" show shortcuts and concepts"),
]),
Line::raw(""),
Line::from(Span::styled(
"CLI examples:",
Style::default().fg(app.theme.text),
)),
Line::from(Span::styled(
" agent new zarvis \"inspect this repo\"",
Style::default().fg(app.theme.dim),
)),
Line::from(Span::styled(
" agent new shell",
Style::default().fg(app.theme.dim),
)),
];
let para = Paragraph::new(lines).wrap(Wrap { trim: false });
f.render_widget(para, centered_rect(area, 72, 12));
}

fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
let width = width.min(area.width);
let height = height.min(area.height);
Rect {
x: area.x + area.width.saturating_sub(width) / 2,
y: area.y + area.height.saturating_sub(height) / 2,
width,
height,
}
}

fn render_group_overview(
f: &mut Frame,
area: Rect,
Expand Down Expand Up @@ -3025,8 +3084,14 @@ fn render_modeline(f: &mut Frame, area: Rect, app: &App) {
} else {
String::new()
};
let status = app.status.as_ref().map(|(m, _)| m.as_str()).unwrap_or("");
let empty_hint = if s.is_none() && app.list_items().is_empty() && status.is_empty() {
"new: C-x C-f help: ? palette: C-x x"
} else {
""
};
let modeline = format!(
" agentd focus:{focus} {sel} {model} {remote}{automode}{scrollback}{chord}{status}{conn} ",
" agentd focus:{focus} {sel} {model} {remote}{automode}{scrollback}{chord}{empty_hint}{status}{conn} ",
focus = focus_label,
scrollback = scrollback_label,
automode = automode_badge,
Expand All @@ -3044,7 +3109,8 @@ fn render_modeline(f: &mut Frame, area: Rect, app: &App) {
} else {
format!("({}) ", app.chord_label)
},
status = app.status.as_ref().map(|(m, _)| m.as_str()).unwrap_or(""),
empty_hint = empty_hint,
status = status,
);
let para = Paragraph::new(modeline).style(
Style::default()
Expand Down Expand Up @@ -3249,6 +3315,13 @@ fn render_help(f: &mut Frame, area: Rect, theme: &Theme) -> Rect {
const HELP_TEXT: &str = "
emacs keymap (default; AGENTD_KEYMAP=vim for vim profile)

getting started
A session is one live task or terminal that agentd keeps in the list.
A harness is the runtime for a session: zarvis, codex, claude, or shell.
The left pane selects sessions; the right pane shows the selected session.
Use C-x C-f to create a session, then choose a harness.
Use C-x x for the command palette when you forget a shortcut.

focus + view
C-x o switch focus (list ↔ view)
RET (on list) focus the selected session's view
Expand Down
Loading