Skip to content
2 changes: 1 addition & 1 deletion cecli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from packaging import version

__version__ = "1.0.2.dev"
__version__ = "1.0.3.dev"
safe_version = __version__

try:
Expand Down
6 changes: 5 additions & 1 deletion cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 7 additions & 0 deletions cecli/helpers/io_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import asyncio
import logging
import queue as _queue
import weakref
from typing import TYPE_CHECKING, Any, Generic, TypeVar
Expand All @@ -15,6 +16,8 @@

T = TypeVar("T")

logger = logging.getLogger(__name__)


class IOProxy(Generic[T]):
"""Facade wrapping an InputOutput instance with coder context.
Expand Down Expand Up @@ -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()
Expand Down
27 changes: 27 additions & 0 deletions cecli/helpers/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
25 changes: 25 additions & 0 deletions cecli/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
13 changes: 13 additions & 0 deletions cecli/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions cecli/tui/widgets/input_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
2 changes: 1 addition & 1 deletion cecli/tui/widgets/selectable_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions cecli/tui/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading