From f29b5757138b72fabf0efcfe038f0cd20a33b7bb Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 00:39:23 -0400 Subject: [PATCH 1/7] Add "!!" for running commands with output addition suppressed and "!!!" to background the tui for commands that require input --- cecli/coders/base_coder.py | 18 +++++++++++++++-- cecli/commands/core.py | 12 +++++++++--- cecli/commands/run.py | 40 +++++++++++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 3af555aa69f..2b2fbdb40be 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -1693,13 +1693,27 @@ async def preproc_user_input(self, inp): inp = inp.strip() if self.commands.is_command(inp): + run_kwargs = {} if inp[0] in "!": - inp = f"/run {inp[1:]}" + # Count and strip all leading exclamation marks + # "!command" -> normal execution + # "!!command" -> suppress adding output to chat + # "!!!command" -> background/obstructive mode (TUI suspended) + num_marks = 0 + while num_marks < len(inp) and inp[num_marks] == "!": + num_marks += 1 + command_text = inp[num_marks:] + inp = f"/run {command_text}" + if num_marks >= 3: + run_kwargs["background"] = True + run_kwargs["suppress_add"] = True + elif num_marks == 2: + run_kwargs["suppress_add"] = True if self.commands.is_run_command(inp): self.commands.cmd_running_event.clear() # Command is running - return await self.commands.run(inp, coder=self) + return await self.commands.run(inp, coder=self, **run_kwargs) await self.check_for_file_mentions(inp) inp = await self.check_for_urls(inp) diff --git a/cecli/commands/core.py b/cecli/commands/core.py index f25500c216d..2ad884fabd3 100644 --- a/cecli/commands/core.py +++ b/cecli/commands/core.py @@ -232,7 +232,13 @@ def matching_commands(self, inp): matching_commands = [cmd for cmd in all_commands if cmd.startswith(first_word)] return matching_commands, first_word, rest_inp - async def run(self, inp, coder=None): + async def run(self, inp, coder=None, **kwargs): + if inp.startswith("!!!"): + return await self.execute( + "run", inp[3:], coder=coder, background=True, suppress_add=True + ) + if inp.startswith("!!"): + return await self.execute("run", inp[2:], coder=coder, suppress_add=True) if inp.startswith("!"): return await self.execute("run", inp[1:], coder=coder) res = self.matching_commands(inp) @@ -241,10 +247,10 @@ async def run(self, inp, coder=None): matching_commands, first_word, rest_inp = res if len(matching_commands) == 1: command = matching_commands[0][1:] - return await self.execute(command, rest_inp, coder=coder) + return await self.execute(command, rest_inp, coder=coder, **kwargs) elif first_word in matching_commands: command = first_word[1:] - return await self.execute(command, rest_inp, coder=coder) + return await self.execute(command, rest_inp, coder=coder, **kwargs) elif len(matching_commands) > 1: self.io.tool_error(f"Ambiguous command: {', '.join(matching_commands)}") else: diff --git a/cecli/commands/run.py b/cecli/commands/run.py index 13f1e028cc5..5604f80a6c6 100644 --- a/cecli/commands/run.py +++ b/cecli/commands/run.py @@ -14,8 +14,14 @@ class RunCommand(BaseCommand): @classmethod async def execute(cls, io, coder, args, **kwargs): """Execute the run command with given parameters.""" + suppress_add = kwargs.get("suppress_add", False) + background = kwargs.get("background", False) add_on_nonzero_exit = kwargs.get("add_on_nonzero_exit", False) + # Background mode: suspend the TUI and run interactively + if background: + return await cls._execute_background(io, coder, args) + should_print = True if coder.args.tui: @@ -44,7 +50,10 @@ async def execute(cls, io, coder, args, **kwargs): token_count = coder.main_model.token_count(combined_output) k_tokens = token_count / 1000 - if add_on_nonzero_exit: + # When suppress_add is True, skip the confirmation and never add + if suppress_add: + add = False + elif add_on_nonzero_exit: add = exit_status != 0 else: add = await io.confirm_ask(f"Add {k_tokens:.1f}k tokens of command output to the chat?") @@ -80,6 +89,35 @@ async def execute(cls, io, coder, args, **kwargs): # Return None if output wasn't added or command succeeded return format_command_result(io, "run", "Command executed successfully") + @classmethod + async def _execute_background(cls, io, coder, args): + """ + Execute a command in background/obstructive mode with the TUI suspended. + + This allows running interactive commands (e.g., sudo) that require + direct terminal access for user input. The TUI is suspended while + the command runs and is resumed upon completion. + """ + import subprocess + + def _run_sync(): + """Run the command synchronously with direct terminal access.""" + subprocess.run( + args, + shell=True, + cwd=coder.root, + ) + + if coder.tui and coder.tui(): + # Suspend the TUI and run the command with direct terminal access + coder.tui().run_obstructive(_run_sync) + else: + # Not in TUI mode, run directly + _run_sync() + + io.tool_output(f"Background command completed: {args}") + return format_command_result(io, "run", "Command executed in background mode") + @classmethod def get_completions(cls, io, coder, args) -> List[str]: """Get completion options for run command.""" From dd2cf79cc02a4fb0e8ad21e5e600943c1f5681f1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 09:38:39 -0400 Subject: [PATCH 2/7] uv install to take advantage of exclude newer in leiu of modern supply chain attacks --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5bde325d144..dd200e535b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,9 @@ include-package-data = true [tool.setuptools.packages.find] include = ["cecli*"] +[tools.uv] +exclude-newer = "2d" + [build-system] requires = ["setuptools>=68", "setuptools_scm[toml]>=8"] build-backend = "setuptools.build_meta" From 966311daca5c1220d247360a5bd5f1b490581857 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 13:57:41 -0700 Subject: [PATCH 3/7] fix: Add KeyboardInterrupt handling to worker.py Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/tui/worker.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cecli/tui/worker.py b/cecli/tui/worker.py index b09cb54d3c7..259ca0775eb 100644 --- a/cecli/tui/worker.py +++ b/cecli/tui/worker.py @@ -79,6 +79,8 @@ def _cleanup_loop(self): ) except RuntimeError: pass # Loop already stopped + except KeyboardInterrupt: + pass # Loop already stopped self.loop.close() except Exception: @@ -186,6 +188,11 @@ def stop(self): # We'll just pass to allow the thread to exit gracefully # without a scary traceback. pass + except KeyboardInterrupt: + # An interrupt was not caught within the async run loop. + # We'll just pass to allow the thread to exit gracefully + # without a scary traceback. + pass self.interrupt() # Wait for thread to finish From 5b3e0b929d90c40335052e45276e192170710d9b Mon Sep 17 00:00:00 2001 From: Jessica Mulein Date: Tue, 26 May 2026 15:48:11 -0700 Subject: [PATCH 4/7] Improve /add ignore feedback and Ollama keep_alive defaults BrightVision desktop integration: - /add: clearer errors when paths are blocked by .gitignore vs .cecli.ignore; RepoSet-aware paths for superproject workspaces. - models: set keep_alive=-1 by default for Ollama chat requests so models stay loaded across turns (matches desktop Local LLM preload behavior). Co-authored-by: Cursor --- cecli/commands/add.py | 85 ++++++++++++++++++++++++++++++++++++++++--- cecli/models.py | 9 +++-- 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/cecli/commands/add.py b/cecli/commands/add.py index 502be13d5bc..ebb2092c3d6 100644 --- a/cecli/commands/add.py +++ b/cecli/commands/add.py @@ -13,6 +13,82 @@ from cecli.utils import is_image_file, run_fzf +def _display_add_path(matched_file: str) -> str: + try: + label = os.path.relpath(matched_file) + except ValueError: + label = matched_file + if len(label) > 64: + label = f".../{os.path.basename(label)}" + return label + + +def _git_repo_for_add_check(coder, matched_file: str): + """Resolve the GitRepo used for ignore checks (RepoSet-aware).""" + repo = coder.repo + if not repo: + return None, matched_file + if hasattr(repo, "repo_for_rel_path"): + git_repo = repo.repo_for_rel_path(matched_file) + try: + rel = repo.path_relative_to_repo(matched_file, git_repo) + except (ValueError, OSError): + rel = matched_file + return git_repo, rel + return repo, matched_file + + +def _add_blocked_message(coder, matched_file: str) -> str | None: + """ + Explain why /add refused a path. Distinguishes .gitignore vs .cecli.ignore when possible. + """ + if not coder.repo or coder.add_gitignore_files: + return None + if not coder.repo.git_ignored_file(matched_file): + return None + + display = _display_add_path(matched_file) + git_repo, rel = _git_repo_for_add_check(coder, matched_file) + if not git_repo: + return ( + f"Can't add {display}: excluded by ignore rules for this session. " + "Confirm Settings → project folder is the git root for this file." + ) + + gitignore_hit = False + cecli_hit = False + if hasattr(git_repo, "_is_gitignored_by_pathspec"): + try: + gitignore_hit = bool(git_repo._is_gitignored_by_pathspec(rel)) + except (ValueError, OSError): + gitignore_hit = False + + ignore_file = getattr(git_repo, "cecli_ignore_file", None) or getattr( + git_repo, "aider_ignore_file", None + ) + if ignore_file and Path(ignore_file).is_file() and hasattr(git_repo, "ignored_file_raw"): + try: + cecli_hit = bool(git_repo.ignored_file_raw(rel)) + except (ValueError, OSError): + cecli_hit = False + + if cecli_hit and not gitignore_hit: + return ( + f"Can't add {display}: blocked by {Path(ignore_file).name} " + "(cecli ignore rules), not .gitignore." + ) + if gitignore_hit: + return ( + f"Can't add {display}: matched .gitignore under the session workspace. " + "If this is normal tracked source, check the project folder in Settings." + ) + return ( + f"Can't add {display}: excluded by ignore rules for this session " + "(.gitignore and/or cecli ignore). " + "Confirm the project folder is the git root that contains this file." + ) + + class AddCommand(BaseCommand): NORM_NAME = "add" DESCRIPTION = "Add files to the chat so cecli can edit them or review them in detail" @@ -84,12 +160,9 @@ async def execute(cls, io, coder, args, **kwargs): for matched_file in sorted(all_matched_files): abs_file_path = coder.abs_root_path(matched_file) - if ( - coder.repo - and coder.repo.git_ignored_file(matched_file) - and not coder.add_gitignore_files - ): - io.tool_error(f"Can't add {matched_file} which is in gitignore") + blocked = _add_blocked_message(coder, matched_file) + if blocked: + io.tool_error(blocked) continue if abs_file_path in coder.abs_fnames: diff --git a/cecli/models.py b/cecli/models.py index 3caebebe6bf..4148682b44b 100644 --- a/cecli/models.py +++ b/cecli/models.py @@ -1206,9 +1206,12 @@ async def send_completion( kwargs["max_tokens"] = max_tokens if "max_tokens" in kwargs and kwargs["max_tokens"]: kwargs["max_completion_tokens"] = kwargs.pop("max_tokens") - if self.is_ollama() and "num_ctx" not in kwargs: - num_ctx = int(self.token_count(messages) * 1.25) + 8192 - kwargs["num_ctx"] = num_ctx + if self.is_ollama(): + # Ollama defaults to ~5m unload unless every request sets keep_alive (see Ollama API docs). + kwargs.setdefault("keep_alive", -1) + if "num_ctx" not in kwargs: + num_ctx = int(self.token_count(messages) * 1.25) + 8192 + kwargs["num_ctx"] = num_ctx key = json.dumps(kwargs, sort_keys=True).encode() hash_object = hashlib.sha1(key) if "timeout" not in kwargs: From eab96c3285e83fcc56a1b6dce53f45c5c3bc06f6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 19:41:37 -0400 Subject: [PATCH 5/7] Make new methods in add.py into class/static methods, fix ollama assertions --- cecli/commands/add.py | 154 ++++++++++++++++++------------------- tests/basic/test_models.py | 2 + 2 files changed, 79 insertions(+), 77 deletions(-) diff --git a/cecli/commands/add.py b/cecli/commands/add.py index ebb2092c3d6..2889841af54 100644 --- a/cecli/commands/add.py +++ b/cecli/commands/add.py @@ -13,82 +13,6 @@ from cecli.utils import is_image_file, run_fzf -def _display_add_path(matched_file: str) -> str: - try: - label = os.path.relpath(matched_file) - except ValueError: - label = matched_file - if len(label) > 64: - label = f".../{os.path.basename(label)}" - return label - - -def _git_repo_for_add_check(coder, matched_file: str): - """Resolve the GitRepo used for ignore checks (RepoSet-aware).""" - repo = coder.repo - if not repo: - return None, matched_file - if hasattr(repo, "repo_for_rel_path"): - git_repo = repo.repo_for_rel_path(matched_file) - try: - rel = repo.path_relative_to_repo(matched_file, git_repo) - except (ValueError, OSError): - rel = matched_file - return git_repo, rel - return repo, matched_file - - -def _add_blocked_message(coder, matched_file: str) -> str | None: - """ - Explain why /add refused a path. Distinguishes .gitignore vs .cecli.ignore when possible. - """ - if not coder.repo or coder.add_gitignore_files: - return None - if not coder.repo.git_ignored_file(matched_file): - return None - - display = _display_add_path(matched_file) - git_repo, rel = _git_repo_for_add_check(coder, matched_file) - if not git_repo: - return ( - f"Can't add {display}: excluded by ignore rules for this session. " - "Confirm Settings → project folder is the git root for this file." - ) - - gitignore_hit = False - cecli_hit = False - if hasattr(git_repo, "_is_gitignored_by_pathspec"): - try: - gitignore_hit = bool(git_repo._is_gitignored_by_pathspec(rel)) - except (ValueError, OSError): - gitignore_hit = False - - ignore_file = getattr(git_repo, "cecli_ignore_file", None) or getattr( - git_repo, "aider_ignore_file", None - ) - if ignore_file and Path(ignore_file).is_file() and hasattr(git_repo, "ignored_file_raw"): - try: - cecli_hit = bool(git_repo.ignored_file_raw(rel)) - except (ValueError, OSError): - cecli_hit = False - - if cecli_hit and not gitignore_hit: - return ( - f"Can't add {display}: blocked by {Path(ignore_file).name} " - "(cecli ignore rules), not .gitignore." - ) - if gitignore_hit: - return ( - f"Can't add {display}: matched .gitignore under the session workspace. " - "If this is normal tracked source, check the project folder in Settings." - ) - return ( - f"Can't add {display}: excluded by ignore rules for this session " - "(.gitignore and/or cecli ignore). " - "Confirm the project folder is the git root that contains this file." - ) - - class AddCommand(BaseCommand): NORM_NAME = "add" DESCRIPTION = "Add files to the chat so cecli can edit them or review them in detail" @@ -160,7 +84,7 @@ async def execute(cls, io, coder, args, **kwargs): for matched_file in sorted(all_matched_files): abs_file_path = coder.abs_root_path(matched_file) - blocked = _add_blocked_message(coder, matched_file) + blocked = cls._add_blocked_message(coder, matched_file) if blocked: io.tool_error(blocked) continue @@ -313,3 +237,79 @@ def get_help(cls) -> str: help_text += "Files can be moved from read-only to editable status.\n" help_text += "Image files can be added if the model supports vision.\n" return help_text + + @staticmethod + def _display_add_path(matched_file: str) -> str: + try: + label = os.path.relpath(matched_file) + except ValueError: + label = matched_file + if len(label) > 64: + label = f".../{os.path.basename(label)}" + return label + + @staticmethod + def _git_repo_for_add_check(coder, matched_file: str): + """Resolve the GitRepo used for ignore checks (RepoSet-aware).""" + repo = coder.repo + if not repo: + return None, matched_file + if hasattr(repo, "repo_for_rel_path"): + git_repo = repo.repo_for_rel_path(matched_file) + try: + rel = repo.path_relative_to_repo(matched_file, git_repo) + except (ValueError, OSError): + rel = matched_file + return git_repo, rel + return repo, matched_file + + @classmethod + def _add_blocked_message(cls, coder, matched_file: str) -> str | None: + """ + Explain why /add refused a path. Distinguishes .gitignore vs .cecli.ignore when possible. + """ + if not coder.repo or coder.add_gitignore_files: + return None + if not coder.repo.git_ignored_file(matched_file): + return None + + display = cls._display_add_path(matched_file) + git_repo, rel = cls._git_repo_for_add_check(coder, matched_file) + if not git_repo: + return ( + f"Can't add {display}: excluded by ignore rules for this session. " + "Confirm Settings \u2192 project folder is the git root for this file." + ) + + gitignore_hit = False + cecli_hit = False + if hasattr(git_repo, "_is_gitignored_by_pathspec"): + try: + gitignore_hit = bool(git_repo._is_gitignored_by_pathspec(rel)) + except (ValueError, OSError): + gitignore_hit = False + + ignore_file = getattr(git_repo, "cecli_ignore_file", None) or getattr( + git_repo, "aider_ignore_file", None + ) + if ignore_file and Path(ignore_file).is_file() and hasattr(git_repo, "ignored_file_raw"): + try: + cecli_hit = bool(git_repo.ignored_file_raw(rel)) + except (ValueError, OSError): + cecli_hit = False + + if cecli_hit and not gitignore_hit: + return ( + f"Can't add {display}: blocked by {Path(ignore_file).name} " + "(cecli ignore rules), not .gitignore." + ) + if gitignore_hit: + return ( + f"Can't add {display}: matched .gitignore under the session workspace. " + "If this is normal tracked source, check the project folder in Settings." + ) + return ( + f"Can't add {display}: excluded by ignore rules for this session " + "(.gitignore and/or cecli ignore). " + "Confirm the project folder is the git root that contains this file." + ) diff --git a/tests/basic/test_models.py b/tests/basic/test_models.py index 5a9e5171d36..354501792f2 100644 --- a/tests/basic/test_models.py +++ b/tests/basic/test_models.py @@ -381,6 +381,7 @@ async def test_ollama_num_ctx_set_when_missing(self, mock_token_count, mock_comp temperature=0, num_ctx=expected_ctx, timeout=600, + keep_alive=-1, drop_params=True, headers={"Connection": "close", "User-Agent": ANY}, cache_control_injection_points=ANY, @@ -426,6 +427,7 @@ async def test_ollama_uses_existing_num_ctx(self, mock_completion): temperature=0, num_ctx=4096, timeout=600, + keep_alive=-1, drop_params=True, headers={"Connection": "close", "User-Agent": ANY}, cache_control_injection_points=ANY, From 98ccc469d2e3dae635d2f753921928fea48e8ce9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 19:43:53 -0400 Subject: [PATCH 6/7] tool.uv not tools.uv --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dd200e535b0..539511b7287 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ include-package-data = true [tool.setuptools.packages.find] include = ["cecli*"] -[tools.uv] +[tool.uv] exclude-newer = "2d" [build-system] From da52a77f8f52c211b1463d4b66bdcd77ef5cd664 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 19:44:49 -0400 Subject: [PATCH 7/7] 2 days instead of 2d --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 539511b7287..18525043ae3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ include-package-data = true include = ["cecli*"] [tool.uv] -exclude-newer = "2d" +exclude-newer = "2 days" [build-system] requires = ["setuptools>=68", "setuptools_scm[toml]>=8"]