|
10 | 10 | from cecli.commands import SwitchCoderSignal |
11 | 11 | from cecli.helpers.conversation import ConversationService, MessageTag |
12 | 12 |
|
| 13 | +logger = logging.getLogger(__name__) |
13 | 14 | # Suppress asyncio task destroyed warnings during shutdown |
14 | 15 | logging.getLogger("asyncio").setLevel(logging.CRITICAL) |
15 | 16 |
|
@@ -46,6 +47,7 @@ def _run_thread(self): |
46 | 47 | """Thread entry point - creates event loop and runs coder.""" |
47 | 48 | self.loop = asyncio.new_event_loop() |
48 | 49 | asyncio.set_event_loop(self.loop) |
| 50 | + self.loop.set_exception_handler(self.worker_loop_exception_handler) |
49 | 51 |
|
50 | 52 | try: |
51 | 53 | self.loop.run_until_complete(self._async_run()) |
@@ -96,46 +98,95 @@ async def _async_run(self): |
96 | 98 | except KeyboardInterrupt: |
97 | 99 | continue |
98 | 100 | except SwitchCoderSignal as switch: |
99 | | - # Handle chat mode switches (e.g., /chat-mode architect) |
100 | | - try: |
101 | | - await self.coder.auto_save_session(force=True) |
102 | | - kwargs = dict(io=self.coder.io, from_coder=self.coder) |
103 | | - kwargs.update(switch.kwargs) |
104 | | - if "show_announcements" in kwargs: |
105 | | - del kwargs["show_announcements"] |
106 | | - kwargs["num_cache_warming_pings"] = 0 |
107 | | - kwargs["args"] = self.coder.args |
108 | | - # Skip summarization to avoid blocking LLM calls during mode switch |
109 | | - kwargs["summarize_from_coder"] = False |
110 | | - |
111 | | - new_coder = await Coder.create(**kwargs) |
112 | | - new_coder.args = self.coder.args |
113 | | - |
114 | | - for tag in [MessageTag.SYSTEM, MessageTag.EXAMPLES, MessageTag.STATIC]: |
115 | | - ConversationService.get_manager(new_coder).clear_tag(tag) |
116 | | - |
117 | | - if switch.kwargs.get("show_announcements") is False: |
118 | | - new_coder.suppress_announcements_for_next_prompt = True |
119 | | - |
120 | | - # Notify TUI of mode change |
121 | | - self.coder = new_coder |
122 | | - edit_format = getattr(self.coder, "edit_format", "code") or "code" |
123 | | - self.output_queue.put( |
124 | | - { |
125 | | - "type": "mode_change", |
126 | | - "mode": edit_format, |
127 | | - } |
128 | | - ) |
129 | | - except Exception as e: |
130 | | - self.output_queue.put( |
131 | | - {"type": "error", "message": f"Failed to switch mode: {e}"} |
132 | | - ) |
133 | | - break |
| 101 | + await self._handle_switch_coder_signal(switch) |
134 | 102 | # Continue the loop with the new coder |
135 | 103 | except Exception as e: |
136 | | - self.output_queue.put({"type": "error", "message": str(e)}) |
| 104 | + self.output_queue.put( |
| 105 | + { |
| 106 | + "type": "error", |
| 107 | + "message": str(e), |
| 108 | + "coder_uuid": self.coder.uuid, |
| 109 | + } |
| 110 | + ) |
137 | 111 | break |
138 | 112 |
|
| 113 | + async def _handle_switch_coder_signal(self, switch): |
| 114 | + """Handle a SwitchCoderSignal, creating a new coder and notifying the TUI.""" |
| 115 | + try: |
| 116 | + from cecli.helpers.agents.service import AgentService |
| 117 | + |
| 118 | + # Determine the active coder — could be a sub-agent in the foreground |
| 119 | + target_coder = self.coder |
| 120 | + try: |
| 121 | + agent_service = AgentService.get_instance(target_coder) |
| 122 | + foreground = agent_service.foreground_coder |
| 123 | + if foreground is not None: |
| 124 | + target_coder = foreground |
| 125 | + except Exception: |
| 126 | + pass |
| 127 | + |
| 128 | + await target_coder.auto_save_session(force=True) |
| 129 | + kwargs = dict(io=target_coder.io, from_coder=target_coder) |
| 130 | + kwargs.update(switch.kwargs) |
| 131 | + if "show_announcements" in kwargs: |
| 132 | + del kwargs["show_announcements"] |
| 133 | + kwargs["num_cache_warming_pings"] = 0 |
| 134 | + kwargs["args"] = target_coder.args |
| 135 | + # Skip summarization to avoid blocking LLM calls during mode switch |
| 136 | + kwargs["summarize_from_coder"] = False |
| 137 | + |
| 138 | + new_coder = await Coder.create(**kwargs) |
| 139 | + new_coder.args = target_coder.args |
| 140 | + |
| 141 | + for tag in [MessageTag.SYSTEM, MessageTag.EXAMPLES, MessageTag.STATIC]: |
| 142 | + ConversationService.get_manager(new_coder).clear_tag(tag) |
| 143 | + |
| 144 | + if switch.kwargs.get("show_announcements") is False: |
| 145 | + new_coder.suppress_announcements_for_next_prompt = True |
| 146 | + |
| 147 | + # Notify TUI of mode change |
| 148 | + if target_coder == self.coder: |
| 149 | + self.coder = new_coder |
| 150 | + else: |
| 151 | + new_coder.show_announcements() |
| 152 | + |
| 153 | + edit_format = getattr(target_coder, "edit_format", "code") or "code" |
| 154 | + self.output_queue.put( |
| 155 | + { |
| 156 | + "type": "mode_change", |
| 157 | + "mode": edit_format, |
| 158 | + "coder_uuid": new_coder.uuid, |
| 159 | + } |
| 160 | + ) |
| 161 | + except Exception as e: |
| 162 | + self.output_queue.put( |
| 163 | + { |
| 164 | + "type": "error", |
| 165 | + "message": f"Failed to switch mode: {e}", |
| 166 | + "coder_uuid": target_coder, |
| 167 | + } |
| 168 | + ) |
| 169 | + |
| 170 | + def worker_loop_exception_handler(self, loop, context): |
| 171 | + """ |
| 172 | + This runs directly on the worker thread whenever an unhandled |
| 173 | + exception occurs in a task or callback. |
| 174 | +
|
| 175 | + Catches SwitchCoderSignal from fire-and-forget tasks and dispatches |
| 176 | + them to the dedicated handler so mode switches work even when the |
| 177 | + signal is raised outside the main coder.run() loop. |
| 178 | + """ |
| 179 | + exception = context.get("exception") |
| 180 | + |
| 181 | + if isinstance(exception, SwitchCoderSignal): |
| 182 | + logger.info("Worker thread caught SwitchCoderSignal in global handler.") |
| 183 | + # Schedule a coroutine to handle the switch logic on the loop |
| 184 | + loop.create_task(self._handle_switch_coder_signal(exception)) |
| 185 | + else: |
| 186 | + # Always fall back to the default handler so you don't swallow |
| 187 | + # normal bugs, tracebacks, or connection errors. |
| 188 | + loop.default_exception_handler(context) |
| 189 | + |
139 | 190 | def interrupt(self): |
140 | 191 | """Cancel the current output task on the active (foreground) coder. |
141 | 192 |
|
|
0 commit comments