kiwi-code 0.0.38__tar.gz → 0.0.39__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.38 → kiwi_code-0.0.39}/PKG-INFO +1 -1
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/pyproject.toml +1 -1
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/dashboard.py +39 -1
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_tui_headless.py +108 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/uv.lock +1 -1
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/.gitignore +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/.python-version +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/CLAUDE.md +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/Makefile +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/README.md +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/cli.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/client.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_runtime/main.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_runtime/snake_game/.gitignore +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/main.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/runtime_agent.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/slash_commands.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/status_words.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/test_hello.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/__init__.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/conftest.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_slash_commands.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_tui_palette.py +0 -0
|
@@ -2354,6 +2354,42 @@ class DashboardScreen(Screen):
|
|
|
2354
2354
|
return None
|
|
2355
2355
|
return None
|
|
2356
2356
|
|
|
2357
|
+
_MESSAGES_AUTOFOLLOW_THRESHOLD: int = 2
|
|
2358
|
+
|
|
2359
|
+
def _messages_is_near_bottom(self, *, threshold: int | None = None) -> bool:
|
|
2360
|
+
"""Return True when the chat viewport is already at or near the bottom.
|
|
2361
|
+
|
|
2362
|
+
Streaming updates should only auto-follow when the user hasn't intentionally
|
|
2363
|
+
scrolled upward to inspect earlier messages.
|
|
2364
|
+
"""
|
|
2365
|
+
try:
|
|
2366
|
+
messages = self.query_one("#messages", VerticalScroll)
|
|
2367
|
+
margin = (
|
|
2368
|
+
self._MESSAGES_AUTOFOLLOW_THRESHOLD
|
|
2369
|
+
if threshold is None
|
|
2370
|
+
else max(0, int(threshold))
|
|
2371
|
+
)
|
|
2372
|
+
current_position = max(
|
|
2373
|
+
int(messages.scroll_offset.y),
|
|
2374
|
+
int(messages.scroll_target_y),
|
|
2375
|
+
)
|
|
2376
|
+
remaining = max(0, int(messages.max_scroll_y) - current_position)
|
|
2377
|
+
return remaining <= margin
|
|
2378
|
+
except Exception:
|
|
2379
|
+
# Fail open so unrelated rendering issues don't block visible output.
|
|
2380
|
+
return True
|
|
2381
|
+
|
|
2382
|
+
def _scroll_messages_to_end(self, *, after_refresh: bool = False) -> None:
|
|
2383
|
+
"""Scroll the messages viewport to the bottom without animation."""
|
|
2384
|
+
try:
|
|
2385
|
+
messages = self.query_one("#messages", VerticalScroll)
|
|
2386
|
+
if after_refresh:
|
|
2387
|
+
self.call_after_refresh(messages.scroll_end, animate=False, immediate=True)
|
|
2388
|
+
else:
|
|
2389
|
+
messages.scroll_end(animate=False)
|
|
2390
|
+
except Exception:
|
|
2391
|
+
pass
|
|
2392
|
+
|
|
2357
2393
|
def _blink_streaming_dot_tick(self) -> None:
|
|
2358
2394
|
"""Timer tick: blink the dot for the active streaming assistant row."""
|
|
2359
2395
|
w = getattr(self, "_streaming_widget_ref", None)
|
|
@@ -3113,6 +3149,7 @@ class DashboardScreen(Screen):
|
|
|
3113
3149
|
return widget_ref
|
|
3114
3150
|
|
|
3115
3151
|
messages = self.query_one("#messages", VerticalScroll)
|
|
3152
|
+
should_autofollow = self._messages_is_near_bottom()
|
|
3116
3153
|
|
|
3117
3154
|
markdown_text = self._prepare_markdown(text_content)
|
|
3118
3155
|
if widget_ref is None:
|
|
@@ -3163,7 +3200,8 @@ class DashboardScreen(Screen):
|
|
|
3163
3200
|
except Exception:
|
|
3164
3201
|
pass
|
|
3165
3202
|
|
|
3166
|
-
|
|
3203
|
+
if should_autofollow:
|
|
3204
|
+
self._scroll_messages_to_end(after_refresh=True)
|
|
3167
3205
|
return widget_ref
|
|
3168
3206
|
|
|
3169
3207
|
def extract_text_from_output(self, output: any) -> str:
|
|
@@ -805,6 +805,114 @@ async def test_tui_allows_empty_prompt(isolated_home: Path, monkeypatch: pytest.
|
|
|
805
805
|
|
|
806
806
|
assert called == [""]
|
|
807
807
|
|
|
808
|
+
|
|
809
|
+
@pytest.mark.asyncio
|
|
810
|
+
async def test_tui_streaming_requests_autofollow_when_viewport_is_near_bottom(
|
|
811
|
+
isolated_home: Path, monkeypatch: pytest.MonkeyPatch
|
|
812
|
+
) -> None:
|
|
813
|
+
"""Streaming updates should request auto-follow only when the viewport is near bottom."""
|
|
814
|
+
tokens_path = isolated_home / ".kiwi" / "tokens.json"
|
|
815
|
+
tokens_path.parent.mkdir(parents=True, exist_ok=True)
|
|
816
|
+
tokens_path.write_text(
|
|
817
|
+
json.dumps(
|
|
818
|
+
{
|
|
819
|
+
"access_token": "test-access-token",
|
|
820
|
+
"refresh_token": "test-refresh-token",
|
|
821
|
+
"token_type": "Bearer",
|
|
822
|
+
"expires_at": None,
|
|
823
|
+
}
|
|
824
|
+
),
|
|
825
|
+
encoding="utf-8",
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
from kiwi_tui.main import AutobotsTUI
|
|
829
|
+
|
|
830
|
+
app = AutobotsTUI()
|
|
831
|
+
async with app.run_test(size=(80, 24)) as pilot:
|
|
832
|
+
await pilot.pause()
|
|
833
|
+
assert type(app.screen).__name__ == "DashboardScreen"
|
|
834
|
+
screen = app.screen
|
|
835
|
+
|
|
836
|
+
calls: list[dict] = []
|
|
837
|
+
monkeypatch.setattr(screen, "_messages_is_near_bottom", lambda *args, **kwargs: True)
|
|
838
|
+
monkeypatch.setattr(
|
|
839
|
+
screen,
|
|
840
|
+
"_scroll_messages_to_end",
|
|
841
|
+
lambda *args, **kwargs: calls.append(dict(kwargs)),
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
widget = screen.update_streaming_message({"blocks": [{"text": "stream start"}]})
|
|
845
|
+
await pilot.pause()
|
|
846
|
+
assert widget is not None
|
|
847
|
+
assert calls == [{"after_refresh": True}]
|
|
848
|
+
|
|
849
|
+
calls.clear()
|
|
850
|
+
screen.update_streaming_message(
|
|
851
|
+
{"blocks": [{"text": "\n".join(f"stream line {i}" for i in range(10))}]},
|
|
852
|
+
widget,
|
|
853
|
+
)
|
|
854
|
+
await pilot.pause()
|
|
855
|
+
assert calls == [{"after_refresh": True}]
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
@pytest.mark.asyncio
|
|
859
|
+
async def test_tui_streaming_does_not_force_scroll_when_user_reads_older_messages(
|
|
860
|
+
isolated_home: Path,
|
|
861
|
+
) -> None:
|
|
862
|
+
"""Streaming updates should not yank the viewport back to the bottom."""
|
|
863
|
+
tokens_path = isolated_home / ".kiwi" / "tokens.json"
|
|
864
|
+
tokens_path.parent.mkdir(parents=True, exist_ok=True)
|
|
865
|
+
tokens_path.write_text(
|
|
866
|
+
json.dumps(
|
|
867
|
+
{
|
|
868
|
+
"access_token": "test-access-token",
|
|
869
|
+
"refresh_token": "test-refresh-token",
|
|
870
|
+
"token_type": "Bearer",
|
|
871
|
+
"expires_at": None,
|
|
872
|
+
}
|
|
873
|
+
),
|
|
874
|
+
encoding="utf-8",
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
from kiwi_tui.main import AutobotsTUI
|
|
878
|
+
|
|
879
|
+
app = AutobotsTUI()
|
|
880
|
+
async with app.run_test(size=(80, 24)) as pilot:
|
|
881
|
+
await pilot.pause()
|
|
882
|
+
assert type(app.screen).__name__ == "DashboardScreen"
|
|
883
|
+
screen = app.screen
|
|
884
|
+
messages = screen.query_one("#messages")
|
|
885
|
+
|
|
886
|
+
for i in range(30):
|
|
887
|
+
screen.add_message(
|
|
888
|
+
"\n".join(f"history {i} line {j}" for j in range(4)),
|
|
889
|
+
"assistant",
|
|
890
|
+
)
|
|
891
|
+
await pilot.pause()
|
|
892
|
+
assert messages.max_scroll_y > 0
|
|
893
|
+
|
|
894
|
+
widget = screen.update_streaming_message({"blocks": [{"text": "stream start"}]})
|
|
895
|
+
await pilot.pause()
|
|
896
|
+
assert widget is not None
|
|
897
|
+
|
|
898
|
+
target_y = max(0, int(messages.max_scroll_y) - 5)
|
|
899
|
+
messages.scroll_to(y=target_y, animate=False, immediate=True)
|
|
900
|
+
await pilot.pause()
|
|
901
|
+
y_before = int(messages.scroll_offset.y)
|
|
902
|
+
target_before = int(messages.scroll_target_y)
|
|
903
|
+
assert y_before == target_before
|
|
904
|
+
assert y_before < int(messages.max_scroll_y)
|
|
905
|
+
|
|
906
|
+
screen.update_streaming_message(
|
|
907
|
+
{"blocks": [{"text": "\n".join(f"stream line {i}" for i in range(60))}]},
|
|
908
|
+
widget,
|
|
909
|
+
)
|
|
910
|
+
await pilot.pause()
|
|
911
|
+
|
|
912
|
+
assert int(messages.scroll_offset.y) == y_before
|
|
913
|
+
assert int(messages.scroll_target_y) == target_before
|
|
914
|
+
assert int(messages.scroll_offset.y) < int(messages.max_scroll_y)
|
|
915
|
+
|
|
808
916
|
@pytest.mark.asyncio
|
|
809
917
|
async def test_tui_quits_cleanly(isolated_home: Path) -> None:
|
|
810
918
|
from kiwi_tui.main import AutobotsTUI
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|