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: diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 8ec70e7c8c7..f7c3b3c2419 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..bc5754f4e1a 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,8 @@ T = TypeVar("T") +logger = logging.getLogger(__name__) + class IOProxy(Generic[T]): """Facade wrapping an InputOutput instance with coder context. @@ -234,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 b092cc842ba..84634906f95 100644 --- a/cecli/helpers/requests.py +++ b/cecli/helpers/requests.py @@ -13,8 +13,35 @@ 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) + + 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 + ) + 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.pop("reasoning_content", None) + + 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.pop("reasoning_content", None) + return messages diff --git a/cecli/llm.py b/cecli/llm.py index edd264a241a..594f81da78f 100644 --- a/cecli/llm.py +++ b/cecli/llm.py @@ -108,6 +108,31 @@ 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_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"] = {} + + 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) 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) 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() 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: 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",