From c6820ae8ce13b56cfe56ca59351358915f05d318 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Jul 2026 08:25:34 -0400 Subject: [PATCH 1/9] Bump Version --- cecli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cecli/__init__.py b/cecli/__init__.py index 45c3b410f3d..000258ef690 100644 --- a/cecli/__init__.py +++ b/cecli/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "1.0.2.dev" +__version__ = "1.0.3.dev" safe_version = __version__ try: From dbb496122f345d9ec98160a6c35ba678e3e2f13f Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Jul 2026 08:29:20 -0400 Subject: [PATCH 2/9] rstrip TUI selected text since Textual includes trailing spaces in the segments in the line iterator --- cecli/tui/widgets/selectable_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cecli/tui/widgets/selectable_log.py b/cecli/tui/widgets/selectable_log.py index f4c85feffbf..4b54b5fa8e2 100644 --- a/cecli/tui/widgets/selectable_log.py +++ b/cecli/tui/widgets/selectable_log.py @@ -96,7 +96,7 @@ def get_selected_text(self, copy=False) -> str | None: lines = [] for i in range(lo, hi + 1): text = "".join(seg.text for seg in self.lines[i] if seg.text) - lines.append(text) + lines.append(text.rstrip()) text = "\n".join(lines) if not copy: From b32250b2b15f8346e78dd3f38c9328fade3471a9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Jul 2026 17:54:49 -0400 Subject: [PATCH 3/9] #624: Fix security_config parsing so URLs can be parsed in commands --- cecli/coders/base_coder.py | 6 +++++- cecli/helpers/io_proxy.py | 6 ++++++ cecli/tui/worker.py | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 8ec70e7c8c7..beb8d44c71d 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -464,9 +464,13 @@ def __init__( self.abs_root_path_cache = {} self.auto_copy_context = auto_copy_context - self.security_config = security_config or {} self.auto_accept_architect = auto_accept_architect + try: + self.security_config = json.loads(security_config) + except (json.JSONDecodeError, TypeError): + self.security_config = {} + self.ignore_mentions = ignore_mentions if not self.ignore_mentions: self.ignore_mentions = set() diff --git a/cecli/helpers/io_proxy.py b/cecli/helpers/io_proxy.py index bc6ce8aa475..cee66e7d876 100644 --- a/cecli/helpers/io_proxy.py +++ b/cecli/helpers/io_proxy.py @@ -6,6 +6,7 @@ """ import asyncio +import logging import queue as _queue import weakref from typing import TYPE_CHECKING, Any, Generic, TypeVar @@ -15,6 +16,7 @@ T = TypeVar("T") +logger = logging.getLogger(__name__) class IOProxy(Generic[T]): """Facade wrapping an InputOutput instance with coder context. @@ -218,6 +220,10 @@ async def stop_output_task(self): state = self._per_coder.get(self._coder_uuid, {}) task = state.get("output_task") if task: + e = task.exception() + if e: + logger.error(e, exc_info=True) + try: task.cancel() await task diff --git a/cecli/tui/worker.py b/cecli/tui/worker.py index 4b28fcf7e89..6ad917b8fda 100644 --- a/cecli/tui/worker.py +++ b/cecli/tui/worker.py @@ -107,6 +107,8 @@ async def _async_run(self): await self._handle_switch_coder_signal(switch) # Continue the loop with the new coder except Exception as e: + logger.error(e, exc_info=True) + self.output_queue.put( { "type": "error", From 098c8cbb681f92c39bee9ce1ab4f37eb9c53b49e Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Jul 2026 17:57:10 -0400 Subject: [PATCH 4/9] Fix formatting --- cecli/coders/base_coder.py | 2 +- cecli/helpers/io_proxy.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index beb8d44c71d..f7c3b3c2419 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -469,7 +469,7 @@ def __init__( try: self.security_config = json.loads(security_config) except (json.JSONDecodeError, TypeError): - self.security_config = {} + self.security_config = {} self.ignore_mentions = ignore_mentions if not self.ignore_mentions: diff --git a/cecli/helpers/io_proxy.py b/cecli/helpers/io_proxy.py index cee66e7d876..4a2cf7165e3 100644 --- a/cecli/helpers/io_proxy.py +++ b/cecli/helpers/io_proxy.py @@ -18,6 +18,7 @@ logger = logging.getLogger(__name__) + class IOProxy(Generic[T]): """Facade wrapping an InputOutput instance with coder context. From 092f8da7fb846d33ab43eac918dd92bdb5b65d4c Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Jul 2026 19:06:02 -0400 Subject: [PATCH 5/9] Use pyperclip to make textual copy to the system clibboard and its internal ledger on internal copy --- cecli/tui/app.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 991d94bc19e..9182abe0a5c 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -745,6 +745,19 @@ def enable_input(self, msg, coder=None): input_area.focus() + def copy_to_clipboard(self, text: str) -> None: + import pyperclip + + try: + pyperclip.copy(text) + self._clipboard = text + except Exception: # pragma: no cover - system clipboard errors + self.worker.coder.io.tool_error("Failed to copy to system clipboard.") + self.worker.coder.io.tool_output( + "You may need to install xclip, xsel, or wl-clipboard on Linux, or pbcopy on macOS." + ) + super().copy_to_clipboard(text) + def update_spinner(self, msg, agent_name: str | None = None): """Update spinner in footer.""" footer = self.query_one(MainFooter) From 37d2395e31f84bacd170f104fa07df8ba3e066bf Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Jul 2026 19:06:53 -0400 Subject: [PATCH 6/9] Don't crash on Textual out of bounds recalculation error onfirst input/undo cycle --- cecli/tui/widgets/input_area.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/cecli/tui/widgets/input_area.py b/cecli/tui/widgets/input_area.py index 453e96ecd90..198dd70dd84 100644 --- a/cecli/tui/widgets/input_area.py +++ b/cecli/tui/widgets/input_area.py @@ -395,3 +395,22 @@ def on_text_area_changed(self, event) -> None: if val.startswith("/") or "@" in val or possible_path or self.completion_active: self.post_message(self.CompletionRequested(val)) + + def _refresh_size(self) -> None: + """Refresh size, clamping cursor to document bounds first. + + Workaround for a Textual bug where undo of a multi-line paste leaves + cursor_location pointing to a line that no longer exists after undo, + causing a ValueError when _refresh_size triggers scroll_cursor_visible. + """ + try: + doc_line_count = self.document.line_count + cursor_row, cursor_col = self.cursor_location + if cursor_row >= doc_line_count: + clamped_row = max(0, doc_line_count - 1) + line_text = self.document.get_line(clamped_row) + clamped_col = min(cursor_col, len(line_text)) + self.cursor_location = (clamped_row, clamped_col) + except (ValueError, IndexError, AttributeError): + pass + super()._refresh_size() From f52e1be6d351b489404d3cb745dc43249162766a Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Jul 2026 22:28:01 -0400 Subject: [PATCH 7/9] Propagate encryted reasoning content back to responses api models --- cecli/helpers/requests.py | 9 +++++++++ cecli/llm.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/cecli/helpers/requests.py b/cecli/helpers/requests.py index b092cc842ba..db947ca9c39 100644 --- a/cecli/helpers/requests.py +++ b/cecli/helpers/requests.py @@ -15,6 +15,15 @@ def add_reasoning_content(messages): msg["reasoning_content"] = "" if msg.get("provider_specific_fields", None): msg["provider_specific_fields"].pop("reasoning_content", None) + + if ( + msg["provider_specific_fields"].get("reasoning_items", None) is not None + and msg.get("reasoning_items", None) is None + ): + msg["reasoning_items"] = msg["provider_specific_fields"].pop( + "reasoning_items", None + ) + return messages diff --git a/cecli/llm.py b/cecli/llm.py index edd264a241a..c344bb6b264 100644 --- a/cecli/llm.py +++ b/cecli/llm.py @@ -108,6 +108,15 @@ def _patched_delta_init(self_delta, *args, **kwargs): # Intercept and map 'reasoning_text' to 'reasoning_content' if kwargs.get("reasoning_content") is None and "reasoning_text" in kwargs: kwargs["reasoning_content"] = kwargs.pop("reasoning_text", None) + + if kwargs.get("reasoning_items", None) is not None: + if kwargs.get("provider_specific_fields") is None: + kwargs["provider_specific_fields"] = {} + + kwargs["provider_specific_fields"]["reasoning_items"] = kwargs.pop( + "reasoning_items", None + ) + # Pass the modified kwargs to the original __init__ _original_delta_init(self_delta, *args, **kwargs) From 9e0171095862589a67e6fe6bf00bedd0f2076b2c Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Jul 2026 22:48:14 -0400 Subject: [PATCH 8/9] Forward all reasoning_* non completions compliant parameters back at top level to improve agent performance and up the cache hit ratio for github_copilot --- cecli/helpers/requests.py | 18 ++++++++++++++++++ cecli/llm.py | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/cecli/helpers/requests.py b/cecli/helpers/requests.py index db947ca9c39..7953c96bb1d 100644 --- a/cecli/helpers/requests.py +++ b/cecli/helpers/requests.py @@ -13,6 +13,7 @@ def add_reasoning_content(messages): for msg in messages: if msg.get("role") == "assistant" and "reasoning_content" not in msg: msg["reasoning_content"] = "" + if msg.get("provider_specific_fields", None): msg["provider_specific_fields"].pop("reasoning_content", None) @@ -23,6 +24,23 @@ def add_reasoning_content(messages): msg["reasoning_items"] = msg["provider_specific_fields"].pop( "reasoning_items", None ) + msg["reasoning_content"] = "" + + if ( + msg["provider_specific_fields"].get("reasoning_text", None) is not None + and msg.get("reasoning_text", None) is None + ): + msg["reasoning_text"] = msg["provider_specific_fields"].pop("reasoning_text", None) + msg["reasoning_content"] = "" + + if ( + msg["provider_specific_fields"].get("reasoning_opaque", None) is not None + and msg.get("reasoning_opaque", None) is None + ): + msg["reasoning_opaque"] = msg["provider_specific_fields"].pop( + "reasoning_opaque", None + ) + msg["reasoning_content"] = "" return messages diff --git a/cecli/llm.py b/cecli/llm.py index c344bb6b264..594f81da78f 100644 --- a/cecli/llm.py +++ b/cecli/llm.py @@ -109,6 +109,22 @@ def _patched_delta_init(self_delta, *args, **kwargs): if kwargs.get("reasoning_content") is None and "reasoning_text" in kwargs: kwargs["reasoning_content"] = kwargs.pop("reasoning_text", None) + if kwargs.get("reasoning_text", None) is not None: + if kwargs.get("provider_specific_fields") is None: + kwargs["provider_specific_fields"] = {} + + kwargs["provider_specific_fields"]["reasoning_text"] = kwargs.pop( + "reasoning_text", None + ) + + if kwargs.get("reasoning_opaque", None) is not None: + if kwargs.get("provider_specific_fields") is None: + kwargs["provider_specific_fields"] = {} + + kwargs["provider_specific_fields"]["reasoning_opaque"] = kwargs.pop( + "reasoning_opaque", None + ) + if kwargs.get("reasoning_items", None) is not None: if kwargs.get("provider_specific_fields") is None: kwargs["provider_specific_fields"] = {} From 5038e236caf93a4959f78d90be36dd1235cd86c9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 30 Jul 2026 03:37:06 -0400 Subject: [PATCH 9/9] Don't add tracebakc for SwitchCoderSignal, and remove reasoning content when one of the other reasoning types is passed in --- cecli/helpers/io_proxy.py | 8 ++++---- cecli/helpers/requests.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cecli/helpers/io_proxy.py b/cecli/helpers/io_proxy.py index 4a2cf7165e3..bc5754f4e1a 100644 --- a/cecli/helpers/io_proxy.py +++ b/cecli/helpers/io_proxy.py @@ -221,10 +221,6 @@ async def stop_output_task(self): state = self._per_coder.get(self._coder_uuid, {}) task = state.get("output_task") if task: - e = task.exception() - if e: - logger.error(e, exc_info=True) - try: task.cancel() await task @@ -241,6 +237,10 @@ async def stop_output_task(self): IndexError, RuntimeError, ): + e = task.exception() + if e: + logger.error(e, exc_info=True) + import traceback traceback_str = traceback.format_exc() diff --git a/cecli/helpers/requests.py b/cecli/helpers/requests.py index 7953c96bb1d..84634906f95 100644 --- a/cecli/helpers/requests.py +++ b/cecli/helpers/requests.py @@ -24,14 +24,14 @@ def add_reasoning_content(messages): msg["reasoning_items"] = msg["provider_specific_fields"].pop( "reasoning_items", None ) - msg["reasoning_content"] = "" + msg.pop("reasoning_content", None) if ( msg["provider_specific_fields"].get("reasoning_text", None) is not None and msg.get("reasoning_text", None) is None ): msg["reasoning_text"] = msg["provider_specific_fields"].pop("reasoning_text", None) - msg["reasoning_content"] = "" + msg.pop("reasoning_content", None) if ( msg["provider_specific_fields"].get("reasoning_opaque", None) is not None @@ -40,7 +40,7 @@ def add_reasoning_content(messages): msg["reasoning_opaque"] = msg["provider_specific_fields"].pop( "reasoning_opaque", None ) - msg["reasoning_content"] = "" + msg.pop("reasoning_content", None) return messages