feat(agents): named and labelled agents with optional ANSI color
test / unit (pull_request) Successful in 32s
test / integration (pull_request) Successful in 43s
lint / lint (push) Successful in 1m32s
prd-number / assign-numbers (push) Successful in 17s
test / unit (push) Successful in 29s
Update Quality Badges / update-badges (push) Successful in 1m18s
test / integration (push) Successful in 45s
test / unit (pull_request) Successful in 32s
test / integration (pull_request) Successful in 43s
lint / lint (push) Successful in 1m32s
prd-number / assign-numbers (push) Successful in 17s
test / unit (push) Successful in 29s
Update Quality Badges / update-badges (push) Successful in 1m18s
test / integration (push) Successful in 45s
Chunk 1 (schema + storage): BottleSpec, ActiveAgent, and BottleMetadata gain label and color fields. Both docker and smolmachines backends persist them to metadata.json on prepare and surface them in enumerate_active_agents(). AgentProvider.provision_plan() passes label/color through to the Claude provider, which injects them into claude.json so claude-code displays the session name and color in its header. Codex provider accepts and ignores the knobs. Chunk 2 (curses modal + display): cmd_start presents a two-step curses modal — first edit the label (first keystroke replaces the pre-fill), then optionally pick a color. cli list active renders label with ANSI escape codes when the terminal supports it, falling back to agent_name when no label is set. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit was merged in pull request #184.
This commit is contained in:
@@ -139,6 +139,8 @@ class AgentProvider(ABC):
|
|||||||
forward_host_credentials: bool = False,
|
forward_host_credentials: bool = False,
|
||||||
host_env: dict[str, str] | None = None,
|
host_env: dict[str, str] | None = None,
|
||||||
trusted_project_path: str = "",
|
trusted_project_path: str = "",
|
||||||
|
label: str = "",
|
||||||
|
color: str = "",
|
||||||
) -> AgentProvisionPlan:
|
) -> AgentProvisionPlan:
|
||||||
"""Build the declarative AgentProvisionPlan for one launch.
|
"""Build the declarative AgentProvisionPlan for one launch.
|
||||||
Backends call this during `prepare` and consume the result as
|
Backends call this during `prepare` and consume the result as
|
||||||
@@ -326,6 +328,8 @@ def agent_provision_plan(
|
|||||||
forward_host_credentials: bool = False,
|
forward_host_credentials: bool = False,
|
||||||
host_env: dict[str, str] | None = None,
|
host_env: dict[str, str] | None = None,
|
||||||
trusted_project_path: str = "",
|
trusted_project_path: str = "",
|
||||||
|
label: str = "",
|
||||||
|
color: str = "",
|
||||||
) -> AgentProvisionPlan:
|
) -> AgentProvisionPlan:
|
||||||
"""Back-compat shim — `prepare` callers stay the same; the work
|
"""Back-compat shim — `prepare` callers stay the same; the work
|
||||||
now lives on the provider plugin."""
|
now lives on the provider plugin."""
|
||||||
@@ -338,6 +342,8 @@ def agent_provision_plan(
|
|||||||
forward_host_credentials=forward_host_credentials,
|
forward_host_credentials=forward_host_credentials,
|
||||||
host_env=host_env,
|
host_env=host_env,
|
||||||
trusted_project_path=trusted_project_path,
|
trusted_project_path=trusted_project_path,
|
||||||
|
label=label,
|
||||||
|
color=color,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ class BottleSpec:
|
|||||||
# (`cli.py resume <identity>`) sets this to continue an existing
|
# (`cli.py resume <identity>`) sets this to continue an existing
|
||||||
# bottle's state. Empty string for a fresh `start`.
|
# bottle's state. Empty string for a fresh `start`.
|
||||||
identity: str = ""
|
identity: str = ""
|
||||||
|
label: str = ""
|
||||||
|
color: str = ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -189,6 +191,8 @@ class ActiveAgent:
|
|||||||
agent_name: str # from metadata.json; "?" if missing
|
agent_name: str # from metadata.json; "?" if missing
|
||||||
started_at: str # ISO 8601 from metadata.json; "" if missing
|
started_at: str # ISO 8601 from metadata.json; "" if missing
|
||||||
services: tuple[str, ...] # alphabetical
|
services: tuple[str, ...] # alphabetical
|
||||||
|
label: str = ""
|
||||||
|
color: str = ""
|
||||||
|
|
||||||
|
|
||||||
class Bottle(ABC):
|
class Bottle(ABC):
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ class BottleMetadata:
|
|||||||
# for state dirs written before PRD 0040; callers default to "docker"
|
# for state dirs written before PRD 0040; callers default to "docker"
|
||||||
# for backward compatibility.
|
# for backward compatibility.
|
||||||
backend: str = ""
|
backend: str = ""
|
||||||
|
label: str = ""
|
||||||
|
color: str = ""
|
||||||
|
|
||||||
|
|
||||||
def metadata_path(identity: str) -> Path:
|
def metadata_path(identity: str) -> Path:
|
||||||
@@ -144,6 +146,8 @@ def read_metadata(identity: str) -> BottleMetadata | None:
|
|||||||
started_at=str(raw_typed.get("started_at", "")),
|
started_at=str(raw_typed.get("started_at", "")),
|
||||||
compose_project=str(raw_typed.get("compose_project", "")),
|
compose_project=str(raw_typed.get("compose_project", "")),
|
||||||
backend=str(raw_typed.get("backend", "")),
|
backend=str(raw_typed.get("backend", "")),
|
||||||
|
label=str(raw_typed.get("label", "")),
|
||||||
|
color=str(raw_typed.get("color", "")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ def enumerate_active() -> list[ActiveAgent]:
|
|||||||
agent_name=metadata.agent_name if metadata else "?",
|
agent_name=metadata.agent_name if metadata else "?",
|
||||||
started_at=metadata.started_at if metadata else "",
|
started_at=metadata.started_at if metadata else "",
|
||||||
services=tuple(sorted(services)),
|
services=tuple(sorted(services)),
|
||||||
|
label=metadata.label if metadata else "",
|
||||||
|
color=metadata.color if metadata else "",
|
||||||
))
|
))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ def resolve_plan(
|
|||||||
started_at=datetime.now(timezone.utc).isoformat(),
|
started_at=datetime.now(timezone.utc).isoformat(),
|
||||||
compose_project=f"bot-bottle-{slug}",
|
compose_project=f"bot-bottle-{slug}",
|
||||||
backend="docker",
|
backend="docker",
|
||||||
|
label=spec.label,
|
||||||
|
color=spec.color,
|
||||||
))
|
))
|
||||||
# Clear any leftover preserve marker from a prior capability-block
|
# Clear any leftover preserve marker from a prior capability-block
|
||||||
# so this fresh launch can be cleaned up at session-end unless
|
# so this fresh launch can be cleaned up at session-end unless
|
||||||
@@ -191,6 +193,8 @@ def resolve_plan(
|
|||||||
auth_token=provider.auth_token,
|
auth_token=provider.auth_token,
|
||||||
host_env=dict(os.environ),
|
host_env=dict(os.environ),
|
||||||
trusted_project_path=workspace_plan.workdir,
|
trusted_project_path=workspace_plan.workdir,
|
||||||
|
label=spec.label,
|
||||||
|
color=spec.color,
|
||||||
)
|
)
|
||||||
guest_env = dict(agent_provision.guest_env)
|
guest_env = dict(agent_provision.guest_env)
|
||||||
for key, val in agent_provision.env_vars.items():
|
for key, val in agent_provision.env_vars.items():
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ def enumerate_active() -> list[ActiveAgent]:
|
|||||||
agent_name=metadata.agent_name if metadata else "?",
|
agent_name=metadata.agent_name if metadata else "?",
|
||||||
started_at=metadata.started_at if metadata else "",
|
started_at=metadata.started_at if metadata else "",
|
||||||
services=services_by_slug.get(slug, ()),
|
services=services_by_slug.get(slug, ()),
|
||||||
|
label=metadata.label if metadata else "",
|
||||||
|
color=metadata.color if metadata else "",
|
||||||
))
|
))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ def resolve_plan(
|
|||||||
started_at=datetime.now(timezone.utc).isoformat(),
|
started_at=datetime.now(timezone.utc).isoformat(),
|
||||||
compose_project="",
|
compose_project="",
|
||||||
backend="smolmachines",
|
backend="smolmachines",
|
||||||
|
label=spec.label,
|
||||||
|
color=spec.color,
|
||||||
))
|
))
|
||||||
|
|
||||||
subnet, gateway, bundle_ip = smolmachines_bundle_subnet(slug)
|
subnet, gateway, bundle_ip = smolmachines_bundle_subnet(slug)
|
||||||
@@ -136,6 +138,8 @@ def resolve_plan(
|
|||||||
auth_token=provider.auth_token,
|
auth_token=provider.auth_token,
|
||||||
host_env=dict(os.environ),
|
host_env=dict(os.environ),
|
||||||
trusted_project_path=workspace_plan.workdir,
|
trusted_project_path=workspace_plan.workdir,
|
||||||
|
label=spec.label,
|
||||||
|
color=spec.color,
|
||||||
)
|
)
|
||||||
merged_guest_env = dict(agent_provision.guest_env)
|
merged_guest_env = dict(agent_provision.guest_env)
|
||||||
for key, val in agent_provision.env_vars.items():
|
for key, val in agent_provision.env_vars.items():
|
||||||
|
|||||||
+40
-5
@@ -3,12 +3,47 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from ..backend import enumerate_active_agents
|
from ..backend import enumerate_active_agents
|
||||||
from ..manifest import Manifest
|
from ..manifest import Manifest
|
||||||
from ._common import PROG, USER_CWD
|
from ._common import PROG, USER_CWD
|
||||||
|
|
||||||
|
_ANSI_COLOR_CODES: dict[str, str] = {
|
||||||
|
"black": "\033[30m",
|
||||||
|
"red": "\033[31m",
|
||||||
|
"green": "\033[32m",
|
||||||
|
"yellow": "\033[33m",
|
||||||
|
"blue": "\033[34m",
|
||||||
|
"magenta": "\033[35m",
|
||||||
|
"cyan": "\033[36m",
|
||||||
|
"white": "\033[37m",
|
||||||
|
"bright-black": "\033[90m",
|
||||||
|
"bright-red": "\033[91m",
|
||||||
|
"bright-green": "\033[92m",
|
||||||
|
"bright-yellow": "\033[93m",
|
||||||
|
"bright-blue": "\033[94m",
|
||||||
|
"bright-magenta": "\033[95m",
|
||||||
|
"bright-cyan": "\033[96m",
|
||||||
|
"bright-white": "\033[97m",
|
||||||
|
}
|
||||||
|
_ANSI_RESET = "\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def _ansi_label(text: str, color: str) -> str:
|
||||||
|
if not color:
|
||||||
|
return text
|
||||||
|
if not sys.stdout.isatty():
|
||||||
|
return text
|
||||||
|
term = os.environ.get("TERM", "")
|
||||||
|
if term in ("dumb", ""):
|
||||||
|
return text
|
||||||
|
code = _ANSI_COLOR_CODES.get(color)
|
||||||
|
if not code:
|
||||||
|
return text
|
||||||
|
return f"{code}{text}{_ANSI_RESET}"
|
||||||
|
|
||||||
|
|
||||||
def cmd_list(argv: list[str]) -> int:
|
def cmd_list(argv: list[str]) -> int:
|
||||||
parser = argparse.ArgumentParser(prog=f"{PROG} list", add_help=True)
|
parser = argparse.ArgumentParser(prog=f"{PROG} list", add_help=True)
|
||||||
@@ -27,11 +62,11 @@ def cmd_list(argv: list[str]) -> int:
|
|||||||
if not active:
|
if not active:
|
||||||
print("no active bot-bottle bottles", file=sys.stderr)
|
print("no active bot-bottle bottles", file=sys.stderr)
|
||||||
return 0
|
return 0
|
||||||
# One line per bottle: `<backend>\t<slug>\t<agent>\t<status>`.
|
# One line per bottle: `<backend>\t<slug>\t<label>\t<services>`.
|
||||||
# Tab-separated keeps the format stable for shell pipelines;
|
# Tab-separated keeps the format stable for shell pipelines.
|
||||||
# the dashboard renders the same data through its own
|
|
||||||
# formatter.
|
|
||||||
for b in active:
|
for b in active:
|
||||||
services = ",".join(b.services) if b.services else "-"
|
services = ",".join(b.services) if b.services else "-"
|
||||||
print(f"{b.backend_name}\t{b.slug}\t{b.agent_name}\t{services}")
|
display_name = b.label if b.label else b.agent_name
|
||||||
|
colored_name = _ansi_label(display_name, b.color)
|
||||||
|
print(f"{b.backend_name}\t{b.slug}\t{colored_name}\t{services}")
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -80,11 +80,15 @@ def cmd_start(argv: list[str]) -> int:
|
|||||||
if backend_name is None:
|
if backend_name is None:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
label, color = tui.name_color_modal(default_label=agent_name)
|
||||||
|
|
||||||
spec = BottleSpec(
|
spec = BottleSpec(
|
||||||
manifest=manifest,
|
manifest=manifest,
|
||||||
agent_name=agent_name,
|
agent_name=agent_name,
|
||||||
copy_cwd=args.cwd,
|
copy_cwd=args.cwd,
|
||||||
user_cwd=USER_CWD,
|
user_cwd=USER_CWD,
|
||||||
|
label=label,
|
||||||
|
color=color,
|
||||||
)
|
)
|
||||||
return _launch_bottle(
|
return _launch_bottle(
|
||||||
spec,
|
spec,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
Exposed surface:
|
Exposed surface:
|
||||||
|
|
||||||
filter_select(items, *, title="", tty_path="/dev/tty") -> str | None
|
filter_select(items, *, title="", tty_path="/dev/tty") -> str | None
|
||||||
|
name_color_modal(default_label, *, tty_path="/dev/tty") -> (str, str)
|
||||||
|
|
||||||
Opens /dev/tty directly so the picker works even when stdout/stdin are
|
Opens /dev/tty directly so the picker works even when stdout/stdin are
|
||||||
redirected. Returns the selected item or None on cancel.
|
redirected. Returns the selected item or None on cancel.
|
||||||
@@ -218,3 +219,219 @@ def _addstr_safe(screen: Any, row: int, col: int, text: str, attr: int = curses.
|
|||||||
screen.addstr(row, col, text, attr)
|
screen.addstr(row, col, text, attr)
|
||||||
except curses.error:
|
except curses.error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# name_color_modal — two-step label + color picker
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_ANSI_COLORS = [
|
||||||
|
"red", "green", "blue", "yellow", "magenta", "cyan", "white", "black",
|
||||||
|
"bright-red", "bright-green", "bright-blue", "bright-yellow",
|
||||||
|
"bright-magenta", "bright-cyan", "bright-white", "bright-black",
|
||||||
|
]
|
||||||
|
|
||||||
|
_CURSES_COLOR_MAP: dict[str, int] = {
|
||||||
|
"black": curses.COLOR_BLACK,
|
||||||
|
"red": curses.COLOR_RED,
|
||||||
|
"green": curses.COLOR_GREEN,
|
||||||
|
"yellow": curses.COLOR_YELLOW,
|
||||||
|
"blue": curses.COLOR_BLUE,
|
||||||
|
"magenta": curses.COLOR_MAGENTA,
|
||||||
|
"cyan": curses.COLOR_CYAN,
|
||||||
|
"white": curses.COLOR_WHITE,
|
||||||
|
}
|
||||||
|
|
||||||
|
_COLOR_NONE = "(none)"
|
||||||
|
|
||||||
|
|
||||||
|
def name_color_modal(
|
||||||
|
default_label: str,
|
||||||
|
*,
|
||||||
|
tty_path: str = "/dev/tty",
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
"""Present a two-step curses modal: first edit the agent label,
|
||||||
|
then optionally pick a color.
|
||||||
|
|
||||||
|
Returns ``(label, color)`` where ``color`` is one of the 16 ANSI
|
||||||
|
color name strings or ``""`` for no color. Falls back to
|
||||||
|
``(default_label, "")`` on any error (terminal too small, not a tty).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
tty_fd = open(tty_path, "r+b", buffering=0) # pylint: disable=consider-using-with
|
||||||
|
except OSError:
|
||||||
|
return default_label, ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
fd_dup = os.dup(tty_fd.fileno())
|
||||||
|
return _run_name_color(default_label, tty_fd=fd_dup)
|
||||||
|
except Exception: # noqa: BLE001 # pylint: disable=broad-exception-caught
|
||||||
|
return default_label, ""
|
||||||
|
finally:
|
||||||
|
tty_fd.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _run_name_color(default_label: str, *, tty_fd: int) -> tuple[str, str]:
|
||||||
|
import io
|
||||||
|
orig_stdin = sys.__stdin__
|
||||||
|
orig_stdout = sys.__stdout__
|
||||||
|
try:
|
||||||
|
tty_text = io.TextIOWrapper(io.FileIO(tty_fd, mode="r+"), write_through=True)
|
||||||
|
sys.__stdin__ = tty_text # type: ignore[assignment]
|
||||||
|
sys.__stdout__ = tty_text # type: ignore[assignment]
|
||||||
|
os.environ.setdefault("TERM", "xterm-256color")
|
||||||
|
|
||||||
|
screen = curses.initscr()
|
||||||
|
curses.noecho()
|
||||||
|
curses.cbreak()
|
||||||
|
screen.keypad(True)
|
||||||
|
try:
|
||||||
|
label = _label_step(screen, default_label)
|
||||||
|
color = _color_step(screen, label)
|
||||||
|
finally:
|
||||||
|
screen.keypad(False)
|
||||||
|
curses.nocbreak()
|
||||||
|
curses.echo()
|
||||||
|
curses.endwin()
|
||||||
|
finally:
|
||||||
|
sys.__stdin__ = orig_stdin # type: ignore[assignment]
|
||||||
|
sys.__stdout__ = orig_stdout # type: ignore[assignment]
|
||||||
|
return label, color
|
||||||
|
|
||||||
|
|
||||||
|
def _label_step(screen: Any, default_label: str) -> str:
|
||||||
|
"""Step 1: edit the label. First printable key replaces the
|
||||||
|
pre-fill; subsequent keys append. Enter confirms."""
|
||||||
|
text = default_label
|
||||||
|
replaced = False # True once the user has typed their first char
|
||||||
|
|
||||||
|
while True:
|
||||||
|
_render_label(screen, text)
|
||||||
|
try:
|
||||||
|
key = screen.getch()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return default_label
|
||||||
|
|
||||||
|
if key in (curses.KEY_ENTER, _KEY_ENTER_ALT, ord("\r")):
|
||||||
|
return text.strip() or default_label
|
||||||
|
|
||||||
|
if key in (curses.KEY_BACKSPACE, _KEY_BACKSPACE_WIN, 127):
|
||||||
|
if replaced:
|
||||||
|
text = text[:-1]
|
||||||
|
else:
|
||||||
|
text = ""
|
||||||
|
replaced = True
|
||||||
|
|
||||||
|
elif 32 <= key <= 126:
|
||||||
|
if not replaced:
|
||||||
|
text = chr(key)
|
||||||
|
replaced = True
|
||||||
|
else:
|
||||||
|
text += chr(key)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_label(screen: Any, text: str) -> None:
|
||||||
|
screen.erase()
|
||||||
|
rows, cols = screen.getmaxyx()
|
||||||
|
sep = "─" * min(cols - 1, 40)
|
||||||
|
_addstr_safe(screen, 0, 0, "Name agent", curses.A_BOLD)
|
||||||
|
_addstr_safe(screen, 1, 0, sep)
|
||||||
|
_addstr_safe(screen, 2, 0, text[:cols - 1], curses.A_REVERSE)
|
||||||
|
_addstr_safe(screen, 3, 0, sep)
|
||||||
|
if rows > 5:
|
||||||
|
_addstr_safe(screen, 5, 0, "[any key] edit [Enter] confirm", curses.A_DIM)
|
||||||
|
screen.refresh()
|
||||||
|
|
||||||
|
|
||||||
|
def _color_step(screen: Any, confirmed_label: str) -> str:
|
||||||
|
"""Step 2: pick a color from the list, or skip."""
|
||||||
|
items = [_COLOR_NONE] + _ANSI_COLORS
|
||||||
|
cursor = 0
|
||||||
|
|
||||||
|
# Initialise color pairs once; index 0 = none, 1..16 = palette.
|
||||||
|
color_attrs = _init_color_pairs()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
_render_color(screen, items, cursor, confirmed_label, color_attrs)
|
||||||
|
try:
|
||||||
|
key = screen.getch()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if key in (ord("q"), _KEY_ESC):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if key in (curses.KEY_ENTER, _KEY_ENTER_ALT, ord("\r")):
|
||||||
|
chosen = items[cursor]
|
||||||
|
return "" if chosen == _COLOR_NONE else chosen
|
||||||
|
|
||||||
|
if key in (curses.KEY_UP, ord("k")) and cursor > 0:
|
||||||
|
cursor -= 1
|
||||||
|
elif key in (curses.KEY_DOWN, ord("j")) and cursor < len(items) - 1:
|
||||||
|
cursor += 1
|
||||||
|
|
||||||
|
|
||||||
|
def _init_color_pairs() -> dict[str, int]:
|
||||||
|
"""Return {color_name: curses_attr} for the palette items."""
|
||||||
|
attrs: dict[str, int] = {_COLOR_NONE: curses.A_NORMAL}
|
||||||
|
try:
|
||||||
|
curses.start_color()
|
||||||
|
curses.use_default_colors()
|
||||||
|
pair_idx = 2 # pair 1 reserved for other uses
|
||||||
|
for name in _ANSI_COLORS:
|
||||||
|
base = name.replace("bright-", "")
|
||||||
|
fg = _CURSES_COLOR_MAP.get(base, curses.COLOR_WHITE)
|
||||||
|
try:
|
||||||
|
curses.init_pair(pair_idx, fg, -1)
|
||||||
|
attr = curses.color_pair(pair_idx)
|
||||||
|
if name.startswith("bright-"):
|
||||||
|
attr |= curses.A_BOLD
|
||||||
|
attrs[name] = attr
|
||||||
|
pair_idx += 1
|
||||||
|
except curses.error:
|
||||||
|
attrs[name] = curses.A_NORMAL
|
||||||
|
except curses.error:
|
||||||
|
for name in _ANSI_COLORS:
|
||||||
|
attrs[name] = curses.A_NORMAL
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
def _render_color(
|
||||||
|
screen: Any,
|
||||||
|
items: list[str],
|
||||||
|
cursor: int,
|
||||||
|
confirmed_label: str,
|
||||||
|
color_attrs: dict[str, int],
|
||||||
|
) -> None:
|
||||||
|
screen.erase()
|
||||||
|
rows, cols = screen.getmaxyx()
|
||||||
|
sep = "─" * min(cols - 1, 40)
|
||||||
|
_addstr_safe(screen, 0, 0, "Name agent", curses.A_BOLD)
|
||||||
|
_addstr_safe(screen, 1, 0, sep)
|
||||||
|
_addstr_safe(screen, 2, 0, confirmed_label[:cols - 1])
|
||||||
|
_addstr_safe(screen, 3, 0, sep)
|
||||||
|
_addstr_safe(screen, 4, 0, "Color (optional)", curses.A_BOLD)
|
||||||
|
|
||||||
|
list_start = 5
|
||||||
|
list_rows = rows - list_start - 2
|
||||||
|
scroll = max(0, cursor - list_rows + 1)
|
||||||
|
visible = items[scroll: scroll + list_rows]
|
||||||
|
|
||||||
|
for idx, name in enumerate(visible):
|
||||||
|
abs_idx = scroll + idx
|
||||||
|
row = list_start + idx
|
||||||
|
if row >= rows - 2:
|
||||||
|
break
|
||||||
|
prefix = "> " if abs_idx == cursor else " "
|
||||||
|
attr = color_attrs.get(name, curses.A_NORMAL)
|
||||||
|
if abs_idx == cursor:
|
||||||
|
attr |= curses.A_REVERSE
|
||||||
|
_addstr_safe(screen, row, 0, (prefix + name)[:cols - 1], attr)
|
||||||
|
|
||||||
|
_addstr_safe(screen, rows - 2, 0, sep)
|
||||||
|
_addstr_safe(
|
||||||
|
screen, rows - 1, 0,
|
||||||
|
"[↑↓/jk] move [Enter] select [Esc/q] skip",
|
||||||
|
curses.A_DIM,
|
||||||
|
)
|
||||||
|
screen.refresh()
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ class ClaudeAgentProvider(AgentProvider):
|
|||||||
forward_host_credentials: bool = False,
|
forward_host_credentials: bool = False,
|
||||||
host_env: dict[str, str] | None = None,
|
host_env: dict[str, str] | None = None,
|
||||||
trusted_project_path: str = "",
|
trusted_project_path: str = "",
|
||||||
|
label: str = "",
|
||||||
|
color: str = "",
|
||||||
) -> AgentProvisionPlan:
|
) -> AgentProvisionPlan:
|
||||||
del forward_host_credentials, host_env # Codex-only knobs
|
del forward_host_credentials, host_env # Codex-only knobs
|
||||||
resolved_guest_env = dict(guest_env or {})
|
resolved_guest_env = dict(guest_env or {})
|
||||||
@@ -80,12 +82,17 @@ class ClaudeAgentProvider(AgentProvider):
|
|||||||
claude_config = state_dir / "claude.json"
|
claude_config = state_dir / "claude.json"
|
||||||
claude_projects = {guest_home: {"hasTrustDialogAccepted": True}}
|
claude_projects = {guest_home: {"hasTrustDialogAccepted": True}}
|
||||||
claude_projects[trusted_path] = {"hasTrustDialogAccepted": True}
|
claude_projects[trusted_path] = {"hasTrustDialogAccepted": True}
|
||||||
claude_config.write_text(json.dumps({
|
payload: dict[str, object] = {
|
||||||
"hasCompletedOnboarding": True,
|
"hasCompletedOnboarding": True,
|
||||||
"theme": "dark",
|
"theme": "dark",
|
||||||
"bypassPermissionsModeAccepted": True,
|
"bypassPermissionsModeAccepted": True,
|
||||||
"projects": claude_projects,
|
"projects": claude_projects,
|
||||||
}, indent=2) + "\n")
|
}
|
||||||
|
if label:
|
||||||
|
payload["name"] = label
|
||||||
|
if color:
|
||||||
|
payload["color"] = color
|
||||||
|
claude_config.write_text(json.dumps(payload, indent=2) + "\n")
|
||||||
claude_config.chmod(0o600)
|
claude_config.chmod(0o600)
|
||||||
files = (
|
files = (
|
||||||
AgentProvisionFile(claude_config, f"{guest_home}/.claude.json"),
|
AgentProvisionFile(claude_config, f"{guest_home}/.claude.json"),
|
||||||
|
|||||||
@@ -76,8 +76,10 @@ class CodexAgentProvider(AgentProvider):
|
|||||||
forward_host_credentials: bool = False,
|
forward_host_credentials: bool = False,
|
||||||
host_env: dict[str, str] | None = None,
|
host_env: dict[str, str] | None = None,
|
||||||
trusted_project_path: str = "",
|
trusted_project_path: str = "",
|
||||||
|
label: str = "",
|
||||||
|
color: str = "",
|
||||||
) -> AgentProvisionPlan:
|
) -> AgentProvisionPlan:
|
||||||
del auth_token # Claude-only knob
|
del auth_token, label, color # Claude-only knobs
|
||||||
resolved_guest_env = dict(guest_env or {})
|
resolved_guest_env = dict(guest_env or {})
|
||||||
trusted_path = trusted_project_path or guest_home
|
trusted_path = trusted_project_path or guest_home
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user