kiwi-code 0.0.18__tar.gz → 0.0.20__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 (48) hide show
  1. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/PKG-INFO +1 -1
  2. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/pyproject.toml +1 -1
  3. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_runtime/main.py +67 -9
  4. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/runtime_agent.py +5 -0
  5. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/dashboard.py +51 -35
  6. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/uv.lock +1 -1
  7. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/.github/workflows/publish.yml +0 -0
  8. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/.github/workflows/test.yml +0 -0
  9. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/.gitignore +0 -0
  10. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/.python-version +0 -0
  11. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/CLAUDE.md +0 -0
  12. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/Makefile +0 -0
  13. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/README.md +0 -0
  14. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/__init__.py +0 -0
  15. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/auth.py +0 -0
  16. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/cli.py +0 -0
  17. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/client.py +0 -0
  18. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/commands.py +0 -0
  19. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/config.py +0 -0
  20. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/logger.py +0 -0
  21. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/models.py +0 -0
  22. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/runtime_manager.py +0 -0
  23. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_runtime/__init__.py +0 -0
  24. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_runtime/__main__.py +0 -0
  25. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_runtime/snake_game/.gitignore +0 -0
  26. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
  27. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/__init__.py +0 -0
  28. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/inline_file_picker.py +0 -0
  29. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/main.py +0 -0
  30. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/__init__.py +0 -0
  31. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/attach_content.py +0 -0
  32. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/command_result.py +0 -0
  33. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/file_browser.py +0 -0
  34. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/id_picker.py +0 -0
  35. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/login.py +0 -0
  36. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
  37. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  38. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/slash_picker.py +0 -0
  39. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/widgets.py +0 -0
  40. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/test_hello.py +0 -0
  41. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/__init__.py +0 -0
  42. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/conftest.py +0 -0
  43. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/test_cli_help.py +0 -0
  44. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/test_config.py +0 -0
  45. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/test_imports.py +0 -0
  46. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/test_reexec_kiwi.py +0 -0
  47. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/test_tokens.py +0 -0
  48. {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/test_tui_headless.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwi-code
3
- Version: 0.0.18
3
+ Version: 0.0.20
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.18"
3
+ version = "0.0.20"
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"
@@ -1039,9 +1039,66 @@ def _render_block_text(text: str) -> list[str]:
1039
1039
  return rows
1040
1040
 
1041
1041
 
1042
+
1043
+ def _stdout_encoding() -> str:
1044
+ enc = getattr(sys.stdout, "encoding", None)
1045
+ return enc or "utf-8"
1046
+
1047
+
1048
+ def _can_encode(text: str) -> bool:
1049
+ try:
1050
+ text.encode(_stdout_encoding())
1051
+ return True
1052
+ except Exception:
1053
+ return False
1054
+
1055
+
1056
+ def _safe_text(text: str) -> str:
1057
+ enc = _stdout_encoding()
1058
+ try:
1059
+ text.encode(enc)
1060
+ return text
1061
+ except Exception:
1062
+ return text.encode(enc, errors="replace").decode(enc, errors="replace")
1063
+
1064
+
1065
+ def _safe_print(text: str = "") -> None:
1066
+ try:
1067
+ print(text)
1068
+ except UnicodeEncodeError:
1069
+ print(_safe_text(text))
1070
+
1071
+
1072
+ def _configure_stdio() -> None:
1073
+ """Best-effort: avoid UnicodeEncodeError on misconfigured terminals (esp. Windows)."""
1074
+ for stream in (sys.stdout, sys.stderr):
1075
+ try:
1076
+ reconfig = getattr(stream, "reconfigure", None)
1077
+ if callable(reconfig):
1078
+ reconfig(errors="replace")
1079
+ except Exception:
1080
+ pass
1081
+
1042
1082
  def print_banner():
1043
1083
  """Print the Kiwi AI startup banner."""
1044
- print()
1084
+ # Ensure we never crash on Windows consoles that still default to legacy
1085
+ # encodings (e.g. cp1252). We'll replace unencodable characters instead of
1086
+ # raising UnicodeEncodeError.
1087
+ _configure_stdio()
1088
+
1089
+ # If the terminal can't represent our box-drawing / block chars, fall back
1090
+ # to a plain banner (still functional, just less pretty).
1091
+ h1 = "━" if _can_encode("━") else "-"
1092
+ plain = os.environ.get("KIWI_PLAIN_BANNER") == "1" or not _can_encode("█") or h1 == "-"
1093
+ if plain:
1094
+ _safe_print("")
1095
+ _safe_print(" Kiwi Runtime")
1096
+ _safe_print(f" {h1 * 72}")
1097
+ _safe_print(f" {TAGLINE}")
1098
+ _safe_print("")
1099
+ return
1100
+
1101
+ _safe_print("")
1045
1102
 
1046
1103
  # Side-by-side: logo (16 lines) with text (6 lines) vertically centered
1047
1104
  gap = " "
@@ -1058,21 +1115,22 @@ def print_banner():
1058
1115
  else:
1059
1116
  row = padded_logo
1060
1117
 
1061
- print(f" {CB}{row}{RESET}")
1118
+ _safe_print(f" {CB}{row}{RESET}")
1062
1119
 
1063
- print(f" {GREY}{'━' * 72}{RESET}")
1064
- print()
1120
+ _safe_print(f" {GREY}{h1 * 72}{RESET}")
1121
+ _safe_print("")
1065
1122
 
1066
1123
 
1067
1124
  def print_status(icon: str, message: str, color: str = C):
1068
1125
  """Print a styled status line."""
1069
- print(f" {color}{icon}{RESET} {message}")
1126
+ _safe_print(f" {color}{icon}{RESET} {message}")
1070
1127
 
1071
1128
 
1072
1129
  def print_section(title: str):
1073
1130
  """Print a section header."""
1074
- print(f"\n {CB}{title}{RESET}")
1075
- print(f" {GREY}{'─' * 40}{RESET}")
1131
+ rule = "─" if _can_encode("") else "-"
1132
+ _safe_print(f"\n {CB}{title}{RESET}")
1133
+ _safe_print(f" {GREY}{rule * 40}{RESET}")
1076
1134
 
1077
1135
 
1078
1136
  def print_cmd_log(request_id: str, message: str, success: bool | None = None):
@@ -1084,7 +1142,7 @@ def print_cmd_log(request_id: str, message: str, success: bool | None = None):
1084
1142
  icon = f"{RED}x{RESET}"
1085
1143
  else:
1086
1144
  icon = f"{C}>{RESET}"
1087
- print(f" {icon} {tag} {message}")
1145
+ _safe_print(f" {icon} {tag} {message}")
1088
1146
 
1089
1147
 
1090
1148
  def print_pty_log(session_id: str, message: str, level: str = "info"):
@@ -1096,7 +1154,7 @@ def print_pty_log(session_id: str, message: str, level: str = "info"):
1096
1154
  icon = f"{GREEN}>{RESET}"
1097
1155
  else:
1098
1156
  icon = f"{C}~{RESET}"
1099
- print(f" {icon} {tag} {message}")
1157
+ _safe_print(f" {icon} {tag} {message}")
1100
1158
 
1101
1159
 
1102
1160
  # ---------------------------------------------------------------------------
@@ -325,6 +325,11 @@ def _spawn_runtime(
325
325
  env = os.environ.copy()
326
326
  env.setdefault("PYTHONUNBUFFERED", "1")
327
327
 
328
+ # Ensure the runtime doesn't crash on Windows consoles/locales that default
329
+ # to legacy encodings (cp1252, cp437).
330
+ env.setdefault("PYTHONUTF8", "1")
331
+ env.setdefault("PYTHONIOENCODING", "utf-8:replace")
332
+
328
333
  try:
329
334
  log_path.parent.mkdir(parents=True, exist_ok=True)
330
335
  log_fp = open(log_path, "a", encoding="utf-8", errors="replace")
@@ -1820,54 +1820,70 @@ class DashboardScreen(Screen):
1820
1820
 
1821
1821
  poll_task = asyncio.create_task(_poll_until_done())
1822
1822
 
1823
+ # Run SSE streaming and polling concurrently. We intentionally do NOT
1824
+ # impose a hard timeout on the SSE stream; long-running actions may
1825
+ # legitimately take more than a few minutes. If the stream ends early
1826
+ # (e.g. network drop), we continue polling until we can fetch a terminal
1827
+ # result. Users can always use /cancel to stop waiting.
1828
+ status_task: asyncio.Task | None = asyncio.create_task(
1829
+ client.stream_action_result(run_id, handle_status_message)
1830
+ )
1831
+
1823
1832
  try:
1824
- status_task = asyncio.create_task(
1825
- asyncio.wait_for(
1826
- client.stream_action_result(run_id, handle_status_message),
1827
- timeout=300.0 # 5 minutes — avoids indefinite hangs
1828
- )
1829
- )
1833
+ while not got_final_result:
1834
+ tasks: list[asyncio.Task] = [poll_task]
1835
+ if status_task is not None:
1836
+ tasks.append(status_task)
1830
1837
 
1831
- try:
1832
- # Wait for EITHER the SSE stream to end OR polling to find the result
1833
- done, pending = await asyncio.wait(
1834
- [status_task, poll_task],
1838
+ done, _pending = await asyncio.wait(
1839
+ tasks,
1835
1840
  return_when=asyncio.FIRST_COMPLETED,
1836
1841
  )
1837
- # Cancel whichever is still running
1838
- for task in pending:
1839
- task.cancel()
1842
+
1843
+ # Polling finished => we have (or attempted) a final result.
1844
+ if poll_task in done:
1845
+ break
1846
+
1847
+ # Streaming ended first (normal end or error). Keep polling.
1848
+ if status_task is not None and status_task in done:
1840
1849
  try:
1841
- await task
1842
- except (asyncio.CancelledError, Exception):
1850
+ status_task.result()
1851
+ except asyncio.CancelledError:
1843
1852
  pass
1844
- # Check for exceptions in completed tasks
1845
- for task in done:
1846
- if task.exception() and not isinstance(task.exception(), (asyncio.CancelledError, asyncio.TimeoutError)):
1847
- logger.error(f"Stream/poll error: {task.exception()}")
1848
- except asyncio.CancelledError:
1849
- logger.info(f"Stream cancelled for {run_id}")
1850
- status_task.cancel()
1851
- poll_task.cancel()
1852
- _remove_status_widget()
1853
- self._set_streaming(False)
1854
- return
1855
- except asyncio.TimeoutError:
1856
- logger.warning(f"SSE timeout for {run_id}")
1857
- poll_task.cancel()
1858
- _remove_status_widget()
1859
- self.add_message("Action timed out waiting for response", "error")
1860
- self._set_streaming(False)
1861
- return
1853
+ except Exception as e:
1854
+ logger.warning(f"SSE stream ended for {run_id}: {e}")
1855
+ status_task = None
1856
+
1857
+ # Ensure polling completes (it exits when got_final_result becomes True).
1858
+ if not poll_task.done():
1859
+ await poll_task
1862
1860
  except asyncio.CancelledError:
1863
- logger.info(f"Stream worker cancelled for {run_id}")
1861
+ logger.info(f"Stream cancelled for {run_id}")
1862
+ if status_task is not None:
1863
+ status_task.cancel()
1864
1864
  poll_task.cancel()
1865
1865
  _remove_status_widget()
1866
1866
  self._set_streaming(False)
1867
1867
  return
1868
1868
  except Exception as e:
1869
- logger.error(f"SSE error for {run_id}: {e}")
1869
+ logger.error(f"Stream/poll error for {run_id}: {e}")
1870
1870
  poll_task.cancel()
1871
+ _remove_status_widget()
1872
+ self.add_message(
1873
+ f"Lost connection while waiting for result. The run may still be processing. "
1874
+ f"Use /continue {run_id} to reattach.",
1875
+ "error",
1876
+ )
1877
+ self._set_streaming(False)
1878
+ return
1879
+ finally:
1880
+ # If we got the result via polling, stop streaming to avoid leaks.
1881
+ if status_task is not None and not status_task.done():
1882
+ status_task.cancel()
1883
+ try:
1884
+ await status_task
1885
+ except (asyncio.CancelledError, Exception):
1886
+ pass
1871
1887
 
1872
1888
  # Always clean up status widget when SSE stream ends
1873
1889
  _remove_status_widget()
@@ -397,7 +397,7 @@ wheels = [
397
397
 
398
398
  [[package]]
399
399
  name = "kiwi-code"
400
- version = "0.0.18"
400
+ version = "0.0.20"
401
401
  source = { editable = "." }
402
402
  dependencies = [
403
403
  { name = "autobots-client" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes