1111from textual .app import App
1212from textual .binding import Binding
1313from textual .containers import Container , Horizontal , VerticalScroll
14- from textual .widgets import Button , Footer , Input , Markdown
14+ from textual .widgets import Button , Footer , Input , LoadingIndicator , Markdown
1515
1616from ..core import command_uses_new_session , get_api , new_session_path
1717from ..session import Assistant , Session , User
@@ -29,7 +29,7 @@ class Markdown(
2929 BINDINGS = [
3030 Binding ("ctrl+y" , "yank" , "Yank text" , show = True ),
3131 Binding ("ctrl+r" , "resubmit" , "resubmit" , show = True ),
32- Binding ("ctrl+x" , "delete " , "delete to end " , show = True ),
32+ Binding ("ctrl+x" , "redraft " , "redraft " , show = True ),
3333 Binding ("ctrl+q" , "toggle_history" , "history toggle" , show = True ),
3434 ]
3535
@@ -51,14 +51,22 @@ class CancelButton(Button):
5151class Tui (App ):
5252 CSS_PATH = "tui.css"
5353 BINDINGS = [
54- Binding ("ctrl+q " , "quit" , "Quit" , show = True , priority = True ),
54+ Binding ("ctrl+c " , "quit" , "Quit" , show = True , priority = True ),
5555 ]
5656
5757 def __init__ (self , api = None , session = None ):
5858 super ().__init__ ()
5959 self .api = api or get_api ("lorem" )
6060 self .session = session or Session .new_session (self .api .system_message )
6161
62+ @property
63+ def spinner (self ):
64+ return self .query_one (LoadingIndicator )
65+
66+ @property
67+ def wait (self ):
68+ return self .query_one ("#wait" )
69+
6270 @property
6371 def input (self ):
6472 return self .query_one (Input )
@@ -75,28 +83,33 @@ def compose(self):
7583 yield Footer ()
7684 yield VerticalScroll (
7785 * [markdown_for_step (step ) for step in self .session .session ],
86+ # The pad container helps reduce flickering when rendering fresh
87+ # content and scrolling. (it's not clear why this makes a
88+ # difference and it'd be nice to be rid of the workaround)
7889 Container (id = "pad" ),
7990 id = "content" ,
8091 )
81- with Horizontal (id = "inputbox" ):
82- yield CancelButton (label = "❌" , id = "cancel" )
83- yield Input (placeholder = "Prompt" )
92+ yield Input (placeholder = "Prompt" )
93+ with Horizontal (id = "wait" ):
94+ yield LoadingIndicator ()
95+ yield CancelButton (label = "❌ Stop Generation" , id = "cancel" , disabled = True )
8496
8597 async def on_mount (self ) -> None :
8698 self .container .scroll_end (animate = False )
8799 self .input .focus ()
88- self .cancel_button .disabled = True
89- self .cancel_button .styles .display = "none"
90100
91101 async def on_input_submitted (self , event ) -> None :
92102 self .get_completion (event .value )
93103
94104 @work (exclusive = True )
95105 async def get_completion (self , query ):
96106 self .scroll_end ()
107+
108+ self .input .styles .display = "none"
109+ self .wait .styles .display = "block"
97110 self .input .disabled = True
98111 self .cancel_button .disabled = False
99- self . cancel_button . styles . display = "block"
112+
100113 self .cancel_button .focus ()
101114 output = markdown_for_step (Assistant ("*query sent*" ))
102115 await self .container .mount_all (
@@ -105,6 +118,9 @@ async def get_completion(self, query):
105118 tokens = []
106119 update = asyncio .Queue (1 )
107120
121+ for markdown in self .container .children :
122+ markdown .disabled = True
123+
108124 # Construct a fake session with only select items
109125 session = Session ()
110126 for si , wi in zip (self .session .session , self .container .children ):
@@ -140,16 +156,21 @@ async def get_token_fun():
140156
141157 try :
142158 await asyncio .gather (render_fun (), get_token_fun ())
143- self .input .value = ""
144159 finally :
160+ self .input .value = ""
145161 all_output = self .session .session [- 1 ].content
146162 output .update (all_output )
147163 output ._markdown = all_output # pylint: disable=protected-access
148164 self .container .scroll_end ()
165+
166+ for markdown in self .container .children :
167+ markdown .disabled = False
168+
169+ self .input .styles .display = "block"
170+ self .wait .styles .display = "none"
149171 self .input .disabled = False
150- self .input .focus ()
151172 self .cancel_button .disabled = True
152- self .cancel_button . styles . display = "none"
173+ self .input . focus ()
153174
154175 def scroll_end (self ):
155176 self .call_after_refresh (self .container .scroll_end )
@@ -166,12 +187,15 @@ def action_toggle_history(self):
166187 return
167188 children = self .container .children
168189 idx = children .index (widget )
190+ if idx == 0 :
191+ return
192+
169193 while idx > 1 and not "role_user" in children [idx ].classes :
170194 idx -= 1
171195 widget = children [idx ]
172196
173- children [idx ]. toggle_class ( "history_exclude" )
174- children [ idx + 1 ] .toggle_class ("history_exclude" )
197+ for m in children [idx : idx + 2 ]:
198+ m .toggle_class ("history_exclude" )
175199
176200 async def action_stop_generating (self ):
177201 self .workers .cancel_all ()
@@ -184,17 +208,20 @@ async def action_quit(self):
184208 self .exit ()
185209
186210 async def action_resubmit (self ):
187- await self .delete_or_resubmit (True )
211+ await self .redraft_or_resubmit (True )
188212
189- async def action_delete (self ):
190- await self .delete_or_resubmit (False )
213+ async def action_redraft (self ):
214+ await self .redraft_or_resubmit (False )
191215
192- async def delete_or_resubmit (self , resubmit ):
216+ async def redraft_or_resubmit (self , resubmit ):
193217 widget = self .focused
194218 if not isinstance (widget , Markdown ):
195219 return
196220 children = self .container .children
197221 idx = children .index (widget )
222+ if idx < 1 :
223+ return
224+
198225 while idx > 1 and not children [idx ].has_class ("role_user" ):
199226 idx -= 1
200227 widget = children [idx ]
0 commit comments