kiwi-code 0.0.42__tar.gz → 0.0.43__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 (57) hide show
  1. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/PKG-INFO +1 -1
  2. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/pyproject.toml +1 -1
  3. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/screens/dashboard.py +1 -0
  4. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/tests/test_tui_headless.py +253 -27
  5. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/uv.lock +1 -1
  6. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/.github/workflows/publish.yml +0 -0
  7. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/.github/workflows/test.yml +0 -0
  8. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/.gitignore +0 -0
  9. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/.python-version +0 -0
  10. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/CLAUDE.md +0 -0
  11. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/Makefile +0 -0
  12. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/README.md +0 -0
  13. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_cli/__init__.py +0 -0
  14. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_cli/auth.py +0 -0
  15. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_cli/cli.py +0 -0
  16. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_cli/client.py +0 -0
  17. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_cli/commands.py +0 -0
  18. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_cli/logger.py +0 -0
  19. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_cli/models.py +0 -0
  20. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_cli/runtime_manager.py +0 -0
  21. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_cli/server.py +0 -0
  22. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_cli/terminal_mode.py +0 -0
  23. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_runtime/__init__.py +0 -0
  24. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_runtime/__main__.py +0 -0
  25. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_runtime/main.py +0 -0
  26. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_runtime/snake_game/.gitignore +0 -0
  27. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
  28. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/__init__.py +0 -0
  29. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/inline_file_picker.py +0 -0
  30. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/main.py +0 -0
  31. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/random_words.py +0 -0
  32. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/runtime_agent.py +0 -0
  33. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/screens/__init__.py +0 -0
  34. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/screens/attach_content.py +0 -0
  35. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/screens/command_result.py +0 -0
  36. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/screens/file_browser.py +0 -0
  37. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/screens/help.py +0 -0
  38. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/screens/id_picker.py +0 -0
  39. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/screens/login.py +0 -0
  40. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
  41. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  42. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/screens/slash_picker.py +0 -0
  43. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/slash_commands.py +0 -0
  44. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/status_words.py +0 -0
  45. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/src/kiwi_tui/widgets.py +0 -0
  46. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/test_hello.py +0 -0
  47. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/tests/__init__.py +0 -0
  48. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/tests/conftest.py +0 -0
  49. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/tests/test_cli_help.py +0 -0
  50. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/tests/test_imports.py +0 -0
  51. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/tests/test_reexec_kiwi.py +0 -0
  52. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/tests/test_runtime_log_trimming.py +0 -0
  53. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/tests/test_slash_commands.py +0 -0
  54. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/tests/test_terminal_mode.py +0 -0
  55. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/tests/test_tokens.py +0 -0
  56. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/tests/test_tui_interactive_runtime.py +0 -0
  57. {kiwi_code-0.0.42 → kiwi_code-0.0.43}/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.42
3
+ Version: 0.0.43
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.42"
3
+ version = "0.0.43"
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"
@@ -138,6 +138,7 @@ class DashboardScreen(Screen):
138
138
  "69c2180355a89324a9926bc6",
139
139
  "6a0625b8ab5f80ba8ac5d012",
140
140
  "69e4be0d70c2d7a89197e66e",
141
+ "6a1c0773c519da8a0fa9b8a4",
141
142
  ]
142
143
 
