From 7651254d243b7698d186be437dfc5168ceebec02 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 26 Oct 2026 22:04:27 -0700 Subject: [PATCH 1/3] fix: drain queued prompts after compaction --- cecli/commands/compact.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cecli/commands/compact.py b/cecli/commands/compact.py index 0ddc5a0ae38..ae91f670900 100644 --- a/cecli/commands/compact.py +++ b/cecli/commands/compact.py @@ -12,6 +12,11 @@ async def execute(cls, io, coder, args, **kwargs): message = args.strip() if args else "" await coder.compact_context_if_needed(force=True, message=message) + # CLI-71: compaction is a command, so run_one() returns early after it. + # Set the drain flag so any queued prompts process automatically once + # compaction finishes, instead of stalling until a manual /run-queue. + coder._drain_queue_after_command = True + @classmethod def get_help(cls) -> str: return "Force compaction of the chat history context." From 1119883dc9d59b1135e4951f70f7b79983896a74 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 27 Oct 2026 14:52:05 -0700 Subject: [PATCH 2/3] feat: use 0-based indexing for queue commands --- cecli/commands/insert_queue.py | 10 +++++----- cecli/commands/list_queue.py | 4 ++-- cecli/commands/remove_queue.py | 10 +++++----- cecli/tui/app.py | 6 +++--- cecli/website/docs/usage/commands.md | 10 +++++----- tests/commands/test_queue_commands.py | 15 ++++++++------- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/cecli/commands/insert_queue.py b/cecli/commands/insert_queue.py index 50ec8246441..6167a50ef56 100644 --- a/cecli/commands/insert_queue.py +++ b/cecli/commands/insert_queue.py @@ -59,7 +59,7 @@ async def execute(cls, io, coder, args, **kwargs): # Happy path: insert the prompt try: item = command_queue.insert_prompt(coder, prompt_text, index) - io.tool_output(f"Prompt inserted at position {index + 1} (id: {item['id']})") + io.tool_output(f"Prompt inserted at position {index} (id: {item['id']})") return f"Successfully executed {cls.NORM_NAME}." except ValueError as e: @@ -81,7 +81,7 @@ def get_completions(cls, io, coder, args) -> List[str]: # Still typing the index; suggest valid positions. if not coder.commands: return [] - return [str(i) for i in range(1, command_queue.get_queue_length(coder) + 2)] + return [str(i) for i in range(command_queue.get_queue_length(coder) + 1)] return command_queue.nested_command_completions(coder, text[first_space + 1 :]) @classmethod @@ -91,10 +91,10 @@ def get_help(cls) -> str: help_text += "\nUsage:\n" help_text += " /insert-queue # Insert at a specific position\n" help_text += "\nDescription:\n" - help_text += " Inserts a prompt into the queue at the given 1-based position.\n" + help_text += " Inserts a prompt into the queue at the given 0-based position.\n" help_text += " Existing items shift down. Index is clamped to the queue bounds.\n" help_text += "\nExamples:\n" - help_text += " /insert-queue 1 Review the changes in src/main.py\n" - help_text += " /insert-queue 3 Write unit tests for the new feature\n" + help_text += " /insert-queue 0 Review the changes in src/main.py\n" + help_text += " /insert-queue 2 Write unit tests for the new feature\n" help_text += "\nSee also: /queue, /list-queue, /remove-queue, /run-queue\n" return help_text diff --git a/cecli/commands/list_queue.py b/cecli/commands/list_queue.py index fb8d8b7f0ad..6426e57c1a9 100644 --- a/cecli/commands/list_queue.py +++ b/cecli/commands/list_queue.py @@ -40,7 +40,7 @@ async def execute(cls, io, coder, args, **kwargs): # Happy path: display numbered list lines = [] - for i, item in enumerate(queue, start=1): + for i, item in enumerate(queue): text = item["text"] display_text = text[:80] + "..." if len(text) > 80 else text ts = datetime.datetime.fromtimestamp(item["timestamp"]).strftime("%H:%M:%S") @@ -62,7 +62,7 @@ def get_help(cls) -> str: help_text += " /list-queue # Display all queued prompts\n" help_text += "\nDescription:\n" help_text += " Displays a numbered list of all prompts currently in the queue,\n" - help_text += " showing each prompt's position, text (truncated to 80 chars),\n" + help_text += " showing each prompt's 0-based index, text (truncated to 80 chars),\n" help_text += " and the time it was queued.\n" help_text += "\nExamples:\n" help_text += " /list-queue # Shows all queued prompts\n" diff --git a/cecli/commands/remove_queue.py b/cecli/commands/remove_queue.py index 21fca516e41..5b4acab0430 100644 --- a/cecli/commands/remove_queue.py +++ b/cecli/commands/remove_queue.py @@ -49,7 +49,7 @@ async def execute(cls, io, coder, args, **kwargs): # Handle specific index if args and args.strip(): try: - index = int(args.strip()) - 1 # Convert to 0-based + index = int(args.strip()) # 0-based index except ValueError: return format_command_result( io, @@ -74,7 +74,7 @@ async def execute(cls, io, coder, args, **kwargs): # Interactive mode: no args provided queue = command_queue.list_queue(coder) io.tool_output("Queued prompts:") - for i, item in enumerate(queue, 1): + for i, item in enumerate(queue): text = item["text"][:80] if len(item["text"]) > 80: text += "..." @@ -93,7 +93,7 @@ def get_completions(cls, io, coder, args) -> List[str]: return [] queue_len = command_queue.get_queue_length(coder) - completions = [str(i) for i in range(1, queue_len + 1)] + completions = [str(i) for i in range(queue_len)] completions.append("*") return completions @@ -106,13 +106,13 @@ def get_help(cls) -> str: help_text += " /remove-queue * # Clear the entire queue\n" help_text += " /remove-queue # Interactive mode (shows list, prompts for index)\n" help_text += "\nDescription:\n" - help_text += " Removes a prompt from the in-memory queue by its index number.\n" + help_text += " Removes a prompt from the in-memory queue by its 0-based index.\n" help_text += " Use '*' to clear all queued prompts at once.\n" help_text += ( " Without arguments, displays the queue and prompts for interactive selection.\n" ) help_text += "\nExamples:\n" - help_text += " /remove-queue 1 # Remove the first queued prompt\n" + help_text += " /remove-queue 0 # Remove the first queued prompt\n" help_text += " /remove-queue * # Clear the entire queue\n" help_text += " /remove-queue # Interactive mode\n" help_text += "\nSee also: /queue, /insert-queue, /list-queue, /run-queue\n" diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 164b0a8316e..405f8f3ff23 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -1135,7 +1135,7 @@ def _handle_queue_command(self, stripped: str) -> None: text = item["text"] preview = text[:80] + ("..." if len(text) > 80 else "") stamp = time.strftime("%H:%M:%S", time.localtime(item["timestamp"])) - lines.append(f"[{index + 1}] {preview} ({stamp})") + lines.append(f"[{index}] {preview} ({stamp})") self._get_visible_container().add_output("\n".join(lines)) @@ -1159,7 +1159,7 @@ def _handle_queue_command(self, stripped: str) -> None: return self._get_visible_container().add_output( - f"Prompt inserted at position {index + 1} (id: {item['id']})" + f"Prompt inserted at position {index} (id: {item['id']})" ) elif cmd == "/remove-queue": @@ -1180,7 +1180,7 @@ def _handle_queue_command(self, stripped: str) -> None: self.show_error("Usage: /remove-queue ") return - removed = command_queue.remove_from_queue(active_coder, index - 1) + removed = command_queue.remove_from_queue(active_coder, index) if removed is None: self.show_error(f"No queued prompt at index {index}.") return diff --git a/cecli/website/docs/usage/commands.md b/cecli/website/docs/usage/commands.md index 2560b0f8555..37fedb09792 100644 --- a/cecli/website/docs/usage/commands.md +++ b/cecli/website/docs/usage/commands.md @@ -90,25 +90,25 @@ If a response is interrupted (for example with Ctrl+C), the in-flight prompt is **Usage:** `/insert-queue ` -**Description:** Inserts a prompt at a specific 1-based position in the queue. Existing items shift down, and the index is clamped to the queue bounds. +**Description:** Inserts a prompt at a specific 0-based position in the queue. Existing items shift down, and the index is clamped to the queue bounds. **Examples:** ```bash -/insert-queue 1 "add tests for login" -/insert-queue 3 "refactor database layer" +/insert-queue 0 "add tests for login" +/insert-queue 2 "refactor database layer" ``` ### `/list-queue` Command **Usage:** `/list-queue` -**Description:** Lists all queued prompts with their position and queue time. +**Description:** Lists all queued prompts with their 0-based index and queue time. ### `/remove-queue` Command **Usage:** `/remove-queue `, `/remove-queue *` -**Description:** Removes the prompt at the given 1-based position, or clears the entire queue when `*` is used. +**Description:** Removes the prompt at the given 0-based index, or clears the entire queue when `*` is used. ### `/run-queue` Command diff --git a/tests/commands/test_queue_commands.py b/tests/commands/test_queue_commands.py index edb2dd70dc9..501ad9abf3a 100644 --- a/tests/commands/test_queue_commands.py +++ b/tests/commands/test_queue_commands.py @@ -378,7 +378,7 @@ async def test_itc_07_list_queue_shows_numbered_list(self, mock_io, populated_qu mock_io.tool_output.assert_called() calls = [str(call) for call in mock_io.tool_output.call_args_list] output_text = " ".join(calls) - assert "[1]" in output_text or "alpha" in output_text + assert "[0]" in output_text or "alpha" in output_text @pytest.mark.asyncio async def test_itc_08_list_queue_empty_shows_message(self, mock_io, mock_coder): @@ -415,10 +415,11 @@ class TestRemoveQueueCommand: @pytest.mark.asyncio async def test_itc_11_remove_by_index(self, mock_io, populated_queue): """ITC-11: /remove-queue removes exact item and confirms removal.""" - result = await RemoveQueueCommand.execute(mock_io, populated_queue, "2") + result = await RemoveQueueCommand.execute(mock_io, populated_queue, "0") assert result == "Successfully executed remove-queue." assert len(populated_queue.prompt_queue) == 2 + assert populated_queue.prompt_queue[0]["text"] == "beta" mock_io.tool_output.assert_called() @pytest.mark.asyncio @@ -481,9 +482,9 @@ def test_itc_19_get_completions_returns_indices_and_wildcard(self, populated_que """ITC-19: RemoveQueueCommand.get_completions() returns valid index completions and '*'.""" completions = RemoveQueueCommand.get_completions(None, populated_queue, "") + assert "0" in completions assert "1" in completions assert "2" in completions - assert "3" in completions assert "*" in completions def test_itc_20_commands_registered_in_registry(self): @@ -622,7 +623,7 @@ async def fake_drain(): coder.process_prompt_queue = fake_drain - result = await InsertQueueCommand.execute(mock_io, coder, "1 idle insert") + result = await InsertQueueCommand.execute(mock_io, coder, "0 idle insert") assert result == "Successfully executed insert-queue." assert drain_calls == [] @@ -641,7 +642,7 @@ async def fake_drain(): coder.process_prompt_queue = fake_drain - result = await InsertQueueCommand.execute(mock_io, coder, "1 busy insert") + result = await InsertQueueCommand.execute(mock_io, coder, "0 busy insert") assert result == "Successfully executed insert-queue." assert drain_calls == [] @@ -996,8 +997,8 @@ def test_ntc_07_insert_queue_index_only_suggests_positions(self, mock_io): result = InsertQueueCommand.get_completions(mock_io, coder, "1") - # Queue holds 1 item, so positions 1..2 (append at end) are valid. - assert result == ["1", "2"] + # Queue holds 1 item, so 0-based positions 0..1 (append at end) are valid. + assert result == ["0", "1"] # Lifecycle tests From 4e3a9db726324a5515be1bcf365bdda2f23d5a6e Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 27 Oct 2026 15:11:35 -0700 Subject: [PATCH 3/3] Revert "feat: use 0-based indexing for queue commands" This reverts commit 1119883dc9d59b1135e4951f70f7b79983896a74. --- cecli/commands/insert_queue.py | 10 +++++----- cecli/commands/list_queue.py | 4 ++-- cecli/commands/remove_queue.py | 10 +++++----- cecli/tui/app.py | 6 +++--- cecli/website/docs/usage/commands.md | 10 +++++----- tests/commands/test_queue_commands.py | 15 +++++++-------- 6 files changed, 27 insertions(+), 28 deletions(-) diff --git a/cecli/commands/insert_queue.py b/cecli/commands/insert_queue.py index 6167a50ef56..50ec8246441 100644 --- a/cecli/commands/insert_queue.py +++ b/cecli/commands/insert_queue.py @@ -59,7 +59,7 @@ async def execute(cls, io, coder, args, **kwargs): # Happy path: insert the prompt try: item = command_queue.insert_prompt(coder, prompt_text, index) - io.tool_output(f"Prompt inserted at position {index} (id: {item['id']})") + io.tool_output(f"Prompt inserted at position {index + 1} (id: {item['id']})") return f"Successfully executed {cls.NORM_NAME}." except ValueError as e: @@ -81,7 +81,7 @@ def get_completions(cls, io, coder, args) -> List[str]: # Still typing the index; suggest valid positions. if not coder.commands: return [] - return [str(i) for i in range(command_queue.get_queue_length(coder) + 1)] + return [str(i) for i in range(1, command_queue.get_queue_length(coder) + 2)] return command_queue.nested_command_completions(coder, text[first_space + 1 :]) @classmethod @@ -91,10 +91,10 @@ def get_help(cls) -> str: help_text += "\nUsage:\n" help_text += " /insert-queue # Insert at a specific position\n" help_text += "\nDescription:\n" - help_text += " Inserts a prompt into the queue at the given 0-based position.\n" + help_text += " Inserts a prompt into the queue at the given 1-based position.\n" help_text += " Existing items shift down. Index is clamped to the queue bounds.\n" help_text += "\nExamples:\n" - help_text += " /insert-queue 0 Review the changes in src/main.py\n" - help_text += " /insert-queue 2 Write unit tests for the new feature\n" + help_text += " /insert-queue 1 Review the changes in src/main.py\n" + help_text += " /insert-queue 3 Write unit tests for the new feature\n" help_text += "\nSee also: /queue, /list-queue, /remove-queue, /run-queue\n" return help_text diff --git a/cecli/commands/list_queue.py b/cecli/commands/list_queue.py index 6426e57c1a9..fb8d8b7f0ad 100644 --- a/cecli/commands/list_queue.py +++ b/cecli/commands/list_queue.py @@ -40,7 +40,7 @@ async def execute(cls, io, coder, args, **kwargs): # Happy path: display numbered list lines = [] - for i, item in enumerate(queue): + for i, item in enumerate(queue, start=1): text = item["text"] display_text = text[:80] + "..." if len(text) > 80 else text ts = datetime.datetime.fromtimestamp(item["timestamp"]).strftime("%H:%M:%S") @@ -62,7 +62,7 @@ def get_help(cls) -> str: help_text += " /list-queue # Display all queued prompts\n" help_text += "\nDescription:\n" help_text += " Displays a numbered list of all prompts currently in the queue,\n" - help_text += " showing each prompt's 0-based index, text (truncated to 80 chars),\n" + help_text += " showing each prompt's position, text (truncated to 80 chars),\n" help_text += " and the time it was queued.\n" help_text += "\nExamples:\n" help_text += " /list-queue # Shows all queued prompts\n" diff --git a/cecli/commands/remove_queue.py b/cecli/commands/remove_queue.py index 5b4acab0430..21fca516e41 100644 --- a/cecli/commands/remove_queue.py +++ b/cecli/commands/remove_queue.py @@ -49,7 +49,7 @@ async def execute(cls, io, coder, args, **kwargs): # Handle specific index if args and args.strip(): try: - index = int(args.strip()) # 0-based index + index = int(args.strip()) - 1 # Convert to 0-based except ValueError: return format_command_result( io, @@ -74,7 +74,7 @@ async def execute(cls, io, coder, args, **kwargs): # Interactive mode: no args provided queue = command_queue.list_queue(coder) io.tool_output("Queued prompts:") - for i, item in enumerate(queue): + for i, item in enumerate(queue, 1): text = item["text"][:80] if len(item["text"]) > 80: text += "..." @@ -93,7 +93,7 @@ def get_completions(cls, io, coder, args) -> List[str]: return [] queue_len = command_queue.get_queue_length(coder) - completions = [str(i) for i in range(queue_len)] + completions = [str(i) for i in range(1, queue_len + 1)] completions.append("*") return completions @@ -106,13 +106,13 @@ def get_help(cls) -> str: help_text += " /remove-queue * # Clear the entire queue\n" help_text += " /remove-queue # Interactive mode (shows list, prompts for index)\n" help_text += "\nDescription:\n" - help_text += " Removes a prompt from the in-memory queue by its 0-based index.\n" + help_text += " Removes a prompt from the in-memory queue by its index number.\n" help_text += " Use '*' to clear all queued prompts at once.\n" help_text += ( " Without arguments, displays the queue and prompts for interactive selection.\n" ) help_text += "\nExamples:\n" - help_text += " /remove-queue 0 # Remove the first queued prompt\n" + help_text += " /remove-queue 1 # Remove the first queued prompt\n" help_text += " /remove-queue * # Clear the entire queue\n" help_text += " /remove-queue # Interactive mode\n" help_text += "\nSee also: /queue, /insert-queue, /list-queue, /run-queue\n" diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 405f8f3ff23..164b0a8316e 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -1135,7 +1135,7 @@ def _handle_queue_command(self, stripped: str) -> None: text = item["text"] preview = text[:80] + ("..." if len(text) > 80 else "") stamp = time.strftime("%H:%M:%S", time.localtime(item["timestamp"])) - lines.append(f"[{index}] {preview} ({stamp})") + lines.append(f"[{index + 1}] {preview} ({stamp})") self._get_visible_container().add_output("\n".join(lines)) @@ -1159,7 +1159,7 @@ def _handle_queue_command(self, stripped: str) -> None: return self._get_visible_container().add_output( - f"Prompt inserted at position {index} (id: {item['id']})" + f"Prompt inserted at position {index + 1} (id: {item['id']})" ) elif cmd == "/remove-queue": @@ -1180,7 +1180,7 @@ def _handle_queue_command(self, stripped: str) -> None: self.show_error("Usage: /remove-queue ") return - removed = command_queue.remove_from_queue(active_coder, index) + removed = command_queue.remove_from_queue(active_coder, index - 1) if removed is None: self.show_error(f"No queued prompt at index {index}.") return diff --git a/cecli/website/docs/usage/commands.md b/cecli/website/docs/usage/commands.md index 37fedb09792..2560b0f8555 100644 --- a/cecli/website/docs/usage/commands.md +++ b/cecli/website/docs/usage/commands.md @@ -90,25 +90,25 @@ If a response is interrupted (for example with Ctrl+C), the in-flight prompt is **Usage:** `/insert-queue ` -**Description:** Inserts a prompt at a specific 0-based position in the queue. Existing items shift down, and the index is clamped to the queue bounds. +**Description:** Inserts a prompt at a specific 1-based position in the queue. Existing items shift down, and the index is clamped to the queue bounds. **Examples:** ```bash -/insert-queue 0 "add tests for login" -/insert-queue 2 "refactor database layer" +/insert-queue 1 "add tests for login" +/insert-queue 3 "refactor database layer" ``` ### `/list-queue` Command **Usage:** `/list-queue` -**Description:** Lists all queued prompts with their 0-based index and queue time. +**Description:** Lists all queued prompts with their position and queue time. ### `/remove-queue` Command **Usage:** `/remove-queue `, `/remove-queue *` -**Description:** Removes the prompt at the given 0-based index, or clears the entire queue when `*` is used. +**Description:** Removes the prompt at the given 1-based position, or clears the entire queue when `*` is used. ### `/run-queue` Command diff --git a/tests/commands/test_queue_commands.py b/tests/commands/test_queue_commands.py index 501ad9abf3a..edb2dd70dc9 100644 --- a/tests/commands/test_queue_commands.py +++ b/tests/commands/test_queue_commands.py @@ -378,7 +378,7 @@ async def test_itc_07_list_queue_shows_numbered_list(self, mock_io, populated_qu mock_io.tool_output.assert_called() calls = [str(call) for call in mock_io.tool_output.call_args_list] output_text = " ".join(calls) - assert "[0]" in output_text or "alpha" in output_text + assert "[1]" in output_text or "alpha" in output_text @pytest.mark.asyncio async def test_itc_08_list_queue_empty_shows_message(self, mock_io, mock_coder): @@ -415,11 +415,10 @@ class TestRemoveQueueCommand: @pytest.mark.asyncio async def test_itc_11_remove_by_index(self, mock_io, populated_queue): """ITC-11: /remove-queue removes exact item and confirms removal.""" - result = await RemoveQueueCommand.execute(mock_io, populated_queue, "0") + result = await RemoveQueueCommand.execute(mock_io, populated_queue, "2") assert result == "Successfully executed remove-queue." assert len(populated_queue.prompt_queue) == 2 - assert populated_queue.prompt_queue[0]["text"] == "beta" mock_io.tool_output.assert_called() @pytest.mark.asyncio @@ -482,9 +481,9 @@ def test_itc_19_get_completions_returns_indices_and_wildcard(self, populated_que """ITC-19: RemoveQueueCommand.get_completions() returns valid index completions and '*'.""" completions = RemoveQueueCommand.get_completions(None, populated_queue, "") - assert "0" in completions assert "1" in completions assert "2" in completions + assert "3" in completions assert "*" in completions def test_itc_20_commands_registered_in_registry(self): @@ -623,7 +622,7 @@ async def fake_drain(): coder.process_prompt_queue = fake_drain - result = await InsertQueueCommand.execute(mock_io, coder, "0 idle insert") + result = await InsertQueueCommand.execute(mock_io, coder, "1 idle insert") assert result == "Successfully executed insert-queue." assert drain_calls == [] @@ -642,7 +641,7 @@ async def fake_drain(): coder.process_prompt_queue = fake_drain - result = await InsertQueueCommand.execute(mock_io, coder, "0 busy insert") + result = await InsertQueueCommand.execute(mock_io, coder, "1 busy insert") assert result == "Successfully executed insert-queue." assert drain_calls == [] @@ -997,8 +996,8 @@ def test_ntc_07_insert_queue_index_only_suggests_positions(self, mock_io): result = InsertQueueCommand.get_completions(mock_io, coder, "1") - # Queue holds 1 item, so 0-based positions 0..1 (append at end) are valid. - assert result == ["0", "1"] + # Queue holds 1 item, so positions 1..2 (append at end) are valid. + assert result == ["1", "2"] # Lifecycle tests