Skip to content
Open
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
459 changes: 456 additions & 3 deletions README.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions cecli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from .include_skill import IncludeSkillCommand
from .lint import LintCommand
from .list_mcp import ListMcpCommand
from .list_queue import ListQueueCommand
from .list_sessions import ListSessionsCommand
from .list_skills import ListSkillsCommand
from .load import LoadCommand
Expand All @@ -52,13 +53,15 @@
from .models import ModelsCommand
from .multiline_mode import MultilineModeCommand
from .paste import PasteCommand
from .queue import QueueCommand
from .quit import QuitCommand
from .read_only import ReadOnlyCommand
from .read_only_stub import ReadOnlyStubCommand
from .reap_agent import ReapAgentCommand
from .reasoning_effort import ReasoningEffortCommand
from .remove_hook import RemoveHookCommand
from .remove_mcp import RemoveMcpCommand
from .remove_queue import RemoveQueueCommand
from .remove_skill import RemoveSkillCommand
from .report import ReportCommand
from .reset import ResetCommand
Expand Down Expand Up @@ -127,6 +130,7 @@
CommandRegistry.register(IncludeSkillCommand)
CommandRegistry.register(LintCommand)
CommandRegistry.register(ListMcpCommand)
CommandRegistry.register(ListQueueCommand)
CommandRegistry.register(ListSessionsCommand)
CommandRegistry.register(ListSkillsCommand)
CommandRegistry.register(LoadCommand)
Expand All @@ -142,12 +146,14 @@
CommandRegistry.register(ModelsCommand)
CommandRegistry.register(MultilineModeCommand)
CommandRegistry.register(PasteCommand)
CommandRegistry.register(QueueCommand)
CommandRegistry.register(QuitCommand)
CommandRegistry.register(ReadOnlyCommand)
CommandRegistry.register(ReadOnlyStubCommand)
CommandRegistry.register(ReasoningEffortCommand)
CommandRegistry.register(RemoveHookCommand)
CommandRegistry.register(RemoveMcpCommand)
CommandRegistry.register(RemoveQueueCommand)
CommandRegistry.register(RemoveSkillCommand)
CommandRegistry.register(ReportCommand)
CommandRegistry.register(ResetCommand)
Expand Down Expand Up @@ -228,13 +234,15 @@
"parse_quoted_filenames",
"PasteCommand",
"quote_filename",
"QueueCommand",
"QuitCommand",
"ReadOnlyCommand",
"ReadOnlyStubCommand",
"ReasoningEffortCommand",
"ReloadProgramSignal",
"RemoveHookCommand",
"RemoveMcpCommand",
"RemoveQueueCommand",
"RemoveSkillCommand",
"ReportCommand",
"ResetCommand",
Expand Down
129 changes: 128 additions & 1 deletion cecli/commands/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import asyncio
import json
import re
import sys
import time
import weakref
from pathlib import Path

Expand Down Expand Up @@ -131,6 +133,117 @@
self.cmd_running_event.set()
self.last_command_show_notification = True

# Prompt queue for CLI-33: in-memory FIFO queue for deferred prompt processing
self.prompt_queue = []
self._queue_counter = 0
self._queue_lock = asyncio.Lock()
self._processing_queue = False

# Commands that should NOT trigger auto-processing of the queue
self._MANAGEMENT_COMMANDS = {"queue", "list-queue", "remove-queue"}

# ── Queue Management Methods (CLI-33) ──────────────────────────────

def _enqueue_prompt(self, text: str) -> dict:
"""Add a prompt to the queue and return the queued item.

Args:
text: The prompt text to enqueue.

Returns:
dict with keys: id (str), text (str), timestamp (float).

Raises:
ValueError: If text is empty, None, or exceeds 10000 characters.
RuntimeError: If the queue is at max capacity (100 items).
"""
if not text or not text.strip():
raise ValueError("Cannot enqueue empty prompt")
if len(text) > 10000:
raise ValueError("Prompt exceeds maximum length of 10000 characters")
if len(self.prompt_queue) >= 100:
raise RuntimeError("Queue is full (max 100 items)")

