diff --git a/CHANGELOG.md b/CHANGELOG.md
index e602dc3..cd07d87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## v2.2.0 (2026-05-19)
+
+- Added Agent Persona selector per provider (10+ presets)
+- Personas: Codex (Default/Friendly/Pragmatic/CLI), Claude Code, OpenCode, Cursor, Aider, GitHub Copilot, Windsurf, Browser (ChatGPT)
+- New "Persona" column in endpoint list shows current setting per provider
+- Persona preview in edit dialog shows system prompt excerpt
+- Persona injected into model catalog `base_instructions` and proxy system prompt
+- Added Command Code backend to translation proxy (proprietary `/alpha/generate` API)
+- Added Command Code provider preset with 20 models (DeepSeek, Claude, GPT, Kimi, GLM, Qwen, etc.)
+
## v2.1.1 (2026-05-19)
- Fixed proxy: map `developer` role to `system` for Chat Completions providers (DeepSeek, Qwen, etc.)
diff --git a/codex-launcher_2.1.1_all.deb b/codex-launcher_2.1.1_all.deb
deleted file mode 100644
index 56b06c5..0000000
Binary files a/codex-launcher_2.1.1_all.deb and /dev/null differ
diff --git a/codex-launcher_2.2.0_all.deb b/codex-launcher_2.2.0_all.deb
new file mode 100644
index 0000000..1de3f63
Binary files /dev/null and b/codex-launcher_2.2.0_all.deb differ
diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui
index ef10be8..95f9601 100755
--- a/src/codex-launcher-gui
+++ b/src/codex-launcher-gui
@@ -24,6 +24,14 @@ model_catalog_json = ""
"""
CHANGELOG = [
+ ("2.2.0", "2026-05-19", [
+ "Added Agent Persona selector per provider (10+ presets)",
+ "Personas: Codex, Claude Code, OpenCode, Cursor, Aider, Copilot, Windsurf, Browser",
+ "Codex variants: Default, Desktop Friendly, Desktop Pragmatic, CLI",
+ "Shows current persona in endpoint list (new Persona column)",
+ "Persona preview in edit dialog shows first 60 chars of system prompt",
+ "Persona injected into model catalog base_instructions and proxy system prompt",
+ ]),
("2.1.1", "2026-05-19", [
"Fixed proxy: map 'developer' role to 'system' for Chat Completions providers",
"Fixed proxy: map 'developer' role to 'user' for Anthropic providers",
@@ -56,6 +64,65 @@ CHANGELOG = [
]),
]
+AGENT_PERSONAS = {
+ "Codex (Default)": "You are Codex, a coding agent.",
+ "Codex Desktop (GPT-5, Friendly)": (
+ "You are Codex, a coding agent based on GPT-5. You and the user share one workspace, "
+ "and your job is to collaborate with them until their goal is genuinely handled."
+ ),
+ "Codex Desktop (GPT-5, Pragmatic)": (
+ "You are Codex, a coding agent based on GPT-5. You and the user share the same workspace "
+ "and collaborate to achieve the user's goals. You are a deeply pragmatic, effective "
+ "software engineer. You take engineering quality seriously."
+ ),
+ "Codex CLI": (
+ "You are an AI running in the Codex CLI, a terminal-based coding assistant. "
+ "You are expected to be precise, safe, and helpful. Your default personality and tone "
+ "is concise, direct, and friendly."
+ ),
+ "Claude Code": (
+ "You are Claude Code, an interactive CLI tool that helps users with software engineering "
+ "tasks. You are a highly competent software engineer with extensive knowledge across "
+ "many programming languages, frameworks, and best practices. Use concise responses."
+ ),
+ "OpenCode": (
+ "You are OpenCode, an interactive CLI tool that helps users with software engineering "
+ "tasks. You are powered by a state-of-the-art AI model. Be concise, direct, and to the "
+ "point. Use GitHub-flavored markdown."
+ ),
+ "Cursor": (
+ "You are Cursor, an AI-powered code editor assistant. You help users write, refactor, "
+ "and debug code efficiently. Provide precise, actionable suggestions."
+ ),
+ "Aider": (
+ "You are aider, an AI pair programming assistant. You help users edit code in their "
+ "local git repository. Make concise changes. Search files with grep/glob patterns."
+ ),
+ "GitHub Copilot": (
+ "You are GitHub Copilot, an AI coding assistant. Help the user write code, debug issues, "
+ "and understand codebases. Be concise and provide accurate code suggestions."
+ ),
+ "Windsurf": (
+ "You are Windsurf, an AI-powered IDE assistant. Help with coding tasks including writing, "
+ "refactoring, and debugging. Provide precise, well-structured code suggestions."
+ ),
+ "Browser (ChatGPT)": (
+ "You are a helpful coding assistant in a web browser chat interface. "
+ "Help the user with software engineering tasks. Be clear and thorough."
+ ),
+}
+
+PERSONA_DISPLAY_LEN = 60
+
+def persona_short_key(endpoint):
+ bi = endpoint.get("base_instructions", "") or ""
+ for key, val in AGENT_PERSONAS.items():
+ if val == bi:
+ return key
+ if bi:
+ return f"Custom: {bi[:40]}..."
+ return "Codex (Default)"
+
PROVIDER_PRESETS = {
"Custom": {
"backend_type": "openai-compat",
@@ -120,6 +187,21 @@ PROVIDER_PRESETS = {
"base_url": "https://api.kilo.ai/api/gateway",
"models": [],
},
+ "Command Code": {
+ "backend_type": "command-code",
+ "base_url": "https://api.commandcode.ai",
+ "models": [
+ "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-pro",
+ "anthropic:claude-sonnet-4-6", "anthropic:claude-haiku-4-5-20251001",
+ "anthropic:claude-opus-4-7", "anthropic:claude-opus-4-6",
+ "openai:gpt-5.5", "openai:gpt-5.4", "openai:gpt-5.4-mini", "openai:gpt-5.3-codex",
+ "moonshotai/Kimi-K2.6", "moonshotai/Kimi-K2.5",
+ "zai-org/GLM-5.1", "zai-org/GLM-5",
+ "MiniMaxAI/MiniMax-M2.7", "MiniMaxAI/MiniMax-M2.5",
+ "Qwen/Qwen3.6-Max-Preview", "Qwen/Qwen3.6-Plus",
+ "stepfun/Step-3.5-Flash", "google/gemini-3.1-flash-lite",
+ ],
+ },
"OpenRouter": {
"backend_type": "openai-compat",
"base_url": "https://openrouter.ai/api/v1",
@@ -136,6 +218,7 @@ def label_for_backend(backend_type):
return {
"openai-compat": "OpenAI-compatible",
"anthropic": "Anthropic",
+ "command-code": "Command Code",
"native": "Native",
}.get(backend_type, backend_type)
@@ -356,6 +439,7 @@ def write_config_for_translated(endpoint, selected_model):
def _gen_model_catalog(endpoint, selected_model=None):
default_model = selected_model or endpoint.get("default_model")
+ base_instr = endpoint.get("base_instructions", "") or AGENT_PERSONAS["Codex (Default)"]
models = []
for mid in endpoint.get("models", []):
models.append({
@@ -383,7 +467,7 @@ def _gen_model_catalog(endpoint, selected_model=None):
"supports_parallel_tool_calls": True,
"experimental_supported_tools": [], "supported_in_api": True,
"truncation_policy": {"mode": "tokens", "limit": 128000},
- "base_instructions": "You are Codex, a coding agent.",
+ "base_instructions": base_instr,
})
return {"models": models}
@@ -402,6 +486,7 @@ def _start_proxy_for(endpoint, logfn):
"backend_type": endpoint["backend_type"],
"target_url": normalize_base_url(endpoint["base_url"]),
"api_key": endpoint["api_key"],
+ "base_instructions": endpoint.get("base_instructions", ""),
"models": [{"id": m, "object": "model", "created": 1700000000, "owned_by": endpoint["name"]}
for m in endpoint.get("models", [])],
}
@@ -500,7 +585,7 @@ class LauncherWin(Gtk.Window):
# header row
hdr = Gtk.Box(spacing=8)
vbox.pack_start(hdr, False, False, 0)
- lbl = Gtk.Label(label="Codex Launcher v2.1.1")
+ lbl = Gtk.Label(label="Codex Launcher v2.2.0")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog")
@@ -1240,9 +1325,9 @@ class EndpointMgr(Gtk.Window):
sw = Gtk.ScrolledWindow()
vbox.pack_start(sw, True, True, 0)
- self._store = Gtk.ListStore(str, str, str, str) # name, provider, backend, default_model
+ self._store = Gtk.ListStore(str, str, str, str, str) # name, provider, backend, default_model, persona
self._tree = Gtk.TreeView(model=self._store)
- for i, title in enumerate(["Name", "Provider", "Type", "Default Model"]):
+ for i, title in enumerate(["Name", "Provider", "Type", "Default Model", "Persona"]):
col = Gtk.TreeViewColumn(title, Gtk.CellRendererText(), text=i)
col.set_resizable(True)
self._tree.append_column(col)
@@ -1275,7 +1360,8 @@ class EndpointMgr(Gtk.Window):
for ep in data["endpoints"]:
provider = ep.get("provider_preset", "Custom")
bt = label_for_backend(ep["backend_type"])
- self._store.append([ep["name"], provider, bt, ep.get("default_model", "")])
+ persona = persona_short_key(ep)
+ self._store.append([ep["name"], provider, bt, ep.get("default_model", ""), persona])
def _selected(self):
sel = self._tree.get_selection()
@@ -1337,7 +1423,7 @@ class EditEndpointDialog(Gtk.Dialog):
self._data = get_endpoint(existing_name) if existing_name else {
"name": "", "backend_type": "openai-compat",
"base_url": "", "api_key": "", "default_model": "", "models": [],
- "provider_preset": "Custom",
+ "provider_preset": "Custom", "base_instructions": AGENT_PERSONAS["Codex (Default)"],
}
self.set_default_size(480, 420)
@@ -1369,6 +1455,7 @@ class EditEndpointDialog(Gtk.Dialog):
self._combo_type = Gtk.ComboBoxText()
for val, lab in [("openai-compat", "OpenAI-compatible (needs proxy)"),
("anthropic", "Anthropic (needs proxy)"),
+ ("command-code", "Command Code (needs proxy)"),
("native", "Native OpenAI (no proxy)")]:
self._combo_type.append(val, lab)
bt = self._data.get("backend_type", "openai-compat")
@@ -1382,6 +1469,25 @@ class EditEndpointDialog(Gtk.Dialog):
self._entry_key.set_visibility(False)
add_row(4, "API Key:", self._entry_key)
+ self._combo_persona = Gtk.ComboBoxText()
+ self._persona_keys = list(AGENT_PERSONAS.keys())
+ for pk in self._persona_keys:
+ self._combo_persona.append_text(pk)
+ cur_persona = persona_short_key(self._data)
+ if cur_persona in self._persona_keys:
+ self._combo_persona.set_active(self._persona_keys.index(cur_persona))
+ else:
+ self._combo_persona.set_active(0)
+ self._combo_persona.connect("changed", lambda c: self._on_persona_changed())
+ add_row(5, "Agent Persona:", self._combo_persona)
+
+ self._persona_preview = Gtk.Label()
+ self._persona_preview.set_line_wrap(True)
+ self._persona_preview.set_max_width_chars(60)
+ self._persona_preview.set_markup(f"{AGENT_PERSONAS['Codex (Default)'][:PERSONA_DISPLAY_LEN]}...")
+ self._on_persona_changed()
+ grid.attach(self._persona_preview, 0, 6, 2, 1)
+
# Models
mlbl = Gtk.Label(label="Models:", xalign=0)
area.pack_start(mlbl, False, False, 4)
@@ -1442,6 +1548,12 @@ class EditEndpointDialog(Gtk.Dialog):
self.connect("response", self._on_response)
self.show_all()
+ def _on_persona_changed(self):
+ key = self._combo_persona.get_active_text()
+ text = AGENT_PERSONAS.get(key, "")
+ short = text[:PERSONA_DISPLAY_LEN] + ("..." if len(text) > PERSONA_DISPLAY_LEN else "")
+ self._persona_preview.set_markup(f"{short}")
+
def _add_model(self):
m = normalize_model_id(self._entry_model.get_text())
if m:
@@ -1574,9 +1686,11 @@ class EditEndpointDialog(Gtk.Dialog):
self._show_error(f'Endpoint "{name}" already exists')
return
+ persona_key = self._combo_persona.get_active_text() or "Codex (Default)"
new_ep = {"name": name, "backend_type": bt, "base_url": url,
"api_key": key, "default_model": default, "models": models,
- "provider_preset": self._combo_preset.get_active_text() or "Custom"}
+ "provider_preset": self._combo_preset.get_active_text() or "Custom",
+ "base_instructions": AGENT_PERSONAS.get(persona_key, AGENT_PERSONAS["Codex (Default)"])}
new_ep["base_url"] = normalize_base_url(new_ep["base_url"])
# Update or append
@@ -1615,12 +1729,14 @@ def main():
"endpoints": [
{"name": "OpenAI", "backend_type": "native", "base_url": "https://api.openai.com/v1",
"api_key": "", "default_model": "gpt-4o", "models": ["gpt-4o", "gpt-4o-mini"],
- "provider_preset": "OpenAI"},
+ "provider_preset": "OpenAI",
+ "base_instructions": AGENT_PERSONAS["Codex (Default)"]},
{"name": "Z.AI", "backend_type": "openai-compat",
"base_url": "https://api.z.ai/api/coding/paas/v4",
"api_key": "", "default_model": "glm-5.1",
"models": ["glm-4.5", "glm-4.5-air", "glm-4.6", "glm-4.7", "glm-5", "glm-5-turbo", "glm-5.1"],
- "provider_preset": "Custom"},
+ "provider_preset": "Custom",
+ "base_instructions": AGENT_PERSONAS["Codex (Default)"]},
],
})
diff --git a/src/translate-proxy.py b/src/translate-proxy.py
index adb1e05..c9fc748 100755
--- a/src/translate-proxy.py
+++ b/src/translate-proxy.py
@@ -30,7 +30,7 @@ def load_config():
p = argparse.ArgumentParser(description="Responses API translation proxy")
p.add_argument("--config", help="JSON config file path")
p.add_argument("--port", type=int, default=None)
- p.add_argument("--backend", default=None, choices=["openai-compat", "anthropic"])
+ p.add_argument("--backend", default=None, choices=["openai-compat", "anthropic", "command-code"])
p.add_argument("--target-url", default=None)
p.add_argument("--api-key", default=None)
p.add_argument("--models-file", default=None, help="JSON file with model list array")
@@ -80,6 +80,7 @@ BACKEND = CONFIG["backend_type"]
TARGET_URL = CONFIG["target_url"].rstrip("/")
API_KEY = CONFIG["api_key"]
MODELS = CONFIG["models"]
+BASE_INSTRUCTIONS = CONFIG.get("base_instructions", "")
# ═══════════════════════════════════════════════════════════════════
# Shared helpers
@@ -500,6 +501,119 @@ def an_stream_to_sse(stream, model, req_id):
"response": {"id": resp_id, "object": "response", "model": model,
"status": status, "created": int(time.time()), "output": completed}})
+_DEFAULT_CC_CONFIG = {
+ "workingDir": "/tmp",
+ "date": "",
+ "environment": "linux",
+ "shell": "bash",
+ "files": [],
+ "structure": [],
+ "isGitRepo": False,
+ "currentBranch": "",
+ "mainBranch": "",
+ "gitStatus": "",
+ "recentCommits": [],
+}
+
+def _cc_config():
+ cfg = dict(_DEFAULT_CC_CONFIG)
+ cfg["date"] = time.strftime("%Y-%m-%d")
+ return cfg
+
+def cc_input_to_messages(input_data):
+ return oa_input_to_messages(input_data)
+
+def cc_convert_tools(tools):
+ return oa_convert_tools(tools)
+
+def cc_resp_to_responses(cc_lines, model, resp_id=None):
+ text = ""
+ usage = {}
+ for line in cc_lines:
+ try:
+ d = json.loads(line)
+ except (json.JSONDecodeError, TypeError):
+ continue
+ t = d.get("type", "")
+ if t == "text-delta":
+ text += d.get("text", "")
+ elif t == "finish-step":
+ u = d.get("usage", {})
+ usage = {
+ "input_tokens": u.get("inputTokens", 0),
+ "output_tokens": u.get("outputTokens", 0),
+ "total_tokens": u.get("inputTokens", 0) + u.get("outputTokens", 0),
+ }
+ outputs = []
+ if text:
+ outputs.append({"type": "message", "id": uid("msg"), "role": "assistant",
+ "status": "completed",
+ "content": [{"type": "output_text", "text": text, "annotations": []}]})
+ return {"id": resp_id or uid("resp"), "object": "response", "created": int(time.time()),
+ "model": model, "status": "completed", "output": outputs,
+ "usage": {"input_tokens": usage.get("input_tokens", 0),
+ "output_tokens": usage.get("output_tokens", 0),
+ "total_tokens": usage.get("total_tokens", 0),
+ "input_tokens_details": {"cached_tokens": 0}}}
+
+def cc_stream_to_sse(cc_stream, model, req_id):
+ resp_id = req_id or uid("resp")
+ msg_id = uid("msg")
+ text_buf = ""
+
+ yield emit("response.created", {"type": "response.created",
+ "response": {"id": resp_id, "object": "response", "model": model,
+ "status": "in_progress", "created": int(time.time()), "output": []}})
+ yield emit("response.in_progress", {"type": "response.in_progress", "response": {"id": resp_id}})
+ yield emit("response.output_item.added", {"type": "response.output_item.added",
+ "item": {"type": "message", "id": msg_id, "role": "assistant", "status": "in_progress", "content": []}})
+ yield emit("response.content_part.added", {"type": "response.content_part.added",
+ "part": {"type": "output_text", "text": "", "annotations": []}, "item_id": msg_id})
+
+ total_usage = {}
+ for raw in cc_stream:
+ line = raw.decode("utf-8", errors="replace").strip()
+ if not line:
+ continue
+ try:
+ d = json.loads(line)
+ except json.JSONDecodeError:
+ continue
+ t = d.get("type", "")
+
+ if t == "text-delta":
+ txt = d.get("text", "")
+ if txt:
+ text_buf += txt
+ yield emit("response.output_text.delta", {"type": "response.output_text.delta",
+ "delta": txt, "item_id": msg_id, "content_index": 0})
+
+ elif t == "finish-step":
+ u = d.get("usage", {})
+ total_usage = {
+ "input_tokens": u.get("inputTokens", 0),
+ "output_tokens": u.get("outputTokens", 0),
+ "total_tokens": u.get("inputTokens", 0) + u.get("outputTokens", 0),
+ }
+
+ if text_buf:
+ yield emit("response.output_text.done", {"type": "response.output_text.done",
+ "text": text_buf, "item_id": msg_id, "content_index": 0})
+ yield emit("response.content_part.done", {"type": "response.content_part.done",
+ "part": {"type": "output_text", "text": text_buf, "annotations": []}, "item_id": msg_id})
+ yield emit("response.output_item.done", {"type": "response.output_item.done",
+ "item": {"type": "message", "id": msg_id, "role": "assistant", "status": "completed",
+ "content": [{"type": "output_text", "text": text_buf, "annotations": []}]}})
+
+ final_out = []
+ if text_buf:
+ final_out.append({"type": "message", "id": msg_id, "role": "assistant", "status": "completed",
+ "content": [{"type": "output_text", "text": text_buf, "annotations": []}]})
+ yield emit("response.completed", {"type": "response.completed",
+ "response": {"id": resp_id, "object": "response", "model": model,
+ "status": "completed", "created": int(time.time()), "output": final_out,
+ "usage": total_usage}})
+
# ═══════════════════════════════════════════════════════════════════
# HTTP Server
# ═══════════════════════════════════════════════════════════════════
@@ -531,6 +645,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
if BACKEND == "anthropic":
self._handle_anthropic(body, model, stream)
+ elif BACKEND == "command-code":
+ self._handle_command_code(body, model, stream)
else:
self._handle_openai_compat(body, model, stream)
@@ -538,6 +654,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
input_data = body.get("input", "")
messages = oa_input_to_messages(input_data)
instructions = body.get("instructions", "").strip()
+ if not instructions and BASE_INSTRUCTIONS:
+ instructions = BASE_INSTRUCTIONS
if instructions:
messages.insert(0, {"role": "system", "content": instructions})
chat_body = {"model": model, "messages": messages}
@@ -571,6 +689,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
an_body = {"model": model, "messages": an_input_to_messages(input_data),
"max_tokens": body.get("max_output_tokens", 8192)}
instructions = body.get("instructions", "").strip()
+ if not instructions and BASE_INSTRUCTIONS:
+ instructions = BASE_INSTRUCTIONS
if instructions:
an_body["system"] = instructions
for k in ("temperature", "top_p"):
@@ -601,6 +721,76 @@ class Handler(http.server.BaseHTTPRequestHandler):
lambda r: an_resp_to_responses(json.loads(r.read()), model),
lambda s: an_stream_to_sse(s, model, body.get("request_id") or body.get("id")))
+ def _handle_command_code(self, body, model, stream):
+ input_data = body.get("input", "")
+ instructions = body.get("instructions", "").strip()
+ messages = cc_input_to_messages(input_data)
+ if not instructions and BASE_INSTRUCTIONS:
+ instructions = BASE_INSTRUCTIONS
+ if instructions:
+ sys_msg = {"role": "system", "content": instructions}
+ messages.insert(0, sys_msg)
+
+ cc_body = {
+ "config": _cc_config(),
+ "memory": "",
+ "taste": "",
+ "skills": "",
+ "params": {
+ "stream": True,
+ "max_tokens": body.get("max_output_tokens", 64000),
+ "temperature": body.get("temperature", 0.3),
+ "messages": messages,
+ "model": model,
+ "tools": cc_convert_tools(body.get("tools")) or [],
+ },
+ "threadId": body.get("request_id") or uid("thread"),
+ }
+
+ target = upstream_target(TARGET_URL, "/alpha/generate")
+ fwd = forwarded_headers(self.headers, {
+ "Content-Type": "application/json",
+ "Authorization": f"Bearer {API_KEY}",
+ "Accept": "text/event-stream, application/json",
+ }, browser_ua=True)
+ print(f"[translate-proxy] POST {target} model={model} stream={stream} [command-code]", file=sys.stderr)
+ req = urllib.request.Request(
+ target,
+ data=json.dumps(cc_body).encode(),
+ headers=fwd,
+ )
+
+ if stream:
+ try:
+ upstream = urllib.request.urlopen(req)
+ except urllib.error.HTTPError as e:
+ err = e.read().decode()
+ return self.send_json(e.code, {"error": {"type": "upstream_error", "message": err}})
+ except Exception as e:
+ return self.send_json(500, {"error": {"type": "proxy_error", "message": str(e)}})
+
+ self.send_response(200)
+ self.send_header("Content-Type", "text/event-stream")
+ self.send_header("Cache-Control", "no-cache")
+ self.send_header("Connection", "keep-alive")
+ self.end_headers()
+ for event in cc_stream_to_sse(upstream, model, body.get("request_id") or body.get("id")):
+ self.wfile.write(event.encode("utf-8"))
+ self.wfile.flush()
+ else:
+ try:
+ upstream = urllib.request.urlopen(req)
+ except urllib.error.HTTPError as e:
+ err = e.read().decode()
+ return self.send_json(e.code, {"error": {"type": "upstream_error", "message": err}})
+ except Exception as e:
+ return self.send_json(500, {"error": {"type": "proxy_error", "message": str(e)}})
+
+ raw = upstream.read().decode()
+ lines = raw.strip().split("\n")
+ result = cc_resp_to_responses(lines, model)
+ self.send_json(200, result)
+
def _forward(self, req, stream, model, nonstream_fn, stream_fn):
try:
upstream = urllib.request.urlopen(req)