From f80b6184b7c3665bdaaec4c9e852e37095c10a26 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sun, 25 Jan 2026 08:02:29 -0500 Subject: [PATCH 1/4] Bump Version --- cecli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cecli/__init__.py b/cecli/__init__.py index 6991056b34c..8fc1c520701 100644 --- a/cecli/__init__.py +++ b/cecli/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.96.3.dev" +__version__ = "0.96.4.dev" safe_version = __version__ try: From 043b0bdd383e8739309a140bf420cb7a996b0121 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sun, 25 Jan 2026 08:39:31 -0500 Subject: [PATCH 2/4] #413: For reasons, /add requires explicit secondary filtration since it's internally dog-fooded by other parts of the system --- cecli/io.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cecli/io.py b/cecli/io.py index ba76f04dcfb..4ee3ee21322 100644 --- a/cecli/io.py +++ b/cecli/io.py @@ -148,6 +148,7 @@ def __init__( self.rel_fnames = rel_fnames self.encoding = encoding self.abs_read_only_fnames = abs_read_only_fnames or [] + self.post_filter_commands = ["/add"] fname_to_rel_fnames = defaultdict(list) for rel_fname in addable_rel_fnames: @@ -240,6 +241,10 @@ def get_command_completions(self, document, complete_event, text, words): if candidates is None: return + + if cmd in self.post_filter_commands: + candidates = [word for word in candidates if partial in word.lower()] + for candidate in sorted(candidates): yield Completion(candidate, start_position=-len(words[-1])) From 6d7ecb6d8fea57883b93adfebf4dc28d0c4cd433 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sun, 25 Jan 2026 13:16:48 -0500 Subject: [PATCH 3/4] Fix branch identification in TUI, move edit format to border title to give more space to file paths and branch names --- cecli/tui/app.py | 19 +++++++++++++------ cecli/tui/styles.tcss | 1 + cecli/tui/widgets/__init__.py | 2 ++ cecli/tui/widgets/footer.py | 10 +++++----- cecli/tui/widgets/input_container.py | 19 +++++++++++++++++++ 5 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 cecli/tui/widgets/input_container.py diff --git a/cecli/tui/app.py b/cecli/tui/app.py index a964541659e..2155e7cac9a 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -8,7 +8,6 @@ from textual.app import App, ComposeResult # from textual.binding import Binding -from textual.containers import Vertical from textual.theme import Theme from cecli.editor import pipe_editor @@ -18,6 +17,7 @@ CompletionBar, FileList, InputArea, + InputContainer, KeyHints, MainFooter, OutputContainer, @@ -256,7 +256,7 @@ def compose(self) -> ComposeResult: else str(coder.repo.root).split("/")[-1] ) else: - project_name = "No repo" + project_name = "No Repo" # Get history file path from coder's io history_file = getattr(coder.io, "input_history_file", None) @@ -265,15 +265,16 @@ def compose(self) -> ComposeResult: # Git info loaded in on_mount to avoid blocking startup yield OutputContainer(id="output") yield StatusBar(id="status-bar") - yield Vertical( + yield InputContainer( InputArea(history_file=history_file, id="input"), FileList(id="file-list", classes="empty"), id="input-container", + coder_mode=coder_mode, ) yield KeyHints(id="key-hints") yield MainFooter( model_name=model_name, - project_name=project_name, + project_name=str(coder.repo.root) if len(str(coder.repo.root)) <= 64 else project_name, git_branch="", # Loaded async in on_mount coder_mode=coder_mode, id="footer", @@ -412,11 +413,14 @@ def _load_git_info(self): footer = self.query_one(MainFooter) if self.worker.coder.repo: try: - branch = self.worker.coder.repo.get_head_branch_name() or "main" + branch = self.worker.coder.repo.repo.active_branch.name or "main" dirty = self.worker.coder.repo.get_dirty_files() footer.update_git(branch, len(dirty) if dirty else 0) except Exception: - footer.update_git("main", 0) + if self.worker.coder.repo: + footer.update_git("main", 0) + else: + footer.update_git("No Repo", 0) def check_output_queue(self): """Process messages from coder worker.""" @@ -470,6 +474,9 @@ def handle_output_message(self, msg): self.action_quit() elif msg_type == "mode_change": # Update footer with new chat mode + container = footer = self.query_one(InputContainer) + container.update_mode(msg.get("mode", "code")) + footer = self.query_one(MainFooter) footer.update_mode(msg.get("mode", "code")) diff --git a/cecli/tui/styles.tcss b/cecli/tui/styles.tcss index 7800dbc633e..d49c83a27ad 100644 --- a/cecli/tui/styles.tcss +++ b/cecli/tui/styles.tcss @@ -41,6 +41,7 @@ Screen { padding: 0 0 0 1; margin: 0 1 0 1; border: round $accent 50%; + border-title-align: right; background: $surface; } diff --git a/cecli/tui/widgets/__init__.py b/cecli/tui/widgets/__init__.py index 34c569435d8..bc634ec6c82 100644 --- a/cecli/tui/widgets/__init__.py +++ b/cecli/tui/widgets/__init__.py @@ -4,6 +4,7 @@ from .file_list import FileList from .footer import MainFooter from .input_area import InputArea +from .input_container import InputContainer from .key_hints import KeyHints from .output import OutputContainer from .status_bar import StatusBar @@ -12,6 +13,7 @@ "MainFooter", "CompletionBar", "InputArea", + "InputContainer", "KeyHints", "OutputContainer", "StatusBar", diff --git a/cecli/tui/widgets/footer.py b/cecli/tui/widgets/footer.py index fbc550972fa..41fb6fdacad 100644 --- a/cecli/tui/widgets/footer.py +++ b/cecli/tui/widgets/footer.py @@ -96,9 +96,9 @@ def render(self) -> Text: # Build right side: mode + model + project + git right = Text() - if self.coder_mode: - right.append(f"{self.coder_mode}") - right.append(" • ") + # if self.coder_mode: + # right.append(f"{self.coder_mode}") + # right.append(" • ") # model_display = self._get_display_model() # if model_display: @@ -111,8 +111,8 @@ def render(self) -> Text: if self.git_branch: right.append(self.git_branch) - if self.git_dirty: - right.append(f" +{self.git_dirty}") + # if self.git_dirty: + # right.append(f" +{self.git_dirty}") # right.append(" ") # Always show cost diff --git a/cecli/tui/widgets/input_container.py b/cecli/tui/widgets/input_container.py new file mode 100644 index 00000000000..2e6b42ce2c4 --- /dev/null +++ b/cecli/tui/widgets/input_container.py @@ -0,0 +1,19 @@ +from textual.containers import Vertical +from textual.reactive import reactive + + +class InputContainer(Vertical): + """Input container widget for input area wrapper""" + + coder_mode = reactive("") + + def __init__(self, *args, coder_mode: str = "", **kwargs): + super().__init__(*args, **kwargs) + self.coder_mode = coder_mode + self.border_title = self.coder_mode + + def update_mode(self, mode: str): + """Update the chat mode display.""" + self.coder_mode = mode + self.border_title = self.coder_mode + self.refresh() From 39dd0f415cbd4630eb8e146656c49dfa2c037d94 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sun, 25 Jan 2026 23:05:27 -0500 Subject: [PATCH 4/4] Message duplication due to extraneous base_coder.py __init__ injection --- cecli/coders/architect_coder.py | 56 +++++++--------------- cecli/coders/base_coder.py | 2 +- cecli/commands/editor_model.py | 42 ++++++---------- cecli/commands/model.py | 42 ++++++---------- cecli/commands/utils/base_command.py | 39 +++++---------- cecli/commands/weak_model.py | 42 ++++++---------- cecli/helpers/conversation/base_message.py | 6 +-- cecli/helpers/conversation/manager.py | 46 ++++++++++++++++-- cecli/sessions.py | 53 ++++++++++---------- 9 files changed, 145 insertions(+), 183 deletions(-) diff --git a/cecli/coders/architect_coder.py b/cecli/coders/architect_coder.py index 507faed0603..aa5789e1b17 100644 --- a/cecli/coders/architect_coder.py +++ b/cecli/coders/architect_coder.py @@ -42,18 +42,21 @@ async def reply_completed(self): kwargs["cache_prompts"] = False kwargs["num_cache_warming_pings"] = 0 kwargs["summarize_from_coder"] = False + kwargs["done_messages"] = [] + kwargs["cur_messages"] = [] new_kwargs = dict(io=self.io, from_coder=self) new_kwargs.update(kwargs) # Save current conversation state - original_all_messages = ConversationManager.get_messages() original_coder = self editor_coder = await Coder.create(**new_kwargs) # Re-initialize ConversationManager with editor coder - ConversationManager.initialize(editor_coder, reset=True, reformat=True) + ConversationManager.initialize( + editor_coder, reset=True, reformat=True, preserve_tags=[MessageTag.DONE, MessageTag.CUR] + ) if self.verbose: editor_coder.show_announcements() @@ -61,49 +64,24 @@ async def reply_completed(self): try: await editor_coder.generate(user_message=content, preproc=False) - # Save editor's ALL messages - editor_all_messages = ConversationManager.get_messages() - # Clear manager and restore original state - ConversationManager.initialize(original_coder or self, reset=True, reformat=True) - - # Restore original messages with all metadata - for msg in original_all_messages: - if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: - ConversationManager.add_message( - message_dict=msg.message_dict, - tag=MessageTag(msg.tag), - priority=msg.priority, - mark_for_delete=msg.mark_for_delete, - force=True, - ) - - # Append editor's DONE and CUR messages (but not other tags like SYSTEM) - for msg in editor_all_messages: - if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: - ConversationManager.add_message( - message_dict=msg.message_dict, - tag=MessageTag(msg.tag), - priority=msg.priority, - mark_for_delete=msg.mark_for_delete, - force=True, - ) + ConversationManager.initialize( + original_coder or self, + reset=True, + reformat=True, + preserve_tags=[MessageTag.DONE, MessageTag.CUR], + ) self.total_cost = editor_coder.total_cost self.coder_commit_hashes = editor_coder.coder_commit_hashes except Exception as e: self.io.tool_error(e) # Restore original state on error - ConversationManager.initialize(original_coder or self, reset=True, reformat=True) - - for msg in original_all_messages: - if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: - ConversationManager.add_message( - message_dict=msg.message_dict, - tag=MessageTag(msg.tag), - priority=msg.priority, - mark_for_delete=msg.mark_for_delete, - force=True, - ) + ConversationManager.initialize( + original_coder or self, + reset=True, + reformat=True, + preserve_tags=[MessageTag.DONE, MessageTag.CUR], + ) raise SwitchCoderSignal(main_model=self.main_model, edit_format="architect") diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index e725c4d0bff..611d6a0c84e 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -2096,7 +2096,7 @@ async def send_message(self, inp): ConversationManager.add_message( message_dict=dict(role="user", content=inp), tag=MessageTag.CUR, - hash_key=("user_message", inp, str(time.time_ns())), + hash_key=("user_message", inp, str(time.monotonic_ns())), ) loop = asyncio.get_running_loop() diff --git a/cecli/commands/editor_model.py b/cecli/commands/editor_model.py index b2cc39c9081..de3b581cb2d 100644 --- a/cecli/commands/editor_model.py +++ b/cecli/commands/editor_model.py @@ -52,18 +52,24 @@ async def execute(cls, io, coder, args, **kwargs): kwargs["total_cost"] = coder.total_cost kwargs["num_cache_warming_pings"] = 0 kwargs["summarize_from_coder"] = False + kwargs["done_messages"] = [] + kwargs["cur_messages"] = [] new_kwargs = dict(io=io, from_coder=coder) new_kwargs.update(kwargs) # Save current conversation state - original_all_messages = ConversationManager.get_messages() original_coder = coder temp_coder = await Coder.create(**new_kwargs) # Re-initialize ConversationManager with temp coder - ConversationManager.initialize(temp_coder, reset=True, reformat=True) + ConversationManager.initialize( + temp_coder, + reset=True, + reformat=True, + preserve_tags=[MessageTag.DONE, MessageTag.CUR], + ) verbose = kwargs.get("verbose", False) if verbose: @@ -74,33 +80,13 @@ async def execute(cls, io, coder, args, **kwargs): coder.total_cost = temp_coder.total_cost coder.coder_commit_hashes = temp_coder.coder_commit_hashes - # Save temp coder's ALL messages - temp_all_messages = ConversationManager.get_messages() - # Clear manager and restore original state - ConversationManager.initialize(original_coder, reset=True, reformat=True) - - # Restore original messages with all metadata - for msg in original_all_messages: - if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: - ConversationManager.add_message( - message_dict=msg.message_dict, - tag=MessageTag(msg.tag), - priority=msg.priority, - mark_for_delete=msg.mark_for_delete, - force=True, - ) - - # Append temp coder's DONE and CUR messages (but not other tags like SYSTEM) - for msg in temp_all_messages: - if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: - ConversationManager.add_message( - message_dict=msg.message_dict, - tag=MessageTag(msg.tag), - priority=msg.priority, - mark_for_delete=msg.mark_for_delete, - force=True, - ) + ConversationManager.initialize( + original_coder, + reset=True, + reformat=True, + preserve_tags=[MessageTag.DONE, MessageTag.CUR], + ) # Restore the original model configuration from cecli.commands import SwitchCoderSignal diff --git a/cecli/commands/model.py b/cecli/commands/model.py index 8dd91098fb8..315d4faf18b 100644 --- a/cecli/commands/model.py +++ b/cecli/commands/model.py @@ -57,18 +57,24 @@ async def execute(cls, io, coder, args, **kwargs): kwargs["total_cost"] = coder.total_cost kwargs["num_cache_warming_pings"] = 0 kwargs["summarize_from_coder"] = False + kwargs["done_messages"] = [] + kwargs["cur_messages"] = [] new_kwargs = dict(io=io, from_coder=coder) new_kwargs.update(kwargs) # Save current conversation state - original_all_messages = ConversationManager.get_messages() original_coder = coder temp_coder = await Coder.create(**new_kwargs) # Re-initialize ConversationManager with temp coder - ConversationManager.initialize(temp_coder, reset=True, reformat=True) + ConversationManager.initialize( + temp_coder, + reset=True, + reformat=True, + preserve_tags=[MessageTag.DONE, MessageTag.CUR], + ) verbose = kwargs.get("verbose", False) if verbose: @@ -79,33 +85,13 @@ async def execute(cls, io, coder, args, **kwargs): coder.total_cost = temp_coder.total_cost coder.coder_commit_hashes = temp_coder.coder_commit_hashes - # Save temp coder's ALL messages - temp_all_messages = ConversationManager.get_messages() - # Clear manager and restore original state - ConversationManager.initialize(original_coder, reset=True, reformat=True) - - # Restore original messages with all metadata - for msg in original_all_messages: - if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: - ConversationManager.add_message( - message_dict=msg.message_dict, - tag=MessageTag(msg.tag), - priority=msg.priority, - mark_for_delete=msg.mark_for_delete, - force=True, - ) - - # Append temp coder's DONE and CUR messages (but not other tags like SYSTEM) - for msg in temp_all_messages: - if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: - ConversationManager.add_message( - message_dict=msg.message_dict, - tag=MessageTag(msg.tag), - priority=msg.priority, - mark_for_delete=msg.mark_for_delete, - force=True, - ) + ConversationManager.initialize( + original_coder, + reset=True, + reformat=True, + preserve_tags=[MessageTag.DONE, MessageTag.CUR], + ) # Restore the original model configuration from cecli.commands import SwitchCoderSignal diff --git a/cecli/commands/utils/base_command.py b/cecli/commands/utils/base_command.py index 40f4a0e84a7..a5f25ddfb78 100644 --- a/cecli/commands/utils/base_command.py +++ b/cecli/commands/utils/base_command.py @@ -142,47 +142,30 @@ async def _generic_chat_command(cls, io, coder, args, edit_format, placeholder=N "num_cache_warming_pings": 0, "coder_commit_hashes": coder.coder_commit_hashes, "args": coder.args, + "done_messages": [], + "cur_messages": [], } # Save current conversation state - original_all_messages = ConversationManager.get_messages() original_coder = coder new_coder = await Coder.create(**kwargs) # Re-initialize ConversationManager with new coder - ConversationManager.initialize(new_coder, reset=True, reformat=True) + ConversationManager.initialize( + new_coder, reset=True, reformat=True, preserve_tags=[MessageTag.DONE, MessageTag.CUR] + ) await new_coder.generate(user_message=user_msg, preproc=False) coder.coder_commit_hashes = new_coder.coder_commit_hashes - # Save new coder's ALL messages - new_all_messages = ConversationManager.get_messages() - # Clear manager and restore original state - ConversationManager.initialize(original_coder, reset=True, reformat=True) - - # Restore original messages with all metadata - for msg in original_all_messages: - if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: - ConversationManager.add_message( - message_dict=msg.message_dict, - tag=MessageTag(msg.tag), - priority=msg.priority, - mark_for_delete=msg.mark_for_delete, - force=True, - ) - - # Append new coder's DONE and CUR messages (but not other tags like SYSTEM) - for msg in new_all_messages: - if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: - ConversationManager.add_message( - message_dict=msg.message_dict, - tag=MessageTag(msg.tag), - priority=msg.priority, - mark_for_delete=msg.mark_for_delete, - force=True, - ) + ConversationManager.initialize( + original_coder, + reset=True, + reformat=True, + preserve_tags=[MessageTag.DONE, MessageTag.CUR], + ) from cecli.commands import SwitchCoderSignal diff --git a/cecli/commands/weak_model.py b/cecli/commands/weak_model.py index ededbe330f4..ff18ef21879 100644 --- a/cecli/commands/weak_model.py +++ b/cecli/commands/weak_model.py @@ -52,18 +52,24 @@ async def execute(cls, io, coder, args, **kwargs): kwargs["total_cost"] = coder.total_cost kwargs["num_cache_warming_pings"] = 0 kwargs["summarize_from_coder"] = False + kwargs["done_messages"] = [] + kwargs["cur_messages"] = [] new_kwargs = dict(io=io, from_coder=coder) new_kwargs.update(kwargs) # Save current conversation state - original_all_messages = ConversationManager.get_messages() original_coder = coder temp_coder = await Coder.create(**new_kwargs) # Re-initialize ConversationManager with temp coder - ConversationManager.initialize(temp_coder, reset=True, reformat=True) + ConversationManager.initialize( + temp_coder, + reset=True, + reformat=True, + preserve_tags=[MessageTag.DONE, MessageTag.CUR], + ) verbose = kwargs.get("verbose", False) if verbose: @@ -74,33 +80,13 @@ async def execute(cls, io, coder, args, **kwargs): coder.total_cost = temp_coder.total_cost coder.coder_commit_hashes = temp_coder.coder_commit_hashes - # Save temp coder's ALL messages - temp_all_messages = ConversationManager.get_messages() - # Clear manager and restore original state - ConversationManager.initialize(original_coder, reset=True, reformat=True) - - # Restore original messages with all metadata - for msg in original_all_messages: - if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: - ConversationManager.add_message( - message_dict=msg.message_dict, - tag=MessageTag(msg.tag), - priority=msg.priority, - mark_for_delete=msg.mark_for_delete, - force=True, - ) - - # Append temp coder's DONE and CUR messages (but not other tags like SYSTEM) - for msg in temp_all_messages: - if msg.tag in [MessageTag.DONE.value, MessageTag.CUR.value]: - ConversationManager.add_message( - message_dict=msg.message_dict, - tag=MessageTag(msg.tag), - priority=msg.priority, - mark_for_delete=msg.mark_for_delete, - force=True, - ) + ConversationManager.initialize( + original_coder, + reset=True, + reformat=True, + preserve_tags=[MessageTag.DONE, MessageTag.CUR], + ) # Restore the original model configuration from cecli.commands import SwitchCoderSignal diff --git a/cecli/helpers/conversation/base_message.py b/cecli/helpers/conversation/base_message.py index e763b2f9bcb..65aa519a04b 100644 --- a/cecli/helpers/conversation/base_message.py +++ b/cecli/helpers/conversation/base_message.py @@ -26,7 +26,7 @@ class BaseMessage: message_dict: Dict[str, Any] tag: str priority: int = field(default=0) - timestamp: int = field(default_factory=lambda: time.time_ns()) + timestamp: int = field(default_factory=lambda: time.monotonic_ns()) mark_for_delete: Optional[int] = field(default=None) hash_key: Optional[Tuple[str, ...]] = field(default=None) message_id: str = field(init=False) @@ -136,6 +136,6 @@ def __repr__(self) -> str: content_preview = str(self.message_dict.get("content", ""))[:50] return ( f"BaseMessage(id={self.message_id[:8]}..., " - f"tag={self.tag}, priority={self.priority}, " - f"role={role}, content='{content_preview}...')" + f"tag={self.tag}, priority={self.priority}, timestamp={self.timestamp}, " + f"role={role}, content='{content_preview}...', )" ) diff --git a/cecli/helpers/conversation/manager.py b/cecli/helpers/conversation/manager.py index 0e6c0155b76..3d1e275afd4 100644 --- a/cecli/helpers/conversation/manager.py +++ b/cecli/helpers/conversation/manager.py @@ -33,7 +33,13 @@ class ConversationManager: _ALL_MESSAGES_CACHE_KEY = "__all__" # Special key for caching all messages (tag=None) @classmethod - def initialize(cls, coder, reset: bool = False, reformat: bool = False) -> None: + def initialize( + cls, + coder, + reset: bool = False, + reformat: bool = False, + preserve_tags: Optional[List[str]] = None, + ) -> None: """ Set up singleton with weak reference to coder. @@ -42,17 +48,49 @@ def initialize(cls, coder, reset: bool = False, reformat: bool = False) -> None: reset: Whether to re-initialize the conversation history itself reformat: Whether to format chat history (useful for initialization outside of coder class) + preserve_tags: Optional list of tag strings to preserve during reset. + If provided, messages with these tags will be preserved + when reset=True and re-added AFTER the reformat block. """ cls._coder_ref = weakref.ref(coder) cls._initialized = True - if reset: + preserved_messages = [] + if reset and preserve_tags: + # New approach: loop over every single tag type and only clear tags NOT in preserve_tags + # Get all MessageTag values + all_tag_types = list(MessageTag) + + # Clear tags that are NOT in preserve_tags + for tag_type in all_tag_types: + if tag_type.value not in preserve_tags: + cls.clear_tag(tag_type) + + # Get all remaining messages left over after preservation + preserved_messages = cls.get_messages() + elif reset: + # Original behavior: clear everything cls.reset() if reformat: if hasattr(coder, "format_chat_chunks"): coder.format_chat_chunks() + # If preserve_tags is truthy, re-add preserved messages with updated timestamps after reformat block + if preserve_tags and preserved_messages: + for tag_type in preserve_tags: + cls.clear_tag(tag_type) + + for msg in preserved_messages: + cls.add_message( + message_dict=msg.message_dict, + tag=MessageTag(msg.tag), + priority=msg.priority, + timestamp=time.monotonic_ns(), # Updated timestamp + mark_for_delete=msg.mark_for_delete, + force=True, + ) + # Enable debug mode if coder has verbose attribute and it's True if hasattr(coder, "verbose") and coder.verbose: cls._debug_enabled = True @@ -110,7 +148,7 @@ def add_message( priority = get_default_priority(tag) if timestamp is None: - timestamp = time.time_ns() + get_default_timestamp_offset(tag) + timestamp = time.monotonic_ns() + get_default_timestamp_offset(tag) # Create message instance message = BaseMessage( @@ -152,7 +190,7 @@ def add_message( @classmethod def get_messages(cls) -> List[BaseMessage]: """ - Returns messages sorted by priority (lowest first), then timestamp (earliest first). + Returns messages sorted by priority (lowest first), then raw order in list. Returns: List of BaseMessage instances in sorted order diff --git a/cecli/sessions.py b/cecli/sessions.py index 83d5061c9f5..18c13d4b9a5 100644 --- a/cecli/sessions.py +++ b/cecli/sessions.py @@ -6,7 +6,11 @@ from typing import Dict, List, Optional from cecli import models -from cecli.helpers.conversation import ConversationManager, MessageTag +from cecli.helpers.conversation import ( + ConversationFiles, + ConversationManager, + MessageTag, +) class SessionManager: @@ -199,29 +203,6 @@ def _apply_session_data(self, session_data: Dict, session_file: Path) -> bool: self.coder.abs_read_only_fnames = set() self.coder.abs_read_only_stubs_fnames = set() - # Clear CUR and DONE messages from ConversationManager - ConversationManager.clear_tag(MessageTag.CUR) - ConversationManager.clear_tag(MessageTag.DONE) - - # Load chat history - chat_history = session_data.get("chat_history", {}) - done_messages = chat_history.get("done_messages", []) - cur_messages = chat_history.get("cur_messages", []) - - # Add messages to ConversationManager (source of truth) - # Add done messages - for msg in done_messages: - ConversationManager.add_message( - message_dict=msg, - tag=MessageTag.DONE, - ) - # Add current messages - for msg in cur_messages: - ConversationManager.add_message( - message_dict=msg, - tag=MessageTag.CUR, - ) - # Load files files = session_data.get("files", {}) for rel_fname in files.get("editable", []): @@ -278,6 +259,30 @@ def _apply_session_data(self, session_data: Dict, session_file: Path) -> bool: except Exception as e: self.io.tool_warning(f"Could not restore todo list: {e}") + # Clear CUR and DONE messages from ConversationManager + ConversationManager.reset() + ConversationFiles.reset() + self.coder.format_chat_chunks() + + # Load chat history + chat_history = session_data.get("chat_history", {}) + done_messages = chat_history.get("done_messages", []) + cur_messages = chat_history.get("cur_messages", []) + + # Add messages to ConversationManager (source of truth) + # Add done messages + for msg in done_messages: + ConversationManager.add_message( + message_dict=msg, + tag=MessageTag.DONE, + ) + # Add current messages + for msg in cur_messages: + ConversationManager.add_message( + message_dict=msg, + tag=MessageTag.CUR, + ) + self.io.tool_output( f"Session loaded: {session_data.get('session_name', session_file.stem)}" )