From 632b34a4e2d190ae70201c9eeae201602d3d8d83 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 7 Jul 2026 02:08:53 -0500 Subject: [PATCH 1/7] refac --- cptr/utils/chat_task.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/cptr/utils/chat_task.py b/cptr/utils/chat_task.py index 9d596ce..9df126b 100644 --- a/cptr/utils/chat_task.py +++ b/cptr/utils/chat_task.py @@ -1522,6 +1522,20 @@ async def _run_agent_target(agent_target: AgentModelTarget): if runner is None: raise RuntimeError(f"Unsupported agent type: {agent_target.agent}") reasoning_buffer = "" + + async def _finish_reasoning_item(): + if not reasoning_buffer: + return + item = { + "type": "reasoning", + "id": f"reasoning-{message_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, @@ -1665,16 +1679,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) @@ -1704,6 +1709,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() From 0504711411b0145b35743c188ef1ce0260f2da9f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 7 Jul 2026 02:14:44 -0500 Subject: [PATCH 2/7] refac --- cptr/utils/agents/claude_code.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cptr/utils/agents/claude_code.py b/cptr/utils/agents/claude_code.py index e0e4a18..1292ca9 100644 --- a/cptr/utils/agents/claude_code.py +++ b/cptr/utils/agents/claude_code.py @@ -160,6 +160,7 @@ async def run_claude_code_agent( usage: dict[str, Any] | None = None observed_session_id = session_id tool_calls: dict[int, AgentToolUpdate] = {} + received_content_delta = False async for message in stream: event = getattr(message, "event", None) @@ -171,11 +172,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_content_delta = True + yield AgentTextDelta(text) elif delta.get("type") == "thinking_delta" and isinstance( delta.get("thinking"), str ): - yield AgentReasoningDelta(delta["thinking"]) + thinking = delta["thinking"] + if thinking: + received_content_delta = True + yield AgentReasoningDelta(thinking) elif event_type == "content_block_start": index, tool = _tool_update_from_claude_start(event) if tool: @@ -197,6 +204,8 @@ async def run_claude_code_agent( class_name = message.__class__.__name__ if class_name == "AssistantMessage": + if received_content_delta: + continue for block in getattr(message, "content", []) or []: if block.__class__.__name__ == "TextBlock": text = getattr(block, "text", "") From 5ac52c756daec41ec069ec9d72eb8dbde8b2730c Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 7 Jul 2026 04:45:13 -0500 Subject: [PATCH 3/7] refac --- cptr/utils/agents/claude_code.py | 27 +++++++++++++++++++++------ cptr/utils/agents/events.py | 6 ++++++ cptr/utils/chat_task.py | 13 ++++++++++--- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/cptr/utils/agents/claude_code.py b/cptr/utils/agents/claude_code.py index 1292ca9..5806d01 100644 --- a/cptr/utils/agents/claude_code.py +++ b/cptr/utils/agents/claude_code.py @@ -13,6 +13,7 @@ AgentDone, AgentError, AgentEvent, + AgentReasoningDone, AgentReasoningDelta, AgentTextDelta, AgentToolUpdate, @@ -160,7 +161,9 @@ async def run_claude_code_agent( usage: dict[str, Any] | None = None observed_session_id = session_id tool_calls: dict[int, AgentToolUpdate] = {} - received_content_delta = False + thinking_blocks: set[int] = set() + received_text_delta = False + received_thinking_delta = False async for message in stream: event = getattr(message, "event", None) @@ -174,23 +177,33 @@ async def run_claude_code_agent( ): text = delta["text"] if text: - received_content_delta = True + received_text_delta = True yield AgentTextDelta(text) elif delta.get("type") == "thinking_delta" and isinstance( delta.get("thinking"), str ): + index = event.get("index") + if isinstance(index, int): + thinking_blocks.add(index) thinking = delta["thinking"] + yield AgentReasoningDelta(thinking) if thinking: - received_content_delta = True - yield AgentReasoningDelta(thinking) + received_thinking_delta = True elif event_type == "content_block_start": index, tool = _tool_update_from_claude_start(event) if tool: if index is not None: tool_calls[index] = tool yield tool + elif isinstance(index, int): + block = event.get("content_block") + if isinstance(block, dict) and block.get("type") == "thinking": + thinking_blocks.add(index) elif event_type == "content_block_stop": index = event.get("index") + if isinstance(index, int) and index in thinking_blocks: + thinking_blocks.discard(index) + yield AgentReasoningDone() tool = tool_calls.get(index) if isinstance(index, int) else None if tool: yield AgentToolUpdate( @@ -204,14 +217,16 @@ async def run_claude_code_agent( class_name = message.__class__.__name__ if class_name == "AssistantMessage": - if received_content_delta: - continue 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) diff --git a/cptr/utils/agents/events.py b/cptr/utils/agents/events.py index d0dcd6d..f1adb88 100644 --- a/cptr/utils/agents/events.py +++ b/cptr/utils/agents/events.py @@ -16,6 +16,11 @@ class AgentReasoningDelta: text: str +@dataclass +class AgentReasoningDone: + pass + + @dataclass class AgentToolUpdate: call_id: str @@ -46,6 +51,7 @@ class AgentError: AgentEvent = ( AgentTextDelta | AgentReasoningDelta + | AgentReasoningDone | AgentToolUpdate | AgentToolOutputDelta | AgentDone diff --git a/cptr/utils/chat_task.py b/cptr/utils/chat_task.py index 9df126b..47924db 100644 --- a/cptr/utils/chat_task.py +++ b/cptr/utils/chat_task.py @@ -49,6 +49,7 @@ AgentDone, AgentError, AgentReasoningDelta, + AgentReasoningDone, AgentTextDelta, AgentToolOutputDelta, AgentToolUpdate, @@ -1522,13 +1523,17 @@ 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(): - if not reasoning_buffer: + 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": f"reasoning-{message_id}", + "id": reasoning_item_id, "status": "completed", "content": [{"type": "reasoning_text", "text": reasoning_buffer}], } @@ -1555,13 +1560,15 @@ async def _finish_reasoning_item(): 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, AgentReasoningDone): + await _finish_reasoning_item() elif isinstance(event, AgentToolUpdate): flushed_item = _flush_text() if flushed_item: From 8d82513d0cdc63b1de2fddc3db17bf99f689a300 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 7 Jul 2026 13:12:29 -0500 Subject: [PATCH 4/7] refac --- cptr/utils/agents/claude_code.py | 12 ------------ cptr/utils/agents/events.py | 6 ------ cptr/utils/chat_task.py | 6 +++--- 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/cptr/utils/agents/claude_code.py b/cptr/utils/agents/claude_code.py index 5806d01..fc16951 100644 --- a/cptr/utils/agents/claude_code.py +++ b/cptr/utils/agents/claude_code.py @@ -13,7 +13,6 @@ AgentDone, AgentError, AgentEvent, - AgentReasoningDone, AgentReasoningDelta, AgentTextDelta, AgentToolUpdate, @@ -161,7 +160,6 @@ async def run_claude_code_agent( usage: dict[str, Any] | None = None observed_session_id = session_id tool_calls: dict[int, AgentToolUpdate] = {} - thinking_blocks: set[int] = set() received_text_delta = False received_thinking_delta = False @@ -182,9 +180,6 @@ async def run_claude_code_agent( elif delta.get("type") == "thinking_delta" and isinstance( delta.get("thinking"), str ): - index = event.get("index") - if isinstance(index, int): - thinking_blocks.add(index) thinking = delta["thinking"] yield AgentReasoningDelta(thinking) if thinking: @@ -195,15 +190,8 @@ async def run_claude_code_agent( if index is not None: tool_calls[index] = tool yield tool - elif isinstance(index, int): - block = event.get("content_block") - if isinstance(block, dict) and block.get("type") == "thinking": - thinking_blocks.add(index) elif event_type == "content_block_stop": index = event.get("index") - if isinstance(index, int) and index in thinking_blocks: - thinking_blocks.discard(index) - yield AgentReasoningDone() tool = tool_calls.get(index) if isinstance(index, int) else None if tool: yield AgentToolUpdate( diff --git a/cptr/utils/agents/events.py b/cptr/utils/agents/events.py index f1adb88..d0dcd6d 100644 --- a/cptr/utils/agents/events.py +++ b/cptr/utils/agents/events.py @@ -16,11 +16,6 @@ class AgentReasoningDelta: text: str -@dataclass -class AgentReasoningDone: - pass - - @dataclass class AgentToolUpdate: call_id: str @@ -51,7 +46,6 @@ class AgentError: AgentEvent = ( AgentTextDelta | AgentReasoningDelta - | AgentReasoningDone | AgentToolUpdate | AgentToolOutputDelta | AgentDone diff --git a/cptr/utils/chat_task.py b/cptr/utils/chat_task.py index 47924db..671b393 100644 --- a/cptr/utils/chat_task.py +++ b/cptr/utils/chat_task.py @@ -49,7 +49,6 @@ AgentDone, AgentError, AgentReasoningDelta, - AgentReasoningDone, AgentTextDelta, AgentToolOutputDelta, AgentToolUpdate, @@ -1552,6 +1551,7 @@ async def _finish_reasoning_item(): attachments=agent_attachments, ): if isinstance(event, AgentTextDelta): + await _finish_reasoning_item() content += event.text text_buffer += event.text await emit(delta=event.text) @@ -1567,9 +1567,8 @@ async def _finish_reasoning_item(): _upsert_output_item(output_items, item) await emit(output=item) _sync_state() - elif isinstance(event, AgentReasoningDone): - await _finish_reasoning_item() elif isinstance(event, AgentToolUpdate): + await _finish_reasoning_item() flushed_item = _flush_text() if flushed_item: await emit(output=flushed_item) @@ -1630,6 +1629,7 @@ async def _finish_reasoning_item(): 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) From cefed28b8284e65e922edd91d8da95bd98d93f49 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 7 Jul 2026 13:48:10 -0500 Subject: [PATCH 5/7] refac --- cptr/utils/skills.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cptr/utils/skills.py b/cptr/utils/skills.py index 4150bb6..790b060 100644 --- a/cptr/utils/skills.py +++ b/cptr/utils/skills.py @@ -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" From cfddf0cf52e78364549b7309e637ad3234e9f9fd Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 7 Jul 2026 13:49:08 -0500 Subject: [PATCH 6/7] refac --- cptr/utils/agents/acp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cptr/utils/agents/acp.py b/cptr/utils/agents/acp.py index 76a0754..747b6d3 100644 --- a/cptr/utils/agents/acp.py +++ b/cptr/utils/agents/acp.py @@ -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 @@ -197,7 +197,7 @@ 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 @@ -205,11 +205,11 @@ async def _handle_message(self, message: dict[str, Any]) -> None: 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( From 7e3f3ffe9293ff272bc65f2903f8f58f802cbb27 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 7 Jul 2026 13:51:47 -0500 Subject: [PATCH 7/7] refac --- CHANGELOG.md | 12 ++++++++++++ pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb69cd..3a58d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 68acd71..b61d28c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/uv.lock b/uv.lock index 865902d..ba606fc 100644 --- a/uv.lock +++ b/uv.lock @@ -284,7 +284,7 @@ wheels = [ [[package]] name = "cptr" -version = "0.8.4" +version = "0.8.5" source = { editable = "." } dependencies = [ { name = "aiosqlite" },