From 39811c9b32d8db691226c5c880e198e6777319c9 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 9 Jun 2026 03:39:54 +0000 Subject: [PATCH 1/4] feat: forward agent display identity to prompts --- bot_bottle/agent_provider.py | 1 + bot_bottle/contrib/claude/agent_provider.py | 25 ++++++++++++- bot_bottle/contrib/codex/agent_provider.py | 27 ++++++++++++-- tests/unit/test_agent_provider.py | 40 +++++++++++++++++++++ tests/unit/test_contrib_claude_provider.py | 15 ++++++++ tests/unit/test_contrib_codex_provider.py | 16 +++++++++ 6 files changed, 121 insertions(+), 3 deletions(-) diff --git a/bot_bottle/agent_provider.py b/bot_bottle/agent_provider.py index 2bca283..c3d6ea9 100644 --- a/bot_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -107,6 +107,7 @@ class AgentProvisionPlan: instance_name: str prompt_file: Path guest_env: dict[str, str] + has_prompt: bool = False env_vars: dict[str, str] = field(default_factory=dict) dirs: tuple[AgentProvisionDir, ...] = () files: tuple[AgentProvisionFile, ...] = () diff --git a/bot_bottle/contrib/claude/agent_provider.py b/bot_bottle/contrib/claude/agent_provider.py index 1085176..9b1bd5d 100644 --- a/bot_bottle/contrib/claude/agent_provider.py +++ b/bot_bottle/contrib/claude/agent_provider.py @@ -38,6 +38,27 @@ def _skills_dir(guest_home: str) -> str: def _prompt_path(guest_home: str) -> str: return f"{guest_home}/.bot-bottle-prompt.txt" + +def _display_identity_prompt(label: str, color: str) -> str: + lines: list[str] = [] + if label: + lines.append(f"Name: {label}") + if color: + lines.append(f"Color: {color}") + if not lines: + return "" + return "Bot-bottle agent display identity:\n" + "\n".join(lines) + + +def _prepend_display_identity(prompt_file: Path, label: str, color: str) -> bool: + identity = _display_identity_prompt(label, color) + original = prompt_file.read_text() if prompt_file.exists() else "" + if not identity: + return bool(original) + prompt_file.write_text(f"{identity}\n\n{original}" if original else f"{identity}\n") + return True + + _RUNTIME = AgentProviderRuntime( template="claude", command="claude", @@ -106,6 +127,7 @@ class ClaudeAgentProvider(AgentProvider): env_vars["CLAUDE_CODE_OAUTH_TOKEN"] = "egress-placeholder" hidden_env_names = frozenset({"CLAUDE_CODE_OAUTH_TOKEN"}) + has_prompt = _prepend_display_identity(prompt_file, label, color) return AgentProvisionPlan( template=_RUNTIME.template, command=_RUNTIME.command, @@ -117,6 +139,7 @@ class ClaudeAgentProvider(AgentProvider): prompt_file=prompt_file, env_vars=env_vars, guest_env=resolved_guest_env, + has_prompt=has_prompt, files=files, egress_routes=egress_routes, hidden_env_names=hidden_env_names, @@ -158,7 +181,7 @@ class ClaudeAgentProvider(AgentProvider): user="root", ) agent = plan.spec.manifest.agents[plan.spec.agent_name] - return prompt_path if agent.prompt else None + return prompt_path if plan.agent_provision.has_prompt or agent.prompt else None def provision(self, plan: "BottlePlan", bottle: "Bottle") -> None: """Apply the claude-side declarative provision steps from diff --git a/bot_bottle/contrib/codex/agent_provider.py b/bot_bottle/contrib/codex/agent_provider.py index 4b2c99b..894e50a 100644 --- a/bot_bottle/contrib/codex/agent_provider.py +++ b/bot_bottle/contrib/codex/agent_provider.py @@ -46,6 +46,27 @@ def _skills_dir(guest_home: str) -> str: def _prompt_path(guest_home: str) -> str: return f"{guest_home}/.bot-bottle-prompt.txt" + +def _display_identity_prompt(label: str, color: str) -> str: + lines: list[str] = [] + if label: + lines.append(f"Name: {label}") + if color: + lines.append(f"Color: {color}") + if not lines: + return "" + return "Bot-bottle agent display identity:\n" + "\n".join(lines) + + +def _prepend_display_identity(prompt_file: Path, label: str, color: str) -> bool: + identity = _display_identity_prompt(label, color) + original = prompt_file.read_text() if prompt_file.exists() else "" + if not identity: + return bool(original) + prompt_file.write_text(f"{identity}\n\n{original}" if original else f"{identity}\n") + return True + + _RUNTIME = AgentProviderRuntime( template="codex", command="codex", @@ -77,7 +98,7 @@ class CodexAgentProvider(AgentProvider): label: str = "", color: str = "", ) -> AgentProvisionPlan: - del auth_token, label, color # Claude-only knobs + del auth_token # Claude-only knob resolved_guest_env = dict(guest_env or {}) guest_home = self.guest_home trusted_path = trusted_project_path or guest_home @@ -143,6 +164,7 @@ class CodexAgentProvider(AgentProvider): "guest, but Codex did not accept it" ))) + has_prompt = _prepend_display_identity(prompt_file, label, color) return AgentProvisionPlan( template=_RUNTIME.template, command=_RUNTIME.command, @@ -154,6 +176,7 @@ class CodexAgentProvider(AgentProvider): prompt_file=prompt_file, env_vars=env_vars, guest_env=resolved_guest_env, + has_prompt=has_prompt, dirs=tuple(dirs), files=tuple(files), pre_copy=tuple(pre_copy), @@ -198,7 +221,7 @@ class CodexAgentProvider(AgentProvider): user="root", ) agent = plan.spec.manifest.agents[plan.spec.agent_name] - return prompt_path if agent.prompt else None + return prompt_path if plan.agent_provision.has_prompt or agent.prompt else None def provision(self, plan: "BottlePlan", bottle: "Bottle") -> None: """Apply the codex-side declarative provision steps from diff --git a/tests/unit/test_agent_provider.py b/tests/unit/test_agent_provider.py index a23094b..6e05ac2 100644 --- a/tests/unit/test_agent_provider.py +++ b/tests/unit/test_agent_provider.py @@ -62,6 +62,26 @@ class TestAgentProviderRuntime(unittest.TestCase): config = Path(tmp, "codex-config.toml").read_text() self.assertIn('[projects."/home/node/workspace"]', config) + def test_codex_injects_name_and_color_into_prompt(self): + with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: + prompt_file = Path(tmp) / "prompt.txt" + prompt_file.write_text("Existing instructions.\n") + plan = build_agent_provision_plan( + template="codex", + dockerfile="", + state_dir=Path(tmp), + instance_name="bot-bottle-test", + prompt_file=prompt_file, + label="review-api", + color="bright-cyan", + ) + prompt = prompt_file.read_text() + self.assertTrue(plan.has_prompt) + self.assertIn("Bot-bottle agent display identity:", prompt) + self.assertIn("Name: review-api", prompt) + self.assertIn("Color: bright-cyan", prompt) + self.assertTrue(prompt.endswith("Existing instructions.\n")) + def test_codex_forward_host_credentials_adds_auth_and_verify(self): with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: home = Path(tmp) / "host-codex" @@ -126,6 +146,26 @@ class TestAgentProviderRuntime(unittest.TestCase): self.assertIn("/home/node", config["projects"]) self.assertIn("/home/node/workspace", config["projects"]) + def test_claude_injects_name_and_color_into_prompt(self): + with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: + prompt_file = Path(tmp) / "prompt.txt" + prompt_file.write_text("Existing instructions.\n") + plan = build_agent_provision_plan( + template="claude", + dockerfile="", + state_dir=Path(tmp), + instance_name="bot-bottle-test", + prompt_file=prompt_file, + label="research-ui", + color="green", + ) + prompt = prompt_file.read_text() + self.assertTrue(plan.has_prompt) + self.assertIn("Bot-bottle agent display identity:", prompt) + self.assertIn("Name: research-ui", prompt) + self.assertIn("Color: green", prompt) + self.assertTrue(prompt.endswith("Existing instructions.\n")) + def test_codex_forward_host_credentials_populates_egress_routes(self): with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: home = Path(tmp) / "host-codex" diff --git a/tests/unit/test_contrib_claude_provider.py b/tests/unit/test_contrib_claude_provider.py index df37fec..08a0813 100644 --- a/tests/unit/test_contrib_claude_provider.py +++ b/tests/unit/test_contrib_claude_provider.py @@ -127,6 +127,21 @@ class TestClaudeProvisionPrompt(unittest.TestCase): self.assertIsNone(r) bottle.cp_in.assert_called_once() + def test_returns_path_when_provider_prompt_has_identity(self): + bottle = _make_bottle() + provision = AgentProvisionPlan( + template="claude", command="claude", prompt_mode="append_file", + image="", dockerfile="", guest_home="/home/node", + instance_name="bot-bottle-demo-abc12", + prompt_file=Path("/tmp/prompt.txt"), + guest_env={}, + has_prompt=True, + ) + r = ClaudeAgentProvider().provision_prompt( + _plan(agent_prompt="", agent_provision=provision), bottle, + ) + self.assertEqual("/home/node/.bot-bottle-prompt.txt", r) + def test_chowns_to_node_after_copy(self): bottle = _make_bottle() ClaudeAgentProvider().provision_prompt(_plan(), bottle) diff --git a/tests/unit/test_contrib_codex_provider.py b/tests/unit/test_contrib_codex_provider.py index e0ab6fc..cf847bb 100644 --- a/tests/unit/test_contrib_codex_provider.py +++ b/tests/unit/test_contrib_codex_provider.py @@ -130,6 +130,22 @@ class TestCodexProvisionPrompt(unittest.TestCase): self.assertIsNone(r) bottle.cp_in.assert_called_once() + def test_returns_path_when_provider_prompt_has_identity(self): + bottle = _make_bottle() + provision = AgentProvisionPlan( + template="codex", command="codex", + prompt_mode="read_prompt_file", + image="", dockerfile="", guest_home="/home/node", + instance_name="bot-bottle-demo-abc12", + prompt_file=Path("/tmp/prompt.txt"), + guest_env={}, + has_prompt=True, + ) + r = CodexAgentProvider().provision_prompt( + _plan(agent_prompt="", agent_provision=provision), bottle, + ) + self.assertEqual("/home/node/.bot-bottle-prompt.txt", r) + class TestCodexProvisionSkills(unittest.TestCase): def test_noop_when_agent_has_no_skills(self): -- 2.52.0 From d02226aab9ea630aa3fbd872b4125ab9b4c2c28a Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 9 Jun 2026 00:00:19 -0400 Subject: [PATCH 2/4] feat: forward agent style via native CLI config and terminal title Replace prompt-injection for display identity with native UI wiring: - Claude: writes a statusline shell script + custom theme JSON, wired up via settings.json so label/color show in the status bar and theme - Codex: writes [tui] block into codex-config.toml (status_line, terminal_title, dark-ansi theme) - Both backends set the terminal title via ANSI OSC 0 escape before exec-ing the agent when a label is present Co-Authored-By: Claude Sonnet 4.6 --- bot_bottle/backend/docker/bottle.py | 14 ++- bot_bottle/backend/docker/launch.py | 1 + bot_bottle/backend/smolmachines/bottle.py | 14 ++- bot_bottle/backend/smolmachines/launch.py | 1 + bot_bottle/contrib/claude/agent_provider.py | 129 ++++++++++++++++---- bot_bottle/contrib/codex/agent_provider.py | 31 ++--- tests/unit/test_agent_provider.py | 21 ++-- tests/unit/test_contrib_claude_provider.py | 33 ++++- tests/unit/test_contrib_codex_provider.py | 25 +++- 9 files changed, 204 insertions(+), 65 deletions(-) diff --git a/bot_bottle/backend/docker/bottle.py b/bot_bottle/backend/docker/bottle.py index 7294051..81ab4a1 100644 --- a/bot_bottle/backend/docker/bottle.py +++ b/bot_bottle/backend/docker/bottle.py @@ -3,6 +3,7 @@ from __future__ import annotations import subprocess +import shlex from typing import Callable from typing import cast @@ -22,12 +23,14 @@ class DockerBottle(Bottle): *, agent_command: str = "claude", agent_prompt_mode: PromptMode = "append_file", + terminal_title: str = "", ): self.name = container self._teardown = teardown self.prompt_path = prompt_path_in_container self._agent_prompt_mode = agent_prompt_mode self.agent_command = agent_command + self.terminal_title = terminal_title self.agent_provider_template = ( "codex" if agent_command == "codex" else "claude" ) @@ -47,9 +50,14 @@ class DockerBottle(Bottle): return cmd def exec_agent(self, argv: list[str], *, tty: bool = True) -> int: - return subprocess.run( - self.agent_argv(argv, tty=tty), check=False, - ).returncode + agent_argv = self.agent_argv(argv, tty=tty) + if self.terminal_title and tty: + shell_script = ( + f"printf '\\033]0;%s\\007' {shlex.quote(self.terminal_title)}; " + f"exec {shlex.join(agent_argv)}" + ) + return subprocess.run(["sh", "-lc", shell_script], check=False).returncode + return subprocess.run(agent_argv, check=False).returncode def exec(self, script: str, *, user: str = "node") -> ExecResult: # Pipe via stdin to `sh -s` so the caller never has to worry diff --git a/bot_bottle/backend/docker/launch.py b/bot_bottle/backend/docker/launch.py index 11380d4..517fdf4 100644 --- a/bot_bottle/backend/docker/launch.py +++ b/bot_bottle/backend/docker/launch.py @@ -175,6 +175,7 @@ def launch( None, agent_command=plan.agent_command, agent_prompt_mode=plan.agent_prompt_mode, + terminal_title=plan.spec.label or plan.spec.agent_name, ) bottle.prompt_path = provision(plan, bottle) diff --git a/bot_bottle/backend/smolmachines/bottle.py b/bot_bottle/backend/smolmachines/bottle.py index 81e0ee6..1538992 100644 --- a/bot_bottle/backend/smolmachines/bottle.py +++ b/bot_bottle/backend/smolmachines/bottle.py @@ -20,6 +20,7 @@ from __future__ import annotations import subprocess import sys import time +import shlex from typing import Mapping, cast from ...agent_provider import PromptMode, prompt_args @@ -68,6 +69,7 @@ class SmolmachinesBottle(Bottle): guest_env: Mapping[str, str] | None = None, agent_command: str = "claude", agent_prompt_mode: PromptMode = "append_file", + terminal_title: str = "", ) -> None: self.name = machine_name # In-VM path to the agent's prompt file. None when the @@ -81,6 +83,7 @@ class SmolmachinesBottle(Bottle): self._guest_env = dict(guest_env or {}) self._agent_prompt_mode = agent_prompt_mode self.agent_command = agent_command + self.terminal_title = terminal_title self.agent_provider_template = ( "codex" if agent_command == "codex" else "claude" ) @@ -128,9 +131,14 @@ class SmolmachinesBottle(Bottle): UID switches via `runuser -u node --` (not `-l`) so we avoid login-shell wiring. HOME / USER come from `smolvm -e` instead, which sets them on the process env.""" - return subprocess.run( - self.agent_argv(argv, tty=tty), check=False, - ).returncode + agent_argv = self.agent_argv(argv, tty=tty) + if self.terminal_title and tty: + shell_script = ( + f"printf '\\033]0;%s\\007' {shlex.quote(self.terminal_title)}; " + f"exec {shlex.join(agent_argv)}" + ) + return subprocess.run(["sh", "-lc", shell_script], check=False).returncode + return subprocess.run(agent_argv, check=False).returncode # smolvm/libkrun can SIGKILL an otherwise-normal exec during # early-VM provisioning. Retry once after a short settle so diff --git a/bot_bottle/backend/smolmachines/launch.py b/bot_bottle/backend/smolmachines/launch.py index cdcb843..7337f3b 100644 --- a/bot_bottle/backend/smolmachines/launch.py +++ b/bot_bottle/backend/smolmachines/launch.py @@ -103,6 +103,7 @@ def launch( guest_env=plan.guest_env, agent_command=plan.agent_command, agent_prompt_mode=plan.agent_prompt_mode, + terminal_title=plan.spec.label or plan.spec.agent_name, ) bottle.prompt_path = provision(plan, bottle) diff --git a/bot_bottle/contrib/claude/agent_provider.py b/bot_bottle/contrib/claude/agent_provider.py index 9b1bd5d..113849b 100644 --- a/bot_bottle/contrib/claude/agent_provider.py +++ b/bot_bottle/contrib/claude/agent_provider.py @@ -17,9 +17,11 @@ from typing import TYPE_CHECKING from ...agent_provider import ( AgentProvider, AgentProviderRuntime, + AgentProvisionDir, AgentProvisionFile, AgentProvisionPlan, ) +from ...backend.docker import util as docker_mod from ...egress import EgressRoute from ...log import die, info, warn @@ -39,24 +41,68 @@ def _prompt_path(guest_home: str) -> str: return f"{guest_home}/.bot-bottle-prompt.txt" -def _display_identity_prompt(label: str, color: str) -> str: - lines: list[str] = [] - if label: - lines.append(f"Name: {label}") - if color: - lines.append(f"Color: {color}") - if not lines: - return "" - return "Bot-bottle agent display identity:\n" + "\n".join(lines) +_STATUS_LINE_COLORS = { + "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", +} + +_CLAUDE_THEME_COLORS = { + "black": "black", + "red": "red", + "green": "green", + "yellow": "yellow", + "blue": "blue", + "magenta": "magenta", + "cyan": "cyan", + "white": "white", + "bright-black": "blackBright", + "bright-red": "redBright", + "bright-green": "greenBright", + "bright-yellow": "yellowBright", + "bright-blue": "blueBright", + "bright-magenta": "magentaBright", + "bright-cyan": "cyanBright", + "bright-white": "whiteBright", +} -def _prepend_display_identity(prompt_file: Path, label: str, color: str) -> bool: - identity = _display_identity_prompt(label, color) - original = prompt_file.read_text() if prompt_file.exists() else "" - if not identity: - return bool(original) - prompt_file.write_text(f"{identity}\n\n{original}" if original else f"{identity}\n") - return True +def _status_line_script(label: str, color: str) -> str: + if not label: + return "#!/bin/sh\nprintf '\\n'\n" + label_q = shlex.quote(label) + if color and color in _STATUS_LINE_COLORS: + return ( + "#!/bin/sh\n" + f"printf '%b%s%b\\n' '{_STATUS_LINE_COLORS[color]}' {label_q} '\\033[0m'\n" + ) + return f"#!/bin/sh\nprintf '%s\\n' {label_q}\n" + + +def _custom_theme_payload(color: str) -> dict[str, object] | None: + theme_color = _CLAUDE_THEME_COLORS.get(color) + if not theme_color: + return None + return { + "name": f"Bot-bottle {color}", + "base": "dark", + "overrides": { + "claude": f"ansi:{theme_color}", + }, + } _RUNTIME = AgentProviderRuntime( @@ -99,6 +145,10 @@ class ClaudeAgentProvider(AgentProvider): "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1", "DISABLE_ERROR_REPORTING": "1", } + dirs = ( + AgentProvisionDir(f"{guest_home}/.claude"), + AgentProvisionDir(f"{guest_home}/.claude/themes"), + ) claude_config = state_dir / "claude.json" claude_projects = {guest_home: {"hasTrustDialogAccepted": True}} claude_projects[trusted_path] = {"hasTrustDialogAccepted": True} @@ -108,15 +158,45 @@ class ClaudeAgentProvider(AgentProvider): "bypassPermissionsModeAccepted": True, "projects": claude_projects, } - if label: - payload["name"] = label - if color: - payload["color"] = color claude_config.write_text(json.dumps(payload, indent=2) + "\n") claude_config.chmod(0o600) - files = ( + files = [ AgentProvisionFile(claude_config, f"{guest_home}/.claude.json"), - ) + ] + + claude_settings = state_dir / "claude-settings.json" + claude_settings_payload: dict[str, object] = {} + if label or color: + statusline_script = state_dir / "claude-statusline.sh" + statusline_script.write_text(_status_line_script(label, color)) + statusline_script.chmod(0o755) + files.append(AgentProvisionFile( + statusline_script, + f"{guest_home}/.claude/statusline.sh", + mode="755", + )) + claude_settings_payload["statusLine"] = { + "type": "command", + "command": "~/.claude/statusline.sh", + } + theme_payload = _custom_theme_payload(color) + if theme_payload is not None: + theme_name = f"bot-bottle-{docker_mod.slugify(label or color)}" + theme_file = state_dir / f"{theme_name}.json" + theme_file.write_text(json.dumps(theme_payload, indent=2) + "\n") + theme_file.chmod(0o644) + files.append(AgentProvisionFile( + theme_file, + f"{guest_home}/.claude/themes/{theme_name}.json", + )) + claude_settings_payload["theme"] = f"custom:{theme_name}" + if claude_settings_payload: + claude_settings.write_text(json.dumps(claude_settings_payload, indent=2) + "\n") + claude_settings.chmod(0o600) + files.append(AgentProvisionFile( + claude_settings, + f"{guest_home}/.claude/settings.json", + )) egress_routes = (EgressRoute( host="api.anthropic.com", auth_scheme="Bearer" if auth_token else "", @@ -127,7 +207,7 @@ class ClaudeAgentProvider(AgentProvider): env_vars["CLAUDE_CODE_OAUTH_TOKEN"] = "egress-placeholder" hidden_env_names = frozenset({"CLAUDE_CODE_OAUTH_TOKEN"}) - has_prompt = _prepend_display_identity(prompt_file, label, color) + has_prompt = prompt_file.exists() and bool(prompt_file.read_text()) return AgentProvisionPlan( template=_RUNTIME.template, command=_RUNTIME.command, @@ -140,7 +220,8 @@ class ClaudeAgentProvider(AgentProvider): env_vars=env_vars, guest_env=resolved_guest_env, has_prompt=has_prompt, - files=files, + dirs=dirs, + files=tuple(files), egress_routes=egress_routes, hidden_env_names=hidden_env_names, ) diff --git a/bot_bottle/contrib/codex/agent_provider.py b/bot_bottle/contrib/codex/agent_provider.py index 894e50a..8deb6d5 100644 --- a/bot_bottle/contrib/codex/agent_provider.py +++ b/bot_bottle/contrib/codex/agent_provider.py @@ -18,8 +18,8 @@ from ...agent_provider import ( CODEX_HOST_CREDENTIAL_HOSTS, AgentProvider, AgentProviderRuntime, - AgentProvisionCommand, AgentProvisionDir, + AgentProvisionCommand, AgentProvisionFile, AgentProvisionPlan, ) @@ -47,26 +47,6 @@ def _prompt_path(guest_home: str) -> str: return f"{guest_home}/.bot-bottle-prompt.txt" -def _display_identity_prompt(label: str, color: str) -> str: - lines: list[str] = [] - if label: - lines.append(f"Name: {label}") - if color: - lines.append(f"Color: {color}") - if not lines: - return "" - return "Bot-bottle agent display identity:\n" + "\n".join(lines) - - -def _prepend_display_identity(prompt_file: Path, label: str, color: str) -> bool: - identity = _display_identity_prompt(label, color) - original = prompt_file.read_text() if prompt_file.exists() else "" - if not identity: - return bool(original) - prompt_file.write_text(f"{identity}\n\n{original}" if original else f"{identity}\n") - return True - - _RUNTIME = AgentProviderRuntime( template="codex", command="codex", @@ -98,7 +78,7 @@ class CodexAgentProvider(AgentProvider): label: str = "", color: str = "", ) -> AgentProvisionPlan: - del auth_token # Claude-only knob + del auth_token, label, color # Claude-only / title-only knobs resolved_guest_env = dict(guest_env or {}) guest_home = self.guest_home trusted_path = trusted_project_path or guest_home @@ -122,6 +102,11 @@ class CodexAgentProvider(AgentProvider): config_file.write_text( f'[projects."{toml_path}"]\n' 'trust_level = "trusted"\n' + "\n" + "[tui]\n" + 'status_line = ["model", "cwd"]\n' + 'terminal_title = ["spinner", "project"]\n' + 'theme = "dark-ansi"\n' ) config_file.chmod(0o600) files.append(AgentProvisionFile(config_file, config_path)) @@ -164,7 +149,7 @@ class CodexAgentProvider(AgentProvider): "guest, but Codex did not accept it" ))) - has_prompt = _prepend_display_identity(prompt_file, label, color) + has_prompt = prompt_file.exists() and bool(prompt_file.read_text()) return AgentProvisionPlan( template=_RUNTIME.template, command=_RUNTIME.command, diff --git a/tests/unit/test_agent_provider.py b/tests/unit/test_agent_provider.py index 6e05ac2..0701619 100644 --- a/tests/unit/test_agent_provider.py +++ b/tests/unit/test_agent_provider.py @@ -62,7 +62,7 @@ class TestAgentProviderRuntime(unittest.TestCase): config = Path(tmp, "codex-config.toml").read_text() self.assertIn('[projects."/home/node/workspace"]', config) - def test_codex_injects_name_and_color_into_prompt(self): + def test_codex_writes_tui_settings_without_mutating_prompt(self): with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: prompt_file = Path(tmp) / "prompt.txt" prompt_file.write_text("Existing instructions.\n") @@ -76,11 +76,12 @@ class TestAgentProviderRuntime(unittest.TestCase): color="bright-cyan", ) prompt = prompt_file.read_text() + config = Path(tmp, "codex-config.toml").read_text() self.assertTrue(plan.has_prompt) - self.assertIn("Bot-bottle agent display identity:", prompt) - self.assertIn("Name: review-api", prompt) - self.assertIn("Color: bright-cyan", prompt) - self.assertTrue(prompt.endswith("Existing instructions.\n")) + self.assertEqual("Existing instructions.\n", prompt) + self.assertIn("[tui]", config) + self.assertIn('status_line = ["model", "cwd"]', config) + self.assertIn('terminal_title = ["spinner", "project"]', config) def test_codex_forward_host_credentials_adds_auth_and_verify(self): with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: @@ -146,7 +147,7 @@ class TestAgentProviderRuntime(unittest.TestCase): self.assertIn("/home/node", config["projects"]) self.assertIn("/home/node/workspace", config["projects"]) - def test_claude_injects_name_and_color_into_prompt(self): + def test_claude_writes_statusline_and_theme_without_mutating_prompt(self): with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: prompt_file = Path(tmp) / "prompt.txt" prompt_file.write_text("Existing instructions.\n") @@ -160,11 +161,11 @@ class TestAgentProviderRuntime(unittest.TestCase): color="green", ) prompt = prompt_file.read_text() + settings = json.loads(Path(tmp, "claude-settings.json").read_text()) self.assertTrue(plan.has_prompt) - self.assertIn("Bot-bottle agent display identity:", prompt) - self.assertIn("Name: research-ui", prompt) - self.assertIn("Color: green", prompt) - self.assertTrue(prompt.endswith("Existing instructions.\n")) + self.assertEqual("Existing instructions.\n", prompt) + self.assertEqual("~/.claude/statusline.sh", settings["statusLine"]["command"]) + self.assertEqual("custom:bot-bottle-research-ui", settings["theme"]) def test_codex_forward_host_credentials_populates_egress_routes(self): with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: diff --git a/tests/unit/test_contrib_claude_provider.py b/tests/unit/test_contrib_claude_provider.py index 08a0813..820e6b6 100644 --- a/tests/unit/test_contrib_claude_provider.py +++ b/tests/unit/test_contrib_claude_provider.py @@ -8,6 +8,8 @@ either side are expected to diverge the implementations.""" from __future__ import annotations +import json +import tempfile import unittest from pathlib import Path from unittest.mock import MagicMock, patch @@ -127,7 +129,7 @@ class TestClaudeProvisionPrompt(unittest.TestCase): self.assertIsNone(r) bottle.cp_in.assert_called_once() - def test_returns_path_when_provider_prompt_has_identity(self): + def test_returns_path_when_provider_prompt_exists(self): bottle = _make_bottle() provision = AgentProvisionPlan( template="claude", command="claude", prompt_mode="append_file", @@ -261,6 +263,35 @@ class TestClaudeProvision(unittest.TestCase): _plan(agent_provision=provision), bottle, ) + +class TestClaudeUiProvision(unittest.TestCase): + def test_writes_statusline_and_custom_theme_files(self): + with tempfile.TemporaryDirectory(prefix="bb-claude-ui.") as tmp: + state_dir = Path(tmp) + prompt_file = state_dir / "prompt.txt" + prompt_file.write_text("Existing instructions.\n") + plan = ClaudeAgentProvider().provision_plan( + dockerfile="", + state_dir=state_dir, + instance_name="bot-bottle-demo-abc12", + prompt_file=prompt_file, + label="research-ui", + color="bright-cyan", + ) + settings = json.loads((state_dir / "claude-settings.json").read_text()) + statusline = (state_dir / "claude-statusline.sh").read_text() + theme = json.loads((state_dir / "bot-bottle-research-ui.json").read_text()) + prompt_text = prompt_file.read_text() + self.assertTrue(plan.has_prompt) + self.assertEqual("Existing instructions.\n", prompt_text) + self.assertEqual("command", settings["statusLine"]["type"]) + self.assertEqual("~/.claude/statusline.sh", settings["statusLine"]["command"]) + self.assertEqual("custom:bot-bottle-research-ui", settings["theme"]) + self.assertIn("research-ui", statusline) + self.assertIn("\x1b[96m", statusline) + self.assertEqual("dark", theme["base"]) + self.assertEqual("ansi:cyanBright", theme["overrides"]["claude"]) + def test_runs_verify_commands(self): provision = AgentProvisionPlan( template="claude", command="claude", prompt_mode="append_file", diff --git a/tests/unit/test_contrib_codex_provider.py b/tests/unit/test_contrib_codex_provider.py index cf847bb..881bb33 100644 --- a/tests/unit/test_contrib_codex_provider.py +++ b/tests/unit/test_contrib_codex_provider.py @@ -9,6 +9,7 @@ no claude equivalent.""" from __future__ import annotations import unittest +import tempfile from pathlib import Path from unittest.mock import MagicMock, patch @@ -130,7 +131,7 @@ class TestCodexProvisionPrompt(unittest.TestCase): self.assertIsNone(r) bottle.cp_in.assert_called_once() - def test_returns_path_when_provider_prompt_has_identity(self): + def test_returns_path_when_provider_prompt_exists(self): bottle = _make_bottle() provision = AgentProvisionPlan( template="codex", command="codex", @@ -146,6 +147,28 @@ class TestCodexProvisionPrompt(unittest.TestCase): ) self.assertEqual("/home/node/.bot-bottle-prompt.txt", r) + def test_writes_tui_settings_into_codex_config(self): + with tempfile.TemporaryDirectory(prefix="bb-codex-ui.") as tmp: + state_dir = Path(tmp) + prompt_file = state_dir / "prompt.txt" + prompt_file.write_text("Existing instructions.\n") + plan = CodexAgentProvider().provision_plan( + dockerfile="", + state_dir=state_dir, + instance_name="bot-bottle-demo-abc12", + prompt_file=prompt_file, + label="research-ui", + color="bright-cyan", + ) + config = (state_dir / "codex-config.toml").read_text() + prompt_text = prompt_file.read_text() + self.assertTrue(plan.has_prompt) + self.assertEqual("Existing instructions.\n", prompt_text) + self.assertIn("[tui]", config) + self.assertIn('status_line = ["model", "cwd"]', config) + self.assertIn('terminal_title = ["spinner", "project"]', config) + self.assertIn('theme = "dark-ansi"', config) + class TestCodexProvisionSkills(unittest.TestCase): def test_noop_when_agent_has_no_skills(self): -- 2.52.0 From b1551045dc53d7a7e89684dfe7a850d08e81ec0f Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 9 Jun 2026 00:45:03 -0400 Subject: [PATCH 3/4] feat(terminal): tint terminal background per agent color Add backend-agnostic terminal color support via OSC escape sequences: - New backend/terminal.py with palette_printf() and exec_shell_script() shared by both Docker and smolmachines bottle backends - Emits OSC 4 (indexed palette) + OSC 11 (default background tint) before launching; resets both on agent exit via OSC 104/111 - OSC 11 background tint is visible even when the TUI uses true/24-bit colors (which bypass the palette), as Codex does for its chrome - Fix Codex [tui] config: status_line=["model-with-reasoning"], theme="ansi" (dark-ansi and cwd/directory were invalid identifiers) Co-Authored-By: Claude Sonnet 4.6 --- bot_bottle/backend/docker/bottle.py | 14 ++-- bot_bottle/backend/docker/launch.py | 1 + bot_bottle/backend/smolmachines/bottle.py | 14 ++-- bot_bottle/backend/smolmachines/launch.py | 1 + bot_bottle/backend/terminal.py | 82 +++++++++++++++++++++ bot_bottle/contrib/codex/agent_provider.py | 4 +- tests/unit/test_agent_provider.py | 2 +- tests/unit/test_backend_terminal.py | 83 ++++++++++++++++++++++ tests/unit/test_contrib_codex_provider.py | 4 +- 9 files changed, 186 insertions(+), 19 deletions(-) create mode 100644 bot_bottle/backend/terminal.py create mode 100644 tests/unit/test_backend_terminal.py diff --git a/bot_bottle/backend/docker/bottle.py b/bot_bottle/backend/docker/bottle.py index 81ab4a1..f463e52 100644 --- a/bot_bottle/backend/docker/bottle.py +++ b/bot_bottle/backend/docker/bottle.py @@ -10,6 +10,7 @@ from typing import cast from ...agent_provider import PromptMode, prompt_args from .. import Bottle, ExecResult +from ..terminal import exec_shell_script class DockerBottle(Bottle): @@ -24,6 +25,7 @@ class DockerBottle(Bottle): agent_command: str = "claude", agent_prompt_mode: PromptMode = "append_file", terminal_title: str = "", + terminal_color: str = "", ): self.name = container self._teardown = teardown @@ -31,6 +33,7 @@ class DockerBottle(Bottle): self._agent_prompt_mode = agent_prompt_mode self.agent_command = agent_command self.terminal_title = terminal_title + self.terminal_color = terminal_color self.agent_provider_template = ( "codex" if agent_command == "codex" else "claude" ) @@ -51,13 +54,10 @@ class DockerBottle(Bottle): def exec_agent(self, argv: list[str], *, tty: bool = True) -> int: agent_argv = self.agent_argv(argv, tty=tty) - if self.terminal_title and tty: - shell_script = ( - f"printf '\\033]0;%s\\007' {shlex.quote(self.terminal_title)}; " - f"exec {shlex.join(agent_argv)}" - ) - return subprocess.run(["sh", "-lc", shell_script], check=False).returncode - return subprocess.run(agent_argv, check=False).returncode + script = exec_shell_script(agent_argv, self.terminal_title, self.terminal_color) if tty else None + if script is None: + return subprocess.run(agent_argv, check=False).returncode + return subprocess.run(["sh", "-lc", script], check=False).returncode def exec(self, script: str, *, user: str = "node") -> ExecResult: # Pipe via stdin to `sh -s` so the caller never has to worry diff --git a/bot_bottle/backend/docker/launch.py b/bot_bottle/backend/docker/launch.py index 517fdf4..18d6ad0 100644 --- a/bot_bottle/backend/docker/launch.py +++ b/bot_bottle/backend/docker/launch.py @@ -176,6 +176,7 @@ def launch( agent_command=plan.agent_command, agent_prompt_mode=plan.agent_prompt_mode, terminal_title=plan.spec.label or plan.spec.agent_name, + terminal_color=plan.spec.color, ) bottle.prompt_path = provision(plan, bottle) diff --git a/bot_bottle/backend/smolmachines/bottle.py b/bot_bottle/backend/smolmachines/bottle.py index 1538992..1d2fa8c 100644 --- a/bot_bottle/backend/smolmachines/bottle.py +++ b/bot_bottle/backend/smolmachines/bottle.py @@ -25,6 +25,7 @@ from typing import Mapping, cast from ...agent_provider import PromptMode, prompt_args from .. import Bottle, ExecResult +from ..terminal import exec_shell_script from . import pty_resize as _pty_resize from . import smolvm as _smolvm @@ -70,6 +71,7 @@ class SmolmachinesBottle(Bottle): agent_command: str = "claude", agent_prompt_mode: PromptMode = "append_file", terminal_title: str = "", + terminal_color: str = "", ) -> None: self.name = machine_name # In-VM path to the agent's prompt file. None when the @@ -84,6 +86,7 @@ class SmolmachinesBottle(Bottle): self._agent_prompt_mode = agent_prompt_mode self.agent_command = agent_command self.terminal_title = terminal_title + self.terminal_color = terminal_color self.agent_provider_template = ( "codex" if agent_command == "codex" else "claude" ) @@ -132,13 +135,10 @@ class SmolmachinesBottle(Bottle): avoid login-shell wiring. HOME / USER come from `smolvm -e` instead, which sets them on the process env.""" agent_argv = self.agent_argv(argv, tty=tty) - if self.terminal_title and tty: - shell_script = ( - f"printf '\\033]0;%s\\007' {shlex.quote(self.terminal_title)}; " - f"exec {shlex.join(agent_argv)}" - ) - return subprocess.run(["sh", "-lc", shell_script], check=False).returncode - return subprocess.run(agent_argv, check=False).returncode + script = exec_shell_script(agent_argv, self.terminal_title, self.terminal_color) if tty else None + if script is None: + return subprocess.run(agent_argv, check=False).returncode + return subprocess.run(["sh", "-lc", script], check=False).returncode # smolvm/libkrun can SIGKILL an otherwise-normal exec during # early-VM provisioning. Retry once after a short settle so diff --git a/bot_bottle/backend/smolmachines/launch.py b/bot_bottle/backend/smolmachines/launch.py index 7337f3b..9520ceb 100644 --- a/bot_bottle/backend/smolmachines/launch.py +++ b/bot_bottle/backend/smolmachines/launch.py @@ -104,6 +104,7 @@ def launch( agent_command=plan.agent_command, agent_prompt_mode=plan.agent_prompt_mode, terminal_title=plan.spec.label or plan.spec.agent_name, + terminal_color=plan.spec.color, ) bottle.prompt_path = provision(plan, bottle) diff --git a/bot_bottle/backend/terminal.py b/bot_bottle/backend/terminal.py new file mode 100644 index 0000000..66cb23a --- /dev/null +++ b/bot_bottle/backend/terminal.py @@ -0,0 +1,82 @@ +"""Terminal escape-sequence helpers shared across all bottle backends.""" + +from __future__ import annotations + +import shlex + + +# color name → (normal_idx, normal_hex, bright_idx, bright_hex, dark_bg_hex) +# OSC 4 sets indexed palette entries (affects syntax-highlighted code and any +# TUI content that uses indexed colors). dark_bg_hex is used for OSC 11 +# (default background) — a very dark tint that's visible even when the TUI +# uses true/24-bit colors for its own chrome, which would otherwise bypass +# the palette entirely. +_COLORS: dict[str, tuple[int, str, int, str, str]] = { + "black": (0, "#2d2d2d", 8, "#5c5c5c", "#0a0a0a"), + "red": (1, "#c0392b", 9, "#e74c3c", "#1a0707"), + "green": (2, "#27ae60", 10, "#2ecc71", "#071a09"), + "yellow": (3, "#d4ac0d", 11, "#f1c40f", "#1a1507"), + "blue": (4, "#2471a3", 12, "#3498db", "#07071a"), + "magenta": (5, "#7d3c98", 13, "#9b59b6", "#12071a"), + "cyan": (6, "#148f77", 14, "#1abc9c", "#071a1a"), + "white": (7, "#bdc3c7", 15, "#ecf0f1", "#111111"), + "bright-black": (8, "#5c5c5c", 0, "#2d2d2d", "#111111"), + "bright-red": (9, "#e74c3c", 1, "#c0392b", "#200808"), + "bright-green": (10, "#2ecc71", 2, "#27ae60", "#082008"), + "bright-yellow": (11, "#f1c40f", 3, "#d4ac0d", "#201808"), + "bright-blue": (12, "#3498db", 4, "#2471a3", "#080820"), + "bright-magenta": (13, "#9b59b6", 5, "#7d3c98", "#160820"), + "bright-cyan": (14, "#1abc9c", 6, "#148f77", "#082020"), + "bright-white": (15, "#ecf0f1", 7, "#bdc3c7", "#151515"), +} + +# OSC 104 resets all indexed palette entries; OSC 111 resets default background. +_RESET_PRINTF = "printf '\\033]104\\007\\033]111\\007'" + + +def palette_printf(color: str) -> str: + """Shell `printf` command that emits OSC 4 + OSC 11 to tint the terminal + for *color*: sets the normal/bright palette entries AND the default + background to a dark shade of that color. Returns '' if unknown.""" + entry = _COLORS.get(color) + if not entry: + return "" + n_idx, n_hex, b_idx, b_hex, bg_hex = entry + seq = ( + f"\\033]4;{n_idx};{n_hex}\\007" + f"\\033]4;{b_idx};{b_hex}\\007" + f"\\033]11;{bg_hex}\\007" + ) + return f"printf '{seq}'" + + +def exec_shell_script( + agent_argv: list[str], + terminal_title: str = "", + terminal_color: str = "", +) -> str | None: + """Build a shell script string that optionally sets the terminal + title and/or palette before running *agent_argv*, and resets the + palette + background on exit. Returns None when no decoration is + needed — callers should run *agent_argv* directly in that case.""" + title_cmd = ( + f"printf '\\033]0;%s\\007' {shlex.quote(terminal_title)}" + if terminal_title else "" + ) + pal_cmd = palette_printf(terminal_color) + + if not title_cmd and not pal_cmd: + return None + + parts: list[str] = [] + if title_cmd: + parts.append(title_cmd) + if pal_cmd: + parts.append(pal_cmd) + parts.append(shlex.join(agent_argv)) + parts.append(_RESET_PRINTF) + else: + # No palette change — exec so the agent replaces the shell. + parts.append(f"exec {shlex.join(agent_argv)}") + + return "; ".join(parts) diff --git a/bot_bottle/contrib/codex/agent_provider.py b/bot_bottle/contrib/codex/agent_provider.py index 8deb6d5..030aa85 100644 --- a/bot_bottle/contrib/codex/agent_provider.py +++ b/bot_bottle/contrib/codex/agent_provider.py @@ -104,9 +104,9 @@ class CodexAgentProvider(AgentProvider): 'trust_level = "trusted"\n' "\n" "[tui]\n" - 'status_line = ["model", "cwd"]\n' + 'status_line = ["model-with-reasoning"]\n' 'terminal_title = ["spinner", "project"]\n' - 'theme = "dark-ansi"\n' + 'theme = "ansi"\n' ) config_file.chmod(0o600) files.append(AgentProvisionFile(config_file, config_path)) diff --git a/tests/unit/test_agent_provider.py b/tests/unit/test_agent_provider.py index 0701619..884c37f 100644 --- a/tests/unit/test_agent_provider.py +++ b/tests/unit/test_agent_provider.py @@ -80,7 +80,7 @@ class TestAgentProviderRuntime(unittest.TestCase): self.assertTrue(plan.has_prompt) self.assertEqual("Existing instructions.\n", prompt) self.assertIn("[tui]", config) - self.assertIn('status_line = ["model", "cwd"]', config) + self.assertIn('status_line = ["model-with-reasoning"]', config) self.assertIn('terminal_title = ["spinner", "project"]', config) def test_codex_forward_host_credentials_adds_auth_and_verify(self): diff --git a/tests/unit/test_backend_terminal.py b/tests/unit/test_backend_terminal.py new file mode 100644 index 0000000..1f639c7 --- /dev/null +++ b/tests/unit/test_backend_terminal.py @@ -0,0 +1,83 @@ +"""Unit tests for backend/terminal.py palette and shell-script helpers.""" + +from __future__ import annotations + +import unittest + +from bot_bottle.backend.terminal import exec_shell_script, palette_printf + + +class TestPalettePrintf(unittest.TestCase): + def test_known_color_returns_printf(self): + cmd = palette_printf("red") + self.assertTrue(cmd.startswith("printf '")) + self.assertIn("\\033]4;1;", cmd) # normal red + self.assertIn("\\033]4;9;", cmd) # bright red + self.assertIn("\\033]11;", cmd) # default background tint + + def test_bright_variant_sets_both_slots(self): + cmd = palette_printf("bright-blue") + self.assertIn("\\033]4;12;", cmd) # bright-blue + self.assertIn("\\033]4;4;", cmd) # blue + + def test_unknown_color_returns_empty(self): + self.assertEqual("", palette_printf("")) + self.assertEqual("", palette_printf("neon-pink")) + + def test_all_named_colors_produce_output(self): + colors = [ + "black", "red", "green", "yellow", + "blue", "magenta", "cyan", "white", + "bright-black", "bright-red", "bright-green", "bright-yellow", + "bright-blue", "bright-magenta", "bright-cyan", "bright-white", + ] + for color in colors: + with self.subTest(color=color): + self.assertTrue(palette_printf(color)) + + +class TestExecShellScript(unittest.TestCase): + _ARGV = ["smolvm", "machine", "exec", "--name", "x", "--", "claude"] + + def test_no_decoration_returns_none(self): + self.assertIsNone(exec_shell_script(self._ARGV)) + self.assertIsNone(exec_shell_script(self._ARGV, terminal_title="", terminal_color="")) + + def test_title_only_uses_exec(self): + script = exec_shell_script(self._ARGV, terminal_title="my-agent") + assert script is not None + self.assertIn("printf", script) + self.assertIn("my-agent", script) + self.assertIn("exec ", script) + # No palette reset when there's no color + self.assertNotIn("\\033]104", script) + + def test_color_only_sets_palette_and_resets(self): + script = exec_shell_script(self._ARGV, terminal_color="green") + assert script is not None + self.assertIn("\\033]4;", script) # indexed palette + self.assertIn("\\033]11;", script) # background tint + self.assertIn("\\033]104", script) # palette reset + self.assertIn("\\033]111", script) # background reset + # No exec-replace when palette is active (shell must survive for reset) + parts = script.split("; ") + agent_part = next(p for p in parts if "smolvm" in p) + self.assertFalse(agent_part.startswith("exec ")) + + def test_title_and_color_both_appear(self): + script = exec_shell_script(self._ARGV, terminal_title="bot", terminal_color="cyan") + assert script is not None + self.assertIn("bot", script) + self.assertIn("\\033]4;", script) + self.assertIn("\\033]11;", script) + self.assertIn("\\033]104", script) + self.assertIn("\\033]111", script) + + def test_title_with_special_chars_is_quoted(self): + script = exec_shell_script(self._ARGV, terminal_title="my agent's label") + assert script is not None + self.assertNotIn("my agent's label", script) # must be shell-quoted + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/test_contrib_codex_provider.py b/tests/unit/test_contrib_codex_provider.py index 881bb33..6137fad 100644 --- a/tests/unit/test_contrib_codex_provider.py +++ b/tests/unit/test_contrib_codex_provider.py @@ -165,9 +165,9 @@ class TestCodexProvisionPrompt(unittest.TestCase): self.assertTrue(plan.has_prompt) self.assertEqual("Existing instructions.\n", prompt_text) self.assertIn("[tui]", config) - self.assertIn('status_line = ["model", "cwd"]', config) + self.assertIn('status_line = ["model-with-reasoning"]', config) self.assertIn('terminal_title = ["spinner", "project"]', config) - self.assertIn('theme = "dark-ansi"', config) + self.assertIn('theme = "ansi"', config) class TestCodexProvisionSkills(unittest.TestCase): -- 2.52.0 From 328069809bf7e416f85f2637d1e8b738af47fd21 Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 9 Jun 2026 00:48:40 -0400 Subject: [PATCH 4/4] fix(pyright): remove unused shlex imports from bottle backends shlex is now only used in terminal.py after the exec_shell_script refactor. Co-Authored-By: Claude Sonnet 4.6 --- bot_bottle/backend/docker/bottle.py | 1 - bot_bottle/backend/smolmachines/bottle.py | 1 - 2 files changed, 2 deletions(-) diff --git a/bot_bottle/backend/docker/bottle.py b/bot_bottle/backend/docker/bottle.py index f463e52..b11deb4 100644 --- a/bot_bottle/backend/docker/bottle.py +++ b/bot_bottle/backend/docker/bottle.py @@ -3,7 +3,6 @@ from __future__ import annotations import subprocess -import shlex from typing import Callable from typing import cast diff --git a/bot_bottle/backend/smolmachines/bottle.py b/bot_bottle/backend/smolmachines/bottle.py index 1d2fa8c..5764dae 100644 --- a/bot_bottle/backend/smolmachines/bottle.py +++ b/bot_bottle/backend/smolmachines/bottle.py @@ -20,7 +20,6 @@ from __future__ import annotations import subprocess import sys import time -import shlex from typing import Mapping, cast from ...agent_provider import PromptMode, prompt_args -- 2.52.0