Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5554c45
feat: Save and load MCP, tool, and skill states in sessions
szmania Apr 26, 2026
9213b2f
fix: Correctly retrieve and manage enabled hooks during session save/…
szmania Apr 26, 2026
999c121
refactor: Save and load agent tools configuration in session
Apr 27, 2026
25294e8
fix: Convert skill lists to serializable format for session saving
Apr 27, 2026
a3ac3b5
fix: Handle None skill lists when saving sessions
Apr 27, 2026
59789b5
refactor: Update session to save skills paths and lists
Apr 27, 2026
a435f07
fix: Convert WindowsPath to string for JSON serialization
Apr 28, 2026
6e4f9eb
fix: Rename agent_config to tools in session data
Apr 28, 2026
d7b6d3b
test: add tests for session save/load agent config
Apr 28, 2026
f4a5bff
fix: Prevent strange characters on Windows TUI
Apr 28, 2026
390fb25
fixed formatting
Apr 28, 2026
274d654
merge in changes
Apr 28, 2026
47c09ad
merge in changes
Apr 28, 2026
fe1b37d
cli-15: fixed formatting
Apr 28, 2026
d390bb3
test: Add tests for app module
Apr 29, 2026
3cc115e
test: add TUI mouse event handling tests
Apr 29, 2026
198737f
test: refactor tests/tui/test_app.py to use pytest
Apr 29, 2026
5a613b6
cli-15: fixed formatting
Apr 29, 2026
eae434e
cli-14: fixed formatting
Apr 29, 2026
2566f6c
cli-15: fixed formatting
Apr 29, 2026
63473d7
cli-14: fixed formatting
Apr 29, 2026
05e431e
Error in UpdateTodoList: [Errno 2] No such file or directory fix
BecoKo Apr 29, 2026
70c2ed0
Bump Version
Apr 29, 2026
91967da
Change repo map assistant message
Apr 29, 2026
cc91c9c
Small formatting changes, remove unnecessary unicode chars near end o…
Apr 29, 2026
67c221a
Fix GetLines setting output content before the tool call in message s…
Apr 30, 2026
fad6956
Merge pull request #500 from szmania/cli-14-tui-keyboard-mouse-capture
dwash96 Apr 30, 2026
e40439d
Merge pull request #499 from szmania/cli-15-enhance-session-management
dwash96 Apr 30, 2026
60a745d
Merge pull request #501 from BecoKo/patch-1
dwash96 Apr 30, 2026
cac2e5f
Preserve agent folder file structure
Apr 30, 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.7.dev"
__version__ = "0.99.9.dev"
safe_version = __version__

try:
Expand Down
8 changes: 2 additions & 6 deletions cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,9 +618,7 @@ def get_context_summary(self):
total_file_tokens += tokens
editable_tokens += tokens
size_indicator = (
"🔴 Large"
if tokens > 5000
else "🟡 Medium" if tokens > 1000 else "🟢 Small"
"Large" if tokens > 5000 else "Medium" if tokens > 1000 else "Small"
)
editable_files.append(
f"- {rel_fname}: {tokens:,} tokens ({size_indicator})"
Expand All @@ -642,9 +640,7 @@ def get_context_summary(self):
total_file_tokens += tokens
readonly_tokens += tokens
size_indicator = (
"🔴 Large"
if tokens > 5000
else "🟡 Medium" if tokens > 1000 else "🟢 Small"
"Large" if tokens > 5000 else "Medium" if tokens > 1000 else "Small"
)
readonly_files.append(
f"- {rel_fname}: {tokens:,} tokens ({size_indicator})"
Expand Down
7 changes: 6 additions & 1 deletion cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2178,6 +2178,8 @@ async def send_message(self, inp):
# Notify IO that LLM processing is starting
self.io.llm_started()

ConversationService.get_manager(self).flush_queue()

if inp:
# Make sure current coder actually has control of conversation system
ConversationService.get_chunks(self).initialize_conversation_system()
Expand Down Expand Up @@ -4003,7 +4005,10 @@ def apply_edits_dry_run(self, edits):
return edits

def local_agent_folder(self, path):
os.makedirs(f".cecli/agents/{GLOBAL_DATE}/{self.uuid}", exist_ok=True)
os.makedirs(
self.abs_root_path(f".cecli/agents/{GLOBAL_DATE}/{self.uuid}"),
exist_ok=True,
)

stripped = path.lstrip("/")
return f".cecli/agents/{GLOBAL_DATE}/{self.uuid}/{stripped}"
Expand Down
2 changes: 1 addition & 1 deletion cecli/commands/load_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async def execute(cls, io, coder, args, **kwargs):
from cecli import sessions

session_manager = sessions.SessionManager(coder, io)
session_manager.load_session(args.strip())
await session_manager.load_session(args.strip())

return format_command_result(io, "load-session", f"Loaded session: {args.strip()}")

Expand Down
6 changes: 3 additions & 3 deletions cecli/helpers/conversation/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ def add_repo_map_messages(self) -> List[Dict[str, Any]]:
dict(role="user", content=repo_content),
dict(
role="assistant",
content="Ok, I won't try and edit those files without asking first.",
content="Thank you, these files will help with navigating the codebase.",
),
]

Expand Down Expand Up @@ -781,14 +781,14 @@ def add_file_context_messages(self, promote_messages=True) -> None:
}

