Angel is a TUI-based autonomous coding agent built on fauxtp GenServers.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fdsajniofsdoinmu

fizzAI da05f105 fa6a8f2c

+37 -14
+37 -14
angel/tui.py
··· 1 1 """Angel TUI — prompt_toolkit Application-based terminal interface. 2 2 3 3 A lightweight full-screen TUI that keeps an output pane above a persistent 4 - input prompt. Coloring is applied by a simple line-prefix lexer — no Rich, 5 - no CSS, no heavyweight widget toolkit. 4 + input prompt. Thinking/tool status is shown in a dedicated status bar above 5 + the input line (à la Claude Code), keeping the chat window clean. 6 6 """ 7 7 8 8 from __future__ import annotations ··· 15 15 from prompt_toolkit.filters import Condition 16 16 from prompt_toolkit.formatted_text import ANSI, StyleAndTextTuples 17 17 from prompt_toolkit.key_binding import KeyBindings 18 - from prompt_toolkit.layout import HSplit, Layout, VSplit, Window 18 + from prompt_toolkit.layout import ConditionalContainer, HSplit, Layout, Window 19 19 from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl 20 20 from prompt_toolkit.layout.processors import BeforeInput 21 21 from prompt_toolkit.lexers import Lexer ··· 35 35 "output.welcome": "#c77dff", 36 36 "output.default": "#e8e0f0", 37 37 "separator": "#c77dff", 38 + "status-bar": "#c77dff italic", 38 39 "input": "#e8e0f0", 39 40 } 40 41 ) ··· 73 74 return "output.title" 74 75 if any(line.startswith(p) for p in ("✨", "🧠", "Still")): 75 76 return "output.status" 76 - if line.startswith("I'm Angel") or line.startswith("Type a message"): 77 + if line.startswith("I'm Angel — your autonomous coding agent.") or line.startswith("Type a message and I'll get to work. Ctrl+C to quit"): 77 78 return "output.welcome" 78 79 return "output.default" 79 80 ··· 98 99 self._cast: Callable[..., Any] | None = cast_fn 99 100 self._busy = False 100 101 self._app: Application[None] | None = None 102 + self._status_text: str = "" 101 103 102 104 self._output_buffer = Buffer( 103 105 name="output", ··· 105 107 ) 106 108 self._input_buffer = Buffer(name="input", multiline=False) 107 109 110 + # ── status bar helpers ────────────────────────────────────────── 111 + 112 + def _get_status_fragments(self) -> StyleAndTextTuples: 113 + """Return formatted text for the thinking/status bar.""" 114 + if self._status_text: 115 + return [("class:status-bar", f" {self._status_text}")] 116 + return [] 117 + 118 + def _set_status(self, text: str) -> None: 119 + """Update the status bar text and repaint.""" 120 + self._status_text = text 121 + if self._app is not None: 122 + self._app.invalidate() 123 + 108 124 # ── build the prompt_toolkit Application ──────────────────────── 109 125 110 126 def _build(self) -> Application[None]: ··· 146 162 height=1, 147 163 ) 148 164 165 + status_bar = ConditionalContainer( 166 + content=Window( 167 + content=FormattedTextControl(self._get_status_fragments), 168 + height=1, 169 + style="class:status-bar", 170 + ), 171 + filter=Condition(lambda: bool(self._status_text)), 172 + ) 173 + 149 174 input_window = Window( 150 175 content=BufferControl( 151 176 buffer=self._input_buffer, ··· 157 182 style="class:input", 158 183 ) 159 184 160 - root = HSplit([output_window, separator, input_window]) 185 + root = HSplit([output_window, separator, status_bar, input_window]) 161 186 162 187 return Application( 163 188 layout=Layout(root, focused_element=input_window), ··· 218 243 # ── callbacks from the actor thread ───────────────────────────── 219 244 220 245 def on_agent_status(self, text: str) -> None: 221 - self._append(text) 246 + """Show thinking status in the status bar, not the chat.""" 247 + self._set_status(text) 222 248 223 249 def on_tool_call(self, name: str, args_preview: str) -> None: 250 + """Log tool calls to the chat and update the status bar.""" 224 251 preview = args_preview 225 252 if len(preview) > 120: 226 253 preview = preview[:120] + "…" 227 254 self._append(f" 🔧 {name} {preview}") 255 + self._set_status(f"🔧 {name}") 228 256 229 257 def on_tool_result(self, name: str, result_preview: str) -> None: 230 - lines = result_preview.split("\n") 231 - if len(lines) > 8: 232 - result_preview = ( 233 - "\n".join(lines[:8]) 234 - + f"\n … ({len(lines)} lines total)" 235 - ) 236 - self._append(f" ↳ {result_preview}") 258 + """Suppressed by default — tool results don't clutter the chat.""" 259 + pass 237 260 238 261 def on_agent_done(self, final_text: str) -> None: 262 + self._set_status("") # clear the status bar 239 263 self._append(f"⟩ Angel\n{final_text}") 240 264 self._append("─" * 60) 241 - self._append("✨ Ready") 242 265 self._busy = False