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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user