systemr-cli 2.0.0__tar.gz → 2.1.1__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. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/PKG-INFO +1 -1
  2. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/__init__.py +1 -1
  3. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/chat_runner.py +9 -0
  4. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/commands/chat_commands.py +1 -1
  5. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/tui.py +173 -62
  6. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/pyproject.toml +1 -1
  7. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/.gitignore +0 -0
  8. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/DESIGN.md +0 -0
  9. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/README.md +0 -0
  10. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/clawhub/systemr-trading/SKILL.md +0 -0
  11. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/clawhub/systemr-trading/scripts/mcp_stdio_proxy.py +0 -0
  12. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/__main__.py +0 -0
  13. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/auth.py +0 -0
  14. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/cli.py +0 -0
  15. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/client.py +0 -0
  16. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/commands/__init__.py +0 -0
  17. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/commands/auth_commands.py +0 -0
  18. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/commands/cron_commands.py +0 -0
  19. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/commands/doctor_command.py +0 -0
  20. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/commands/eval_commands.py +0 -0
  21. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/commands/journal_commands.py +0 -0
  22. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/commands/plan_commands.py +0 -0
  23. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/commands/risk_commands.py +0 -0
  24. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/commands/scan_commands.py +0 -0
  25. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/commands/size_commands.py +0 -0
  26. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/config.py +0 -0
  27. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/confirmation.py +0 -0
  28. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/credits.py +0 -0
  29. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/cron.py +0 -0
  30. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/display/__init__.py +0 -0
  31. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/display/chat_renderer.py +0 -0
  32. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/display/formatters.py +0 -0
  33. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/display/tables.py +0 -0
  34. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/display/theme.py +0 -0
  35. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/hooks.py +0 -0
  36. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/logging.py +0 -0
  37. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/model_failover.py +0 -0
  38. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/orchestrator.py +0 -0
  39. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/profile.py +0 -0
  40. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/store.py +0 -0
  41. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/streaming.py +0 -0
  42. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/tool_router.py +0 -0
  43. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/neo/types.py +0 -0
  44. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/__init__.py +0 -0
  45. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_auth.py +0 -0
  46. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_chat_helpers.py +0 -0
  47. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_chat_renderer.py +0 -0
  48. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_cli.py +0 -0
  49. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_config.py +0 -0
  50. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_confirmation.py +0 -0
  51. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_credits.py +0 -0
  52. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_cron.py +0 -0
  53. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_doctor.py +0 -0
  54. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_formatters.py +0 -0
  55. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_hooks.py +0 -0
  56. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_logging.py +0 -0
  57. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_model_failover.py +0 -0
  58. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_orchestrator.py +0 -0
  59. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_profile.py +0 -0
  60. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_store.py +0 -0
  61. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_streaming.py +0 -0
  62. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_tool_router.py +0 -0
  63. {systemr_cli-2.0.0 → systemr_cli-2.1.1}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: systemr-cli
3
- Version: 2.0.0
3
+ Version: 2.1.1
4
4
  Summary: System R AI — trading operating system for agents
5
5
  Author-email: System R AI <ashim@systemr.ai>
6
6
  License-Expression: MIT
@@ -1,3 +1,3 @@
1
1
  """System R CLI — trading operating system in your terminal."""
2
2
 
