kiwi-code 0.0.442__tar.gz → 0.0.443__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/PKG-INFO +1 -1
  2. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/pyproject.toml +1 -1
  3. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_cli/__init__.py +1 -1
  4. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_runtime/__init__.py +1 -1
  5. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_runtime/main.py +283 -32
  6. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/__init__.py +1 -1
  7. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/uv.lock +1 -1
  8. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/.github/workflows/publish.yml +0 -0
  9. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/.github/workflows/test.yml +0 -0
  10. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/.gitignore +0 -0
  11. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/.python-version +0 -0
  12. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/CLAUDE.md +0 -0
  13. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/Makefile +0 -0
  14. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/README.md +0 -0
  15. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_cli/auth.py +0 -0
  16. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_cli/checkpoints.py +0 -0
  17. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_cli/cli.py +0 -0
  18. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_cli/client.py +0 -0
  19. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_cli/commands.py +0 -0
  20. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_cli/logger.py +0 -0
  21. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_cli/models.py +0 -0
  22. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_cli/runtime_manager.py +0 -0
  23. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_cli/server.py +0 -0
  24. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_cli/terminal_mode.py +0 -0
  25. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_runtime/__main__.py +0 -0
  26. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/inline_file_picker.py +0 -0
  27. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/main.py +0 -0
  28. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/random_words.py +0 -0
  29. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/runtime_agent.py +0 -0
  30. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/screens/__init__.py +0 -0
  31. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/screens/attach_content.py +0 -0
  32. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/screens/command_result.py +0 -0
  33. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/screens/dashboard.py +0 -0
  34. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/screens/detach_files.py +0 -0
  35. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/screens/file_browser.py +0 -0
  36. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/screens/help.py +0 -0
  37. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/screens/id_picker.py +0 -0
  38. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/screens/login.py +0 -0
  39. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
  40. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  41. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/screens/slash_picker.py +0 -0
  42. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/screens/term_dashboard.py +0 -0
  43. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/slash_commands.py +0 -0
  44. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/status_words.py +0 -0
  45. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/term_app.py +0 -0
  46. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/widgets.py +0 -0
  47. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/src/kiwi_tui/worktrees.py +0 -0
  48. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/test_hello.py +0 -0
  49. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/__init__.py +0 -0
  50. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/conftest.py +0 -0
  51. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_batch_fs_tool.py +0 -0
  52. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_checkpoints.py +0 -0
  53. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_cli_help.py +0 -0
  54. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_copy_path_fs_tool.py +0 -0
  55. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_imports.py +0 -0
  56. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_read_file_streaming.py +0 -0
  57. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_reexec_kiwi.py +0 -0
  58. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_runtime_log_trimming.py +0 -0
  59. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_search_in_files_fs_tool.py +0 -0
  60. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_slash_commands.py +0 -0
  61. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_term_dashboard_ui.py +0 -0
  62. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_terminal_mode.py +0 -0
  63. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_tokens.py +0 -0
  64. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_tui_headless.py +0 -0
  65. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_tui_interactive_runtime.py +0 -0
  66. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_tui_palette.py +0 -0
  67. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_unified_diff.py +0 -0
  68. {kiwi_code-0.0.442 → kiwi_code-0.0.443}/tests/test_worktrees.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwi-code
3
- Version: 0.0.442
3
+ Version: 0.0.443
4
4
  Summary: A textual-based terminal user interface application
5
5
  Project-URL: Homepage, https://meetkiwi.ai
6
6
  Project-URL: Repository, https://github.com/jetoslabs/kiwi-code
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiwi-code"
3
- version = "0.0.442"
3
+ version = "0.0.443"
4
4
  description = "A textual-based terminal user interface application"
5
5
  readme = {file = "README.md", content-type = "text/markdown"}
6
6
  requires-python = ">=3.11,<4.0"
@@ -1,3 +1,3 @@
1
1
  """Kiwi CLI - command-line interface and shared infrastructure modules."""
2
2
 
3
- __version__ = "0.0.442"
3
+ __version__ = "0.0.443"
@@ -1,3 +1,3 @@
1
1
  """Kiwi Runtime — terminal agent that connects to the server via WebSocket."""
2
2
 
3
- __version__ = "0.0.442"
3
+ __version__ = "0.0.443"
@@ -12,6 +12,7 @@ Usage:
12
12
  kiwi connect --server dev --scope full
13
13
  kiwi connect --server dev --allow /path/to/extra/dir
