kiwi-code 0.0.437.dev1__tar.gz → 0.0.438__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 (63) hide show
  1. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/PKG-INFO +1 -1
  2. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/pyproject.toml +1 -1
  3. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/__init__.py +1 -1
  4. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_runtime/__init__.py +1 -1
  5. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/__init__.py +1 -1
  6. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/main.py +54 -6
  7. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/dashboard.py +46 -4
  8. kiwi_code-0.0.438/src/kiwi_tui/screens/detach_files.py +146 -0
  9. kiwi_code-0.0.438/src/kiwi_tui/screens/term_dashboard.py +3898 -0
  10. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/slash_commands.py +1 -0
  11. kiwi_code-0.0.438/src/kiwi_tui/term_app.py +9 -0
  12. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_cli_help.py +9 -0
  13. kiwi_code-0.0.438/tests/test_term_dashboard_ui.py +302 -0
  14. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_terminal_mode.py +33 -3
  15. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/uv.lock +1 -1
  16. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/.github/workflows/publish.yml +0 -0
  17. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/.github/workflows/test.yml +0 -0
  18. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/.gitignore +0 -0
  19. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/.python-version +0 -0
  20. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/CLAUDE.md +0 -0
  21. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/Makefile +0 -0
  22. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/README.md +0 -0
  23. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/auth.py +0 -0
  24. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/checkpoints.py +0 -0
  25. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/cli.py +0 -0
  26. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/client.py +0 -0
  27. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/commands.py +0 -0
  28. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/logger.py +0 -0
  29. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/models.py +0 -0
  30. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/runtime_manager.py +0 -0
  31. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/server.py +0 -0
  32. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/terminal_mode.py +0 -0
  33. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_runtime/__main__.py +0 -0
  34. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_runtime/main.py +0 -0
  35. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/inline_file_picker.py +0 -0
  36. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/random_words.py +0 -0
  37. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/runtime_agent.py +0 -0
  38. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/__init__.py +0 -0
  39. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/attach_content.py +0 -0
  40. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/command_result.py +0 -0
  41. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/file_browser.py +0 -0
  42. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/help.py +0 -0
  43. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/id_picker.py +0 -0
  44. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/login.py +0 -0
  45. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
  46. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  47. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/slash_picker.py +0 -0
  48. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/status_words.py +0 -0
  49. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/widgets.py +0 -0
  50. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/worktrees.py +0 -0
  51. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/test_hello.py +0 -0
  52. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/__init__.py +0 -0
  53. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/conftest.py +0 -0
  54. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_checkpoints.py +0 -0
  55. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_imports.py +0 -0
  56. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_reexec_kiwi.py +0 -0
  57. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_runtime_log_trimming.py +0 -0
  58. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_slash_commands.py +0 -0
  59. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_tokens.py +0 -0
  60. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_tui_headless.py +0 -0
  61. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_tui_interactive_runtime.py +0 -0
  62. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_tui_palette.py +0 -0
  63. {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_worktrees.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwi-code
3
- Version: 0.0.437.dev1
3
+ Version: 0.0.438
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.437.dev1"
3
+ version = "0.0.438"
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"
@@ -1,3 +1,3 @@
1
1
  """Kiwi CLI - command-line interface and shared infrastructure modules."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.0.438"
@@ -1,3 +1,3 @@
1
1
  """Kiwi Runtime — terminal agent that connects to the server via WebSocket."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.0.438"
@@ -1,3 +1,3 @@
1
1
  """Autobots TUI - A textual-based terminal user interface."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.0.438"
@@ -962,15 +962,47 @@ def _run_tui(runtime_args: RuntimeConnectArgs | None = None):
962
962
  logger.info("Autobots TUI terminated")
963
963
 
964
964
 
965
- def _serve_tui(port: int = 8566):
965
+ def _run_term_tui(runtime_args: RuntimeConnectArgs | None = None) -> None:
966
+ """Run the *term* (terminal-first) TUI.
967
+
968
+ This is the default UI for `kiwi`. Use `kiwi alt` for the legacy UI.
969
+ """
970
+ from kiwi_tui.term_app import TermApp
971
+
972
+ config = AppConfig()
973
+
974
+ # If the user passed `--server`, use the same value for the TUI HTTP backend.
975
+ if runtime_args and runtime_args.server:
976
+ try:
977
+ config.backend_url = http_url_from_server(runtime_args.server)
978
+ except Exception:
979
+ pass
980
+
981
+ setup_logging(log_level=config.log_level)
982
+
983
+ logger.info("=" * 60)
984
+ logger.info("Starting Kiwi Term TUI")
985
+ logger.info("=" * 60)
986
+
987
+ app = TermApp(config=config, runtime_args=runtime_args)
988
+ app.run()
989
+
990
+ logger.info("Kiwi Term TUI terminated")
991
+
992
+
993
+ def _serve_tui(port: int = 8566, extra_argv: list[str] | None = None) -> None:
966
994
  """Serve the TUI in the browser via textual serve."""
995
+ import shlex
967
996
  import sys
997
+
968
998
  from textual_serve.server import Server
969
999
 
970
- server = Server(f"{sys.executable} -m kiwi_tui.main", port=port)
1000
+ cmd = [sys.executable, "-m", "kiwi_tui.main", *(extra_argv or [])]
1001
+ server = Server(shlex.join(cmd), port=port)
971
1002
  server.serve()
972
1003
 
973
1004
 
1005
+
974
1006
  def _reexec_as_kiwi() -> None:
975
1007
  """Re-exec via a symlink named ``kiwi`` so the kernel-reported
976
1008
  process name (``ps -o comm`` / VS Code's ``${process}``) becomes
@@ -1126,6 +1158,19 @@ def main() -> int:
1126
1158
  sys.stdout.flush()
1127
1159
 
1128
1160
  argv = sys.argv[1:]
1161
+
1162
+ # UI selector (defaults to term UI).
1163
+ # - `kiwi` => term UI
1164
+ # - `kiwi alt` => legacy UI
1165
+ # - `kiwi term` => explicit term UI (compat)
1166
+ ui_mode = "term"
1167
+ if argv and argv[0] == "alt":
1168
+ ui_mode = "legacy"
1169
+ argv = argv[1:]
1170
+ elif argv and argv[0] == "term":
1171
+ ui_mode = "term"
1172
+ argv = argv[1:]
1173
+
1129
1174
  if argv and argv[0] in {"login", "logout", "whoami"}:
1130
1175
  return _run_auth_command(argv)
1131
1176
 
@@ -1137,14 +1182,14 @@ def main() -> int:
1137
1182
  "--port", "-p", type=int, default=8566, help="Port to serve on (default: 8566)"
1138
1183
  )
1139
1184
  serve_args = serve_parser.parse_args(argv[1:])
1140
- _serve_tui(port=serve_args.port)
1185
+ _serve_tui(port=serve_args.port, extra_argv=["alt"] if ui_mode == "legacy" else None)
1141
1186
  return 0
1142
1187
 
1143
1188
  parser = argparse.ArgumentParser(
1144
- prog="kiwi",
1189
+ prog="kiwi alt" if ui_mode == "legacy" else "kiwi",
1145
1190
  description="Kiwi Code",
1146
1191
  epilog=(
1147
- "Examples: `kiwi` (TUI), `kiwi --terminal \"Hi\"`, "
1192
+ "Examples: `kiwi` (TUI), `kiwi alt` (legacy TUI), `kiwi --terminal \"Hi\"`, "
1148
1193
  "`kiwi login`, `kiwi whoami`, `kiwi serve`."
1149
1194
  ),
1150
1195
  )
@@ -1241,7 +1286,10 @@ def main() -> int:
1241
1286
  )
