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.
Files changed (54) hide show
  1. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/PKG-INFO +1 -1
  2. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/pyproject.toml +1 -1
  3. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/dashboard.py +39 -1
  4. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_tui_headless.py +108 -0
  5. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/uv.lock +1 -1
  6. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/.github/workflows/publish.yml +0 -0
  7. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/.github/workflows/test.yml +0 -0
  8. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/.gitignore +0 -0
  9. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/.python-version +0 -0
  10. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/CLAUDE.md +0 -0
  11. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/Makefile +0 -0
  12. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/README.md +0 -0
  13. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/__init__.py +0 -0
  14. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/auth.py +0 -0
  15. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/cli.py +0 -0
  16. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/client.py +0 -0
  17. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/commands.py +0 -0
  18. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/logger.py +0 -0
  19. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/models.py +0 -0
  20. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/runtime_manager.py +0 -0
  21. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_cli/server.py +0 -0
  22. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_runtime/__init__.py +0 -0
  23. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_runtime/__main__.py +0 -0
  24. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_runtime/main.py +0 -0
  25. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_runtime/snake_game/.gitignore +0 -0
  26. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
  27. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/__init__.py +0 -0
  28. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/inline_file_picker.py +0 -0
  29. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/main.py +0 -0
  30. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/random_words.py +0 -0
  31. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/runtime_agent.py +0 -0
  32. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/__init__.py +0 -0
  33. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/attach_content.py +0 -0
  34. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/command_result.py +0 -0
  35. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/file_browser.py +0 -0
  36. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/help.py +0 -0
  37. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/id_picker.py +0 -0
  38. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/login.py +0 -0
  39. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
  40. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  41. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/screens/slash_picker.py +0 -0
  42. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/slash_commands.py +0 -0
  43. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/status_words.py +0 -0
  44. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/src/kiwi_tui/widgets.py +0 -0
  45. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/test_hello.py +0 -0
  46. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/__init__.py +0 -0
  47. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/conftest.py +0 -0
  48. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_cli_help.py +0 -0
  49. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_imports.py +0 -0
  50. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_reexec_kiwi.py +0 -0
  51. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_runtime_log_trimming.py +0 -0
  52. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_slash_commands.py +0 -0
  53. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_tokens.py +0 -0
  54. {kiwi_code-0.0.38 → kiwi_code-0.0.39}/tests/test_tui_palette.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwi-code
3
- Version: 0.0.38
3
+ Version: 0.0.39
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.38"
3
+ version = "0.0.39"
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"
@@ -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
- messages.scroll_end(animate=False)
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
@@ -397,7 +397,7 @@ wheels = [
397
397
 
398
398
  [[package]]
399
399
  name = "kiwi-code"
400
- version = "0.0.38"
400
+ version = "0.0.39"
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