3
- __version__ = "2.0.0"
3
+ __version__ = "2.1.1"
@@ -190,6 +190,8 @@ async def _stream_to_tui(
190
190
  streaming_started = False
191
191
  start_time = time.time()
192
192
 
193
+ tui.set_processing(True)
194
+
193
195
  try:
194
196
  async for event in stream_chat(request, access_token):
195
197
  if event.event == "thinking":
@@ -248,6 +250,11 @@ async def _stream_to_tui(
248
250
  credits_used=credits_used, balance=balance,
249
251
  tools=tool_count, trade=has_trade,
250
252
  )
253
+ if credits_used:
254
+ try:
255
+ tui.add_credits(float(credits_used))
256
+ except (ValueError, TypeError):
257
+ pass
251
258
 
252
259
  elif event.event == "error":
253
260
  raw = event.parsed.get("message", event.parsed.get("error", ""))
@@ -263,6 +270,8 @@ async def _stream_to_tui(
263
270
  except Exception as exc:
264
271
  tui.add_error(f"Connection error: {str(exc)[:60]}")
265
272
  logger.error("stream_error", error=str(exc))
273
+ finally:
274
+ tui.set_processing(False)
266
275
 
267
276
  return collected_text
268
277
 
@@ -138,7 +138,7 @@ async def _pipe_mode(token, user_input: str, model_name: str | None, research: b
138
138
  text_parts: list[str] = []
139
139
  tools_used: list[str] = []
140
140
 
141
- async for event in stream_chat(token.access_token, request):
141
+ async for event in stream_chat(request, token.access_token):
142
142
  if event.event == "text_delta" and event.data:
143
143
  text_parts.append(event.data.get("text", ""))
144
144
  elif event.event == "action" and event.data:
@@ -1,17 +1,22 @@
1
1
  """Terminal UI Application — fixed-layout chat interface.
2
2
 
3
- Uses prompt_toolkit's Application to create a layout with:
4
- - Scrollable conversation area (top)
5
- - Fixed separator + input + separator + status bar (bottom)
6
-
7
- This replaces the Rich console.print() approach with a proper
8
- TUI that supports streaming text, fixed bars, and live updates.
3
+ Full-screen prompt_toolkit Application with:
4
+ - Scrollable conversation area (top) with auto-scroll
5
+ - Fixed separator + input prompt (middle)
6
+ - Fixed separator + live status bar (bottom)
7
+
8
+ Keyboard shortcuts:
9
+ - Enter: submit message
10
+ - Ctrl+D / Ctrl+C: exit
11
+ - Ctrl+L: clear conversation
12
+ - Page Up/Down: scroll conversation
13
+ - Up/Down: input history
14
+ - Escape: cancel current input
9
15
  """
10
16
 
11
17
  from __future__ import annotations
12
18
 
13
19
  import asyncio
14
- import sys
15
20
  import time
16
21
  from typing import Any, Callable
17
22
 
@@ -27,11 +32,12 @@ from prompt_toolkit.layout import (
27
32
  )
28
33
  from prompt_toolkit.layout.layout import Layout
29
34
  from prompt_toolkit.layout.dimension import Dimension
30
- from prompt_toolkit.formatted_text import FormattedText, HTML
35
+ from prompt_toolkit.formatted_text import FormattedText
31
36
  from prompt_toolkit.history import FileHistory
32
37
  from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
38
+ from prompt_toolkit.styles import Style
33
39
 
34
- # Colors matching theme.py
40
+ # ── Colors (match theme.py) ──
35
41
  GREEN = "#3ECF8E"
36
42
  GREEN_DIM = "#34B87A"
37
43
  RED = "#EF4444"
@@ -41,6 +47,25 @@ GRAY = "#A1A1AA"
41
47
  WHITE = "#F5F5F5"
42
48
  AMBER = "#F59E0B"
43
49
  BG = "#09090B"
50
+ BG_INPUT = "#111114"
51
+
52
+ # ── Prompt toolkit style ──
53
+ TUI_STYLE = Style.from_dict({
54
+ # Conversation area
55
+ "conversation": f"bg:{BG} {GRAY}",
56
+ # Input area
57
+ "input-prompt": f"bold {WHITE}",
58
+ # Separators
59
+ "separator": MUTED,
60
+ # Status bar
61
+ "status": f"bg:{BG_INPUT} {DIM}",
62
+ "status.green": GREEN,
63
+ "status.red": RED,
64
+ "status.dim": DIM,
65
+ "status.muted": MUTED,
66
+ # Auto-suggest
67
+ "auto-suggest": DIM,
68
+ })
44
69
 
45
70
 
46
71
  class ChatTUI:
@@ -55,7 +80,7 @@ class ChatTUI:
55
80
  ├──────────────────────────────────────┤
56
81
  │ ❯ user input │
57
82
  ├──────────────────────────────────────┤
58
- │ ● sonnet │ ○ broker │ /help commands
83
+ │ ● sonnet │ ○ broker │ /help │ 2m 3s │
59
84
  └──────────────────────────────────────┘
60
85
  """
61
86
 
@@ -64,15 +89,17 @@ class ChatTUI:
64
89
  model: str = "sonnet",
65
90
  broker: str | None = None,
66
91
  history_file: str = "",
67
- on_submit: Callable[[str], None] | None = None,
68
92
  ) -> None:
69
93
  self._model = model
70
94
  self._broker = broker
71
- self._on_submit = on_submit
72
- self._conversation_lines: list[tuple[str, str]] = [] # (style, text) pairs
95
+ self._conversation_lines: list[tuple[str, str]] = []
73
96
  self._running = True
74
97
  self._input_ready = asyncio.Event()
75
98
  self._last_input = ""
99
+ self._credits_used: float = 0.0
100
+ self._tools_called: int = 0
101
+ self._start_time = time.time()
102
+ self._processing = False
76
103
 
77
104
  # Conversation display
78
105
  self._conversation_control = FormattedTextControl(
@@ -80,6 +107,12 @@ class ChatTUI:
80
107
  focusable=False,
81
108
  )
82
109
 
110
+ # Scrollable conversation window
111
+ self._conversation_window = Window(
112
+ content=self._conversation_control,
113
+ wrap_lines=True,
114
+ )
115
+
83
116
  # Input buffer
84
117
  self._input_buffer = Buffer(
85
118
  name="input",
@@ -89,8 +122,11 @@ class ChatTUI:
89
122
  multiline=False,
90
123
  )
91
124
 
92
- # Separator bar
93
- sep_text = FormattedText([(MUTED, "─" * 200)])
125
+ # Separator
126
+ self._sep_control = FormattedTextControl(
127
+ text=lambda: FormattedText([(MUTED, "─" * 300)]),
128
+ focusable=False,
129
+ )
94
130
 
95
131
  # Status bar
96
132
  self._status_control = FormattedTextControl(
@@ -104,44 +140,49 @@ class ChatTUI:
104
140
  @kb.add("c-d")
105
141
  def _exit(event: Any) -> None:
106
142
  self._running = False
143
+ self._input_ready.set()
107
144
  event.app.exit(result=None)
108
145
 
109
146
  @kb.add("c-c")
110
- def _cancel(event: Any) -> None:
111
- self._running = False
112
- event.app.exit(result=None)
147
+ def _interrupt(event: Any) -> None:
148
+ if self._processing:
149
+ # During processing: just signal (future: cancel stream)
150
+ pass
151
+ else:
152
+ self._running = False
153
+ self._input_ready.set()
154
+ event.app.exit(result=None)
155
+
156
+ @kb.add("c-l")
157
+ def _clear_screen(event: Any) -> None:
158
+ self._conversation_lines.clear()
159
+ self._invalidate()
160
+
161
+ @kb.add("escape")
162
+ def _cancel_input(event: Any) -> None:
163
+ self._input_buffer.text = ""
113
164
 
114
165
  # Layout
115
166
  self._layout = Layout(
116
167
  HSplit([
117
- # Scrollable conversation
168
+ # Scrollable conversation (takes all available space)
118
169
  ScrollablePane(
119
- Window(
120
- content=self._conversation_control,
121
- wrap_lines=True,
122
- ),
170
+ self._conversation_window,
123
171
  ),
124
172
  # Top separator
125
- Window(
126
- content=FormattedTextControl(text=sep_text),
127
- height=1,
128
- ),
129
- # Input line
173
+ Window(content=self._sep_control, height=1),
174
+ # Input line with ❯ prefix
130
175
  Window(
131
176
  content=BufferControl(buffer=self._input_buffer),
132
177
  height=1,
133
- get_line_prefix=lambda lineno, wrap: [("bold " + WHITE, " ❯ ")],
178
+ get_line_prefix=lambda lineno, wrap: FormattedText(
179
+ [("bold " + WHITE, " ❯ ")]
180
+ ),
134
181
  ),
135
182
  # Bottom separator
136
- Window(
137
- content=FormattedTextControl(text=sep_text),
138
- height=1,
139
- ),
183
+ Window(content=self._sep_control, height=1),
140
184
  # Status bar
141
- Window(
142
- content=self._status_control,
143
- height=1,
144
- ),
185
+ Window(content=self._status_control, height=1),
145
186
  ]),
146
187
  focused_element=self._input_buffer,
147
188
  )
@@ -151,94 +192,152 @@ class ChatTUI:
151
192
  layout=self._layout,
152
193
  key_bindings=kb,
153
194
  full_screen=True,
154
- mouse_support=False,
155
- style=None,
195
+ mouse_support=True,
196
+ style=TUI_STYLE,
156
197
  )
157
198
 
199
+ # ── Internal handlers ──
200
+
158
201
  def _on_accept(self, buffer: Buffer) -> bool:
159
- """Handle Enter press in input buffer."""
160
- self._last_input = buffer.text.strip()
202
+ """Handle Enter press."""
203
+ text = buffer.text.strip()
204
+ if text:
205
+ # Echo user input in conversation
206
+ self._conversation_lines.append((WHITE, f"\n ❯ {text}\n"))
207
+ self._last_input = text
161
208
  self._input_ready.set()
162
- return False # Don't keep text in buffer
209
+ return False
163
210
 
164
211
  def _get_conversation_text(self) -> FormattedText:
165
- """Render conversation lines as FormattedText."""
212
+ """Render conversation area."""
166
213
  if not self._conversation_lines:
167
- return FormattedText([(DIM, " Type your message below. /help for commands.\n")])
214
+ return FormattedText([
215
+ (DIM, "\n"),
216
+ (DIM, " Type your message below.\n"),
217
+ (DIM, " /help for commands. Ctrl+D to exit.\n"),
218
+ ])
168
219
  return FormattedText(self._conversation_lines)
169
220
 
170
221
  def _get_status_text(self) -> FormattedText:
171
- """Render status bar."""
222
+ """Render status bar with live data."""
223
+ elapsed = int(time.time() - self._start_time)
224
+ mins, secs = divmod(elapsed, 60)
225
+ time_str = f"{mins}m {secs}s" if mins else f"{secs}s"
226
+
172
227
  parts: list[tuple[str, str]] = [("", " ")]
228
+
229
+ # Model
173
230
  parts.append((GREEN, "● "))
174
231
  parts.append((DIM, self._model))
175
232
  parts.append((MUTED, " │ "))
233
+
234
+ # Broker
176
235
  if self._broker:
177
236
  parts.append((GREEN, "● "))
178
237
  parts.append((DIM, self._broker))
179
238
  else:
180
239
  parts.append((RED, "○ "))
181
- parts.append((DIM, "broker not connected"))
240
+ parts.append((DIM, "no broker"))
182
241
  parts.append((MUTED, " │ "))
183
- parts.append((DIM, "/help commands"))
242
+
243
+ # Stats
244
+ if self._tools_called > 0:
245
+ parts.append((DIM, f"{self._tools_called} tools"))
246
+ parts.append((MUTED, " │ "))
247
+
248
+ if self._credits_used > 0:
249
+ parts.append((DIM, f"${self._credits_used:.3f}"))
250
+ parts.append((MUTED, " │ "))
251
+
252
+ # Timer
253
+ parts.append((DIM, time_str))
254
+ parts.append((MUTED, " │ "))
255
+
256
+ # Help hint
257
+ parts.append((DIM, "/help"))
258
+
184
259
  return FormattedText(parts)
185
260
 
186
- # ── Public API for streaming content ──
261
+ # ── Public API ──
187
262
 
188
263
  def add_line(self, style: str, text: str) -> None:
189
- """Add a line to the conversation area."""
264
+ """Add a line to conversation."""
190
265
  self._conversation_lines.append((style, text + "\n"))
191
- self._invalidate()
266
+ self._scroll_to_bottom()
192
267
 
193
268
  def add_text(self, style: str, text: str) -> None:
194
269
  """Add inline text (no newline) for streaming."""
195
270
  self._conversation_lines.append((style, text))
196
- self._invalidate()
271
+ self._scroll_to_bottom()
197
272
 
198
273
  def add_thinking(self, text: str) -> None:
199
- """Show thinking status."""
200
- self.add_line(DIM, f" ◐ {text}")
274
+ """Show thinking status. Replaces previous thinking line."""
275
+ # Remove previous thinking line if exists
276
+ if (self._conversation_lines and
277
+ self._conversation_lines[-1][1].startswith(" ◐")):
278
+ self._conversation_lines.pop()
279
+ self._conversation_lines.append((DIM, f" ◐ {text}\n"))
280
+ self._scroll_to_bottom()
281
+
282
+ def clear_thinking(self) -> None:
283
+ """Remove the current thinking line."""
284
+ if (self._conversation_lines and
285
+ self._conversation_lines[-1][1].strip().startswith("◐")):
286
+ self._conversation_lines.pop()
287
+ self._invalidate()
201
288
 
202
289
  def add_tool_running(self, name: str) -> None:
203
290
  """Show tool starting."""
291
+ self.clear_thinking()
204
292
  self.add_line(DIM, f" ◐ {name}...")
205
293
 
206
294
  def add_tool_done(self, name: str, cost: str = "") -> None:
207
295
  """Show tool completed."""
296
+ # Replace running line
297
+ if (self._conversation_lines and
298
+ "◐" in self._conversation_lines[-1][1]):
299
+ self._conversation_lines.pop()
208
300
  suffix = f" {cost}" if cost else ""
209
301
  self.add_line(GREEN, f" ✓ {name}{suffix}")
302
+ self._tools_called += 1
210
303
 
211
304
  def add_tool_error(self, name: str, error: str = "") -> None:
212
305
  """Show tool error."""
306
+ if (self._conversation_lines and
307
+ "◐" in self._conversation_lines[-1][1]):
308
+ self._conversation_lines.pop()
213
309
  suffix = f" {error}" if error else ""
214
310
  self.add_line(RED, f" ✗ {name}{suffix}")
215
311
 
216
312
  def add_error(self, msg: str) -> None:
217
313
  """Show error message."""
314
+ self.clear_thinking()
218
315
  self.add_line(RED, f" ✗ {msg}")
219
316
 
220
317
  def start_response(self) -> None:
221
318
  """Start a new response block with green border."""
319
+ self.clear_thinking()
222
320
  self.add_text(GREEN, "\n ┃ ")
223
321
 
224
322
  def stream_text(self, text: str) -> None:
225
- """Stream text within a response block."""
323
+ """Stream text within a response block, char by char."""
226
324
  for ch in text:
227
325
  if ch == "\n":
228
- self.add_text(GREEN, "\n ┃ ")
326
+ self._conversation_lines.append((GREEN, "\n ┃ "))
229
327
  else:
230
- self.add_text("", ch)
231
- self._invalidate()
328
+ self._conversation_lines.append(("", ch))
329
+ self._scroll_to_bottom()
232
330
 
233
331
  def end_response(self, elapsed: float) -> None:
234
332
  """End response block with timing."""
235
333
  self.add_text(GREEN, "\n ┃")
236
- self.add_line(DIM, f"\n ✦ {elapsed:.1f}s\n")
334
+ self.add_line(DIM, f"\n ✦ {elapsed:.1f}s")
335
+ self.add_text("", "\n")
237
336
 
238
- def add_result_row(self, key: str, value: str) -> None:
239
- """Add a key-value result row."""
240
- self.add_line("", f" {key}: ")
241
- self.add_text(WHITE, value)
337
+ def add_credits(self, amount: float) -> None:
338
+ """Track credits used (updates status bar)."""
339
+ self._credits_used += amount
340
+ self._invalidate()
242
341
 
243
342
  def set_model(self, model: str) -> None:
244
343
  """Update model in status bar."""
@@ -250,11 +349,20 @@ class ChatTUI:
250
349
  self._broker = broker
251
350
  self._invalidate()
252
351
 
352
+ def set_processing(self, active: bool) -> None:
353
+ """Set processing state (affects Ctrl+C behavior)."""
354
+ self._processing = active
355
+ self._invalidate()
356
+
253
357
  def clear_conversation(self) -> None:
254
358
  """Clear conversation area."""
255
359
  self._conversation_lines.clear()
256
360
  self._invalidate()
257
361
 
362
+ def _scroll_to_bottom(self) -> None:
363
+ """Scroll conversation to bottom and redraw."""
364
+ self._invalidate()
365
+
258
366
  def _invalidate(self) -> None:
259
367
  """Request UI redraw."""
260
368
  if self._app and self._app.is_running:
@@ -264,12 +372,14 @@ class ChatTUI:
264
372
  """Wait for user to submit input. Returns None on exit."""
265
373
  self._input_ready.clear()
266
374
  self._input_buffer.text = ""
375
+ self._processing = False
267
376
  self._invalidate()
268
377
 
269
378
  await self._input_ready.wait()
270
379
 
271
380
  if not self._running:
272
381
  return None
382
+ self._processing = True
273
383
  return self._last_input
274
384
 
275
385
  async def run_async(self) -> None:
@@ -279,5 +389,6 @@ class ChatTUI:
279
389
  def exit(self) -> None:
280
390
  """Exit the application."""
281
391
  self._running = False
392
+ self._input_ready.set()
282
393
  if self._app.is_running:
283
394
  self._app.exit()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "systemr-cli"
7
- version = "2.0.0"
7
+ version = "2.1.1"
8
8
  description = "System R AI — trading operating system for agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
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