Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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__ = "1.0.3.dev"
__version__ = "1.0.5.dev"
safe_version = __version__

try:
Expand Down
1 change: 0 additions & 1 deletion cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ def __init__(self, *args, **kwargs):
self.file_read_cache = set()
self.tool_call_count = 0
self.turn_count = 0
self.max_reflections = 15
self.use_enhanced_context = True
self._last_edited_file = None
self._cur_message_divider = None
Expand Down
1 change: 1 addition & 0 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1886,6 +1886,7 @@ async def run_one(self, user_message, preproc):
ConversationService.get_chunks(self).flush_removals()
self.last_user_message = user_message
self.error_code = None
self.num_tool_calls = 0
# Trim memory in the background so it doesn't delay the response
coroutines.fire_and_forget(asyncio.to_thread(trim_memory))
# Fire memorizer after each user request
Expand Down
13 changes: 12 additions & 1 deletion cecli/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,22 @@ def fname_to_url(filepath):
if relevant_parts and relevant_parts[0].lower() == "_includes":
return ""
url_path = "/".join(relevant_parts)

# docmd renders each .md source to <path>/index.html, so the published
# URLs are directory-style (e.g. /docs/usage/) rather than .html files.
is_doc = False
if url_path.lower().endswith(index.lower()):
url_path = url_path[: -len(index)]
is_doc = True
elif url_path.lower().endswith(md.lower()):
url_path = url_path[: -len(md)] + ".html"
url_path = url_path[: -len(md)]
is_doc = True

url_path = url_path.strip("/")
if not url_path:
return "https://cecli.dev/"
if is_doc:
return f"https://cecli.dev/{url_path}/"
return f"https://cecli.dev/{url_path}"


Expand Down
22 changes: 22 additions & 0 deletions cecli/helpers/hashline.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,20 @@ def _resolve_to_hash_id(lines, idx, hp):
key=lambda idx: abs(idx - start_hint_line),
)
resolved_start = _resolve_to_hash_id(lines, resolved_start_idx, hp)
else:
# Fallback: the value may be line content whose whitespace was
# normalized (e.g. stripped indentation). Resolve it against a
# single unique line — mirroring apply_hashline_operations — so
# the preview resolves exactly like the actual edit.
unique_resolved = _try_resolve_as_unique_line(hp, first_line)
if unique_resolved is not None:
resolved_start = unique_resolved
try:
candidates = hp.resolve_to_lines(normalize_hashline(unique_resolved))
if candidates:
resolved_start_idx = candidates[0]
except (ContentHashError, ValueError):
pass
elif start_value is not None and _looks_like_content_id(start_value):
# Already a content ID - try to resolve it to find the line position
# for proximity matching with end_value
Expand Down Expand Up @@ -432,6 +446,14 @@ def _resolve_to_hash_id(lines, idx, hp):
key=lambda idx: abs(idx - resolved_start_idx),
)
resolved_end = _resolve_to_hash_id(lines, closest_idx, hp)
else:
# Fallback: the value may be line content whose whitespace was
# normalized (e.g. stripped indentation). Resolve it against a
# single unique line — mirroring apply_hashline_operations — so
# the preview resolves exactly like the actual edit.
unique_resolved = _try_resolve_as_unique_line(hp, first_line)
if unique_resolved is not None:
resolved_end = unique_resolved
elif end_value is not None and _looks_like_content_id(end_value):
# Already a content ID - try to resolve it
try:
Expand Down
29 changes: 26 additions & 3 deletions cecli/helpers/queues.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,23 @@ def wake_input_waiters() -> None:

Safe to call from any thread; the wake is marshaled onto the input loop
via call_soon_threadsafe. No-op if no consumer has bound a loop yet.

If the bound loop was closed (e.g. a hot reload tore down the previous
coder worker loop), the stale binding is dropped so the next
wait_for_input() rebinds to the current loop instead of raising
"Event loop is closed".
"""
global _input_loop, _input_wake

loop = _input_loop
if loop is None or _input_wake is None:
return

if loop.is_closed():
_input_loop = None
_input_wake = None
return

loop.call_soon_threadsafe(_input_wake.set)


Expand All @@ -102,9 +115,19 @@ async def wait_for_input() -> None:

Must be called from the input loop (the coder worker loop). Consumers
sweep the payload queues first, then block here until wake_input_waiters()
fires. Initializes the wake state from the running loop on first use.
fires. Initializes the wake state from the running loop on first use and
rebinds whenever the previous loop was closed or is no longer the
running loop (which happens across a hot reload).
"""
if _input_loop is None or _input_wake is None:
set_input_loop(asyncio.get_running_loop())
loop = asyncio.get_running_loop()

