|
61 | 61 | from cecli.repo import ANY_GIT_ERROR, GitRepo |
62 | 62 | from cecli.repomap import RepoMap |
63 | 63 | from cecli.report import update_error_prefix |
64 | | -from cecli.run_cmd import run_cmd |
| 64 | +from cecli.run_cmd import run_cmd_async |
65 | 65 | from cecli.sessions import SessionManager |
66 | 66 | from cecli.tools.utils.output import print_tool_response |
67 | 67 | from cecli.tools.utils.registry import ToolRegistry |
@@ -600,7 +600,9 @@ def __init__( |
600 | 600 | self.files_edited_by_tools = set() |
601 | 601 |
|
602 | 602 | # Linting and testing |
603 | | - self.linter = Linter(root=self.root, encoding=io.encoding) |
| 603 | + self.linter = Linter( |
| 604 | + root=self.root, encoding=io.encoding, interrupt_event=self.interrupt_event |
| 605 | + ) |
604 | 606 | self.auto_lint = auto_lint |
605 | 607 | self.setup_lint_cmds(lint_cmds) |
606 | 608 | self.lint_cmds = lint_cmds |
@@ -2242,7 +2244,15 @@ async def send_message(self, inp): |
2242 | 2244 | import asyncio |
2243 | 2245 |
|
2244 | 2246 | loop = asyncio.get_running_loop() |
2245 | | - result = await loop.run_in_executor(None, self.format_messages) |
| 2247 | + |
| 2248 | + async def format_in_executor(): |
| 2249 | + return await loop.run_in_executor(None, self.format_messages) |
| 2250 | + |
| 2251 | + result, interrupted = await self.coroutines.interruptible( |
| 2252 | + format_in_executor(), self.interrupt_event |
| 2253 | + ) |
| 2254 | + if interrupted: |
| 2255 | + raise KeyboardInterrupt("Interrupted during message formatting") |
2246 | 2256 | messages = result |
2247 | 2257 |
|
2248 | 2258 | if not await self.check_tokens(messages): |
@@ -2451,7 +2461,10 @@ async def send_message(self, inp): |
2451 | 2461 | return |
2452 | 2462 |
|
2453 | 2463 | if edited and self.auto_lint: |
2454 | | - lint_errors = self.lint_edited(edited) |
| 2464 | + lint_errors = await self.lint_edited(edited) |
| 2465 | + if lint_errors is None: # Interrupted |
| 2466 | + return |
| 2467 | + |
2455 | 2468 | await self.auto_commit(edited, context="Ran the linter") |
2456 | 2469 | self.lint_outcome = not lint_errors |
2457 | 2470 | if lint_errors: |
@@ -2952,12 +2965,16 @@ async def show_exhausted_error(self): |
2952 | 2965 | self.io.tool_error(res) |
2953 | 2966 | await self.io.offer_url(urls.token_limits) |
2954 | 2967 |
|
2955 | | - def lint_edited(self, fnames, show_output=True): |
| 2968 | + async def lint_edited(self, fnames, show_output=True): |
2956 | 2969 | res = "" |
2957 | 2970 | for fname in fnames: |
2958 | 2971 | if not fname: |
2959 | 2972 | continue |
2960 | | - errors = self.linter.lint(self.abs_root_path(fname)) |
| 2973 | + try: |
| 2974 | + errors = await self.linter.lint(self.abs_root_path(fname)) |
| 2975 | + except asyncio.CancelledError: |
| 2976 | + self.io.tool_warning("Linting interrupted.") |
| 2977 | + return None |
2961 | 2978 |
|
2962 | 2979 | if errors: |
2963 | 2980 | res += "\n" |
@@ -3276,122 +3293,127 @@ async def show_send_output_stream(self, completion): |
3276 | 3293 | received_content = False |
3277 | 3294 | chunk_index = 0 |
3278 | 3295 |
|
3279 | | - async for chunk in completion: |
3280 | | - if self.args.debug: |
3281 | | - with open(".cecli/logs/chunks.log", "a") as f: |
3282 | | - print(chunk, file=f) |
| 3296 | + try: |
| 3297 | + async for chunk in coroutines.interruptible_async_generator( |
| 3298 | + completion, self.interrupt_event |
| 3299 | + ): |
| 3300 | + if self.args.debug: |
| 3301 | + with open(".cecli/logs/chunks.log", "a") as f: |
| 3302 | + print(chunk, file=f) |
3283 | 3303 |
|
3284 | | - # Check if confirmation is in progress and wait if needed |
3285 | | - if not self.io.confirmation_in_progress_event.is_set(): |
3286 | | - await self.io.confirmation_in_progress_event.wait() |
| 3304 | + # Check if confirmation is in progress and wait if needed |
| 3305 | + if not self.io.confirmation_in_progress_event.is_set(): |
| 3306 | + await self.io.confirmation_in_progress_event.wait() |
3287 | 3307 |
|
3288 | | - if isinstance(chunk, str): |
3289 | | - self.io.tool_error(chunk) |
3290 | | - continue |
3291 | | - else: |
3292 | | - if len(chunk.choices) == 0: |
| 3308 | + if isinstance(chunk, str): |
| 3309 | + self.io.tool_error(chunk) |
3293 | 3310 | continue |
| 3311 | + else: |
| 3312 | + if len(chunk.choices) == 0: |
| 3313 | + continue |
3294 | 3314 |
|
3295 | | - if ( |
3296 | | - hasattr(chunk.choices[0], "finish_reason") |
3297 | | - and chunk.choices[0].finish_reason == "length" |
3298 | | - ): |
3299 | | - raise FinishReasonLength() |
3300 | | - |
3301 | | - try: |
3302 | | - if chunk.choices[0].delta.tool_calls: |
3303 | | - received_content = True |
3304 | | - self.token_profiler.on_token() |
3305 | | - for tool_call_chunk in chunk.choices[0].delta.tool_calls: |
3306 | | - self.tool_reflection = True |
3307 | | - |
3308 | | - if tool_call_chunk.type: |
3309 | | - self.io.update_spinner_suffix(tool_call_chunk.type) |
| 3315 | + if ( |
| 3316 | + hasattr(chunk.choices[0], "finish_reason") |
| 3317 | + and chunk.choices[0].finish_reason == "length" |
| 3318 | + ): |
| 3319 | + raise FinishReasonLength() |
3310 | 3320 |
|
3311 | | - if tool_call_chunk.function: |
3312 | | - if tool_call_chunk.function.name: |
3313 | | - self.io.update_spinner_suffix(tool_call_chunk.function.name) |
| 3321 | + try: |
| 3322 | + if chunk.choices[0].delta.tool_calls: |
| 3323 | + received_content = True |
| 3324 | + self.token_profiler.on_token() |
| 3325 | + for tool_call_chunk in chunk.choices[0].delta.tool_calls: |
| 3326 | + self.tool_reflection = True |
3314 | 3327 |
|
3315 | | - if tool_call_chunk.function.arguments: |
3316 | | - self.io.update_spinner_suffix( |
3317 | | - tool_call_chunk.function.arguments |
3318 | | - ) |
| 3328 | + if tool_call_chunk.type: |
| 3329 | + self.io.update_spinner_suffix(tool_call_chunk.type) |
3319 | 3330 |
|
3320 | | - except (AttributeError, IndexError): |
3321 | | - # Handle cases where the response structure doesn't match expectations |
3322 | | - pass |
| 3331 | + if tool_call_chunk.function: |
| 3332 | + if tool_call_chunk.function.name: |
| 3333 | + self.io.update_spinner_suffix(tool_call_chunk.function.name) |
3323 | 3334 |
|
3324 | | - try: |
3325 | | - func = chunk.choices[0].delta.function_call |
3326 | | - # dump(func) |
3327 | | - if func: |
3328 | | - for k, v in func.items(): |
3329 | | - self.tool_reflection = True |
3330 | | - self.io.update_spinner_suffix(v) |
3331 | | - |
3332 | | - received_content = True |
3333 | | - self.token_profiler.on_token() |
3334 | | - except AttributeError: |
3335 | | - pass |
| 3335 | + if tool_call_chunk.function.arguments: |
| 3336 | + self.io.update_spinner_suffix( |
| 3337 | + tool_call_chunk.function.arguments |
| 3338 | + ) |
3336 | 3339 |
|
3337 | | - text = "" |
| 3340 | + except (AttributeError, IndexError): |
| 3341 | + # Handle cases where the response structure doesn't match expectations |
| 3342 | + pass |
3338 | 3343 |
|
3339 | | - try: |
3340 | | - reasoning_content = chunk.choices[0].delta.reasoning_content |
3341 | | - except AttributeError: |
3342 | 3344 | try: |
3343 | | - reasoning_content = chunk.choices[0].delta.reasoning |
| 3345 | + func = chunk.choices[0].delta.function_call |
| 3346 | + # dump(func) |
| 3347 | + if func: |
| 3348 | + for k, v in func.items(): |
| 3349 | + self.tool_reflection = True |
| 3350 | + self.io.update_spinner_suffix(v) |
| 3351 | + |
| 3352 | + received_content = True |
| 3353 | + self.token_profiler.on_token() |
3344 | 3354 | except AttributeError: |
3345 | | - reasoning_content = None |
| 3355 | + pass |
3346 | 3356 |
|
3347 | | - if reasoning_content: |
3348 | | - if nested.getter(self.args, "show_thinking"): |
3349 | | - if not self.got_reasoning_content: |
3350 | | - text += f"<{REASONING_TAG}>\n\n" |
3351 | | - text += reasoning_content |
3352 | | - self.got_reasoning_content = True |
3353 | | - received_content = True |
3354 | | - self.token_profiler.on_token() |
3355 | | - self.io.update_spinner_suffix(reasoning_content) |
3356 | | - self.partial_response_reasoning_content += reasoning_content |
| 3357 | + text = "" |
3357 | 3358 |
|
3358 | | - try: |
3359 | | - content = chunk.choices[0].delta.content |
3360 | | - if content: |
3361 | | - if self.got_reasoning_content and not self.ended_reasoning_content: |
3362 | | - text += f"\n\n</{self.reasoning_tag_name}>\n\n" |
3363 | | - self.ended_reasoning_content = True |
3364 | | - |
3365 | | - text += content |
3366 | | - received_content = True |
| 3359 | + try: |
| 3360 | + reasoning_content = chunk.choices[0].delta.reasoning_content |
| 3361 | + except AttributeError: |
| 3362 | + try: |
| 3363 | + reasoning_content = chunk.choices[0].delta.reasoning |
| 3364 | + except AttributeError: |
| 3365 | + reasoning_content = None |
| 3366 | + |
| 3367 | + if reasoning_content: |
| 3368 | + if nested.getter(self.args, "show_thinking"): |
| 3369 | + if not self.got_reasoning_content: |
| 3370 | + text += f"<{REASONING_TAG}>\n\n" |
| 3371 | + text += reasoning_content |
| 3372 | + self.got_reasoning_content = True |
| 3373 | + received_content = True |
3367 | 3374 | self.token_profiler.on_token() |
3368 | | - self.io.update_spinner_suffix(content) |
3369 | | - except AttributeError: |
3370 | | - pass |
| 3375 | + self.io.update_spinner_suffix(reasoning_content) |
| 3376 | + self.partial_response_reasoning_content += reasoning_content |
| 3377 | + |
| 3378 | + try: |
| 3379 | + content = chunk.choices[0].delta.content |
| 3380 | + if content: |
| 3381 | + if self.got_reasoning_content and not self.ended_reasoning_content: |
| 3382 | + text += f"\n\n</{self.reasoning_tag_name}>\n\n" |
| 3383 | + self.ended_reasoning_content = True |
| 3384 | + |
| 3385 | + text += content |
| 3386 | + received_content = True |
| 3387 | + self.token_profiler.on_token() |
| 3388 | + self.io.update_spinner_suffix(content) |
| 3389 | + except AttributeError: |
| 3390 | + pass |
3371 | 3391 |
|
3372 | | - self.partial_response_content += text |
| 3392 | + self.partial_response_content += text |
3373 | 3393 |
|
3374 | | - chunk_index += 1 |
3375 | | - chunk._hidden_params["created_at"] = chunk_index |
3376 | | - self.partial_response_chunks.append(chunk) |
| 3394 | + chunk_index += 1 |
| 3395 | + chunk._hidden_params["created_at"] = chunk_index |
| 3396 | + self.partial_response_chunks.append(chunk) |
3377 | 3397 |
|
3378 | | - if self.show_pretty(): |
3379 | | - # Use simplified streaming - just call the method with full content |
3380 | | - content_to_show = self.live_incremental_response(False) |
3381 | | - self.stream_wrapper(content_to_show, final=False) |
3382 | | - elif text: |
3383 | | - # Apply reasoning tag formatting for non-pretty output |
3384 | | - if nested.getter(self.args, "show_thinking"): |
3385 | | - text = replace_reasoning_tags(text, self.reasoning_tag_name) |
3386 | | - try: |
3387 | | - self.stream_wrapper(text, final=False) |
3388 | | - except UnicodeEncodeError: |
3389 | | - # Safely encode and decode the text |
3390 | | - safe_text = text.encode(sys.stdout.encoding, errors="backslashreplace").decode( |
3391 | | - sys.stdout.encoding |
3392 | | - ) |
3393 | | - self.stream_wrapper(safe_text, final=False) |
3394 | | - yield text |
| 3398 | + if self.show_pretty(): |
| 3399 | + # Use simplified streaming - just call the method with full content |
| 3400 | + content_to_show = self.live_incremental_response(False) |
| 3401 | + self.stream_wrapper(content_to_show, final=False) |
| 3402 | + elif text: |
| 3403 | + # Apply reasoning tag formatting for non-pretty output |
| 3404 | + if nested.getter(self.args, "show_thinking"): |
| 3405 | + text = replace_reasoning_tags(text, self.reasoning_tag_name) |
| 3406 | + try: |
| 3407 | + self.stream_wrapper(text, final=False) |
| 3408 | + except UnicodeEncodeError: |
| 3409 | + # Safely encode and decode the text |
| 3410 | + safe_text = text.encode( |
| 3411 | + sys.stdout.encoding, errors="backslashreplace" |
| 3412 | + ).decode(sys.stdout.encoding) |
| 3413 | + self.stream_wrapper(safe_text, final=False) |
| 3414 | + yield text |
| 3415 | + except (asyncio.CancelledError, KeyboardInterrupt): |
| 3416 | + raise KeyboardInterrupt |
3395 | 3417 |
|
3396 | 3418 | # The Part Doing the Heavy Lifting Now |
3397 | 3419 | self.consolidate_chunks() |
@@ -4226,8 +4248,10 @@ async def handle_shell_commands(self, commands_str, group): |
4226 | 4248 | self.io.tool_output(f"Running {command}") |
4227 | 4249 | # Add the command to input history |
4228 | 4250 | # self.io.add_to_input_history(f"/run {command.strip()}") |
4229 | | - exit_status, output = await asyncio.to_thread( |
4230 | | - run_cmd, command, error_print=self.io.tool_error, cwd=self.root |
| 4251 | + exit_status, output = await run_cmd_async( |
| 4252 | + command, |
| 4253 | + self.interrupt_event, |
| 4254 | + cwd=self.root, |
4231 | 4255 | ) |
4232 | 4256 |
|
4233 | 4257 | if output: |
|
0 commit comments