kiwi-code 0.0.439__tar.gz → 0.0.440__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.439 → kiwi_code-0.0.440}/PKG-INFO +2 -2
  2. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/pyproject.toml +2 -2
  3. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_cli/__init__.py +1 -1
  4. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_runtime/__init__.py +1 -1
  5. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_runtime/main.py +3 -0
  6. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/__init__.py +1 -1
  7. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/screens/term_dashboard.py +178 -40
  8. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/uv.lock +5 -5
  9. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/.github/workflows/publish.yml +0 -0
  10. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/.github/workflows/test.yml +0 -0
  11. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/.gitignore +0 -0
  12. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/.python-version +0 -0
  13. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/CLAUDE.md +0 -0
  14. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/Makefile +0 -0
  15. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/README.md +0 -0
  16. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_cli/auth.py +0 -0
  17. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_cli/checkpoints.py +0 -0
  18. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_cli/cli.py +0 -0
  19. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_cli/client.py +0 -0
  20. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_cli/commands.py +0 -0
  21. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_cli/logger.py +0 -0
  22. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_cli/models.py +0 -0
  23. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_cli/runtime_manager.py +0 -0
  24. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_cli/server.py +0 -0
  25. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_cli/terminal_mode.py +0 -0
  26. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_runtime/__main__.py +0 -0
  27. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/inline_file_picker.py +0 -0
  28. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/main.py +0 -0
  29. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/random_words.py +0 -0
  30. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/runtime_agent.py +0 -0
  31. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/screens/__init__.py +0 -0
  32. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/screens/attach_content.py +0 -0
  33. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/screens/command_result.py +0 -0
  34. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/screens/dashboard.py +0 -0
  35. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/screens/detach_files.py +0 -0
  36. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/screens/file_browser.py +0 -0
  37. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/screens/help.py +0 -0
  38. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/screens/id_picker.py +0 -0
  39. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/screens/login.py +0 -0
  40. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
  41. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  42. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/screens/slash_picker.py +0 -0
  43. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/slash_commands.py +0 -0
  44. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/status_words.py +0 -0
  45. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/term_app.py +0 -0
  46. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/widgets.py +0 -0
  47. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/src/kiwi_tui/worktrees.py +0 -0
  48. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/test_hello.py +0 -0
  49. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/__init__.py +0 -0
  50. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/conftest.py +0 -0
  51. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/test_checkpoints.py +0 -0
  52. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/test_cli_help.py +0 -0
  53. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/test_imports.py +0 -0
  54. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/test_reexec_kiwi.py +0 -0
  55. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/test_runtime_log_trimming.py +0 -0
  56. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/test_slash_commands.py +0 -0
  57. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/test_term_dashboard_ui.py +0 -0
  58. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/test_terminal_mode.py +0 -0
  59. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/test_tokens.py +0 -0
  60. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/test_tui_headless.py +0 -0
  61. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/test_tui_interactive_runtime.py +0 -0
  62. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/tests/test_tui_palette.py +0 -0
  63. {kiwi_code-0.0.439 → kiwi_code-0.0.440}/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.439
3
+ Version: 0.0.440
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
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Requires-Python: <4.0,>=3.11
18
- Requires-Dist: autobots-client==0.1.0
18
+ Requires-Dist: autobots-client==0.1.1
19
19
  Requires-Dist: httpx>=0.25.0
20
20
  Requires-Dist: loguru>=0.7.3
