Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0e2cafc
fix: Escape special characters in tool descriptions
Apr 30, 2026
8c11bb0
fix: Add test for tool description special character escaping
Apr 30, 2026
642f51b
cli-17: merged
May 9, 2026
89f2499
fix: Hide PowerShell window when sending notifications
May 9, 2026
3d0f6b6
Bump Version
May 10, 2026
2104bfb
Throttle observations just in case large tool responses would retrigg…
May 10, 2026
34533b9
Merge branch 'main' of github.com-personal:dwash96/cecli into cli-12-…
May 10, 2026
2e89564
fix: Use MessageBox for Windows notifications to hide terminal
May 10, 2026
c9f8771
fix: Remove DETACHED_PROCESS flag for Windows notifications
May 10, 2026
40a8f23
cli-12: fixed formatting
May 10, 2026
d4f892c
cli-12: fixed formatting
May 10, 2026
90622d2
Fix observation manager usage in commands
May 10, 2026
bceb0b5
cli-12: fix
May 11, 2026
d6ea785
Merge branch 'main' of github.com-personal:cecli-dev/cecli into cli-1…
May 12, 2026
b77e031
cli-17: formatted
May 12, 2026
3ab3d86
fix: Remove unconditional bell ring in tool call info printing
May 12, 2026
60c81d9
Ask about auto loading before doing it
May 12, 2026
ffc5933
Merge pull request #513 from szmania/cli-21-agent-mode-all-notificati…
dwash96 May 12, 2026
44c39e0
Merge pull request #511 from szmania/cli-12-prevent-closing-notificti…
dwash96 May 12, 2026
363541b
Remove extraneous space in schema
May 12, 2026
be784ab
#512: General fix for tool escaping but just once on registration
May 13, 2026
8df6f24
cli-17: fixed merge conflicts
May 13, 2026
818d33f
Merge pull request #512 from szmania/cli-17-fix-special-character-esc…
dwash96 May 13, 2026
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
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__ = "0.99.10.dev"
__version__ = "0.99.12.dev"
safe_version = __version__

try:
Expand Down
25 changes: 16 additions & 9 deletions cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,7 @@ async def _exec_async():
content_parts.append(item.text)
return "".join(content_parts)
except Exception as e:
self.io.tool_warning(
(f"Executing {tool_name} on {server.name} failed:\nError: {e}")
)
self.io.tool_warning(f"Executing {tool_name} on {server.name} failed:\nError: {e}")
return f"Error executing tool call {tool_name}: {e}"