# Add to conversation manager
ConversationService.get_manager(coder).add_message(
ConversationService.get_manager(coder).queue_message(
message_dict=user_msg,
tag=MessageTag.FILE_CONTEXTS,
hash_key=("file_context_user", file_path),
force=True,
)

ConversationService.get_manager(coder).add_message(
ConversationService.get_manager(coder).queue_message(
message_dict=assistant_msg,
tag=MessageTag.FILE_CONTEXTS,
hash_key=("file_context_assistant", file_path),
Expand Down
28 changes: 28 additions & 0 deletions cecli/helpers/conversation/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def __init__(self, coder):
self._tag_cache: Dict[str, List[Dict[str, Any]]] = {}
self._ALL_MESSAGES_CACHE_KEY = "__all__"
self.DEFAULT_TAG_PROMOTION_VALUE: int = 999
self._queue: List[Dict[str, Any]] = []

@classmethod
def get_instance(cls, coder) -> "ConversationManager":
Expand Down Expand Up @@ -193,6 +194,33 @@ def add_message(
self._tag_cache.pop(self._ALL_MESSAGES_CACHE_KEY, None)
return message

def queue_message(self, **kwargs) -> None:
"""
Queue an add_message() call for later insertion.

Accepts the same keyword arguments as add_message()
and stores them in an internal queue to be flushed
later via flush_queue().
"""
self._queue.append(kwargs)

def flush_queue(self) -> List[Any]:
"""
Flush all queued add_message() calls.

Calls add_message() for each set of kwargs in the
internal queue, then clears the queue.

Returns:
List of BaseMessage instances returned by add_message()
"""
results = []
while self._queue:
kwargs = self._queue.pop(0)
result = self.add_message(**kwargs)
results.append(result)
return results

def base_sort(self, messages: List[BaseMessage]) -> List[BaseMessage]:
"""
Sorts messages by effective priority (promotion if mark_for_demotion has not elapsed yet), then timestamp.
Expand Down
2 changes: 1 addition & 1 deletion cecli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1198,7 +1198,7 @@ def get_io(pretty):
from cecli.sessions import SessionManager

session_manager = SessionManager(coder, io)
session_manager.load_session(
await session_manager.load_session(
args.auto_save_session_name if args.auto_save_session_name else "auto-save"
)
except Exception:
Expand Down
6 changes: 5 additions & 1 deletion cecli/prompts/agent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ files_no_full_files_with_repo_map: |
I have a repository map. I will use it to target my navigation and add relevant files to the context.
</context>

repo_content_prefix: |
Here are summaries of some files present in my git repository.
These files should be helpful for navigating the codebase.

main_system: |
<context name="role_and_directives">
## Core Directives
Expand All @@ -34,7 +38,7 @@ main_system: |
uXdn::def example_method():
WAR5:: return "example"
vwkS::
</context>
</context>

<context name="workflow_and_tool_usage">
## Core Workflow
Expand Down
86 changes: 83 additions & 3 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

def load_session(self, session_identifier: str) -> bool:
async def load_session(self, session_identifier: str) -> 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 @@ -112,7 +112,17 @@ def load_session(self, session_identifier: str) -> bool:
return False

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

raise SwitchCoderSignal(
edit_format=self.coder.edit_format,
from_coder=self.coder,
summarize_from_coder=False,
show_announcements=True,
)
return applied

def _build_session_data(self, session_name) -> Dict:
"""Build session data dictionary from current coder state."""
Expand Down Expand Up @@ -140,6 +150,39 @@ def _build_session_data(self, session_name) -> Dict:
self.io.tool_warning(f"Could not read todo list file: {e}")

# Get CUR and DONE messages from ConversationManager
connected_mcps = []
if hasattr(self.coder, "mcp_manager") and self.coder.mcp_manager:
connected_mcps = [server.name for server in self.coder.mcp_manager.connected_servers]

# Get CUR and DONE messages from ConversationManager
connected_mcps = []
if hasattr(self.coder, "mcp_manager") and self.coder.mcp_manager:
connected_mcps = [server.name for server in self.coder.mcp_manager.connected_servers]

skills_data = None
if hasattr(self.coder, "skills_manager") and self.coder.skills_manager:
skills_data = {
"skills_paths": [str(p) for p in self.coder.skills_manager.directory_paths],
"skills_includelist": (
list(self.coder.skills_manager.include_list)
if self.coder.skills_manager.include_list is not None
else []
),
"skills_excludelist": (
list(self.coder.skills_manager.exclude_list)
if self.coder.skills_manager.exclude_list is not None
else []
),
}

agent_config_data = None
if hasattr(self.coder, "agent_config"):
agent_config_data = {
"tools_paths": self.coder.agent_config.get("tools_paths", []),
"tools_includelist": self.coder.agent_config.get("tools_includelist", []),
"tools_excludelist": self.coder.agent_config.get("tools_excludelist", []),
}

return {
"version": 1,
"session_name": session_name,
Expand Down Expand Up @@ -168,6 +211,9 @@ def _build_session_data(self, session_name) -> Dict:
"auto_test": self.coder.auto_test,
},
"todo_list": todo_content,
"mcps": connected_mcps,
"skills": skills_data,
"tools": agent_config_data,
}

def _find_session_file(self, session_identifier: str) -> Optional[Path]:
Expand All @@ -194,7 +240,7 @@ def _find_session_file(self, session_identifier: str) -> Optional[Path]:
self.io.tool_output("Use /list-sessions to see available sessions.")
return None

def _apply_session_data(self, session_data: Dict, session_file: Path) -> bool:
async def _apply_session_data(self, session_data: Dict, session_file: Path) -> bool:
"""Apply session data to current coder state."""
try:
# Clear current state
Expand Down Expand Up @@ -303,6 +349,40 @@ def _apply_session_data(self, session_data: Dict, session_file: Path) -> bool:
)
self.io.tool_output(f"Loaded {num_messages} messages and {num_files} files")

# Load MCPs
saved_mcps = session_data.get("mcps", [])
if hasattr(self.coder, "mcp_manager") and self.coder.mcp_manager:
current_mcps = {server.name for server in self.coder.mcp_manager.connected_servers}
saved_mcps_set = set(saved_mcps)

to_disconnect = current_mcps - saved_mcps_set
for mcp_name in to_disconnect:
await self.coder.mcp_manager.disconnect_server(mcp_name)

to_connect = saved_mcps_set - current_mcps
for mcp_name in to_connect:
await self.coder.mcp_manager.connect_server(mcp_name)

# Load skills
skills_data = session_data.get("skills")
if skills_data and hasattr(self.coder, "skills_manager") and self.coder.skills_manager:
self.coder.skills_manager.directory_paths = skills_data.get("skills_paths", [])
self.coder.skills_manager.include_list = set(
skills_data.get("skills_includelist", [])
)
self.coder.skills_manager.exclude_list = set(
skills_data.get("skills_excludelist", [])
)

# Load tools config
agent_config_data = session_data.get("tools")
if agent_config_data and hasattr(self.coder, "agent_config"):
self.coder.agent_config.update(agent_config_data)
from cecli.tools.utils.registry import ToolRegistry

ToolRegistry.build_registry(agent_config=self.coder.agent_config)
self.coder.loaded_custom_tools = ToolRegistry.loaded_custom_tools

return True

except Exception as e:
Expand Down
18 changes: 9 additions & 9 deletions cecli/tools/edit_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ class Tool(BaseTool):
"type": "string",
"enum": ["replace", "delete", "insert"],
"description": (
"The type of operation: 'replace' (replace range with text), "
"'delete' (remove range), or 'insert' (insert text after start_line). "
"Defaults to 'replace'."
"The type of operation: 'replace' (replace range with"
" text), 'delete' (remove range), or 'insert' (insert text"
" after start_line). Defaults to 'replace'."
),
},
"text": {
Expand All @@ -78,8 +78,8 @@ class Tool(BaseTool):
"end_line": {
"type": "string",
"description": (
'Hashline format for end line: "{4 char hash}" (without the '
"braces)"
'Hashline format for end line: "{4 char hash}" (without the'
" braces)"
),
},
},
Expand Down Expand Up @@ -179,8 +179,8 @@ def execute(
if operation in ("replace", "delete"):
if edit_start_line is None:
raise ToolError(
f"Edit {edit_index + 1}: 'start_line' parameter is required "
f"for '{operation}' operation"
f"Edit {edit_index + 1}: 'start_line' parameter is required"
f" for '{operation}' operation"
)
if edit_end_line is None:
raise ToolError(
Expand All @@ -190,8 +190,8 @@ def execute(
if operation == "insert":
if edit_start_line is None:
raise ToolError(
f"Edit {edit_index + 1}: 'start_line' parameter is required "
"for 'insert' operation"
f"Edit {edit_index + 1}: 'start_line' parameter is required"
" for 'insert' operation"
)
# For insert, end_line defaults to start_line
edit_end_line = edit_end_line or edit_start_line
Expand Down
14 changes: 8 additions & 6 deletions cecli/tools/get_lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ class Tool(BaseTool):
"description": (
"Get hashline prefixes of context between start and end patterns in multiple files."
" Accepts an array of show objects, each with file_path, start_text,"
" end_text, and optional padding. Special markers '@000' and '000@' can be"
" used for start_text and end_text to represent the first and last lines of"
" the file respectively. Never use hashlines as the start_text and end_text"
" values. These values must be lines from the content of the file."
" end_text, and optional padding."
" These values must be lines from the content of the file."
" They can contain up to 3 lines but newlines should generally be avoided."
" Avoid using generic keywords."
" Avoid using generic keywords and symbols. Special markers '@000' and '000@' can be"
" used for start_text and end_text to represent the first and last lines of"
" the file respectively. Avoid using the special markers on files with contents."
" They are intended to be used with empty files."
" Never use hashlines as the start_text and end_text values."
" Do not use the same pattern for the start_text and end_text."
" It is usually best to use function names and other block identifiers as "
" start_texts and end_texts."
Expand Down Expand Up @@ -279,7 +281,7 @@ def execute(cls, coder, show, **kwargs):
coder.io.tool_output("File contents already up to date")
return (
"File contents already up to date."
"Do not call GetLines again with these parameters until you edit the file."
" Do not call GetLines again with these parameters until you edit the file."
)
else:
coder.io.tool_output(f"✅ Successfully retrieved context for {len(show)} file(s)")
Expand Down
10 changes: 5 additions & 5 deletions cecli/tools/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ def validate_file_for_edit(coder, file_path):
raise ToolError(
f"File '{file_path}' is read-only. Make editable with `ContextManager` first."
)
else:
# File exists but is not in context at all
raise ToolError(
f"File '{file_path}' not in context. Make editable with `ContextManager` first."
)
# else:
# # File exists but is not in context at all
# raise ToolError(
# f"File '{file_path}' not in context. Make editable with `ContextManager` first."
# )

# Reread content immediately before potential modification
content = coder.io.read_text(abs_path)
Expand Down
6 changes: 6 additions & 0 deletions cecli/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import concurrent.futures
import json
import platform
import queue
import time
from functools import lru_cache
Expand Down Expand Up @@ -359,6 +360,11 @@ def on_mouse_up(self, event: events.MouseUp) -> None:
self._mouse_hold_timer = None
self.update_key_hints(generating=self._currently_generating)

def on_mouse_move(self, event: events.MouseMove) -> None:
"""Handle mouse move events to prevent strange characters on Windows."""
if platform.system() == "Windows":
event.stop()

def _show_select_hint(self) -> None:
"""Show the shift+drag to select hint."""
try:
Expand Down
Loading
Loading