21
21
  Requires-Dist: psutil>=5.9.0
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "kiwi-code"
3
- version = "0.0.439"
3
+ version = "0.0.440"
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"
7
7
  dependencies = [
8
- "autobots-client==0.1.0",
8
+ "autobots-client==0.1.1",
9
9
  "loguru>=0.7.3",
10
10
  "pydantic>=2.12.5",
11
11
  "textual>=8.1.1",
@@ -1,3 +1,3 @@
1
1
  """Kiwi CLI - command-line interface and shared infrastructure modules."""
2
2
 
3
- __version__ = "0.0.439"
3
+ __version__ = "0.0.440"
@@ -1,3 +1,3 @@
1
1
  """Kiwi Runtime — terminal agent that connects to the server via WebSocket."""
2
2
 
3
- __version__ = "0.0.439"
3
+ __version__ = "0.0.440"
@@ -3182,6 +3182,9 @@ async def connect(
3182
3182
 
3183
3183
 
3184
3184
  def main():
3185
+ import setproctitle
3186
+ setproctitle.setproctitle("kiwi")
3187
+
3185
3188
  parser = argparse.ArgumentParser(
3186
3189
  description="Kiwi AI CLI Agent — execute terminal commands for LLM agents"
3187
3190
  )
@@ -1,3 +1,3 @@
1
1
  """Autobots TUI - A textual-based terminal user interface."""
2
2
 
3
- __version__ = "0.0.439"
3
+ __version__ = "0.0.440"
@@ -1184,6 +1184,9 @@ class TermDashboardScreen(Screen):
1184
1184
  self._cmd_result_visible: bool = False # True while inline cmd output is shown
1185
1185
  self._cmd_running: bool = False
1186
1186
  self._is_streaming: bool = False
1187
+ # True when the current run's latest prompt is still processing server-side.
1188
+ # Used to prevent sending concurrent prompts (especially after reattaching with /continue).
1189
+ self._run_inflight: bool = False
1187
1190
  self._pending_urls: list[str] = []
1188
1191
  self._detach_selected_urls: set[str] = set()
1189
1192
  self._metadata: dict = {}
@@ -1198,6 +1201,7 @@ class TermDashboardScreen(Screen):
1198
1201
  self._spinner_i: int = 0
1199
1202
 
1200
1203
  self._last_copy_notify_at: float = 0.0
1204
+ self._last_submit_block_notify_at: float = 0.0
1201
1205
  self._clickable_list_seq: int = 0
1202
1206
 
1203
1207
  # Inline list mode for #term-runs-list (affects click behavior)
@@ -1737,7 +1741,6 @@ class TermDashboardScreen(Screen):
1737
1741
  elif event.action == "logs":
1738
1742
  self.action_show_logs()
1739
1743
  elif event.action == "quit":
1740
- # Use the app-level quit flow so we still prompt for runtime cleanup if needed.
1741
1744
  try:
1742
1745
  self.app.action_quit()
1743
1746
  except Exception:
@@ -1872,7 +1875,6 @@ class TermDashboardScreen(Screen):
1872
1875
  logger.debug(f"_show_slash_autocomplete: {exc}")
1873
1876
  return
1874
1877
 
1875
- # Tell the input widget to intercept navigation keys
1876
1878
  try:
1877
1879
  inp = self.query_one("#term-chat-input", TermSlashInput)
1878
1880
  inp.autocomplete_active = True
@@ -1929,25 +1931,75 @@ class TermDashboardScreen(Screen):
1929
1931
  if not command.needs_args:
1930
1932
  self.call_after_refresh(lambda v=val: self._do_send_value(v))
1931
1933
 
1934
+ def _toast_submit_blocked(self, message: str) -> None:
1935
+ """Show a throttled toast when the user tries to submit while busy."""
1936
+ try:
1937
+ now = time.monotonic()
1938
+ if (now - self._last_submit_block_notify_at) < 1.0:
1939
+ return
1940
+ self._last_submit_block_notify_at = now
1941
+ self.notify(
1942
+ message,
1943
+ title="Please wait",
1944
+ severity="warning",
1945
+ timeout=2.5,
1946
+ markup=False,
1947
+ )
1948
+ except Exception:
1949
+ pass
1950
+
1932
1951
  def _do_send_value(self, value: str) -> None:
1933
1952
  """Programmatically send a value as if the user typed + pressed Enter."""
1934
1953
  self._hide_slash_autocomplete()
1954
+ val = str(value or "")
1955
+ msg = val.strip()
1956
+ if not msg:
1957
+ return
1958
+
1935
1959
  if self._is_streaming or self._cmd_running:
1960
+ try:
1961
+ inp = self.query_one("#term-chat-input", TermSlashInput)
1962
+ if not inp.value.strip():
1963
+ inp.value = msg
1964
+ except Exception:
1965
+ pass
1966
+ self._toast_submit_blocked("Busy streaming — you can keep typing, but can't send yet.")
1967
+ return
1968
+
1969
+ # Slash commands are allowed when we're not actively streaming.
1970
+ if msg.startswith("/"):
1971
+ try:
1972
+ inp = self.query_one("#term-chat-input", TermSlashInput)
1973
+ inp.record(msg)
1974
+ inp.value = ""
1975
+ except Exception:
1976
+ pass
1977
+ self._dispatch_slash_command(msg)
1978
+ return
1979
+
1980
+ if self._run_inflight:
1981
+ # Keep the value in the input as a draft (best-effort).
1982
+ try:
1983
+ inp = self.query_one("#term-chat-input", TermSlashInput)
1984
+ if not inp.value.strip():
1985
+ inp.value = msg
1986
+ except Exception:
1987
+ pass
1988
+ self._toast_submit_blocked(
1989
+ f"Run {self.current_run_id or '(unknown)'} is still processing — draft your next message and send when done."
1990
+ )
1936
1991
  return
1992
+
1937
1993
  try:
1938
1994
  inp = self.query_one("#term-chat-input", TermSlashInput)
1939
- inp.record(value)
1995
+ inp.record(msg)
1940
1996
  inp.value = ""
1941
1997
  except Exception:
1942
1998
  pass
1943
- if value.strip().startswith("/"):
1944
- self._dispatch_slash_command(value.strip())
1945
- else:
1946
- self._enter_conversation_mode()
1947
- self._hide_cmd_result()
1948
- self._add_message(value, "user")
1949
- self._process_message(value)
1950
-
1999
+ self._enter_conversation_mode()
2000
+ self._hide_cmd_result()
2001
+ self._add_message(msg, "user")
2002
+ self._process_message(msg)
1951
2003
  # Top bar / hints
1952
2004
  def _refresh_top_bar(self) -> None:
1953
2005
  try:
@@ -1962,6 +2014,7 @@ class TermDashboardScreen(Screen):
1962
2014
  run = self.current_run_id or ""
1963
2015
  run_short = (run[:8] + "…") if len(run) > 8 else run
1964
2016
  streaming_flag = " ● streaming" if self._is_streaming else ""
2017
+ processing_flag = " ● processing" if (self._run_inflight and not self._is_streaming) else ""
1965
2018
 
1966
2019
  parts: list[str] = [f"Kiwi Code v{KIWI_CODE_VERSION}", now, cwd_short]
1967
2020
  if action_short:
@@ -1977,7 +2030,9 @@ class TermDashboardScreen(Screen):
1977
2030
  x0 = cell_len(prefix)
1978
2031
  self._top_bar_run_bounds = (x0, x0 + cell_len(run_part))
1979
2032
 
1980
- self.query_one("#term-top-bar", Static).update(" " + " ── ".join(parts) + streaming_flag)
2033
+ self.query_one("#term-top-bar", Static).update(
2034
+ " " + " ── ".join(parts) + streaming_flag + processing_flag
2035
+ )
1981
2036
  self._refresh_welcome_meta()
1982
2037
  except Exception:
1983
2038
  pass
@@ -1991,8 +2046,6 @@ class TermDashboardScreen(Screen):
1991
2046
  except Exception:
1992
2047
  pass
1993
2048
 
1994
-
1995
-
1996
2049
  def _refresh_hint_model(self) -> None:
1997
2050
  try:
1998
2051
  action = self.current_action_name or ""
@@ -2151,10 +2204,8 @@ class TermDashboardScreen(Screen):
2151
2204
  pass
2152
2205
  self._on_files_selected(file_paths)
2153
2206
  return
2154
- # If the input is focused, let TextArea's paste handling do its normal thing.
2155
2207
  if chat_input.has_focus:
2156
2208
  return
2157
- # Forward paste to the input when focus is elsewhere (prevents "paste does nothing").
2158
2209
  event.prevent_default()
2159
2210
  event.stop()
2160
2211
  try:
@@ -2171,24 +2222,37 @@ class TermDashboardScreen(Screen):
2171
2222
  pass
2172
2223
  # Send
2173
2224
  def _do_send(self) -> None:
2174
- # Always dismiss autocomplete before sending
2175
2225
  self._hide_slash_autocomplete()
2226
+
2176
2227
  if self._is_streaming or self._cmd_running:
2228
+ self._toast_submit_blocked("Busy streaming — you can keep typing, but can't send yet.")
2177
2229
  return
2230
+
2178
2231
  chat_input = self.query_one("#term-chat-input", ChatInput)
2179
- message = chat_input.value.strip()
2180
- if message:
2181
- chat_input.record(message)
2182
- chat_input.value = ""
2232
+ raw = chat_input.value
2233
+ message = raw.strip()
2234
+ if not message:
2235
+ return
2183
2236
 
2184
2237
  if message.startswith("/"):
2238
+ chat_input.record(message)
2239
+ chat_input.value = ""
2185
2240
  self._dispatch_slash_command(message)
2186
- else:
2187
- # First real prompt — switch to conversation mode
2188
- self._enter_conversation_mode()
2189
- self._hide_cmd_result()
2190
- self._add_message(message if message else " ", "user")
2191
- self._process_message(message)
2241
+ return
2242
+
2243
+ if self._run_inflight:
2244
+ self._toast_submit_blocked(
2245
+ f"Run {self.current_run_id or '(unknown)'} is still processing — draft your next message and send when done."
2246
+ )
2247
+ return
2248
+
2249
+ chat_input.record(message)
2250
+ chat_input.value = ""
2251
+
2252
+ self._enter_conversation_mode()
2253
+ self._hide_cmd_result()
2254
+ self._add_message(message, "user")
2255
+ self._process_message(message)
2192
2256
 
2193
2257
  # Messages
2194
2258
  @staticmethod
@@ -2298,6 +2362,7 @@ class TermDashboardScreen(Screen):
2298
2362
  if not w.is_finished:
2299
2363
  w.cancel()
2300
2364
  self._set_streaming(False)
2365
+ self._set_run_inflight(False)
2301
2366
  self.current_action_id = self.DEFAULT_ACTION_ID
2302
2367
  self.current_action_name = None
2303
2368
  self.current_run_id = None
@@ -2345,6 +2410,7 @@ class TermDashboardScreen(Screen):
2345
2410
  self.current_action_name = None
2346
2411
  self.current_run_id = None
2347
2412
  self.current_run_kind = None
2413
+ self._set_run_inflight(False)
2348
2414
  self._clear_messages()
2349
2415
  self._refresh_top_bar()
2350
2416
  self.run_worker(self._fetch_action_name_async(), group="status",
@@ -2862,6 +2928,7 @@ class TermDashboardScreen(Screen):
2862
2928
  self.current_action_name = None
2863
2929
  self.current_run_id = None
2864
2930
  self.current_run_kind = None
2931
+ self._set_run_inflight(False)
2865
2932
  self._refresh_top_bar()
2866
2933
  self._clear_messages()
2867
2934
  self.run_worker(self._fetch_action_name_async(), group="status", exclusive=False, exit_on_error=False)
@@ -2972,12 +3039,10 @@ class TermDashboardScreen(Screen):
2972
3039
  scope=effective_args.scope,
2973
3040
  allow_dirs=effective_args.allow_dirs,
2974
3041
  )
2975
- # Ensure runtime won't attribute our restore edits to an active prompt.
2976
3042
  try:
2977
3043
  set_active_entry(run_dir, None)
2978
3044
  except Exception:
2979
3045
  pass
2980
- # If latest doesn't exist yet, create it from current state (no-op restore).
2981
3046
  try:
2982
3047
  ensure_latest_snapshot_exists(run_dir=run_dir, meta=meta)
2983
3048
  except Exception:
@@ -3077,8 +3142,16 @@ class TermDashboardScreen(Screen):
3077
3142
  )
3078
3143
  if not success or not result:
3079
3144
  return
3145
+ if self.current_run_id and run_id != self.current_run_id:
3146
+ return
3080
3147
  self._render_history(result)
3081
3148
 
3149
+ status = str(result.get("status", "") or "").lower()
3150
+ inflight = status in ("processing", "running", "pending")
3151
+ self._set_run_inflight(inflight)
3152
+ if inflight:
3153
+ self._start_reattach_stream(run_id)
3154
+
3082
3155
  def _render_history(self, result: dict) -> None:
3083
3156
  if not isinstance(result, dict):
3084
3157
  return
@@ -3103,8 +3176,62 @@ class TermDashboardScreen(Screen):
3103
3176
  if text_content:
3104
3177
  self._add_message(text_content, "assistant")
3105
3178
 
3179
+
3180
+ def _start_reattach_stream(self, run_id: str) -> None:
3181
+ """Resume SSE streaming for an *existing* run that is still processing."""
3182
+ if self._is_streaming:
3183
+ return
3184
+ if not run_id:
3185
+ return
3186
+ if self.current_run_id and run_id != self.current_run_id:
3187
+ return
3188
+ if not hasattr(self.app, "autobots_client"):
3189
+ return
3190
+ # Mount a streaming placeholder if one isn't already present.
3191
+ try:
3192
+ existing = self._active_streaming_row()
3193
+ if existing:
3194
+ self._streaming_widget_ref = existing
3195
+ else:
3196
+ messages = self.query_one("#term-messages", Vertical)
3197
+ placeholder = AssistantMessageRow(
3198
+ "",
3199
+ verb="Reattaching",
3200
+ streaming=True,
3201
+ classes="term-message term-assistant-message",
3202
+ )
3203
+ messages.mount(placeholder)
3204
+ self._scroll_end_root(after_refresh=True)
3205
+ self._streaming_widget_ref = placeholder
3206
+ except Exception:
3207
+ self._streaming_widget_ref = None
3208
+ # Mark in-flight + disable input while actively streaming.
3209
+ self._set_run_inflight(True)
3210
+ self._set_streaming(True)
3211
+ self.run_worker(
3212
+ self._reattach_worker(run_id),
3213
+ exclusive=True,
3214
+ group="stream",
3215
+ exit_on_error=False,
3216
+ )
3217
+
3218
+ async def _reattach_worker(self, run_id: str) -> None:
3219
+ """Worker that attaches to SSE/polling for an already-started run."""
3220
+ try:
3221
+ self.app._sync_autobots_client_from_disk()
3222
+ except Exception:
3223
+ pass
3224
+ if not hasattr(self.app, "autobots_client"):
3225
+ return
3226
+ client = self.app.autobots_client
3227
+ # Best-effort: ensure CLI runtime is connected for this run while we stream.
3228
+ try:
3229
+ self.app.ensure_runtime_for_run_id(run_id)
3230
+ except Exception:
3231
+ pass
3232
+ await self._consume_sse(run_id, client)
3233
+
3106
3234
  # Checkpoints / rewind
3107
- # --------------------
3108
3235
  # kiwi-runtime has built-in checkpointing hooks for filesystem mutations, but only when
3109
3236
  # an "active entry" is set for the current run. The classic dashboard wires this up;
3110
3237
  # TermDashboard must do the same so /rewind has checkpoints to restore.
@@ -3188,6 +3315,7 @@ class TermDashboardScreen(Screen):
3188
3315
  except Exception:
3189
3316
  self._streaming_widget_ref = None
3190
3317
 
3318
+ self._set_run_inflight(True)
3191
3319
  self._set_streaming(True)
3192
3320
  self.run_worker(self._sse_worker(user_input), exclusive=True, group="stream")
3193
3321
 
@@ -3210,7 +3338,6 @@ class TermDashboardScreen(Screen):
3210
3338
 
3211
3339
  def _cleanup_checkpoint() -> None:
3212
3340
  if checkpoint_run_dir is not None and checkpoint_entry_id is not None:
3213
- # Update latest snapshot so "Latest Code" stays accurate across prompts.
3214
3341
  try:
3215
3342
  from kiwi_cli.checkpoints import ensure_meta, update_latest_from_entry, workspace_root_from_cwd
3216
3343
  workspace_root = workspace_root_from_cwd()
@@ -3230,15 +3357,11 @@ class TermDashboardScreen(Screen):
3230
3357
  pass
3231
3358
  self._checkpoint_clear_active(run_dir=checkpoint_run_dir)
3232
3359
 
3233
- # Auto-connect local CLI runtime for every prompt.
3234
- # For existing runs, ensure a pinned runtime is available before running the action.
3235
3360
  if self.current_run_id:
3236
3361
  try:
3237
3362
  self.app.ensure_runtime_for_run_id(self.current_run_id)
3238
3363
  except Exception:
3239
3364
  pass
3240
- # If this is a continuation, start the checkpoint entry before sending the request
3241
- # to reduce races with early file edits.
3242
3365
  checkpoint_run_dir, checkpoint_entry_id = self._checkpoint_start_for_prompt(
3243
3366
  run_id=self.current_run_id,
3244
3367
  user_prompt=user_input,
@@ -3260,6 +3383,7 @@ class TermDashboardScreen(Screen):
3260
3383
  self._add_message(f"Error starting action: {e}", "error")
3261
3384
  self._drop_placeholder()
3262
3385
  self._set_streaming(False)
3386
+ self._set_run_inflight(False)
3263
3387
  _cleanup_checkpoint()
3264
3388
  return
3265
3389
 
@@ -3285,6 +3409,7 @@ class TermDashboardScreen(Screen):
3285
3409
  self._add_message(f"Error starting action: {message}", "error")
3286
3410
  self._drop_placeholder()
3287
3411
  self._set_streaming(False)
3412
+ self._set_run_inflight(False)
3288
3413
  _cleanup_checkpoint()
3289
3414
  return
3290
3415
 
@@ -3413,6 +3538,7 @@ class TermDashboardScreen(Screen):
3413
3538
  f"(HTTP {c2}). Please log in again."
3414
3539
  )
3415
3540
  got_final = True
3541
+ self._set_run_inflight(False)
3416
3542
  self._set_streaming(False)
3417
3543
  return True
3418
3544
  if not ok or not res:
@@ -3431,6 +3557,7 @@ class TermDashboardScreen(Screen):
3431
3557
  else:
3432
3558
  self._add_message(f"Error: {err_msg}", "error")
3433
3559
  got_final = True
3560
+ self._set_run_inflight(False)
3434
3561
  self._set_streaming(False)
3435
3562
  return True
3436
3563
  if status not in ("completed", "success", "finished"):
@@ -3447,6 +3574,7 @@ class TermDashboardScreen(Screen):
3447
3574
  sw_ref[0] = None
3448
3575
  self._streaming_widget_ref = None
3449
3576
  got_final = True
3577
+ self._set_run_inflight(False)
3450
3578
  self._set_streaming(False)
3451
3579
  return True
3452
3580
  return False
@@ -3716,6 +3844,13 @@ class TermDashboardScreen(Screen):
3716
3844
  except Exception:
3717
3845
  pass
3718
3846
 
3847
+ def _set_run_inflight(self, inflight: bool) -> None:
3848
+ """Track whether the backend is still processing the latest prompt for this run."""
3849
+ self._run_inflight = bool(inflight)
3850
+ self._apply_blocking_state()
3851
+ self._refresh_top_bar()
3852
+
3853
+
3719
3854
  def _set_streaming(self, streaming: bool) -> None:
3720
3855
  self._is_streaming = streaming
3721
3856
  try:
@@ -3754,19 +3889,21 @@ class TermDashboardScreen(Screen):
3754
3889
  self._refresh_top_bar()
3755
3890
 
3756
3891
  def _apply_blocking_state(self) -> None:
3757
- disabled = self._is_streaming or self._cmd_running
3892
+ blocked = self._is_streaming or self._cmd_running or self._run_inflight
3758
3893
  try:
3759
3894
  ci = self.query_one("#term-chat-input", ChatInput)
3760
- ci.disabled = disabled
3895
+ ci.disabled = False
3761
3896
  if self._is_streaming:
3762
- ci.placeholder = "\u25cf Streaming\u2026"
3897
+ ci.placeholder = "\u25cf Streaming\u2026 (draft now; send when finished)"
3763
3898
  elif self._cmd_running:
3764
- ci.placeholder = "Running command\u2026"
3899
+ ci.placeholder = "Busy\u2026 (draft now; send when finished)"
3900
+ elif self._run_inflight:
3901
+ ci.placeholder = "\u25cf Processing\u2026 (draft now; send when finished)"
3765
3902
  else:
3766
3903
  ci.placeholder = (
3767
3904
  "Message\u2026" if self._has_conversation else "What would you like to do?"
3768
3905
  )
3769
- if not disabled:
3906
+ if not self._cmd_running:
3770
3907
  ci.focus()
3771
3908
  except Exception:
3772
3909
  pass
@@ -3939,6 +4076,7 @@ class TermDashboardScreen(Screen):
3939
4076
  pass
3940
4077
  try:
3941
4078
  self._set_streaming(False)
4079
+ self._set_run_inflight(False)
3942
4080
  except Exception:
3943
4081
  pass
3944
4082
  self._drop_placeholder()
@@ -181,16 +181,16 @@ wheels = [
181
181
 
182
182
  [[package]]
183
183
  name = "autobots-client"
184
- version = "0.1.0"
184
+ version = "0.1.1"
185
185
  source = { registry = "https://pypi.org/simple" }
186
186
  dependencies = [
187
187
  { name = "attrs" },
188
188
  { name = "httpx" },
189
189
  { name = "python-dateutil" },
190
190
  ]
191
- sdist = { url = "https://files.pythonhosted.org/packages/ac/53/976cb848a3662e429fd215399c232cdab6b4f7d75489e2a634c0e3ca5e4c/autobots_client-0.1.0.tar.gz", hash = "sha256:2df4614ba19c05d214f291471a80a7788eadfd71d0fabbe582f4d3db688ce89d", size = 179502, upload-time = "2026-03-12T22:49:44.669Z" }
191
+ sdist = { url = "https://files.pythonhosted.org/packages/1d/ab/1e6a7c2a71d3080dc0b07abffe023acf97c1aa23437b9c1fc1bdb493c33e/autobots_client-0.1.1.tar.gz", hash = "sha256:6c8164713793e6424458260a7a67216a8ffb52fccf6c34d6c8c45684fe78b7a3", size = 152893, upload-time = "2026-06-11T21:36:36.548Z" }
192
192
  wheels = [
193
- { url = "https://files.pythonhosted.org/packages/c6/c1/367c0f57909453125ac115770f426970c88012a7a5cb4076be89b613ae7f/autobots_client-0.1.0-py3-none-any.whl", hash = "sha256:ef3ca67267ac61006204a8fb9a79c9f5b6d1023ae27266c85973799888cc0108", size = 706909, upload-time = "2026-03-12T22:49:46.313Z" },
193
+ { url = "https://files.pythonhosted.org/packages/87/57/b5c5ed1a6cba22b6f243e13143d8b31ebb5075fec4451c0a9ea0b1f3046b/autobots_client-0.1.1-py3-none-any.whl", hash = "sha256:a3c2aa84c6b53eb6818f218fc0eff369d16b46244a239132ead5d0b111757183", size = 622952, upload-time = "2026-06-11T21:36:37.776Z" },
194
194
  ]
195
195
 
196
196
  [[package]]
@@ -397,7 +397,7 @@ wheels = [
397
397
 
398
398
  [[package]]
399
399
  name = "kiwi-code"
400
- version = "0.0.439"
400
+ version = "0.0.440"
401
401
  source = { editable = "." }
402
402
  dependencies = [
403
403
  { name = "autobots-client" },
@@ -421,7 +421,7 @@ test = [
421
421
 
422
422
  [package.metadata]
423
423
  requires-dist = [
424
- { name = "autobots-client", specifier = "==0.1.0" },
424
+ { name = "autobots-client", specifier = "==0.1.1" },
425
425
  { name = "httpx", specifier = ">=0.25.0" },
426
426
  { name = "loguru", specifier = ">=0.7.3" },
427
427
  { name = "psutil", specifier = ">=5.9.0" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes