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
90 changes: 57 additions & 33 deletions src/vibepod/commands/list_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,35 @@
from vibepod.utils.console import console, error


def _running_map(containers: list[Any]) -> dict[str, Any]:
by_agent: dict[str, Any] = {}
def _configured_agent_rows() -> list[dict[str, str]]:
rows: list[dict[str, str]] = []
for agent in SUPPORTED_AGENTS:
rows.append(
{
"short": get_agent_shortcut(agent) or "-",
"agent": agent,
"image": DEFAULT_IMAGES[agent],
}
)
return rows


def _running_rows(containers: list[Any]) -> list[dict[str, str]]:
rows: list[dict[str, str]] = []
for container in containers:
agent = container.labels.get("vibepod.agent")
if agent and agent not in by_agent:
by_agent[agent] = container
return by_agent
labels = getattr(container, "labels", {}) or {}
agent = labels.get("vibepod.agent")
status = getattr(container, "status", "-")
if not agent or status != "running":
continue
rows.append(
{
"agent": agent,
"container": getattr(container, "name", "-"),
"context": labels.get("vibepod.workspace", "-"),
}
)
return sorted(rows, key=lambda row: (row["agent"], row["container"]))


def list_agents(
Expand All @@ -38,36 +60,38 @@ def list_agents(
raise typer.Exit(EXIT_DOCKER_NOT_RUNNING) from exc
containers = []

mapped = _running_map(containers)
rows: list[dict[str, str]] = []
for agent in SUPPORTED_AGENTS:
container = mapped.get(agent)
shortcut = get_agent_shortcut(agent) or "-"
rows.append(
{
"short": shortcut,
"agent": agent,
"image": DEFAULT_IMAGES[agent],
"status": container.status if container else "stopped",
"workspace": container.labels.get("vibepod.workspace", "-") if container else "-",
}
)

if running:
rows = [r for r in rows if r["status"] == "running"]
running_rows = _running_rows(containers)
configured_rows = _configured_agent_rows()

if as_json:
import json

print(json.dumps(rows, indent=2))
payload: dict[str, Any] = {"running": running_rows}
if not running:
payload["agents"] = configured_rows
print(json.dumps(payload, indent=2))
return

running_table = Table(title="Running Agents", title_justify="left")
running_table.add_column("AGENT", style="cyan")
running_table.add_column("CONTAINER", style="magenta")
running_table.add_column("CONTEXT")

if running_rows:
for row in running_rows:
running_table.add_row(row["agent"], row["container"], row["context"])
console.print(running_table)
else:
console.print("No running agents.")

if running:
return

table = Table(title="VibePod Agents")
table.add_column("SHORT", style="green")
table.add_column("AGENT", style="cyan")
table.add_column("IMAGE", style="magenta")
table.add_column("STATUS")
table.add_column("WORKSPACE")
for row in rows:
table.add_row(row["short"], row["agent"], row["image"], row["status"], row["workspace"])
console.print(table)
console.print()
reference_table = Table(title="Configured Agents", title_justify="left")
reference_table.add_column("SHORT", style="green")
reference_table.add_column("AGENT", style="cyan")
reference_table.add_column("BASE IMAGE", style="magenta")
for row in configured_rows:
reference_table.add_row(row["short"], row["agent"], row["image"])
console.print(reference_table)
46 changes: 45 additions & 1 deletion tests/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,53 @@ def list_managed(self, all_containers: bool = True): # noqa: ARG002
result = runner.invoke(app, ["list", "--json"])
assert result.exit_code == 0

rows = json.loads(result.stdout)
payload = json.loads(result.stdout)
assert payload["running"] == []

rows = payload["agents"]
by_agent = {row["agent"]: row for row in rows}
assert set(by_agent.keys()) == set(SUPPORTED_AGENTS)

for shortcut, agent in AGENT_SHORTCUTS.items():
assert by_agent[agent]["short"] == shortcut


def test_list_running_json_preserves_multiple_instances(monkeypatch) -> None:
class _FakeContainer:
def __init__(self, name: str, status: str, labels: dict[str, str]) -> None:
self.name = name
self.status = status
self.labels = labels

class _FakeDockerManager:
def list_managed(self, all_containers: bool = True): # noqa: ARG002
return [
_FakeContainer(
"vibepod-claude-1",
"running",
{"vibepod.agent": "claude", "vibepod.workspace": "/workspace/a"},
),
_FakeContainer(
"vibepod-claude-2",
"running",
{"vibepod.agent": "claude", "vibepod.workspace": "/workspace/b"},
),
_FakeContainer(
"vibepod-codex-1",
"exited",
{"vibepod.agent": "codex", "vibepod.workspace": "/workspace/c"},
),
]

monkeypatch.setattr(list_cmd, "DockerManager", _FakeDockerManager)

result = runner.invoke(app, ["list", "--running", "--json"])
assert result.exit_code == 0

payload = json.loads(result.stdout)
assert "agents" not in payload
rows = payload["running"]
assert len(rows) == 2
assert [row["container"] for row in rows] == ["vibepod-claude-1", "vibepod-claude-2"]
assert {row["context"] for row in rows} == {"/workspace/a", "/workspace/b"}
assert all(set(row) == {"agent", "container", "context"} for row in rows)