Skip to content
Merged

0.8.5 #109

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ 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.8.5] - 2026-07-07

### Added

- ✨ **Better shared skill discovery.** Computer now finds skills saved in the common agents skill folder, so more reusable workflows show up without extra setup.

### Fixed

- 🤖 **Smoother agent replies.** Coding agents no longer repeat final answers or thinking notes that already appeared while the chat was running.
- ✅ **Cleaner chat endings.** Thinking notes now close before answers, tool use, or the final saved message, keeping the chat display and completion state tidier.
- 🔐 **More reliable agent approvals.** Agent approval prompts now handle request IDs more flexibly, so approvals are less likely to get stuck.

## [0.8.4] - 2026-07-07

### Fixed
Expand Down
8 changes: 4 additions & 4 deletions cptr/utils/agents/acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(
self.proc: asyncio.subprocess.Process | None = None
self.reader_task: asyncio.Task | None = None
self.stderr_task: asyncio.Task | None = None
self.pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
self.pending: dict[Any, asyncio.Future[dict[str, Any]]] = {}
self.events: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
self.next_id = 1
self.session_id: str | None = None
Expand Down Expand Up @@ -197,19 +197,19 @@ async def _handle_message(self, message: dict[str, Any]) -> None:
and ("result" in message or "error" in message)
and "method" not in message
):
future = self.pending.pop(int(message["id"]), None)
future = self.pending.pop(message["id"], None)
if future and not future.done():
future.set_result(message)
return

method = message.get("method")
params = message.get("params") if isinstance(message.get("params"), dict) else {}
if "id" in message and method == "session/request_permission":
await self._reply_permission(int(message["id"]), params)
await self._reply_permission(message["id"], params)
return
await self.events.put(message)

async def _reply_permission(self, request_id: int, params: dict[str, Any]) -> None:
async def _reply_permission(self, request_id: Any, params: dict[str, Any]) -> None:
option_id = None
if self.auto_approve_permissions:
option_id = _select_permission_option(
Expand Down
16 changes: 14 additions & 2 deletions cptr/utils/agents/claude_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ async def run_claude_code_agent(
usage: dict[str, Any] | None = None
observed_session_id = session_id
tool_calls: dict[int, AgentToolUpdate] = {}
received_text_delta = False
received_thinking_delta = False

async for message in stream:
event = getattr(message, "event", None)
Expand All @@ -171,11 +173,17 @@ async def run_claude_code_agent(
if delta.get("type") == "text_delta" and isinstance(
delta.get("text"), str
):
yield AgentTextDelta(delta["text"])
text = delta["text"]
if text:
received_text_delta = True
yield AgentTextDelta(text)
elif delta.get("type") == "thinking_delta" and isinstance(
delta.get("thinking"), str
):
yield AgentReasoningDelta(delta["thinking"])
thinking = delta["thinking"]
yield AgentReasoningDelta(thinking)
if thinking:
received_thinking_delta = True
elif event_type == "content_block_start":
index, tool = _tool_update_from_claude_start(event)
if tool:
Expand All @@ -199,10 +207,14 @@ async def run_claude_code_agent(
if class_name == "AssistantMessage":
for block in getattr(message, "content", []) or []:
if block.__class__.__name__ == "TextBlock":
if received_text_delta:
continue
text = getattr(block, "text", "")
if text:
yield AgentTextDelta(text)
elif block.__class__.__name__ == "ThinkingBlock":
if received_thinking_delta:
continue
text = getattr(block, "thinking", "")
if text:
yield AgentReasoningDelta(text)
Expand Down
35 changes: 24 additions & 11 deletions cptr/utils/chat_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,24 @@ async def _run_agent_target(agent_target: AgentModelTarget):
if runner is None:
raise RuntimeError(f"Unsupported agent type: {agent_target.agent}")
reasoning_buffer = ""
reasoning_item_id = f"reasoning-{message_id}"

async def _finish_reasoning_item():
existing = next(
(item for item in output_items if item.get("id") == reasoning_item_id), None
)
if not existing or existing.get("status") == "completed":
return
item = {
"type": "reasoning",
"id": reasoning_item_id,
"status": "completed",
"content": [{"type": "reasoning_text", "text": reasoning_buffer}],
}
_upsert_output_item(output_items, item)
await emit(output=item)
_sync_state()

async for event in runner(
profile=agent_target.config,
model=agent_target.model,
Expand All @@ -1533,6 +1551,7 @@ async def _run_agent_target(agent_target: AgentModelTarget):
attachments=agent_attachments,
):
if isinstance(event, AgentTextDelta):
await _finish_reasoning_item()
content += event.text
text_buffer += event.text
await emit(delta=event.text)
Expand All @@ -1541,14 +1560,15 @@ async def _run_agent_target(agent_target: AgentModelTarget):
reasoning_buffer += event.text
item = {
"type": "reasoning",
"id": f"reasoning-{message_id}",
"id": reasoning_item_id,
"status": "in_progress",
"content": [{"type": "reasoning_text", "text": reasoning_buffer}],
}
_upsert_output_item(output_items, item)
await emit(output=item)
_sync_state()
elif isinstance(event, AgentToolUpdate):
await _finish_reasoning_item()
flushed_item = _flush_text()
if flushed_item:
await emit(output=flushed_item)
Expand Down Expand Up @@ -1609,6 +1629,7 @@ async def _run_agent_target(agent_target: AgentModelTarget):
await emit(output=output_item)
_sync_state()
elif isinstance(event, AgentToolOutputDelta):
await _finish_reasoning_item()
flushed_item = _flush_text()
if flushed_item:
await emit(output=flushed_item)
Expand Down Expand Up @@ -1665,16 +1686,7 @@ async def _run_agent_target(agent_target: AgentModelTarget):
elif isinstance(event, AgentError):
raise RuntimeError(event.message)
elif isinstance(event, AgentDone):
if reasoning_buffer:
_upsert_output_item(
output_items,
{
"type": "reasoning",
"id": f"reasoning-{message_id}",
"status": "completed",
"content": [{"type": "reasoning_text", "text": reasoning_buffer}],
},
)
await _finish_reasoning_item()
flushed_item = _flush_text()
if flushed_item:
await emit(output=flushed_item)
Expand Down Expand Up @@ -1704,6 +1716,7 @@ async def _run_agent_target(agent_target: AgentModelTarget):
flushed_item = _flush_text()
if flushed_item:
await emit(output=flushed_item)
await _finish_reasoning_item()
await _save_message("agent stream ended", content=content, output=output_items, done=True)
_task_state.pop(message_id, None)
await _emit_done()
Expand Down
3 changes: 2 additions & 1 deletion cptr/utils/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@
".codex/skills",
]

# Global (user-level): only our own directory
# Global (user-level): scan our own directory plus the cross-agent convention.
GLOBAL_SKILL_DIRS = [
str(Path.home() / ".cptr" / "skills"),
str(Path.home() / ".agents" / "skills"),
]

MANAGED_WORKSPACE_SKILL_DIR = ".cptr/skills"
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cptr"
version = "0.8.4"
version = "0.8.5"
description = "Your computer, from anywhere. Code, manage, and control your machine from the web."
license = {file = "LICENSE"}
readme = "README.md"
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading