Skip to content
This repository was archived by the owner on Oct 7, 2025. It is now read-only.

Commit 7cf8496

Browse files
authored
Merge pull request #18 from jepler/misc-ux
Misc ux improvements
2 parents 892b66a + 94562de commit 7cf8496

File tree

2 files changed

+51
-20
lines changed

2 files changed

+51
-20
lines changed

src/chap/commands/tui.css

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,16 @@ Footer {
4040
dock: top;
4141
}
4242

43-
#inputbox {
43+
Input, #wait {
4444
dock: bottom;
45+
}
46+
47+
Input {
4548
height: 3;
4649
}
4750

48-
#inputbox Button { dock: right; display: none; }
51+
#wait { display: none; height: 3 }
52+
#wait CancelButton { dock: right; border: none; margin: 0; height: 3; text-style: none}
4953

5054
Markdown {
5155
margin: 0 1 0 0;

src/chap/commands/tui.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from textual.app import App
1212
from textual.binding import Binding
1313
from 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

1616
from ..core import command_uses_new_session, get_api, new_session_path
1717
from ..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):
5151
class 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

Comments
 (0)