result, interrupted = await interruptible(_exec_async(), self.interrupt_event)
Expand Down Expand Up @@ -381,7 +379,10 @@ def get_context_symbol_outline(self):
try:
result = '<context name="symbol_outline" from="agent">\n'
result += "## Symbol Outline (Current Context)\n\n"
result += "Code definitions (classes, functions, methods, etc.) found in files currently in chat context."
result += (
"Code definitions (classes, functions, methods, etc.) found in files currently in"
" chat context."
)
result += "\n\n"
files_to_outline = list(self.abs_fnames) + list(self.abs_read_only_fnames)
if not files_to_outline:
Expand Down Expand Up @@ -522,7 +523,10 @@ def get_context_summary(self):
)
if editable_files:
result += "\n".join(editable_files) + "\n\n"
result += f"**Total editable: {len(editable_files)} files, {editable_tokens:,} tokens**\n\n"
result += (
f"**Total editable: {len(editable_files)} files,"
f" {editable_tokens:,} tokens**\n\n"
)
else:
result += "No editable files in context\n\n"
if self.abs_read_only_fnames:
Expand All @@ -542,7 +546,10 @@ def get_context_summary(self):
)
if readonly_files:
result += "\n".join(readonly_files) + "\n\n"
result += f"**Total read-only: {len(readonly_files)} files, {readonly_tokens:,} tokens**\n\n"
result += (
f"**Total read-only: {len(readonly_files)} files,"
f" {readonly_tokens:,} tokens**\n\n"
)
else:
result += "No read-only files in context\n\n"
extra_tokens = sum(self.context_block_tokens.values())
Expand Down Expand Up @@ -730,7 +737,7 @@ async def gather_and_await():
self.model_kwargs = {}
result_message = f"Error executing {tool_name}: {e}"
self.io.tool_error(
(f"Error during {tool_name} execution: {e}\n{traceback.format_exc()}")
f"Error during {tool_name} execution: {e}\n{traceback.format_exc()}"
)
tool_responses.append(
{"role": "tool", "tool_call_id": tool_call.id, "content": result_message}
Expand Down Expand Up @@ -1020,8 +1027,8 @@ def _generate_tool_context(self, repetitive_tools):
context_parts.append("## File Editing Tools Disabled")
context_parts.append(
"File editing tools are currently disabled. Use `ReadRange` to determine the"
" current content hash prefixes needed to perform an edit and activate them when you"
" are ready to edit a file."
" current content hash prefixes needed to perform an edit and activate them when"
" you are ready to edit a file."
)

context_parts.append("\n\n")
Expand Down
8 changes: 3 additions & 5 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,6 @@ def __init__(

# Initialize conversation system if enabled
ConversationService.get_chunks(self).initialize_conversation_system()
self.observation_manager = ObservationManager.get_instance(self)

self.commands = commands or Commands(self.io, self, args=args)
self.commands.coder = self
Expand Down Expand Up @@ -1752,7 +1751,7 @@ async def compact_context_if_needed(self, force=False, message=""):
return

# Trigger background observation/reflection check
await self.observation_manager.check_and_trigger()
await ObservationManager.get_instance(self).check_and_trigger()

manager = ConversationService.get_manager(self)
done_messages = manager.get_messages_dict(MessageTag.DONE)
Expand Down Expand Up @@ -1789,8 +1788,8 @@ async def summarize_and_update(messages, tag):
if not text:
raise ValueError(f"Summarization of {tag} messages returned empty.")

if self.observation_manager.observations:
obs_text = "\n".join(self.observation_manager.observations)
if ObservationManager.get_instance(self).observations:
obs_text = "\n".join(ObservationManager.get_instance(self).observations)
text = f"HISTORICAL OBSERVATIONS:\n{obs_text}\n\n{text}"

manager.clear_tag(tag)
Expand Down Expand Up @@ -2780,7 +2779,6 @@ async def process_tool_calls(self, tool_call_response):

def _print_tool_call_info(self, server_tool_calls):
"""Print information about an MCP tool call."""
self.io.ring_bell()
# self.io.tool_output("Preparing to run MCP tools", bold=False)

for server, tool_calls in server_tool_calls.items():
Expand Down
3 changes: 2 additions & 1 deletion cecli/commands/clear.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from cecli.commands.utils.base_command import BaseCommand
from cecli.commands.utils.helpers import format_command_result
from cecli.helpers.observations.manager import ObservationManager


class ClearCommand(BaseCommand):
Expand All @@ -19,7 +20,7 @@ async def execute(cls, io, coder, args, **kwargs):
ConversationService.get_manager(coder).clear_tag(MessageTag.FILE_CONTEXTS)

ConversationService.get_files(coder).reset()
coder.observation_manager.reset()
ObservationManager.get_instance(coder).reset()

# Clear TUI output if available
if coder.tui and coder.tui():
Expand Down
3 changes: 2 additions & 1 deletion cecli/commands/reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from cecli.commands.utils.base_command import BaseCommand
from cecli.commands.utils.helpers import format_command_result
from cecli.helpers.conversation import ConversationService
from cecli.helpers.observations.manager import ObservationManager


class ResetCommand(BaseCommand):
Expand All @@ -24,7 +25,7 @@ async def execute(cls, io, coder, args, **kwargs):
# Re-initialize Conversation components with current coder
ConversationService.get_manager(coder).initialize(reformat=True)
ConversationService.get_files(coder) # Ensure instance exists/initialized
coder.observation_manager.reset()
ObservationManager.get_instance(coder).reset()

# Clear TUI output if available
if coder.tui and coder.tui():
Expand Down
13 changes: 8 additions & 5 deletions cecli/helpers/observations/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,21 @@ async def check_and_trigger(self):
if self.is_processing:
return

manager = ConversationService.get_manager(self.coder)
cur_messages = manager.get_messages_dict(MessageTag.CUR)
cur_messages = ConversationService.get_manager(self.coder).get_messages_dict(MessageTag.CUR)

# Calculate unobserved tokens
unobserved = cur_messages[self._last_observed_index :]
current_index = len(cur_messages)

if not unobserved:
return

tokens = self.coder.summarizer.count_tokens(unobserved)

if tokens >= self.observation_threshold:
if (
tokens >= self.observation_threshold
and (not self._last_observed_index or current_index - self._last_observed_index >= 10)
) or tokens >= 2 * self.observation_threshold:
asyncio.create_task(self.run_observation(unobserved))
self._last_observed_index = len(cur_messages)

Expand All @@ -50,8 +54,7 @@ async def check_and_trigger(self):
async def run_observation(self, messages):
self.is_processing = True
try:
manager = ConversationService.get_manager(self.coder)
all_messages = manager.get_messages_dict()
all_messages = ConversationService.get_manager(self.coder).get_messages_dict()
prompt = self.coder.gpt_prompts.observation_prompt
observation = await self.coder.summarizer.summarize_all_as_text(
all_messages, prompt, max_tokens=8192
Expand Down
8 changes: 3 additions & 5 deletions cecli/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -1718,13 +1718,13 @@ def get_default_notification_command(self):
"$toastXml = $template.GetXml(); "
"$toastXml.GetElementsByTagName('text')[0].AppendChild"
"($template.CreateTextNode('cecli')) > $null; "
f"$toastXml.GetElementsByTagName('text')[1].AppendChild"
"$toastXml.GetElementsByTagName('text')[1].AppendChild"
f"($template.CreateTextNode('{NOTIFICATION_MESSAGE}')) > $null; "
"$toast = [Windows.UI.Notifications.ToastNotification]::new($toastXml); "
"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('cecli')"
'.Show($toast)"'
)
return "powershell -command" + ps_command
return "powershell -WindowStyle Hidden -Command" + ps_command

return None # Unknown system

Expand All @@ -1739,9 +1739,7 @@ def _send_notification(self):
"stderr": subprocess.DEVNULL,
}
if platform.system() == "Windows":
kwargs["creationflags"] = (
subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS
)
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
else:
# For non-Windows systems, start a new session to detach
kwargs["start_new_session"] = True
Expand Down
22 changes: 14 additions & 8 deletions cecli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1194,15 +1194,21 @@ def get_io(pretty):
if args.exit:
return await graceful_exit(coder)
if args.auto_load:
try:
from cecli.sessions import SessionManager
if await pre_init_io.confirm_ask(
"Do you want to load your previous session?",
acknowledge=True,
explicit_yes_required=True,
):
try:
from cecli.sessions import SessionManager

session_manager = SessionManager(coder, io)
await session_manager.load_session(
args.auto_save_session_name if args.auto_save_session_name else "auto-save"
)
except Exception:
pass
session_manager = SessionManager(coder, io)
await session_manager.load_session(
args.auto_save_session_name if args.auto_save_session_name else "auto-save",
switch=False,
)
except Exception:
pass

if suppress_pre_init:
await graceful_exit(coder)
Expand Down
20 changes: 20 additions & 0 deletions cecli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,26 @@ async def send_completion(
sorted_tools = sorted(
effective_tools, key=lambda x: x.get("function", {}).get("name", "Invalid Name")
)

try:
# Deep copy to avoid modifying original tool schemas
sorted_tools = json.loads(json.dumps(sorted_tools))

for tool in sorted_tools:
function_schema = tool.get("function")
if function_schema and "description" in function_schema:
desc = function_schema.get("description")
if isinstance(desc, str):
# Escape the description string for JSON, but without the outer quotes.
# This is a workaround for issues with special characters in descriptions.
function_schema["description"] = json.dumps(desc, ensure_ascii=False)[
1:-1
]
except (TypeError, json.JSONDecodeError):
# If deep copy fails, proceed with original tools.
# This is a safeguard.
pass

kwargs["tools"] = sorted_tools

if functions and len(functions) == 1:
Expand Down
4 changes: 2 additions & 2 deletions cecli/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def list_sessions(self) -> List[Dict]:

return sessions

async def load_session(self, session_identifier: str) -> bool:
async def load_session(self, session_identifier: str, switch=True) -> bool:
"""Load a saved session by name or file path."""
if not session_identifier:
self.io.tool_error("Please provide a session name or file path.")
Expand All @@ -113,7 +113,7 @@ async def load_session(self, session_identifier: str) -> bool:

# Apply session data
applied, loaded_edit_format = await self._apply_session_data(session_data, session_file)
if applied:
if applied and switch:
from cecli.commands import SwitchCoderSignal

edit_format_to_switch_to = self.coder.edit_format
Expand Down
3 changes: 1 addition & 2 deletions cecli/tools/context_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ class Tool(BaseTool):
"type": "array",
"items": {"type": "string"},
"description": (
"List of file paths to add as read-only. "
"Limit to at most 2 at a time."
"List of file paths to add as read-only. Limit to at most 2 at a time."
),
},
"create": {
Expand Down
6 changes: 3 additions & 3 deletions cecli/tools/edit_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class Tool(BaseTool):
"Can handle an array of up to 10 edits across multiple files. "
"Each edit must include its own file_path and operation type. "
"Use content hash ranges with the start_line and end_line parameters with format "
'"{4 char hash}" (without the braces). For empty files, use "@000" as the '
"`{4 char hash}` (without the braces). For empty files, use `@000` as the "
"content hash references."
),
"parameters": {
Expand Down Expand Up @@ -71,14 +71,14 @@ class Tool(BaseTool):
"start_line": {
"type": "string",
"description": (
'Content hash for start line: "{4 char hash}" (without '
"Content hash for start line: `{4 char hash}` (without "
"the braces)"
),
},
"end_line": {
"type": "string",
"description": (
'Content hash for end line: "{4 char hash}" (without the'
"Content hash for end line: `{4 char hash}` (without the"
" braces)"
),
},
Expand Down
5 changes: 1 addition & 4 deletions cecli/tools/load_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ class Tool(BaseTool):
"type": "function",
"function": {
"name": "LoadSkill",
"description": (
"Load a skill by name (agent mode only). Adds skill to include list and removes"
" from exclude list."
),
"description": "Load a skill by name.",
"parameters": {
"type": "object",
"properties": {
Expand Down
Loading
Loading