14
14
  """
15
+ from collections import deque
15
16
  from datetime import datetime
16
17
  import argparse
17
18
  import asyncio
@@ -24,6 +25,8 @@ import shlex
24
25
  import signal
25
26
  import subprocess
26
27
  import sys
28
+ import random
29
+ import time
27
30
  from pathlib import Path
28
31
  from typing import Any
29
32
  import shutil
@@ -3243,6 +3246,7 @@ async def run_command(
3243
3246
  if not ok:
3244
3247
  return {"stdout": "", "stderr": reason, "exit_code": 1}
3245
3248
 
3249
+ proc: asyncio.subprocess.Process | None = None
3246
3250
  try:
3247
3251
  cwd = allowed_dirs[0] if allowed_dirs else None
3248
3252
  proc = await asyncio.create_subprocess_shell(
@@ -3258,10 +3262,33 @@ async def run_command(
3258
3262
  stderr = stderr_bytes.decode("utf-8", errors="replace")[:MAX_OUTPUT_BYTES]
3259
3263
  return {"stdout": stdout, "stderr": stderr, "exit_code": proc.returncode}
3260
3264
  except asyncio.TimeoutError:
3261
- proc.kill()
3262
- await proc.wait()
3265
+ if proc and proc.returncode is None:
3266
+ try:
3267
+ proc.kill()
3268
+ except ProcessLookupError:
3269
+ pass
3270
+ try:
3271
+ await proc.wait()
3272
+ except Exception:
3273
+ pass
3263
3274
  return {"stdout": "", "stderr": f"Command timed out after {timeout}s", "exit_code": -1}
3275
+ except asyncio.CancelledError:
3276
+ if proc and proc.returncode is None:
3277
+ try:
3278
+ proc.kill()
3279
+ except ProcessLookupError:
3280
+ pass
3281
+ try:
3282
+ await proc.wait()
3283
+ except Exception:
3284
+ pass
3285
+ raise
3264
3286
  except Exception as e:
3287
+ if proc and proc.returncode is None:
3288
+ try:
3289
+ proc.kill()
3290
+ except Exception:
3291
+ pass
3265
3292
  return {"stdout": "", "stderr": str(e), "exit_code": -1}
3266
3293
 
3267
3294
 
@@ -4199,16 +4226,152 @@ async def connect(
4199
4226
  allowed_dirs: list[str] | None = None,
4200
4227
  agent_id: str | None = None,
4201
4228
  consumer_id: str | None = None,
4229
+ *,
4230
+ max_retries: int = 12,
4231
+ ):
4232
+ ws_endpoint = f"{ws_url}/v1/terminal/ws/connect"
4233
+ retries = 0
4234
+
4235
+ # Shared state across reconnect attempts.
4236
+ pending_results: deque[dict[str, Any]] = deque()
4237
+ pending_lock = asyncio.Lock()
4238
+ pending_event = asyncio.Event()
4239
+
4240
+ command_queue: asyncio.Queue[tuple[str, str]] = asyncio.Queue(maxsize=64)
4241
+ cmd_state: dict[str, Any] = {"running": False, "command": None}
4242
+
4243
+ async def _command_worker() -> None:
4244
+ while True:
4245
+ request_id, command = await command_queue.get()
4246
+ result_msg: dict[str, Any] | None = None
4247
+ cmd_state["running"] = True
4248
+ cmd_state["command"] = command
4249
+ try:
4250
+ result = await run_command(
4251
+ command,
4252
+ mode=mode,
4253
+ allowed_dirs=allowed_dirs,
4254
+ )
4255
+ exit_code = 0
4256
+ try:
4257
+ exit_code = int(result.get("exit_code", 0))
4258
+ except Exception:
4259
+ exit_code = 0
4260
+ success = exit_code == 0
4261
+ status_msg = (
4262
+ f"{GREEN}OK{RESET}"
4263
+ if success
4264
+ else f"{RED}FAILED (exit {exit_code}){RESET}"
4265
+ )
4266
+ print_cmd_log(request_id, status_msg, success)
4267
+ result_msg = {
4268
+ "type": "result",
4269
+ "request_id": request_id,
4270
+ **result,
4271
+ }
4272
+ except asyncio.CancelledError:
4273
+ # Cancellation = runtime is shutting down. Let it propagate after cleanup.
4274
+ raise
4275
+ except Exception as e:
4276
+ print_cmd_log(request_id, f"{RED}FAILED{RESET}", False)
4277
+ result_msg = {
4278
+ "type": "result",
4279
+ "request_id": request_id,
4280
+ "stdout": "",
4281
+ "stderr": str(e),
4282
+ "exit_code": -1,
4283
+ }
4284
+ finally:
4285
+ cmd_state["running"] = False
4286
+ cmd_state["command"] = None
4287
+ try:
4288
+ command_queue.task_done()
4289
+ except Exception:
4290
+ pass
4291
+ if result_msg is not None:
4292
+ async with pending_lock:
4293
+ pending_results.append(result_msg)
4294
+ pending_event.set()
4295
+
4296
+ command_worker_task = asyncio.create_task(_command_worker())
4297
+ try:
4298
+ while True:
4299
+ attempt = retries + 1
4300
+ total_attempts = max_retries + 1
4301
+ print_status(
4302
+ "~",
4303
+ f"Connecting to {BOLD}{ws_endpoint}{RESET} {GREY}(attempt {attempt}/{total_attempts}){RESET}",
4304
+ C,
4305
+ )
4306
+ try:
4307
+ await _connect_once(
4308
+ ws_url,
4309
+ token,
4310
+ http_base_url=http_base_url,
4311
+ mode=mode,
4312
+ allowed_dirs=allowed_dirs,
4313
+ agent_id=agent_id,
4314
+ consumer_id=consumer_id,
4315
+ command_queue=command_queue,
4316
+ pending_results=pending_results,
4317
+ pending_lock=pending_lock,
4318
+ pending_event=pending_event,
4319
+ cmd_state=cmd_state,
4320
+ )
4321
+ return
4322
+ except asyncio.CancelledError:
4323
+ raise
4324
+ except websockets.exceptions.ConnectionClosed as e:
4325
+ print()
4326
+ print_status("!", f"Connection closed: {e}", YELLOW)
4327
+ except ConnectionRefusedError:
4328
+ print()
4329
+ print_status("x", f"Could not connect to {ws_endpoint}. Is the server running?", RED)
4330
+ except Exception as e:
4331
+ print()
4332
+ print_status("x", f"Error: {e}", RED)
4333
+
4334
+ if retries >= max_retries:
4335
+ print_status("x", f"Max reconnect attempts reached ({max_retries}).", RED)
4336
+ return
4337
+
4338
+ retries += 1
4339
+ base_delay = min(30.0, 1.5 * (2 ** (retries - 1)))
4340
+ delay = base_delay + random.uniform(0.0, 0.6)
4341
+ print_status(
4342
+ "~",
4343
+ f"Reconnecting in {delay:.1f}s... {GREY}(retry {retries}/{max_retries}){RESET}",
4344
+ YELLOW,
4345
+ )
4346
+ await asyncio.sleep(delay)
4347
+ finally:
4348
+ if command_worker_task and not command_worker_task.done():
4349
+ command_worker_task.cancel()
4350
+ await asyncio.gather(command_worker_task, return_exceptions=True)
4351
+
4352
+
4353
+ async def _connect_once(
4354
+ ws_url: str,
4355
+ token: str,
4356
+ http_base_url: str,
4357
+ mode: str = "restricted",
4358
+ allowed_dirs: list[str] | None = None,
4359
+ agent_id: str | None = None,
4360
+ consumer_id: str | None = None,
4361
+ *,
4362
+ command_queue: asyncio.Queue[tuple[str, str]],
4363
+ pending_results: deque[dict[str, Any]],
4364
+ pending_lock: asyncio.Lock,
4365
+ pending_event: asyncio.Event,
4366
+ cmd_state: dict[str, Any],
4202
4367
  ):
4203
- """Connect to the server WebSocket and process commands."""
4204
4368
  ws_endpoint = f"{ws_url}/v1/terminal/ws/connect"
4205
- print_status("~", f"Connecting to {BOLD}{ws_endpoint}{RESET}", C)
4206
4369
  background_tasks: set[asyncio.Task] = set()
4207
4370
  try:
4208
4371
  async with websockets.connect(
4209
4372
  ws_endpoint,
4210
4373
  ping_interval=20,
4211
- ping_timeout=60,
4374
+ ping_timeout=90,
4212
4375
  close_timeout=10,
4213
4376
  max_queue=64,
4214
4377
  ) as ws:
@@ -4262,34 +4425,114 @@ async def connect(
4262
4425
  chunked_writes: dict[str, ChunkedWriteState] = {}
4263
4426
  paired_msg_count = 0
4264
4427
  fs_message_types = { "read_file","write_file","append_file","replace_in_file","stat_file","list_dir","mkdir","move_path","copy_path", "delete_path","apply_unified_diff","replace_line_range","insert_at_line","search_in_file","search_in_files","batch","file_write_start","file_write_chunk","file_write_finish","file_write_abort"}
4428
+
4429
+ hb_debug_env = os.environ.get("KIWI_RUNTIME_DEBUG_HEARTBEAT", "").strip().lower()
4430
+ hb_debug = hb_debug_env not in ("", "0", "false", "no")
4431
+ hb_log_every_s = 5.0
4432
+ try:
4433
+ hb_log_every_s = float(os.environ.get("KIWI_RUNTIME_DEBUG_HEARTBEAT_INTERVAL", "5") or "5")
4434
+ except Exception:
4435
+ hb_log_every_s = 5.0
4436
+
4437
+ hb_ws_ping_env = os.environ.get("KIWI_RUNTIME_DEBUG_HEARTBEAT_WS_PING", "").strip().lower()
4438
+ hb_ws_ping = hb_ws_ping_env not in ("", "0", "false", "no")
4439
+ hb_ws_ping_timeout_s = 5.0
4440
+ try:
4441
+ hb_ws_ping_timeout_s = float(os.environ.get("KIWI_RUNTIME_DEBUG_HEARTBEAT_WS_PING_TIMEOUT", "5") or "5")
4442
+ except Exception:
4443
+ hb_ws_ping_timeout_s = 5.0
4444
+
4445
+ heartbeat_debug_task: asyncio.Task | None = None
4446
+ pending_sender_task: asyncio.Task | None = None
4447
+
4448
+ async def _pending_sender_loop() -> None:
4449
+ while True:
4450
+ item: dict[str, Any] | None = None
4451
+ async with pending_lock:
4452
+ if pending_results:
4453
+ item = pending_results.popleft()
4454
+
4455
+ if item is None:
4456
+ pending_event.clear()
4457
+ async with pending_lock:
4458
+ if pending_results:
4459
+ continue
4460
+ await pending_event.wait()
4461
+ continue
4462
+ try:
4463
+ await ws.send(json.dumps(item))
4464
+ except asyncio.CancelledError:
4465
+ raise
4466
+ except Exception:
4467
+ async with pending_lock:
4468
+ pending_results.appendleft(item)
4469
+ pending_event.set()
4470
+ raise
4471
+
4472
+ pending_sender_task = asyncio.create_task(_pending_sender_loop())
4473
+
4474
+ async def _heartbeat_debug_loop() -> None:
4475
+ while True:
4476
+ await asyncio.sleep(max(0.2, hb_log_every_s))
4477
+ if not bool(cmd_state.get("running")):
4478
+ continue
4479
+ qsize = 0
4480
+ try:
4481
+ qsize = command_queue.qsize()
4482
+ except Exception:
4483
+ qsize = 0
4484
+ msg = f"Heartbeat(debug) | busy=True queue={qsize}"
4485
+ cmd = cmd_state.get("command")
4486
+ if cmd:
4487
+ cleaned = str(cmd).strip().replace("\n", " ")
4488
+ if len(cleaned) > 60:
4489
+ cleaned = cleaned[:57] + "..."
4490
+ msg += f" | cmd={cleaned}"
4491
+ if hb_ws_ping:
4492
+ try:
4493
+ waiter = await ws.ping()
4494
+ await asyncio.wait_for(waiter, timeout=hb_ws_ping_timeout_s)
4495
+ msg += " | ws_pong=ok"
4496
+ except Exception:
4497
+ msg += " | ws_pong=fail"
4498
+ print_status("·", msg, GREY)
4499
+
4500
+ if hb_debug:
4501
+ heartbeat_debug_task = asyncio.create_task(_heartbeat_debug_loop())
4502
+
4265
4503
  try:
4266
4504
  async for message in ws:
4267
4505
  msg = json.loads(message)
4268
4506
  msg_type = msg.get("type")
4269
4507
 
4508
+ if pending_sender_task and pending_sender_task.done():
4509
+ try:
4510
+ exc = pending_sender_task.exception()
4511
+ except asyncio.CancelledError:
4512
+ exc = None
4513
+ if exc:
4514
+ raise exc
4515
+ raise RuntimeError("Pending sender exited unexpectedly")
4516
+
4517
+
4270
4518
  if msg_type == "command":
4271
4519
  request_id = msg.get("request_id", "")
4272
4520
  command = msg.get("command", "")
4273
4521
  print_cmd_log(request_id, f"$ {BOLD}{command}{RESET}")
4274
4522
 
4275
- result = await run_command(
4276
- command,
4277
- mode=mode,
4278
- allowed_dirs=allowed_dirs,
4279
- )
4280
- exit_code = result["exit_code"]
4281
- success = exit_code == 0
4282
- status_text = (
4283
- f"{GREEN}OK{RESET}" if success
4284
- else f"{RED}FAILED (exit {exit_code}){RESET}"
4285
- )
4286
- print_cmd_log(request_id, status_text, success)
4523
+ try:
4524
+ command_queue.put_nowait((str(request_id), str(command)))
4525
+ except asyncio.QueueFull:
4526
+ err = "Runtime command queue full; please retry."
4527
+ print_cmd_log(request_id, f"{RED}FAILED{RESET}", False)
4528
+ await ws.send(json.dumps({
4529
+ "type": "result",
4530
+ "request_id": request_id,
4531
+ "stdout": "",
4532
+ "stderr": err,
4533
+ "exit_code": 1,
4534
+ }))
4287
4535
 
4288
- await ws.send(json.dumps({
4289
- "type": "result",
4290
- "request_id": request_id,
4291
- **result,
4292
- }))
4293
4536
 
4294
4537
  elif msg_type == "interactive":
4295
4538
  request_id = msg.get("request_id", "")
@@ -4304,7 +4547,12 @@ async def connect(
4304
4547
  )
4305
4548
  exit_code = result["exit_code"]
4306
4549
  success = exit_code == 0
4307
- print_cmd_log(request_id, f"{'OK' if success else f'FAILED (exit {exit_code})'}", success)
4550
+ status_msg = (
4551
+ f"{GREEN}OK{RESET}"
4552
+ if success
4553
+ else f"{RED}FAILED (exit {exit_code}){RESET}"
4554
+ )
4555
+ print_cmd_log(request_id, status_msg, success)
4308
4556
 
4309
4557
  await ws.send(json.dumps({
4310
4558
  "type": "result",
@@ -4557,9 +4805,17 @@ async def connect(
4557
4805
  elif msg_type == "ping":
4558
4806
  await ws.send(json.dumps({"type": "pong"}))
4559
4807
 
4808
+
4560
4809
  else:
4561
4810
  print_status("?", f"Unknown message: {msg_type}", YELLOW)
4562
4811
  finally:
4812
+ if heartbeat_debug_task and not heartbeat_debug_task.done():
4813
+ heartbeat_debug_task.cancel()
4814
+ await asyncio.gather(heartbeat_debug_task, return_exceptions=True)
4815
+
4816
+ if pending_sender_task and not pending_sender_task.done():
4817
+ pending_sender_task.cancel()
4818
+ await asyncio.gather(pending_sender_task, return_exceptions=True)
4563
4819
  bg_tasks = list(background_tasks)
4564
4820
  for task in bg_tasks:
4565
4821
  task.cancel()
@@ -4584,15 +4840,10 @@ async def connect(
4584
4840
  except Exception:
4585
4841
  pass
4586
4842
 
4587
- except websockets.exceptions.ConnectionClosed as e:
4588
- print()
4589
- print_status("!", f"Connection closed: {e}", YELLOW)
4590
- except ConnectionRefusedError:
4591
- print()
4592
- print_status("x", f"Could not connect to {ws_endpoint}. Is the server running?", RED)
4593
- except Exception as e:
4594
- print()
4595
- print_status("x", f"Error: {e}", RED)
4843
+ except asyncio.CancelledError:
4844
+ raise
4845
+ except Exception:
4846
+ raise
4596
4847
 
4597
4848
 
4598
4849
  def main():
@@ -1,3 +1,3 @@
1
1
  """Autobots TUI - A textual-based terminal user interface."""
2
2
 
3
- __version__ = "0.0.442"
3
+ __version__ = "0.0.443"
@@ -396,7 +396,7 @@ wheels = [
396
396
 
397
397
  [[package]]
398
398
  name = "kiwi-code"
399
- version = "0.0.442"
399
+ version = "0.0.443"
400
400
  source = { editable = "." }
401
401
  dependencies = [
402
402
  { name = "autobots-client" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes