agent output
This commit is contained in:
99
cht/ui/agent_output.py
Normal file
99
cht/ui/agent_output.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Agent output panel — scrollable text view with markdown rendering."""
|
||||
|
||||
import gi
|
||||
gi.require_version("Gtk", "4.0")
|
||||
from gi.repository import Gtk
|
||||
|
||||
from cht.ui import markdown
|
||||
|
||||
|
||||
class AgentOutputPanel(Gtk.Frame):
|
||||
"""Scrollable text view that displays agent responses with markdown."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
|
||||
header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
|
||||
header.set_margin_start(8)
|
||||
header.set_margin_end(8)
|
||||
header.set_margin_top(8)
|
||||
header.set_margin_bottom(8)
|
||||
label = Gtk.Label(label="Agent Output")
|
||||
label.add_css_class("heading")
|
||||
label.set_hexpand(True)
|
||||
label.set_halign(Gtk.Align.START)
|
||||
header.append(label)
|
||||
|
||||
clear_btn = Gtk.Button(label="Clear")
|
||||
clear_btn.add_css_class("flat")
|
||||
clear_btn.connect("clicked", lambda _: self.clear())
|
||||
header.append(clear_btn)
|
||||
|
||||
box.append(header)
|
||||
|
||||
self._view = Gtk.TextView()
|
||||
self._view.set_editable(False)
|
||||
self._view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
|
||||
self._view.set_cursor_visible(False)
|
||||
self._view.set_left_margin(8)
|
||||
self._view.set_right_margin(8)
|
||||
markdown.setup_tags(self._view.get_buffer())
|
||||
|
||||
scroll = Gtk.ScrolledWindow()
|
||||
scroll.set_vexpand(True)
|
||||
scroll.set_child(self._view)
|
||||
box.append(scroll)
|
||||
|
||||
self.set_child(box)
|
||||
|
||||
# Streaming state
|
||||
self._thinking_replaced = False
|
||||
self._response_start_mark = None
|
||||
self._response_accum = []
|
||||
|
||||
def append(self, text: str) -> None:
|
||||
"""Append plain text and auto-scroll."""
|
||||
buf = self._view.get_buffer()
|
||||
buf.insert(buf.get_end_iter(), text)
|
||||
self._view.scroll_to_iter(buf.get_end_iter(), 0, False, 0, 0)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all output."""
|
||||
self._view.get_buffer().set_text("")
|
||||
|
||||
def begin_response(self) -> None:
|
||||
"""Reset streaming state for a new response (call before sending)."""
|
||||
self._thinking_replaced = False
|
||||
self._response_start_mark = None
|
||||
self._response_accum = []
|
||||
|
||||
def replace_thinking(self, chunk: str) -> None:
|
||||
"""Replace the '...' placeholder with streamed chunks."""
|
||||
buf = self._view.get_buffer()
|
||||
if not self._thinking_replaced:
|
||||
self._thinking_replaced = True
|
||||
end = buf.get_end_iter()
|
||||
start = end.copy()
|
||||
start.backward_chars(2)
|
||||
buf.delete(start, end)
|
||||
self._response_start_mark = buf.create_mark(
|
||||
None, buf.get_end_iter(), left_gravity=True
|
||||
)
|
||||
self._response_accum.append(chunk)
|
||||
self.append(chunk)
|
||||
|
||||
def finish_response(self, err: str | None) -> None:
|
||||
"""Finalize the response — render markdown or show error."""
|
||||
if err:
|
||||
self.append(f"[Error: {err}]\n")
|
||||
return
|
||||
if self._response_start_mark and self._response_accum:
|
||||
buf = self._view.get_buffer()
|
||||
start = buf.get_iter_at_mark(self._response_start_mark)
|
||||
end = buf.get_end_iter()
|
||||
buf.delete(start, end)
|
||||
markdown.render(buf, start, "".join(self._response_accum))
|
||||
buf.delete_mark(self._response_start_mark)
|
||||
self.append("\n")
|
||||
Reference in New Issue
Block a user