1242
1287
  )
1243
1288
 
1244
- _run_tui(runtime_args=runtime_args)
1289
+ if ui_mode == "legacy":
1290
+ _run_tui(runtime_args=runtime_args)
1291
+ else:
1292
+ _run_term_tui(runtime_args=runtime_args)
1245
1293
  return 0
1246
1294
 
1247
1295
 
@@ -49,7 +49,7 @@ class AssistantMessageRow(Horizontal):
49
49
  """A single assistant message rendered with a left dot + markdown body.
50
50
 
51
51
  While streaming, shows a footer line under the message:
52
- [Kiwi] <verb> <spinner>
52
+ <verb> <spinner>
53
53
 
54
54
  Requirement: the footer is visible only while streaming and disappears when
55
55
  the final output is available.
@@ -80,7 +80,7 @@ class AssistantMessageRow(Horizontal):
80
80
 
81
81
  # Footer comes after the body, and is only visible while streaming.
82
82
  with Horizontal(classes="assistant-footer"):
83
- yield Static(f"[Kiwi] {self._verb}", classes="assistant-name", markup=False)
83
+ yield Static(f"{self._verb}", classes="assistant-name", markup=False)
84
84
  yield Static("⣾", classes="assistant-spinner", markup=False)
85
85
 
86
86
  def update_markdown(self, markdown_text: str) -> None:
@@ -299,7 +299,8 @@ class DashboardScreen(Screen):
299
299
 
300
300
  /* Grayish background to indicate "in progress" assistant output. */
301
301
  .assistant-message.streaming {
302
- background: $assistant-stream-bg;
302
+ /* Match the normal background so the streaming row doesn't look like a separate "block". */
303
+ background: $background;
303
304
  }
304
305
 
305
306
  .assistant-dot {
@@ -937,6 +938,47 @@ class DashboardScreen(Screen):
937
938
  show(body)
938
939
  return
939
940
 
941
+ if cmd == "/detach":
942
+ if not self._pending_urls:
943
+ toast("No files attached.", title="/detach", severity="warning")
944
+ return
945
+
946
+ from kiwi_tui.screens.detach_files import PendingFilesDetachScreen
947
+
948
+ pending_snapshot = list(self._pending_urls)
949
+
950
+ def _on_detach_result(result: list[str] | None) -> None:
951
+ if not result:
952
+ return
953
+ if "__all__" in result:
954
+ removed = self._clear_pending_files()
955
+ if removed:
956
+ toast(
957
+ f"Detached {removed} file{'s' if removed != 1 else ''}.",
958
+ title="Attachments",
959
+ timeout=4,
960
+ )
961
+ return
962
+
963
+ removed_urls: list[str] = []
964
+ for u in result:
965
+ if self._detach_pending_file(u):
966
+ removed_urls.append(u)
967
+
968
+ if removed_urls:
969
+ names = [Path(u.rsplit('/', 1)[-1]).name for u in removed_urls]
970
+ toast(
971
+ f"Detached {len(removed_urls)} file{'s' if len(removed_urls) != 1 else ''}: {', '.join(names)}",
972
+ title="Attachments",
973
+ timeout=4,
974
+ )
975
+
976
+ self.app.push_screen(
977
+ PendingFilesDetachScreen(pending_snapshot),
978
+ callback=_on_detach_result,
979
+ )
980
+ return
981
+
940
982
  if cmd == "/clear-files":
941
983
  count = self._clear_pending_files()
942
984
  show(f"Cleared {count} pending file(s).")
@@ -2874,7 +2916,7 @@ class DashboardScreen(Screen):
2874
2916
  """Run action and stream results via SSE.
2875
2917
 
2876
2918
  UX requirement: as soon as the user hits Enter, show an "in progress"
2877
- assistant row (dot + [Kiwi] header + spinner) and animate it until we
2919
+ assistant row (dot + header + spinner) and animate it until we
2878
2920
  have a terminal result (success/error), regardless of whether SSE is
2879
2921
  currently emitting tokens.
2880
2922
  """
@@ -0,0 +1,146 @@
1
+ """Modal UI for detaching one or more pending file attachments.
2
+
3
+ Used by both DashboardScreen (stable) and TermDashboardScreen (term mode).
4
+
5
+ The modal returns:
6
+ - None when cancelled
7
+ - ["__all__"] when user chose to detach all
8
+ - list[str] of attachment URLs selected for detaching
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+
15
+ from textual.app import ComposeResult
16
+ from textual.binding import Binding
17
+ from textual.containers import Horizontal, Vertical
18
+ from textual.screen import ModalScreen
19
+ from textual.widgets import Button, SelectionList, Static
20
+ from textual.widgets.selection_list import Selection
21
+
22
+
23
+ class PendingFilesDetachScreen(ModalScreen[list[str] | None]):
24
+ """Multi-select detach UI for pending attachments."""
25
+
26
+ BINDINGS = [
27
+ Binding("escape", "cancel", "Cancel", show=True),
28
+ ]
29
+
30
+ CSS = """
31
+ PendingFilesDetachScreen {
32
+ align: center middle;
33
+ }
34
+
35
+ #detach-container {
36
+ width: 90;
37
+ height: 70%;
38
+ background: $primary-background;
39
+ border: solid $accent;
40
+ padding: 1 2;
41
+ }
42
+
43
+ #detach-title {
44
+ width: 100%;
45
+ text-style: bold;
46
+ color: $primary;
47
+ height: 1;
48
+ content-align: center middle;
49
+ }
50
+
51
+ #detach-subtitle {
52
+ width: 100%;
53
+ height: 1;
54
+ color: $foreground;
55
+ opacity: 0.75;
56
+ content-align: center middle;
57
+ margin-bottom: 1;
58
+ }
59
+
60
+ #detach-list {
61
+ height: 1fr;
62
+ width: 100%;
63
+ border: none;
64
+ background: $primary-background;
65
+ color: $foreground;
66
+ scrollbar-size-vertical: 1;
67
+ }
68
+
69
+ #detach-buttons {
70
+ width: 100%;
71
+ height: auto;
72
+ margin-top: 1;
73
+ content-align: center middle;
74
+ }
75
+
76
+ #detach-buttons Button {
77
+ margin: 0 1;
78
+ }
79
+ """
80
+
81
+ def __init__(self, pending_urls: list[str]):
82
+ super().__init__()
83
+ self._pending_urls = list(pending_urls)
84
+
85
+ def compose(self) -> ComposeResult:
86
+ with Vertical(id="detach-container"):
87
+ yield Static("Detach attachments", id="detach-title", markup=False)
88
+ yield Static("Select file(s) to detach", id="detach-subtitle", markup=False)
89
+
90
+ # Deduplicate display names to avoid confusing duplicates.
91
+ name_counts: dict[str, int] = {}
92
+ selections: list[Selection] = []
93
+ for url in self._pending_urls:
94
+ name = Path(str(url).rsplit("/", 1)[-1]).name or "(unnamed)"
95
+ count = name_counts.get(name, 0) + 1
96
+ name_counts[name] = count
97
+ label = name if count == 1 else f"{name} ({count})"
98
+ selections.append(Selection(label, url, initial_state=False))
99
+
100
+ yield SelectionList(*selections, id="detach-list")
101
+
102
+ with Horizontal(id="detach-buttons"):
103
+ yield Button("Detach selected", id="detach-selected", variant="default")
104
+ yield Button("Detach all", id="detach-all", variant="default")
105
+ yield Button("Cancel", id="cancel", variant="default")
106
+
107
+ def on_mount(self) -> None:
108
+ try:
109
+ self.query_one("#detach-list", SelectionList).focus()
110
+ except Exception:
111
+ pass
112
+
113
+ def action_cancel(self) -> None:
114
+ self.dismiss(None)
115
+
116
+ def on_button_pressed(self, event: Button.Pressed) -> None:
117
+ btn_id = (event.button.id or "").strip()
118
+ if btn_id == "cancel":
119
+ self.dismiss(None)
120
+ return
121
+
122
+ if btn_id == "detach-all":
123
+ self.dismiss(["__all__"])
124
+ return
125
+
126
+ if btn_id == "detach-selected":
127
+ try:
128
+ selected = list(self.query_one("#detach-list", SelectionList).selected)
129
+ except Exception:
130
+ selected = []
131
+
132
+ if not selected:
133
+ try:
134
+ self.app.notify(
135
+ "No files selected.",
136
+ title="Detach",
137
+ severity="warning",
138
+ timeout=2.5,
139
+ markup=False,
140
+ )
141
+ except Exception:
142
+ pass
143
+ return
144
+
145
+ self.dismiss(selected)
146
+ return