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.
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/PKG-INFO +1 -1
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/pyproject.toml +1 -1
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_runtime/main.py +67 -9
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/runtime_agent.py +5 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/dashboard.py +51 -35
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/uv.lock +1 -1
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/.gitignore +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/.python-version +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/CLAUDE.md +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/Makefile +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/README.md +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/cli.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/client.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/config.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_runtime/snake_game/.gitignore +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/main.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/test_hello.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/__init__.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/conftest.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/test_config.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.18 → kiwi_code-0.0.20}/tests/test_tui_headless.py +0 -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
|
-
|
|
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
|
-
|
|
1118
|
+
_safe_print(f" {CB}{row}{RESET}")
|
|
1062
1119
|
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1075
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1825
|
-
asyncio.
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
-
|
|
1832
|
-
|
|
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
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
-
|
|
1842
|
-
except
|
|
1850
|
+
status_task.result()
|
|
1851
|
+
except asyncio.CancelledError:
|
|
1843
1852
|
pass
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
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
|
|
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"
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|