self._queue_counter += 1
item = {
"id": str(self._queue_counter),
"text": text,
"timestamp": time.time(),
}
self.prompt_queue.append(item)
return item

def _dequeue_prompt(self) -> dict | None:
"""Remove and return the first item from the queue (FIFO).

Returns:
The dequeued item dict, or None if the queue is empty.
"""
if not self.prompt_queue:
return None
return self.prompt_queue.pop(0)

def _get_queue_length(self) -> int:
"""Return the current number of items in the queue."""
return len(self.prompt_queue)

def _remove_from_queue(self, index: int) -> dict | None:
"""Remove and return the item at the given index.

Args:
index: 0-based index of the item to remove.

Check failure on line 194 in cecli/commands/core.py

View workflow job for this annotation

GitHub Actions / pre-commit

F821 undefined name 'ReloadProgramSignal'

Returns:
The removed item dict, or None if the index is out of bounds.
"""
if index < 0 or index >= len(self.prompt_queue):
return None
return self.prompt_queue.pop(index)

def _clear_queue(self) -> list:
"""Remove all items from the queue and return them.

Returns:
List of all items that were in the queue.
"""
items = list(self.prompt_queue)
self.prompt_queue.clear()
return items

async def _process_queued_prompts(self):
"""Process all prompts currently in the queue sequentially.

This method is called from the finally block of execute() after
cmd_running_event is set, ensuring the system is idle before
processing queued prompts. Management commands (queue, list-queue,
remove-queue) are excluded from triggering this method.

Uses _processing_queue flag to prevent re-entrant processing
(e.g., if a queued prompt itself queues another prompt).
"""
self._processing_queue = True
try:
while self.prompt_queue:
item = self._dequeue_prompt()
if not item:
break
if self.io:
self.io.tool_output(f"Processing queued prompt (id: {item['id']})...")
try:
await self.run(item["text"])
except SwitchCoderSignal:
raise
except ReloadProgramSignal:
raise
except Exception as e:
if self.io:
self.io.tool_error(
f"Error processing queued prompt (id: {item['id']}): {e}"
)
continue
finally:
self._processing_queue = False

def _load_custom_commands(self, custom_commands):
"""
Load custom commands from plugin paths.
Expand Down Expand Up @@ -223,7 +336,8 @@
return

self.last_command_show_notification = command_class.show_completion_notification
self.cmd_running_event.clear()
if cmd_name not in self._MANAGEMENT_COMMANDS:
self.cmd_running_event.clear()

try:
kwargs.update(
Expand Down Expand Up @@ -254,6 +368,13 @@
self.cmd_running_event.set()
if self.coder.tui and self.coder.tui():
self.coder.tui().refresh()
# Queue processing integration: auto-process queued prompts when system is idle
if (
self.prompt_queue
and cmd_name not in self._MANAGEMENT_COMMANDS
and not self._processing_queue
):
await self._process_queued_prompts()

def matching_commands(self, inp):
words = inp.strip().split()
Expand All @@ -266,6 +387,12 @@
return matching_commands, first_word, rest_inp

async def run(self, inp, coder=None, **kwargs):
if inp.startswith("/"):
words = inp.strip().split()
cmd_name = words[0][1:]
rest_inp = inp[len(words[0]) :].strip()
return await self.execute(cmd_name, rest_inp, coder=coder, **kwargs)

if inp.startswith("!!!"):
return await self.execute(
"run", inp[3:], coder=coder, background=True, suppress_add=True
Expand Down
69 changes: 69 additions & 0 deletions cecli/commands/list_queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""List-queue command for CLI-33: displays all prompts in the processing queue."""

import datetime
from typing import List

from cecli.commands.utils.base_command import BaseCommand
from cecli.commands.utils.helpers import format_command_result


class ListQueueCommand(BaseCommand):
NORM_NAME = "list-queue"
DESCRIPTION = "List all prompts currently in the queue"

@classmethod
async def execute(cls, io, coder, args, **kwargs):
"""Execute the list-queue command with given parameters.

Args:
io: InputOutput instance
coder: Coder instance (may be None for some commands)
args: Command arguments (unused for list-queue)
**kwargs: Additional context

Returns:
Formatted result string
"""
# Sad path: coder.commands is None
if not coder.commands:
return format_command_result(
io, cls.NORM_NAME, error="Command system not available. Cannot list queue."
)

queue = coder.commands.prompt_queue

# Sad path: empty queue
if not queue:
io.tool_output("Queue is empty.")
return f"Successfully executed {cls.NORM_NAME}."

# Happy path: display numbered list
lines = []
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")
lines.append(f"[{i}] {display_text} ({ts})")

io.tool_output("\n".join(lines))
return f"Successfully executed {cls.NORM_NAME}."

@classmethod
def get_completions(cls, io, coder, args) -> List[str]:
"""Get completion options for list-queue command."""
return []

@classmethod
def get_help(cls) -> str:
"""Get help text for the list-queue command."""
help_text = super().get_help()
help_text += "\nUsage:\n"
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 += " and the time it was queued.\n"
help_text += "\nExamples:\n"
help_text += " /list-queue # Shows all queued prompts\n"
help_text += "\nSee also: /queue, /remove-queue\n"
return help_text
85 changes: 85 additions & 0 deletions cecli/commands/queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Queue command for CLI-33: adds a prompt to the processing queue."""

from typing import List

from cecli.commands.utils.base_command import BaseCommand
from cecli.commands.utils.helpers import format_command_result


class QueueCommand(BaseCommand):
NORM_NAME = "queue"
DESCRIPTION = "Queue a prompt for processing after current tasks complete"

@classmethod
async def execute(cls, io, coder, args, **kwargs):
"""Execute the queue command with given parameters.

Args:
io: InputOutput instance
coder: Coder instance (may be None for some commands)
args: Command arguments as string (the prompt text to queue)
**kwargs: Additional context

Returns:
Formatted result string
"""
# Sad path: coder.commands is None
if not coder.commands:
return format_command_result(
io, cls.NORM_NAME, error="Command system not available. Cannot queue prompts."
)

# Sad path: no args (empty prompt text)
if not args or not args.strip():
return format_command_result(
io,
cls.NORM_NAME,
"Usage: /queue <prompt text>\n"
"Add a prompt to the queue for processing after current tasks complete.",
)

prompt_text = args.strip()

# Sad path: prompt exceeds 10000 characters
if len(prompt_text) > 10000:
return format_command_result(
io,
cls.NORM_NAME,
error=f"Prompt exceeds maximum length of 10000 characters "
f"(got {len(prompt_text)}).",
)

# Happy path: enqueue the prompt
try:
item = coder.commands._enqueue_prompt(prompt_text)
position = len(coder.commands.prompt_queue)
io.tool_output(f"Prompt queued at position {position} (id: {item['id']})")
return f"Successfully executed {cls.NORM_NAME}."
except ValueError as e:
return format_command_result(io, cls.NORM_NAME, error=str(e))
except RuntimeError as e:
return format_command_result(io, cls.NORM_NAME, error=str(e))

@classmethod
def get_completions(cls, io, coder, args) -> List[str]:
"""Get completion options for queue command."""
return []

@classmethod
def get_help(cls) -> str:
"""Get help text for the queue command."""
help_text = super().get_help()
help_text += "\nUsage:\n"
help_text += " /queue <prompt text> # Queue a prompt for later processing\n"
help_text += "\nDescription:\n"
help_text += " Adds a prompt to an in-memory FIFO queue. Queued prompts are\n"
help_text += " processed sequentially after the current command completes.\n"
help_text += "\nConstraints:\n"
help_text += " - Maximum prompt length: 10,000 characters\n"
help_text += " - Maximum queue size: 100 items\n"
help_text += " - Queue is in-memory only (lost on session restart)\n"
help_text += "\nExamples:\n"
help_text += " /queue Review the changes in src/main.py\n"
help_text += " /queue Write unit tests for the new feature\n"
help_text += "\nSee also: /list-queue, /remove-queue\n"
return help_text
Loading
Loading