From 21730358217a4b95edb685d314bde6757b8da085 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 19 Jul 2026 16:16:07 -0400 Subject: [PATCH 1/2] refac --- cptr/utils/adapters/signal.py | 11 ++- cptr/utils/agents/detection.py | 10 ++- cptr/utils/agents/gemini.py | 118 +++++++++++++++++++++++++++++++++ cptr/utils/chat_task.py | 3 +- 4 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 cptr/utils/agents/gemini.py diff --git a/cptr/utils/adapters/signal.py b/cptr/utils/adapters/signal.py index 3d466b05..506bbff6 100644 --- a/cptr/utils/adapters/signal.py +++ b/cptr/utils/adapters/signal.py @@ -13,6 +13,7 @@ from __future__ import annotations import asyncio +import base64 import logging from typing import Any, Optional from urllib.parse import quote @@ -222,7 +223,14 @@ async def _process_message(self, msg: dict) -> None: # Use source number as chat_id (DM) or groupId for groups group_info = data_message.get("groupInfo") - chat_id = group_info.get("groupId", "") if group_info else source + chat_id = source + if group_info: + group_id = group_info.get("groupId", "") + chat_id = group_info.get("id") or ( + "group." + base64.b64encode(group_id.encode("utf-8")).decode("ascii") + if group_id + else source + ) event = MessageEvent( platform="signal", @@ -265,7 +273,6 @@ async def _send_with_attachment( """Send a message with a base64-encoded attachment via signal-cli.""" if not self._http: return None - import base64 try: resp = await self._http.post( f"{self._base_url}/v2/send", diff --git a/cptr/utils/agents/detection.py b/cptr/utils/agents/detection.py index 318c45eb..3ef05614 100644 --- a/cptr/utils/agents/detection.py +++ b/cptr/utils/agents/detection.py @@ -171,7 +171,7 @@ async def detect_profile(profile: dict[str, Any]) -> AgentDetection: return AgentDetection("ready", command, version, None, models) if profile.get("agent") == "gemini": - models = await _probe_acp_models(command, profile) + models = await _probe_acp_models(command, profile, auth_method_id="oauth-personal") if not models: return AgentDetection( "auth_unknown", @@ -456,7 +456,11 @@ async def _probe_opencode_models(command: str, profile: dict[str, Any]) -> list[ await proc.wait() -async def _probe_acp_models(command: str, profile: dict[str, Any]) -> list[str] | None: +async def _probe_acp_models( + command: str, + profile: dict[str, Any], + auth_method_id: str | None = None, +) -> list[str] | None: from cptr.utils.agents.acp import AcpClient, acp_models_from_setup env = os.environ.copy() @@ -467,7 +471,7 @@ async def _probe_acp_models(command: str, profile: dict[str, Any]) -> list[str] args=["--acp"], cwd=os.getcwd(), env=env, - auth_method_id=None, + auth_method_id=auth_method_id, ) try: await asyncio.wait_for(client.start(), timeout=10) diff --git a/cptr/utils/agents/gemini.py b/cptr/utils/agents/gemini.py new file mode 100644 index 00000000..f75a6e52 --- /dev/null +++ b/cptr/utils/agents/gemini.py @@ -0,0 +1,118 @@ +"""Gemini ACP adapter.""" + +from __future__ import annotations + +import asyncio +import os +from contextlib import suppress +from typing import Any, AsyncIterator + +from cptr.utils.agents.acp import ( + AcpClient, + acp_event_stream, + acp_text_from_update, + acp_tool_from_update, +) +from cptr.utils.agents.attachments import PreparedAgentAttachments +from cptr.utils.agents.events import ( + AgentDone, + AgentError, + AgentEvent, + AgentTextDelta, + AgentToolUpdate, +) +from cptr.utils.agents.prompts import turn_prompt_text + + +def _auto_approve(chat_params: dict[str, Any]) -> bool: + if chat_params.get("tool_approval_mode") == "full": + return True + return bool(chat_params.get("auto_approve_tools")) + + +async def run_gemini_agent( + *, + profile: dict[str, Any], + model: str, + workspace: str, + messages: list[dict[str, Any]], + system_prompt: str, + chat_params: dict[str, Any], + resume_state: dict[str, Any] | None, + attachments: PreparedAgentAttachments, +) -> AsyncIterator[AgentEvent]: + env = os.environ.copy() + if profile.get("home"): + env["HOME"] = os.path.expanduser(str(profile["home"])) + + session_id = None + if resume_state and isinstance(resume_state.get("session_id"), str): + session_id = resume_state["session_id"] + + client = AcpClient( + command=str(profile["command"]), + args=["--acp"], + cwd=workspace, + env=env, + auth_method_id="oauth-personal", + resume_session_id=session_id, + auto_approve_permissions=_auto_approve(chat_params), + ) + try: + await client.start() + if model != "default": + await client.set_model(model) + + prompt = turn_prompt_text(messages, system_prompt, resumed=bool(session_id)) + + images = [ + {"data": image.base64, "mimeType": image.mime_type} for image in attachments.images + ] + prompt_task = asyncio.create_task(client.prompt(prompt, images=images)) + try: + async for event in acp_event_stream(client): + params = event.get("params") if isinstance(event.get("params"), dict) else {} + text = acp_text_from_update(params) + if text: + yield AgentTextDelta(text) + tool = acp_tool_from_update(params) + if tool: + yield AgentToolUpdate(**tool) + if prompt_task.done(): + try: + next_event = await asyncio.wait_for(client.events.get(), timeout=0.25) + except asyncio.TimeoutError: + break + next_params = ( + next_event.get("params") + if isinstance(next_event.get("params"), dict) + else {} + ) + next_text = acp_text_from_update(next_params) + if next_text: + yield AgentTextDelta(next_text) + next_tool = acp_tool_from_update(next_params) + if next_tool: + yield AgentToolUpdate(**next_tool) + await prompt_task + finally: + if not prompt_task.done(): + prompt_task.cancel() + with suppress(asyncio.CancelledError): + await prompt_task + + yield AgentDone( + resume_state={ + "profile_id": profile["id"], + "session_id": client.session_id, + "workspace": workspace, + "model": model, + } + ) + except asyncio.CancelledError: + await client.cancel() + raise + except Exception as exc: # noqa: BLE001 - surfaced in chat. + yield AgentError(str(exc)) + finally: + await client.close() diff --git a/cptr/utils/chat_task.py b/cptr/utils/chat_task.py index dea9aa56..f27cf299 100644 --- a/cptr/utils/chat_task.py +++ b/cptr/utils/chat_task.py @@ -1645,6 +1645,7 @@ async def _run_agent_target(agent_target: AgentModelTarget): from cptr.utils.agents.cline import run_cline_agent from cptr.utils.agents.codex import run_codex_agent from cptr.utils.agents.cursor import run_cursor_agent + from cptr.utils.agents.gemini import run_gemini_agent from cptr.utils.agents.grok import run_grok_agent from cptr.utils.agents.opencode import run_opencode_agent from cptr.utils.agents.pi import run_pi_agent @@ -1706,7 +1707,7 @@ async def _run_agent_target(agent_target: AgentModelTarget): "grok": run_grok_agent, "opencode": run_opencode_agent, "cline": run_cline_agent, - "gemini": run_cline_agent, + "gemini": run_gemini_agent, "pi": run_pi_agent, } runner = runners.get(agent_target.agent) From cec0a069736e4a7b1b49273eaf3ddc745e1996f9 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 19 Jul 2026 16:17:29 -0400 Subject: [PATCH 2/2] refac --- CHANGELOG.md | 10 ++++++++++ pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecb9377a..61f1bcae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.11] - 2026-07-19 + +### Added + +- 🤖 **Gemini is now its own coding agent.** Choose Gemini as an agent and Computer will start the Gemini CLI with its Google sign-in flow, keep the chat resumable, and handle image attachments like the other coding agents. + +### Fixed + +- 📱 **Signal group chats reply to the right group.** When a Signal group message reaches Computer, replies, typing indicators, and attachments now use the group address Signal expects instead of falling back to the sender or losing the group target. + ## [0.9.10] - 2026-07-18 ### Added diff --git a/pyproject.toml b/pyproject.toml index ca02b922..1db23441 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cptr" -version = "0.9.10" +version = "0.9.11" description = "Your computer, from anywhere. Code, manage, and control your machine from the web." license = {file = "LICENSE"} readme = "README.md" diff --git a/uv.lock b/uv.lock index 79411b6c..6cdcc6e9 100644 --- a/uv.lock +++ b/uv.lock @@ -284,7 +284,7 @@ wheels = [ [[package]] name = "cptr" -version = "0.9.10" +version = "0.9.11" source = { editable = "." } dependencies = [ { name = "aiosqlite" },