agentram 0.1.9 → 0.1.10

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.
package/README.md CHANGED
@@ -998,3 +998,14 @@ Supervisor creates plan -> Main agent executes -> Small agent records RAM notes
998
998
  ```
999
999
 
1000
1000
  Small agent should not own big direction. It records plans, decisions, files, risks, next tasks, and coding facts into RAM.
1001
+
1002
+ ## Textual TUI
1003
+
1004
+ Install rich terminal UI with native scrollable chat pane:
1005
+
1006
+ ```bash
1007
+ pip install agentram[tui]
1008
+ agentram tui
1009
+ ```
1010
+
1011
+ When `textual` is installed, AgentRAM uses a real full-screen TUI with mouse wheel scrolling inside the chat frame. If `textual` is missing, it falls back to the basic print-based terminal UI.
package/agentram/cli.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import asyncio
4
5
  from concurrent.futures import ThreadPoolExecutor
5
6
  import json
6
7
  import os
@@ -44,10 +45,10 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
44
45
  ("/workflow", "Show supervisor workflow agents"),
45
46
  ("/route on|off|status", "Toggle auto prompt routing"),
46
47
  ("/orchestrate <task>", "Build multi-model agent plan"),
47
- ("/ask <prompt>", "Execute bound models and return outputs"),
48
- ("/claude-install [path]", "Install .claude commands and agents"),
49
- ("/note <text>", "Save decision note"),
50
- ("/clear", "Clear chat"),
48
+ ("/ask <prompt>", "Execute bound models and return outputs"),
49
+ ("/claude-install [path]", "Install .claude commands and agents"),
50
+ ("/note <text>", "Save decision note"),
51
+ ("/clear", "Clear chat"),
51
52
  ("/exit", "Quit"),
52
53
  ("Plain text", "Capture coding chat message and retrieve context"),
53
54
  ]
@@ -76,9 +77,9 @@ SLASH_HELP = """Slash commands:
76
77
  /route on|off|status Toggle auto prompt routing
77
78
  /orchestrate <task> Build multi-model agent plan
78
79
  /ask <prompt> Execute bound models and return outputs
79
- /claude-install [path] Install .claude commands and agents
80
- /note <text> Save decision note
81
- /clear Clear chat
80
+ /claude-install [path] Install .claude commands and agents
81
+ /note <text> Save decision note
82
+ /clear Clear chat
82
83
  /exit Quit
83
84
  Plain text Capture coding chat message and retrieve context
84
85
  """.strip()
@@ -237,34 +238,121 @@ def run_command(args: argparse.Namespace) -> int:
237
238
  raise SystemExit(f"unknown command: {command}")
238
239
 
239
240
 
240
- def run_tui(context: McpContext) -> None:
241
- context.init_storage()
241
+ def run_tui(context: McpContext) -> None:
242
+ if run_textual_tui(context):
243
+ return
244
+ context.init_storage()
242
245
  prompt_session = create_prompt_session()
243
246
  input_hint = "live slash autocomplete enabled" if prompt_session else "basic input fallback; install prompt_toolkit for live slash dropdown"
244
- messages = [
245
- "system > AgentRAM coding-agent CLI ready",
246
- f"system > RAM root: {context.ram_root}",
247
- f"system > {input_hint}",
248
- ]
249
- while True:
250
- draw_screen(context, messages)
247
+ messages = [
248
+ "system > AgentRAM coding-agent CLI ready",
249
+ f"system > RAM root: {context.ram_root}",
250
+ f"system > {input_hint}",
251
+ ]
252
+ while True:
253
+ draw_screen(context, messages)
251
254
  try:
252
255
  user_input = read_tui_input(prompt_session).strip()
253
256
  except (EOFError, KeyboardInterrupt):
254
257
  return
255
- if user_input in {"0", "q", "quit", "exit", "/exit", "/quit"}:
256
- return
257
- if user_input in BUTTONS:
258
- user_input = BUTTONS[user_input]
259
- if user_input:
260
- messages.append(f"user > {user_input}")
258
+ if user_input in {"0", "q", "quit", "exit", "/exit", "/quit"}:
259
+ return
260
+ if user_input in BUTTONS:
261
+ user_input = BUTTONS[user_input]
262
+ if user_input:
263
+ messages.append(f"user > {user_input}")
261
264
  response = run_with_activity(context, messages, user_input)
262
265
  if response == "__clear__":
263
266
  messages.clear()
264
267
  messages.append("system > Chat cleared.")
265
268
  elif response:
266
269
  messages.append("agentram > " + response)
267
- del messages[:-10]
270
+ del messages[:-200]
271
+
272
+ def run_textual_tui(context: McpContext) -> bool:
273
+ try:
274
+ from textual.app import App, ComposeResult
275
+ from textual.containers import Horizontal, Vertical
276
+ from textual.widgets import Footer, Header, Input, RichLog, Static
277
+ except ImportError:
278
+ return False
279
+
280
+ class AgentRAMTextualApp(App[None]):
281
+ CSS = """
282
+ Screen { background: #0b0f14; color: #d7dde8; }
283
+ #layout { height: 1fr; }
284
+ #sidebar { width: 32; border: solid #263241; padding: 1; }
285
+ #main { width: 1fr; }
286
+ #chat { height: 1fr; border: solid #263241; padding: 1; overflow-y: scroll; }
287
+ #input { dock: bottom; border: solid #3b82f6; }
288
+ .muted { color: #8b9bb0; }
289
+ """
290
+ BINDINGS = [("ctrl+c", "quit", "Quit")]
291
+
292
+ def __init__(self, mcp_context: McpContext) -> None:
293
+ super().__init__()
294
+ self.context = mcp_context
295
+ self.messages: list[str] = []
296
+ self.busy = False
297
+
298
+ def compose(self) -> ComposeResult:
299
+ yield Header(show_clock=True)
300
+ with Horizontal(id="layout"):
301
+ yield Static(id="sidebar")
302
+ with Vertical(id="main"):
303
+ yield RichLog(id="chat", wrap=True, highlight=True, markup=False, auto_scroll=True)
304
+ yield Input(placeholder="Prompt hoặc /command. Mouse wheel scroll trong khung chat.", id="input")
305
+ yield Footer()
306
+
307
+ def on_mount(self) -> None:
308
+ self.context.init_storage()
309
+ self.chat.write("system > AgentRAM Textual TUI ready")
310
+ self.chat.write(f"system > RAM root: {self.context.ram_root}")
311
+ self.refresh_sidebar()
312
+ self.set_interval(1.0, self.refresh_sidebar)
313
+
314
+ @property
315
+ def chat(self) -> RichLog:
316
+ return self.query_one("#chat", RichLog)
317
+
318
+ def refresh_sidebar(self) -> None:
319
+ self.query_one("#sidebar", Static).update("\n".join(sidebar_lines(self.context)))
320
+
321
+ async def on_input_submitted(self, event: Input.Submitted) -> None:
322
+ text = event.value.strip()
323
+ event.input.value = ""
324
+ if not text:
325
+ return
326
+ if text in {"0", "q", "quit", "exit", "/exit", "/quit"}:
327
+ self.exit()
328
+ return
329
+ if text in BUTTONS:
330
+ text = BUTTONS[text]
331
+ self.chat.write(f"user > {text}")
332
+ if text == "/clear":
333
+ self.chat.clear()
334
+ self.chat.write("system > Chat cleared.")
335
+ return
336
+ if self.busy:
337
+ self.chat.write("agentram > busy, wait current task")
338
+ return
339
+ self.busy = True
340
+ self.chat.write("agentram > thinking...")
341
+ try:
342
+ response = await asyncio.to_thread(handle_chat_input, self.context, text, self.messages)
343
+ except Exception as error: # noqa: BLE001 - TUI boundary
344
+ response = f"error: {error}"
345
+ finally:
346
+ self.busy = False
347
+ if response == "__clear__":
348
+ self.chat.clear()
349
+ self.chat.write("system > Chat cleared.")
350
+ elif response:
351
+ self.chat.write("agentram > " + response)
352
+ self.refresh_sidebar()
353
+
354
+ AgentRAMTextualApp(context).run()
355
+ return True
268
356
 
269
357
  def run_with_activity(context: McpContext, messages: list[str], user_input: str) -> str:
270
358
  with ThreadPoolExecutor(max_workers=1) as executor:
@@ -908,7 +996,7 @@ def draw_screen(context: McpContext, messages: list[str], activity: str | None =
908
996
  print(box_text("◆ AgentRAM Code | RAM + Claude subagents + slash commands", width))
909
997
  print("+" + "-" * (width - 2) + "+")
910
998
 
911
- sidebar = sidebar_lines(context)
999
+ sidebar = sidebar_lines(context)
912
1000
  display_messages = [*messages, activity] if activity else messages
913
1001
  chat = wrap_messages(display_messages, right_width - 4)[-22:]
914
1002
  rows = max(len(sidebar), len(chat), 22)
@@ -917,7 +1005,7 @@ def draw_screen(context: McpContext, messages: list[str], activity: str | None =
917
1005
  right = chat[index] if index < len(chat) else ""
918
1006
  print("| " + left[: left_width - 2].ljust(left_width - 2) + "| " + right[: right_width - 2].ljust(right_width - 2) + "|")
919
1007
  print("+" + "-" * (left_width - 1) + "+" + "-" * (right_width) + "+")
920
- footer = "Thinking/acting..." if activity else "Input: type / for live dropdown. Enter '/' for full palette. /ask calls models."
1008
+ footer = "Thinking/acting..." if activity else "Input: type / for commands. Use mouse wheel or terminal scrollbar for previous output."
921
1009
  print(box_text(footer, width))
922
1010
  print("+" + "-" * (width - 2) + "+")
923
1011
 
@@ -957,7 +1045,8 @@ def sidebar_lines(context: McpContext) -> list[str]:
957
1045
  " /ask <prompt>",
958
1046
  ]
959
1047
 
960
-
1048
+
1049
+
961
1050
  def trim(text: str, limit: int) -> str:
962
1051
  return text if len(text) <= limit else text[: limit - 1] + "…"
963
1052
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentram",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Async, model-agnostic Working RAM for coding agents.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/trancongnghia/AGENT_RAM#readme",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentram"
7
- version = "0.1.9"
7
+ version = "0.1.10"
8
8
  description = "Async, model-agnostic RAM layer for agentic coding tools."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -22,7 +22,7 @@ requires-python = ">=3.10"
22
22
  dependencies = []
23
23
 
24
24
  [project.optional-dependencies]
25
- tui = ["prompt_toolkit>=3.0"]
25
+ tui = ["prompt_toolkit>=3.0", "textual>=0.80"]
26
26
 
27
27
  [project.urls]
28
28
  Homepage = "https://github.com/your-org/agentram"