if (
_input_loop is None
or _input_wake is None
or _input_loop.is_closed()
or _input_loop is not loop
):
set_input_loop(loop)

_input_wake.clear()
await _input_wake.wait()
49 changes: 49 additions & 0 deletions cecli/tools/grep.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@ def execute(
Returns a JSON string with structured results including per-file groupings,
match counts, and summary metadata.
"""
import json

if not isinstance(searches, list):
response = ToolResponse(cls.NORM_NAME, result_type=cls.RESULT_TYPE)
response.append_error("'searches' parameter must be an array.")
Expand Down Expand Up @@ -553,6 +555,53 @@ def execute(

all_operation_results.append(op_result)

# Cap the output size to 50k characters for the LLM
# Heuristic: Prioritize shallowness (short paths) and fewer matches
# Removal order: Longest paths first, then most matches first.
MAX_TOTAL_SIZE = 50000
if len(json.dumps(all_operation_results)) > MAX_TOTAL_SIZE:
# Flatten files with their metadata for sorting
all_files_to_rank = []
for op_idx, op in enumerate(all_operation_results):
for file_idx, f_data in enumerate(op.get("files", [])):
all_files_to_rank.append(
{
"op_idx": op_idx,
"file_idx": file_idx,
"path_len": len(f_data.get("file", "")),
"match_count": f_data.get("match_count", 0),
}
)

# Sort for REMOVAL (worst first): Longest path, then most matches
all_files_to_rank.sort(key=lambda x: (x["path_len"], x["match_count"]), reverse=True)

# Progressively remove files until under limit
removed_set = set()
trimmed_results = all_operation_results
for rank_info in all_files_to_rank:
removed_set.add((rank_info["op_idx"], rank_info["file_idx"]))

# Reconstruct to check size
trimmed_results = []
for o_idx, op in enumerate(all_operation_results):
new_op = op.copy()
original_files = op.get("files", [])
new_op["files"] = [
f
for f_idx, f in enumerate(original_files)
if (o_idx, f_idx) not in removed_set
]
if len(new_op["files"]) < len(original_files):
new_op["has_more_files"] = True
trimmed_results.append(new_op)

if len(json.dumps(trimmed_results)) <= MAX_TOTAL_SIZE:
all_operation_results = trimmed_results
break
else:
all_operation_results = trimmed_results

# TUI summary
if coder.tui and coder.tui():
ui_summaries = []
Expand Down
14 changes: 8 additions & 6 deletions cecli/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,16 +365,18 @@ def compose(self) -> ComposeResult:
"cyan2",
"cyan1",
"bright_white",
"medium_spring_green",
]

E = f"[bold {BANNER_COLORS[6]}]▓▓▓[/bold {BANNER_COLORS[6]}]"
# ASCII banner for startup
BANNER = f"""
[bold {BANNER_COLORS[0]}] ▒▒▒▒▒▒╗▒▒▒▒▒▒▒╗ ▒▒▒▒▒▒╗▒▒╗ ▒▒╗[/bold {BANNER_COLORS[0]}]
[bold {BANNER_COLORS[1]}] ▒▒╔════╝▒▒╔════╝▒▒╔════╝▒▒║ ▒▒║[/bold {BANNER_COLORS[1]}]
[bold {BANNER_COLORS[2]}] ▒▒║ ▒▒▒▒▒╗ ▒▒║ ▒▒║ ▒▒║[/bold {BANNER_COLORS[2]}]
[bold {BANNER_COLORS[3]}] ▒▒║ ▒▒╔══▒▒║ ▒▒║ ▒▒║[/bold {BANNER_COLORS[3]}]
[bold {BANNER_COLORS[4]}] ╚▒▒▒▒▒▒╗▒▒▒▒▒▒▒╗╚▒▒▒▒▒╗▒▒▒▒▒▒▒╗▒▒║[/bold {BANNER_COLORS[4]}]
[bold {BANNER_COLORS[5]}] ╚═════╝╚══════╝ ╚═════╝╚══════╝╚═╝ v{__version__}[/bold {BANNER_COLORS[5]}]
[bold {BANNER_COLORS[0]}] ▒▒╗▒▒╗[/bold {BANNER_COLORS[0]}]
[bold {BANNER_COLORS[1]}] ▒▒▒▒▒╗ ▒▒▒▒▒╗ ▒▒▒▒▒╗▒▒║╚═╝[/bold {BANNER_COLORS[1]}]
[bold {BANNER_COLORS[2]}] ▒▒╔═══╝▒▒{E}▒║▒▒╔═══╝▒▒║▒▒╗[/bold {BANNER_COLORS[2]}]
[bold {BANNER_COLORS[3]}] ▒▒║ ▒▒╔═══╝▒▒║ ▒▒║▒▒║[/bold {BANNER_COLORS[3]}]
[bold {BANNER_COLORS[4]}] ╚▒▒▒▒▒╗╚▒▒▒▒▒╗╚▒▒▒▒▒╗▒▒▒▒║[/bold {BANNER_COLORS[4]}]
[bold {BANNER_COLORS[5]}] ╚════╚════╝ ╚════╝╚═╝╚═╝ v{__version__}[/bold {BANNER_COLORS[5]}]

"""

Expand Down
8 changes: 8 additions & 0 deletions cecli/tui/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ def _run_thread(self):
asyncio.set_event_loop(self.loop)
self.loop.set_exception_handler(self.worker_loop_exception_handler)

# Bind the global input wake-up state to this worker loop so
# producers (TUI, WebSocket, ACP) wake consumers on the correct
# loop. A fresh binding is required after a hot reload, where the
# previous worker loop was closed.
from cecli.helpers import queues

queues.set_input_loop(self.loop)

try:
self.loop.run_until_complete(self._async_run())
except BaseException:
Expand Down
18 changes: 9 additions & 9 deletions cecli/urls.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
website = "https://cecli.dev/"
edit_errors = "https://cecli.dev/docs/troubleshooting/edit-errors.html"
git = "https://cecli.dev/docs/git.html"
enable_playwright = "https://cecli.dev/docs/usage/optional.html#enable-playwright"
edit_errors = "https://cecli.dev/docs/troubleshooting/edit-errors/"
git = "https://cecli.dev/docs/git/"
enable_playwright = "https://cecli.dev/docs/usage/optional/#enable-playwright"
favicon = "https://cecli.dev/assets/cecli-temp-logo-favicon.svg"
model_warnings = "https://cecli.dev/docs/llms/warnings.html"
token_limits = "https://cecli.dev/docs/troubleshooting/token-limits.html"
llms = "https://cecli.dev/docs/llms.html"
model_warnings = "https://cecli.dev/docs/llms/warnings/"
token_limits = "https://cecli.dev/docs/troubleshooting/token-limits/"
llms = "https://cecli.dev/docs/llms/"
github_issues = "https://github.com/cecli-dev/cecli/issues/new"
git_index_version = "https://github.com/Aider-AI/aider/issues/211"
install_properly = "https://cecli.dev/docs/troubleshooting/imports.html"
install_properly = "https://cecli.dev/docs/troubleshooting/imports/"
release_notes = "https://github.com/cecli-dev/cecli/releases/latest"
edit_formats = "https://cecli.dev/docs/more/edit-formats.html"
models_and_keys = "https://cecli.dev/docs/troubleshooting/models-and-keys.html"
edit_formats = "https://cecli.dev/docs/more/edit-formats/"
models_and_keys = "https://cecli.dev/docs/troubleshooting/models-and-keys/"
11 changes: 7 additions & 4 deletions cecli/website/assets/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,14 @@ a {
color: var(--text);
}

.nav-logo-icon {
width: 17px;
height: 17px;
color: var(--accent);
.nav-logo-mark {
display: block;
width: 24px;
height: 24px;
flex: 0 0 24px;
}


.nav-links {
display: flex;
align-items: center;
Expand Down Expand Up @@ -489,6 +491,7 @@ a {
.footer {
padding: 24px 0 28px;
border-top: 1px solid var(--border);
background: #fff;
}

.footer-inner {
Expand Down
Loading
Loading