Redesign --tui chat scene: colorful borderless layout, markdown bubbles, tool chips, rounded composer; fix clipped composer and hidden-panel gap
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+113
-26
@@ -5758,10 +5758,10 @@ class TuiThemeTests(unittest.TestCase):
|
||||
|
||||
def test_role_resolves_at_draw(self):
|
||||
tai.tui_set_theme("dark")
|
||||
self.assertEqual(tai.TuiStyle(fg="role:accent").sgr(), "\x1b[0;38;2;47;129;247m")
|
||||
self.assertEqual(tai.TuiStyle(bg="role:bg").sgr(), "\x1b[0;48;2;16;20;24m")
|
||||
self.assertEqual(tai.TuiStyle(fg="role:accent").sgr(), "\x1b[0;38;2;139;92;246m")
|
||||
self.assertEqual(tai.TuiStyle(bg="role:bg").sgr(), "\x1b[0;48;2;10;14;26m")
|
||||
tai.tui_set_theme("light")
|
||||
self.assertEqual(tai.TuiStyle(fg="role:accent").sgr(), "\x1b[0;38;2;9;105;218m")
|
||||
self.assertEqual(tai.TuiStyle(fg="role:accent").sgr(), "\x1b[0;38;2;109;74;255m")
|
||||
|
||||
def test_unknown_role_is_empty(self):
|
||||
self.assertEqual(tai.TuiStyle(fg="role:nope").sgr(), "\x1b[0m")
|
||||
@@ -5897,9 +5897,36 @@ class TuiChatTests(unittest.TestCase):
|
||||
self.assertIn("coder", text)
|
||||
self.assertIn("hello", text)
|
||||
self.assertIn("Write a message...", text)
|
||||
self.assertIn("[M] main", text)
|
||||
self.assertIn("main", text)
|
||||
self.assertIn("✦ tai", text)
|
||||
self.assertIn("online", text)
|
||||
|
||||
def test_long_transcript_shows_tail_not_blank(self):
|
||||
app, _agent, _root, refs = self.make_chat(rows=24)
|
||||
refs["box"].focus()
|
||||
for n in range(60):
|
||||
refs["show"]("bot", "line number %d" % n, None)
|
||||
transcript = refs["transcript"]
|
||||
self.assertGreater(len(transcript.lines), transcript._content_h())
|
||||
text = app.render_text()
|
||||
self.assertIn("line number 59", text)
|
||||
self.assertNotIn("line number 0 ", text)
|
||||
|
||||
def test_many_messages_stay_pinned_and_scroll(self):
|
||||
app, _agent, _root, refs = self.make_chat(rows=24)
|
||||
refs["box"].focus()
|
||||
transcript = refs["transcript"]
|
||||
for n in range(tai.TUI_CHAT_LINE_CAP):
|
||||
self.type(app, "m%d" % n)
|
||||
self.key(app, "enter")
|
||||
refs["run_pending"]()
|
||||
self.assertLessEqual(len(transcript.lines), tai.TUI_CHAT_LINE_CAP + 10)
|
||||
self.assertIn("echo: m%d" % (tai.TUI_CHAT_LINE_CAP - 1), app.render_text())
|
||||
transcript.scroll_by(0, -10)
|
||||
self.assertNotIn("echo: m%d" % (tai.TUI_CHAT_LINE_CAP - 1), app.render_text())
|
||||
transcript.scroll_by(0, 10)
|
||||
self.assertIn("echo: m%d" % (tai.TUI_CHAT_LINE_CAP - 1), app.render_text())
|
||||
|
||||
def test_narrow_hides_and_restores_sidebar(self):
|
||||
app, _agent, root, refs = self.make_chat(cols=100)
|
||||
self.assertTrue(refs["sidebar_wrap"].visible)
|
||||
@@ -5910,6 +5937,21 @@ class TuiChatTests(unittest.TestCase):
|
||||
app.dispatch(tai.TuiResize(panel=root, rows=24, cols=100))
|
||||
self.assertTrue(refs["sidebar_wrap"].visible)
|
||||
|
||||
def test_hidden_sidebar_frees_its_columns(self):
|
||||
app, _agent, _root, refs = self.make_chat(cols=100)
|
||||
right = refs["transcript"].parent
|
||||
self.assertEqual(right.ax, 30)
|
||||
refs["box"].focus()
|
||||
self.key(app, "f2")
|
||||
app.layout()
|
||||
self.assertEqual(right.ax, 0)
|
||||
self.assertEqual(right.aw, 100)
|
||||
|
||||
def test_narrow_start_hides_sidebar_and_compacts_send(self):
|
||||
app, _agent, _root, refs = self.make_chat(cols=44)
|
||||
self.assertFalse(refs["sidebar_wrap"].visible)
|
||||
self.assertEqual(refs["send_btn"].w, 5)
|
||||
|
||||
def test_f2_manual_toggle_overrides_auto(self):
|
||||
app, _agent, root, refs = self.make_chat(cols=100)
|
||||
refs["box"].focus()
|
||||
@@ -6005,7 +6047,7 @@ class TuiChatTests(unittest.TestCase):
|
||||
refs["sidebar"].choose(1)
|
||||
self.assertEqual(agent.bot, "coder")
|
||||
self.assertEqual(refs["box"].value, "")
|
||||
self.assertEqual(refs["head_name"].text, "[C] coder")
|
||||
self.assertEqual(refs["head_name"].text, "coder")
|
||||
refs["sidebar"].choose(0)
|
||||
self.assertEqual(agent.bot, "main")
|
||||
self.assertEqual(refs["box"].value, "draft-1")
|
||||
@@ -6018,37 +6060,82 @@ class TuiChatTests(unittest.TestCase):
|
||||
plain = tai.TuiStyle()
|
||||
return {"base": plain, "bubble": plain, "meta": plain, "tick": plain}
|
||||
|
||||
def row_text(self, row):
|
||||
return "".join(span.text for span in row)
|
||||
|
||||
def test_bubble_rows_align(self):
|
||||
rows = tai.tui_chat_bubble_rows("hi", 60, "out", self.bubble_styles(), "12:00", tai.TUI_CHAT_TICK_READ, True)
|
||||
text = "".join(span.text for span in rows[0])
|
||||
self.assertEqual(len(text), 60)
|
||||
self.assertTrue(text.startswith(" "))
|
||||
self.assertTrue(text.rstrip().endswith("12:00 " + tai.TUI_CHAT_TICK_READ))
|
||||
rows = tai.tui_chat_bubble_rows("hi", 60, "in", self.bubble_styles())
|
||||
text = "".join(span.text for span in rows[0])
|
||||
self.assertTrue(text.startswith(" hi "))
|
||||
self.assertTrue(text.endswith(" "))
|
||||
texts = [self.row_text(row) for row in rows]
|
||||
self.assertTrue(all(len(text) == 60 for text in texts))
|
||||
self.assertEqual(len(texts), 3)
|
||||
self.assertTrue(texts[0].startswith(" "))
|
||||
self.assertIn("hi", texts[1])
|
||||
self.assertTrue(texts[2].rstrip().endswith("12:00 " + tai.TUI_CHAT_TICK_READ))
|
||||
rows = tai.tui_chat_bubble_rows("hi", 60, "in", self.bubble_styles(), sender="main")
|
||||
texts = [self.row_text(row) for row in rows]
|
||||
self.assertTrue(texts[0].startswith(" main"))
|
||||
self.assertTrue(texts[1].startswith(" hi"))
|
||||
self.assertTrue(texts[1].endswith(" "))
|
||||
|
||||
def test_bubble_rows_cap_width(self):
|
||||
rows = tai.tui_chat_bubble_rows("x" * 100, 60, "in", self.bubble_styles())
|
||||
self.assertTrue(len(rows) > 2)
|
||||
first = "".join(span.text for span in rows[0])
|
||||
self.assertEqual(len(first), 60)
|
||||
self.assertEqual(first.strip(), "x" * 40)
|
||||
self.assertTrue(len(rows) > 3)
|
||||
body = [self.row_text(row) for row in rows][1:-1]
|
||||
self.assertEqual(len(body[0]), 60)
|
||||
self.assertEqual(body[0].strip(), "x" * 38)
|
||||
self.assertEqual("".join(line.strip() for line in body), "x" * 100)
|
||||
|
||||
def test_bubble_rows_tucked_meta_keeps_body(self):
|
||||
def test_bubble_rows_meta_row_keeps_body(self):
|
||||
rows = tai.tui_chat_bubble_rows("first line here with padding words second line tail end", 60, "out",
|
||||
self.bubble_styles(), "12:00", "x")
|
||||
texts = ["".join(span.text for span in row) for row in rows]
|
||||
self.assertTrue(len(texts) >= 2)
|
||||
self.bubble_styles(), "12:00", "x")
|
||||
texts = [self.row_text(row) for row in rows]
|
||||
self.assertTrue(any("first line here" in line for line in texts))
|
||||
self.assertTrue(any("tail end" in line and "12:00" in line for line in texts))
|
||||
self.assertTrue(any("tail end" in line for line in texts))
|
||||
self.assertIn("12:00", texts[-1])
|
||||
|
||||
def test_bubble_rows_narrow_degrade(self):
|
||||
rows = tai.tui_chat_bubble_rows("hi there", 20, "out", self.bubble_styles(), "12:00", "x")
|
||||
text = "".join(span.text for span in rows[0])
|
||||
text = self.row_text(rows[0])
|
||||
self.assertTrue(text.startswith("hi there"))
|
||||
self.assertIn("12:00", "".join(span.text for span in rows[-1]))
|
||||
self.assertIn("12:00", self.row_text(rows[-1]))
|
||||
|
||||
def test_markdown_blocks(self):
|
||||
blocks = tai.tui_md_blocks("# Title\n\nplain **bold** and `code`\n- one\n2. two\n> quote\n```py\nx = 1\n```\n---")
|
||||
kinds = [kind for kind, _segs, _lead in blocks]
|
||||
self.assertEqual(kinds, ["h", "blank", "p", "li", "li", "quote", "code", "rule"])
|
||||
self.assertEqual(blocks[2][1], [("plain ", ""), ("bold", "b"), (" and ", ""), ("code", "c")])
|
||||
self.assertEqual(blocks[3][2], "•")
|
||||
self.assertEqual(blocks[4][2], "2.")
|
||||
|
||||
def test_wrap_segs_chops_long_words_and_keeps_tags(self):
|
||||
lines = tai.tui_wrap_segs([("aaaa bbbb ", ""), ("cccccccccc", "b")], 6)
|
||||
self.assertEqual(["".join(text for text, _tag in line) for line in lines], ["aaaa", "bbbb", "cccccc", "cccc"])
|
||||
self.assertEqual(lines[2][0][1], "b")
|
||||
|
||||
def test_markdown_markers_not_shown_in_bubble(self):
|
||||
rows = tai.tui_chat_bubble_rows("use **bold** and `code`", 60, "in", self.bubble_styles())
|
||||
text = " ".join(self.row_text(row) for row in rows)
|
||||
self.assertIn("use bold and code", text)
|
||||
self.assertNotIn("**", text)
|
||||
self.assertNotIn("`", text)
|
||||
|
||||
def test_tool_chip_summarizes_json_and_truncates(self):
|
||||
row = tai.tui_chat_tool_row("fork", '{"task": "do it", "n": 2}', 60, tai.TuiStyle())
|
||||
text = self.row_text(row)
|
||||
self.assertEqual(len(text), 60)
|
||||
self.assertIn("fork", text)
|
||||
self.assertIn("task=do it", text)
|
||||
self.assertIn("n=2", text)
|
||||
short = self.row_text(tai.tui_chat_tool_row("fork", '{"task": "' + "z" * 200 + '"}', 40, tai.TuiStyle()))
|
||||
self.assertEqual(len(short), 40)
|
||||
self.assertIn("…", short)
|
||||
|
||||
def test_composer_and_sidebar_fit_screen(self):
|
||||
app, _agent, _root, refs = self.make_chat(rows=24)
|
||||
lines = app.render_text().split("\n")
|
||||
self.assertIn("╰", lines[-2])
|
||||
self.assertIn("Send", lines[-3])
|
||||
self.assertIn("F2", lines[-1])
|
||||
|
||||
def test_pill_row_centered(self):
|
||||
spans = tai.tui_chat_pill_row("Today", 40, tai.TuiStyle(), tai.TuiStyle())
|
||||
@@ -6117,12 +6204,12 @@ class TuiChatTests(unittest.TestCase):
|
||||
self.assertEqual(refs["pending"], "via button")
|
||||
|
||||
def test_telegram_roles_registered(self):
|
||||
self.assertEqual(len(tai.TUI_THEME_ROLES), 22)
|
||||
self.assertEqual(len(tai.TUI_THEME_ROLES), 27)
|
||||
for role in ("bubble_in", "bubble_out", "bubble_fg", "meta_in", "meta_out",
|
||||
"tick_read", "service_bg", "service_fg", "unread", "unread_muted"):
|
||||
self.assertIn(role, tai.TUI_THEME_ROLES)
|
||||
tai.tui_set_theme("dark")
|
||||
self.assertEqual(tai.TuiStyle(bg="role:bubble_out").sgr(), "\x1b[0;48;2;43;82;120m")
|
||||
self.assertEqual(tai.TuiStyle(bg="role:bubble_out").sgr(), "\x1b[0;48;2;67;56;202m")
|
||||
|
||||
def test_parser_flags(self):
|
||||
args = tai.build_parser().parse_args(["--tui", "--theme", "nord"])
|
||||
|
||||
Reference in New Issue
Block a user