143
144
  BINDINGS = [
@@ -701,7 +701,7 @@ async def test_tui_continue_run_updates_action_name_from_run(
701
701
  async def test_tui_autocode_select_shows_only_special_action_names(
702
702
  isolated_home: Path, monkeypatch: pytest.MonkeyPatch
703
703
  ) -> None:
704
- """/autocode-select should open the picker with only the 3 special actions."""
704
+ """/autocode-select should open the picker with only the 4 special actions."""
705
705
  tokens_path = isolated_home / ".kiwi" / "tokens.json"
706
706
  tokens_path.parent.mkdir(parents=True, exist_ok=True)
707
707
  tokens_path.write_text(
@@ -720,6 +720,7 @@ async def test_tui_autocode_select_shows_only_special_action_names(
720
720
  "69c2180355a89324a9926bc6": "AutoCode Alpha",
721
721
  "6a0625b8ab5f80ba8ac5d012": "AutoCode Beta",
722
722
  "69e4be0d70c2d7a89197e66e": "AutoCode Gamma",
723
+ "6a1c0773c519da8a0fa9b8a4": "AutoCode Delta",
723
724
  }
724
725
 
725
726
  from kiwi_tui.screens.dashboard import DashboardScreen
@@ -753,10 +754,11 @@ async def test_tui_autocode_select_shows_only_special_action_names(
753
754
 
754
755
  assert type(app.screen).__name__ == "IdPickerScreen"
755
756
  table = app.screen.query_one("#idpicker-table", DataTable)
756
- assert table.row_count == 3
757
+ assert table.row_count == 4
757
758
  assert list(table.get_row_at(0)) == ["1", "AutoCode Alpha"]
758
759
  assert list(table.get_row_at(1)) == ["2", "AutoCode Beta"]
759
760
  assert list(table.get_row_at(2)) == ["3", "AutoCode Gamma"]
761
+ assert list(table.get_row_at(3)) == ["4", "AutoCode Delta"]
760
762
 
761
763
 
762
764
 
@@ -853,8 +855,6 @@ async def test_tui_streaming_requests_autofollow_when_viewport_is_near_bottom(
853
855
  )
854
856
  await pilot.pause()
855
857
  assert calls == [{"after_refresh": True}]
856
-
857
-
858
858
  @pytest.mark.asyncio
859
859
  async def test_tui_streaming_does_not_force_scroll_when_user_reads_older_messages(
860
860
  isolated_home: Path,
@@ -881,45 +881,271 @@ async def test_tui_streaming_does_not_force_scroll_when_user_reads_older_message
881
881
  await pilot.pause()
882
882
  assert type(app.screen).__name__ == "DashboardScreen"
883
883
  screen = app.screen
884
- messages = screen.query_one("#messages")
885
884
 
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",
885
+ calls: list[dict] = []
886
+ monkeypatch = pytest.MonkeyPatch()
887
+ monkeypatch.setattr(screen, "_messages_is_near_bottom", lambda *args, **kwargs: False)
888
+ monkeypatch.setattr(
889
+ screen,
890
+ "_scroll_messages_to_end",
891
+ lambda *args, **kwargs: calls.append(dict(kwargs)),
892
+ )
893
+ try:
894
+ widget = screen.update_streaming_message({"blocks": [{"text": "stream start"}]})
895
+ await pilot.pause()
896
+ assert widget is not None
897
+ assert calls == []
898
+
899
+ screen.update_streaming_message(
900
+ {"blocks": [{"text": "\n".join(f"stream line {i}" for i in range(60))}]},
901
+ widget,
890
902
  )
903
+ await pilot.pause()
904
+ assert calls == []
905
+ finally:
906
+ monkeypatch.undo()
907
+
908
+ @pytest.mark.asyncio
909
+ async def test_tui_quits_cleanly(isolated_home: Path) -> None:
910
+ from kiwi_tui.main import AutobotsTUI
911
+
912
+ app = AutobotsTUI()
913
+ async with app.run_test() as pilot:
891
914
  await pilot.pause()
892
- assert messages.max_scroll_y > 0
915
+ await pilot.press("ctrl+c")
916
+ await pilot.pause()
917
+ # Reaching here without an exception is the assertion.
893
918
 
894
- widget = screen.update_streaming_message({"blocks": [{"text": "stream start"}]})
919
+
920
+ @pytest.mark.asyncio
921
+ async def test_tui_redirects_to_login_when_auth_expires(isolated_home: Path, monkeypatch: pytest.MonkeyPatch) -> None:
922
+ tokens_path = isolated_home / ".kiwi" / "tokens.json"
923
+ tokens_path.parent.mkdir(parents=True, exist_ok=True)
924
+ tokens_path.write_text(
925
+ json.dumps(
926
+ {
927
+ "access_token": "test-access-token",
928
+ "refresh_token": "test-refresh-token",
929
+ "token_type": "Bearer",
930
+ "expires_at": None,
931
+ }
932
+ ),
933
+ encoding="utf-8",
934
+ )
935
+
936
+ from autobots_client.api.actions import get_action_v1_actions_id_get
937
+ monkeypatch.setattr(
938
+ get_action_v1_actions_id_get,
939
+ "sync_detailed",
940
+ lambda *, id, client: SimpleNamespace(
941
+ status_code=200,
942
+ parsed=SimpleNamespace(field_id=id, name="AutoCode Default"),
943
+ ),
944
+ )
945
+
946
+ from kiwi_tui.main import AutobotsTUI
947
+
948
+ app = AutobotsTUI()
949
+ async with app.run_test() as pilot:
895
950
  await pilot.pause()
896
- assert widget is not None
951
+ assert type(app.screen).__name__ == "DashboardScreen"
897
952
 
898
- target_y = max(0, int(messages.max_scroll_y) - 5)
899
- messages.scroll_to(y=target_y, animate=False, immediate=True)
953
+ app._handle_auth_expired("Session expired during test.")
900
954
  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
955
 
906
- screen.update_streaming_message(
907
- {"blocks": [{"text": "\n".join(f"stream line {i}" for i in range(60))}]},
908
- widget,
956
+ assert type(app.screen).__name__ == "LoginScreen"
957
+ assert not tokens_path.exists()
958
+
959
+
960
+ @pytest.mark.asyncio
961
+ async def test_tui_run_action_403_redirects_to_login_when_refresh_cannot_run(
962
+ isolated_home: Path, monkeypatch: pytest.MonkeyPatch
963
+ ) -> None:
964
+ tokens_path = isolated_home / ".kiwi" / "tokens.json"
965
+ tokens_path.parent.mkdir(parents=True, exist_ok=True)
966
+ tokens_path.write_text(
967
+ json.dumps(
968
+ {
969
+ "access_token": "test-access-token",
970
+ "refresh_token": "test-refresh-token",
971
+ "token_type": "Bearer",
972
+ "expires_at": None,
973
+ }
974
+ ),
975
+ encoding="utf-8",
976
+ )
977
+
978
+ from autobots_client.api.actions import get_action_v1_actions_id_get
979
+ monkeypatch.setattr(
980
+ get_action_v1_actions_id_get,
981
+ "sync_detailed",
982
+ lambda *, id, client: SimpleNamespace(
983
+ status_code=200,
984
+ parsed=SimpleNamespace(field_id=id, name="AutoCode Default"),
985
+ ),
986
+ )
987
+
988
+ from kiwi_tui.main import AutobotsTUI
989
+
990
+ app = AutobotsTUI()
991
+ async with app.run_test() as pilot:
992
+ await pilot.pause()
993
+ assert type(app.screen).__name__ == "DashboardScreen"
994
+ screen = app.screen
995
+
996
+ monkeypatch.setattr(app, "_refresh_token_if_needed", lambda force=False: False)
997
+ monkeypatch.setattr(
998
+ app.autobots_client,
999
+ "run_action_async",
1000
+ lambda *args, **kwargs: (False, None, "Failed to start action: status 403"),
909
1001
  )
1002
+
1003
+ screen.run_action_with_polling("hello")
1004
+ await pilot.pause(0.3)
1005
+ await pilot.pause(0.3)
1006
+
1007
+ assert type(app.screen).__name__ == "LoginScreen"
1008
+ assert not tokens_path.exists()
1009
+
1010
+
1011
+ @pytest.mark.asyncio
1012
+ async def test_tui_run_action_403_redirects_to_login_on_second_failure(
1013
+ isolated_home: Path, monkeypatch: pytest.MonkeyPatch
1014
+ ) -> None:
1015
+ tokens_path = isolated_home / ".kiwi" / "tokens.json"
1016
+ tokens_path.parent.mkdir(parents=True, exist_ok=True)
1017
+ tokens_path.write_text(
1018
+ json.dumps(
1019
+ {
1020
+ "access_token": "test-access-token",
1021
+ "refresh_token": "test-refresh-token",
1022
+ "token_type": "Bearer",
1023
+ "expires_at": None,
1024
+ }
1025
+ ),
1026
+ encoding="utf-8",
1027
+ )
1028
+
1029
+ from autobots_client.api.actions import get_action_v1_actions_id_get
1030
+ monkeypatch.setattr(
1031
+ get_action_v1_actions_id_get,
1032
+ "sync_detailed",
1033
+ lambda *, id, client: SimpleNamespace(
1034
+ status_code=200,
1035
+ parsed=SimpleNamespace(field_id=id, name="AutoCode Default"),
1036
+ ),
1037
+ )
1038
+
1039
+ from kiwi_tui.main import AutobotsTUI
1040
+
1041
+ app = AutobotsTUI()
1042
+ async with app.run_test() as pilot:
910
1043
  await pilot.pause()
1044
+ assert type(app.screen).__name__ == "DashboardScreen"
1045
+ screen = app.screen
1046
+
1047
+ monkeypatch.setattr(app, "_refresh_token_if_needed", lambda force=False: True)
1048
+ monkeypatch.setattr(
1049
+ app.autobots_client,
1050
+ "run_action_async",
1051
+ lambda *args, **kwargs: (False, None, "Failed to start action: status 403"),
1052
+ )
1053
+
1054
+ screen.run_action_with_polling("hello")
1055
+ await pilot.pause(0.3)
1056
+ await pilot.pause(0.3)
1057
+
1058
+ assert type(app.screen).__name__ == "LoginScreen"
1059
+ assert not tokens_path.exists()
1060
+
1061
+
1062
+ def test_tui_force_refresh_uses_refresh_token_even_when_access_token_not_expired(
1063
+ isolated_home: Path, monkeypatch: pytest.MonkeyPatch
1064
+ ) -> None:
1065
+ tokens_path = isolated_home / ".kiwi" / "tokens.json"
1066
+ tokens_path.parent.mkdir(parents=True, exist_ok=True)
1067
+ tokens_path.write_text(
1068
+ json.dumps(
1069
+ {
1070
+ "access_token": "test-access-token",
1071
+ "refresh_token": "test-refresh-token",
1072
+ "token_type": "Bearer",
1073
+ "expires_at": "2999-01-01T00:00:00",
1074
+ }
1075
+ ),
1076
+ encoding="utf-8",
1077
+ )
1078
+
1079
+ from kiwi_cli.models import AuthTokens
1080
+ from kiwi_tui.main import AutobotsTUI
1081
+
1082
+ app = AutobotsTUI()
1083
+
1084
+ calls: list[str] = []
1085
+
1086
+ def _fake_refresh(refresh_token: str):
1087
+ calls.append(refresh_token)
1088
+ return True, AuthTokens(
1089
+ access_token="new-access-token",
1090
+ refresh_token="new-refresh-token",
1091
+ token_type="Bearer",
1092
+ expires_at=None,
1093
+ ), "ok"
1094
+
1095
+ monkeypatch.setattr(app.autobots_client, "refresh_token", _fake_refresh)
1096
+
1097
+ assert app._refresh_token_if_needed(force=True) is True
1098
+ assert calls == ["test-refresh-token"]
911
1099
 
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
1100
 
916
1101
  @pytest.mark.asyncio
917
- async def test_tui_quits_cleanly(isolated_home: Path) -> None:
1102
+ async def test_tui_auth_expired_clears_dashboard_draft_and_messages(
1103
+ isolated_home: Path, monkeypatch: pytest.MonkeyPatch
1104
+ ) -> None:
1105
+ tokens_path = isolated_home / ".kiwi" / "tokens.json"
1106
+ tokens_path.parent.mkdir(parents=True, exist_ok=True)
1107
+ tokens_path.write_text(
1108
+ json.dumps(
1109
+ {
1110
+ "access_token": "test-access-token",
1111
+ "refresh_token": "test-refresh-token",
1112
+ "token_type": "Bearer",
1113
+ "expires_at": None,
1114
+ }
1115
+ ),
1116
+ encoding="utf-8",
1117
+ )
1118
+
1119
+ from autobots_client.api.actions import get_action_v1_actions_id_get
1120
+ monkeypatch.setattr(
1121
+ get_action_v1_actions_id_get,
1122
+ "sync_detailed",
1123
+ lambda *, id, client: SimpleNamespace(
1124
+ status_code=200,
1125
+ parsed=SimpleNamespace(field_id=id, name="AutoCode Default"),
1126
+ ),
1127
+ )
1128
+
1129
+ from textual.containers import VerticalScroll
918
1130
  from kiwi_tui.main import AutobotsTUI
1131
+ from kiwi_tui.widgets import ChatInput
919
1132
 
920
1133
  app = AutobotsTUI()
921
1134
  async with app.run_test() as pilot:
922
1135
  await pilot.pause()
923
- await pilot.press("ctrl+c")
1136
+ assert type(app.screen).__name__ == "DashboardScreen"
1137
+ dashboard = app.screen
1138
+ chat_input = dashboard.query_one("#chat-input", ChatInput)
1139
+ messages = dashboard.query_one("#messages", VerticalScroll)
1140
+
1141
+ chat_input.value = "hi"
1142
+ dashboard.add_message("hi", "user")
924
1143
  await pilot.pause()
925
- # Reaching here without an exception is the assertion.
1144
+ assert len(list(messages.children)) > 0
1145
+
1146
+ app._handle_auth_expired("Session expired during test.")
1147
+ await pilot.pause()
1148
+
1149
+ assert chat_input.value == ""
1150
+ assert len(list(messages.children)) == 0
1151
+ assert type(app.screen).__name__ == "LoginScreen"
@@ -397,7 +397,7 @@ wheels = [
397
397
 
398
398
  [[package]]
399
399
  name = "kiwi-code"
400
- version = "0.0.42"
400
+ version = "0.0.43"
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