Skip to content
Closed
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
.agent/
.claude/worktrees/
demo.tape
.idea/
.idea/
.perles/
33 changes: 33 additions & 0 deletions scripts/install-abtop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail

script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd -- "$script_dir/.." && pwd)"
install_dir="${INSTALL_DIR:-$HOME/bin}"
install_path="${ABTOP_INSTALL_PATH:-$install_dir/abtop}"
target_dir="$repo_root/target"
built_binary="$target_dir/release/abtop"

if ! command -v cargo >/dev/null 2>&1; then
echo "error: cargo not found in PATH" >&2
exit 1
fi

cargo build --release --locked --manifest-path "$repo_root/Cargo.toml" --target-dir "$target_dir"

if [[ ! -x "$built_binary" ]]; then
echo "error: built binary missing: $built_binary" >&2
exit 1
fi

mkdir -p -- "$(dirname -- "$install_path")"
tmp="$(mktemp "${install_path}.tmp.XXXXXX")"
trap 'rm -f -- "$tmp"' EXIT

cp -- "$built_binary" "$tmp"
chmod 0755 "$tmp"
mv -f -- "$tmp" "$install_path"
trap - EXIT

echo "installed $install_path"
"$install_path" --version
209 changes: 209 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,54 @@ pub enum JumpOutcome {
NoOp,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NarrowTab {
Work,
Usage,
System,
}

impl NarrowTab {
pub const ALL: [Self; 3] = [Self::Work, Self::Usage, Self::System];

pub fn label(self) -> &'static str {
match self {
Self::Work => "Work",
Self::Usage => "Usage",
Self::System => "System",
}
}

pub fn shortcut(self) -> char {
match self {
Self::Work => 'w',
Self::Usage => 'u',
Self::System => 's',
}
}
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NarrowSection {
Sessions,
Projects,
Context,
Quota,
Tokens,
Ports,
Mcp,
}

impl NarrowSection {
pub fn tab(self) -> NarrowTab {
match self {
Self::Sessions | Self::Projects => NarrowTab::Work,
Self::Context | Self::Quota | Self::Tokens => NarrowTab::Usage,
Self::Ports | Self::Mcp => NarrowTab::System,
}
}
}

pub struct App {
pub sessions: Vec<AgentSession>,
pub selected: usize,
Expand Down Expand Up @@ -71,6 +119,9 @@ pub struct App {
pub show_ports: bool,
pub show_sessions: bool,
pub show_mcp: bool,
pub narrow_tab: NarrowTab,
pub active_narrow_section: Option<NarrowSection>,
pub maximized_narrow_section: Option<NarrowSection>,
/// MCP servers detected on the most recent tick (sourced from
/// MultiCollector). Populated regardless of `show_mcp` so panel
/// toggling doesn't cost a discovery roundtrip.
Expand Down Expand Up @@ -133,6 +184,9 @@ impl App {
show_ports: panels.ports,
show_sessions: panels.sessions,
show_mcp: panels.mcp,
narrow_tab: NarrowTab::Work,
active_narrow_section: Some(NarrowSection::Sessions),
maximized_narrow_section: None,
mcp_servers: Vec::new(),
mcp_suppress_sessions: true,
config_open: false,
Expand Down Expand Up @@ -177,6 +231,7 @@ impl App {
_ => return,
}
self.persist_panel_visibility();
self.clamp_narrow_tab();
}

/// Toggle whether mcp-server-owned rollouts are hidden from the
Expand Down Expand Up @@ -249,6 +304,153 @@ impl App {
_ => return,
}
self.persist_panel_visibility();
self.clamp_narrow_tab();
}

pub fn narrow_tab_visible(&self, tab: NarrowTab) -> bool {
match tab {
NarrowTab::Work => self.show_sessions || self.show_projects,
NarrowTab::Usage => self.show_context || self.show_quota || self.show_tokens,
NarrowTab::System => self.show_ports || self.show_mcp,
}
}

pub fn visible_narrow_tabs(&self) -> Vec<NarrowTab> {
NarrowTab::ALL
.into_iter()
.filter(|&tab| self.narrow_tab_visible(tab))
.collect()
}

pub fn active_narrow_tab(&self) -> Option<NarrowTab> {
if self.narrow_tab_visible(self.narrow_tab) {
Some(self.narrow_tab)
} else {
NarrowTab::ALL
.into_iter()
.find(|&tab| self.narrow_tab_visible(tab))
}
}

pub fn set_narrow_tab(&mut self, tab: NarrowTab) {
if self.narrow_tab_visible(tab) {
self.narrow_tab = tab;
self.clamp_narrow_section();
}
}

pub fn select_next_narrow_tab(&mut self) {
let tabs = self.visible_narrow_tabs();
if tabs.is_empty() {
return;
}
let current = self.active_narrow_tab().unwrap_or(tabs[0]);
let pos = tabs.iter().position(|&tab| tab == current).unwrap_or(0);
self.narrow_tab = tabs[(pos + 1) % tabs.len()];
self.clamp_narrow_section();
}

pub fn select_prev_narrow_tab(&mut self) {
let tabs = self.visible_narrow_tabs();
if tabs.is_empty() {
return;
}
let current = self.active_narrow_tab().unwrap_or(tabs[0]);
let pos = tabs.iter().position(|&tab| tab == current).unwrap_or(0);
self.narrow_tab = tabs[(pos + tabs.len() - 1) % tabs.len()];
self.clamp_narrow_section();
}

fn clamp_narrow_tab(&mut self) {
if let Some(tab) = self.active_narrow_tab() {
self.narrow_tab = tab;
}
self.clamp_narrow_section();
}

pub fn narrow_section_visible(&self, section: NarrowSection) -> bool {
match section {
NarrowSection::Sessions => self.show_sessions,
NarrowSection::Projects => self.show_projects,
NarrowSection::Context => self.show_context,
NarrowSection::Quota => self.show_quota,
NarrowSection::Tokens => self.show_tokens,
NarrowSection::Ports => self.show_ports,
NarrowSection::Mcp => self.show_mcp,
}
}

pub fn visible_narrow_sections(&self, tab: NarrowTab) -> Vec<NarrowSection> {
let sections: &[NarrowSection] = match tab {
NarrowTab::Work => &[NarrowSection::Sessions, NarrowSection::Projects],
NarrowTab::Usage => &[
NarrowSection::Context,
NarrowSection::Quota,
NarrowSection::Tokens,
],
NarrowTab::System => &[NarrowSection::Ports, NarrowSection::Mcp],
};
sections
.iter()
.copied()
.filter(|&section| self.narrow_section_visible(section))
.collect()
}

pub fn active_narrow_section(&self) -> Option<NarrowSection> {
let tab = self.active_narrow_tab()?;
if let Some(section) = self.active_narrow_section {
if section.tab() == tab && self.narrow_section_visible(section) {
return Some(section);
}
}
self.visible_narrow_sections(tab).into_iter().next()
}

pub fn set_active_narrow_section(&mut self, section: NarrowSection) {
if self.narrow_section_visible(section) {
self.narrow_tab = section.tab();
self.active_narrow_section = Some(section);
self.clamp_narrow_section();
}
}

pub fn maximized_narrow_section(&self) -> Option<NarrowSection> {
let section = self.maximized_narrow_section?;
if self.active_narrow_tab() == Some(section.tab()) && self.narrow_section_visible(section) {
Some(section)
} else {
None
}
}

pub fn toggle_narrow_section_zoom(&mut self, section: NarrowSection) {
if !self.narrow_section_visible(section) {
return;
}
self.set_active_narrow_section(section);
self.maximized_narrow_section = if self.maximized_narrow_section() == Some(section) {
None
} else {
Some(section)
};
}

pub fn maximize_active_narrow_section(&mut self) {
if let Some(section) = self.active_narrow_section() {
self.maximized_narrow_section = Some(section);
}
}

pub fn restore_narrow_sections(&mut self) {
self.maximized_narrow_section = None;
}

fn clamp_narrow_section(&mut self) {
self.active_narrow_section = self.active_narrow_section();
if self.maximized_narrow_section().is_none() {
self.maximized_narrow_section = None;
}
}

pub fn toggle_timeline(&mut self) {
Expand Down Expand Up @@ -474,6 +676,12 @@ impl App {
}
}

pub fn select_session(&mut self, index: usize) {
if index < self.sessions.len() && self.visible_indices().contains(&index) {
self.selected = index;
}
}

pub fn kill_selected(&mut self) {
if self.sessions.is_empty() {
return;
Expand Down Expand Up @@ -898,6 +1106,7 @@ mod tests {
children: vec![],
initial_prompt: String::new(),
first_assistant_text: String::new(),
chat_messages: vec![],
tool_calls: vec![],
pending_since_ms: 0,
thinking_since_ms: 0,
Expand Down
Loading