Skip to content

Commit 2e2319a

Browse files
committed
Add vp attach and extend vp stop to accept container names
1 parent 9e907a1 commit 2e2319a

8 files changed

Lines changed: 511 additions & 13 deletions

File tree

docs/agents/index.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -244,20 +244,41 @@ vp list --running # shows only running agents
244244
vp list --json # machine-readable output
245245
```
246246

247-
Stop a specific agent or all agents:
247+
Stop a specific agent, a single container, or all agents:
248248

249249
```bash
250-
vp stop claude # graceful stop (10 s timeout)
251-
vp stop claude -f # force stop immediately
252-
vp stop --all # stop every VibePod container
250+
vp stop claude # stop every container for the `claude` agent
251+
vp stop vibepod-claude-a1b2c3d4 # stop one specific container (from `vp list`)
252+
vp stop claude -f # force stop immediately
253+
vp stop --all # stop every VibePod container
253254
```
254255

256+
The argument is resolved as an agent name/shortcut first; anything else is looked up as a container name or ID. Only VibePod-managed containers can be stopped this way.
257+
255258
### Caveats
256259

257260
- **`auto_remove` (default: `true`)** — By default, containers are automatically removed when they stop. This means you cannot restart a stopped detached container; you need to `vp run` again. Set `auto_remove: false` in your [configuration](../configuration.md) if you want stopped containers to persist.
258-
- **No built-in re-attach** — VibePod does not currently have a command to re-attach your terminal to a detached container. Use `docker attach <container>` or `docker exec -it <container> bash` directly.
259261
- **Session logging** — Sessions started with `--detach` are not recorded in the VibePod session log since VibePod does not capture the interactive I/O. If you need session logging, run without `--detach`.
260262

263+
## Reattaching a terminal
264+
265+
Closing the terminal window that runs `vp run` does **not** stop the container — the agent keeps running in the background under Docker. This is by design: the container's lifecycle is tied to Docker, not to your shell. Use it as a feature when you want to keep a long-running session alive across terminal restarts.
266+
267+
To rejoin a running container:
268+
269+
```bash
270+
vp list --running # find the container name
271+
vp attach <container> # reattach your terminal
272+
```
273+
274+
If exactly one managed container is running you can omit the name:
275+
276+
```bash
277+
vp attach
278+
```
279+
280+
`vp attach` only works for containers that are already running and managed by VibePod. When you are done, close the terminal to leave it running, or stop it explicitly with `vp stop <container>`, `vp stop <agent>`, or `vp stop --all`.
281+
261282
## Connecting to a Docker Compose network
262283

263284
When your workspace contains a `docker-compose.yml` or `compose.yml`, VibePod detects it and offers to connect the agent container to an existing network so it can reach your running services.

docs/quickstart.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ VibePod will:
3535

3636
Press **Ctrl+C** to stop the container when you are done.
3737

38+
!!! note
39+
Closing the terminal window does not stop the container — the agent keeps running in the background. Use `vp list --running` to see it and `vp attach <container>` to rejoin the session. See [Reattaching a terminal](agents/index.md#reattaching-a-terminal) for details.
40+
3841
## Shortcuts
3942

4043
You can start agents with either the full name or a single-letter shortcut:

src/vibepod/cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import typer
99

10-
from vibepod.commands import config, doctor, list_cmd, logs, proxy, run, stop, update
10+
from vibepod.commands import attach, config, doctor, list_cmd, logs, proxy, run, stop, update
1111
from vibepod.constants import AGENT_SHORTCUTS, SUPPORTED_AGENTS
1212

1313
app = typer.Typer(
@@ -22,6 +22,7 @@
2222
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
2323
)(run.run)
2424
app.command(name="stop")(stop.stop)
25+
app.command(name="attach")(attach.attach)
2526
app.command(name="list")(list_cmd.list_agents)
2627
app.command(name="version")(update.version)
2728

src/vibepod/commands/attach.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Attach command implementation."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Annotated
6+
7+
import typer
8+
9+
from vibepod.constants import CONTAINER_LABEL_MANAGED, EXIT_DOCKER_NOT_RUNNING
10+
from vibepod.core.docker import DockerClientError, DockerManager
11+
from vibepod.utils.console import error, info, warning
12+
13+
14+
def attach(
15+
container: Annotated[
16+
str | None,
17+
typer.Argument(
18+
help=(
19+
"Container name or ID to attach to (see `vp list`). "
20+
"Omit when exactly one managed container is running."
21+
),
22+
),
23+
] = None,
24+
) -> None:
25+
"""Reattach your terminal to a running VibePod-managed container.
26+
27+
Use this to rejoin an agent session after the terminal that started it
28+
was closed. Find candidate containers with `vp list`.
29+
"""
30+
try:
31+
manager = DockerManager()
32+
except DockerClientError as exc:
33+
error(str(exc))
34+
raise typer.Exit(EXIT_DOCKER_NOT_RUNNING) from exc
35+
36+
if container is None:
37+
running = [
38+
c
39+
for c in manager.list_managed()
40+
if getattr(c, "status", "") == "running"
41+
and (getattr(c, "labels", {}) or {}).get("vibepod.agent")
42+
]
43+
if not running:
44+
error(
45+
"No running VibePod agent containers to attach to. "
46+
"Start one with `vp run`, or check `vp list --running`."
47+
)
48+
raise typer.Exit(1)
49+
if len(running) > 1:
50+
names = ", ".join(sorted(c.name for c in running))
51+
error(
52+
f"Multiple running containers: {names}. "
53+
"Specify one explicitly: `vp attach <container>`."
54+
)
55+
raise typer.Exit(1)
56+
target = running[0]
57+
else:
58+
try:
59+
target = manager.get_container(container)
60+
except DockerClientError as exc:
61+
error(str(exc))
62+
raise typer.Exit(1) from exc
63+
64+
labels = getattr(target, "labels", {}) or {}
65+
if labels.get(CONTAINER_LABEL_MANAGED) != "true":
66+
error(f"Container '{container}' is not managed by VibePod.")
67+
raise typer.Exit(1)
68+
if getattr(target, "status", "") != "running":
69+
error(
70+
f"Container '{container}' is not running "
71+
f"(status: {getattr(target, 'status', 'unknown')})."
72+
)
73+
raise typer.Exit(1)
74+
75+
agent = (getattr(target, "labels", {}) or {}).get("vibepod.agent", "agent")
76+
info(f"Attaching to {target.name} ({agent})")
77+
warning(
78+
f"Close the terminal to leave it running, or stop it with `vp stop {target.name}`."
79+
)
80+
try:
81+
manager.attach_interactive(target)
82+
except DockerClientError as exc:
83+
error(str(exc))
84+
raise typer.Exit(1) from exc

src/vibepod/commands/stop.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,30 @@
77
import typer
88

99
from vibepod.constants import EXIT_DOCKER_NOT_RUNNING
10+
from vibepod.core.agents import resolve_agent_name
1011
from vibepod.core.docker import DockerClientError, DockerManager
1112
from vibepod.utils.console import error, success
1213

1314

1415
def stop(
15-
agent: Annotated[str | None, typer.Argument(help="Agent to stop")] = None,
16+
target: Annotated[
17+
str | None,
18+
typer.Argument(
19+
help=(
20+
"Agent name/shortcut (stops all its containers) or a container "
21+
"name or ID from `vp list` (stops just that container)."
22+
),
23+
),
24+
] = None,
1625
all_containers: Annotated[
1726
bool,
1827
typer.Option("-a", "--all", help="Stop all VibePod managed containers"),
1928
] = False,
2029
force: Annotated[bool, typer.Option("-f", "--force", help="Force stop")] = False,
2130
) -> None:
22-
"""Stop one agent container, or all managed containers."""
23-
if not all_containers and agent is None:
24-
raise typer.BadParameter("Provide an AGENT or use --all")
31+
"""Stop an agent's containers, a specific container, or all managed containers."""
32+
if not all_containers and target is None:
33+
raise typer.BadParameter("Provide an AGENT or CONTAINER, or use --all")
2534

2635
try:
2736
manager = DockerManager()
@@ -34,6 +43,16 @@ def stop(
3443
success(f"Stopped {stopped} container(s)")
3544
return
3645

37-
assert agent is not None
38-
stopped = manager.stop_agent(agent=agent, force=force)
39-
success(f"Stopped {stopped} container(s) for {agent}")
46+
assert target is not None
47+
resolved_agent = resolve_agent_name(target)
48+
if resolved_agent is not None:
49+
stopped = manager.stop_agent(agent=resolved_agent, force=force)
50+
success(f"Stopped {stopped} container(s) for {resolved_agent}")
51+
return
52+
53+
try:
54+
container = manager.stop_container(target, force=force)
55+
except DockerClientError as exc:
56+
error(str(exc))
57+
raise typer.Exit(1) from exc
58+
success(f"Stopped {container.name}")

src/vibepod/core/docker.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ def connect_network(self, container: Any, network_name: str) -> None:
124124
except APIError as exc:
125125
raise DockerClientError(f"Failed to connect to network {network_name}: {exc}") from exc
126126

127+
def get_container(self, name_or_id: str) -> Any:
128+
try:
129+
return self.client.containers.get(name_or_id)
130+
except NotFound as exc:
131+
raise DockerClientError(f"Container '{name_or_id}' not found") from exc
132+
except APIError as exc:
133+
raise DockerClientError(f"Failed to look up container '{name_or_id}': {exc}") from exc
134+
except DockerException as exc:
135+
raise DockerClientError(f"Failed to look up container '{name_or_id}': {exc}") from exc
136+
127137
def resolve_launch_command(self, image: str, command: list[str] | None) -> list[str]:
128138
"""Resolve the full executable argv for a container start."""
129139
try:
@@ -225,6 +235,25 @@ def stop_agent(self, agent: str, force: bool = False) -> int:
225235
stopped += 1
226236
return stopped
227237

238+
def stop_container(self, name_or_id: str, force: bool = False) -> Any:
239+
container = self.get_container(name_or_id)
240+
labels = getattr(container, "labels", {}) or {}
241+
if labels.get(CONTAINER_LABEL_MANAGED) != "true":
242+
raise DockerClientError(
243+
f"Container '{name_or_id}' is not managed by VibePod; refusing to stop."
244+
)
245+
try:
246+
container.stop(timeout=0 if force else 10)
247+
except APIError as exc:
248+
raise DockerClientError(
249+
f"Failed to stop container '{name_or_id}': {exc}"
250+
) from exc
251+
except DockerException as exc:
252+
raise DockerClientError(
253+
f"Failed to stop container '{name_or_id}': {exc}"
254+
) from exc
255+
return container
256+
228257
def stop_all(self, force: bool = False) -> int:
229258
stopped = 0
230259
for container in self.list_managed(all_containers=True):

0 commit comments

Comments
 (0)