some changes

This commit is contained in:
2026-04-02 21:08:17 -03:00
parent 76ff720906
commit 8c1138c746
8 changed files with 1245 additions and 26 deletions

View File

@@ -36,3 +36,19 @@ class AgentProvider(ABC):
@abstractmethod
def name(self) -> str:
...
@property
@abstractmethod
def available_models(self) -> list[str]:
"""Return list of model IDs this provider supports."""
...
@property
@abstractmethod
def model(self) -> str:
...
@model.setter
@abstractmethod
def model(self, value: str):
...

View File

@@ -51,16 +51,36 @@ def _build_prompt(message: str, context: SessionContext) -> str:
return "\n".join(lines)
MODELS = [
"claude-sonnet-4-6",
"claude-opus-4-6",
"claude-haiku-4-5",
]
class ClaudeSDKProvider(AgentProvider):
"""Uses claude_agent_sdk — requires Claude Code CLI to be installed."""
def __init__(self, cwd: str | None = None, max_turns: int = 5):
def __init__(self, cwd: str | None = None, max_turns: int = 5, model: str = MODELS[0]):
self._cwd = cwd
self._max_turns = max_turns
self._model = model
@property
def name(self) -> str:
return "claude-code-sdk"
return f"claude-sdk/{self._model}"
@property
def available_models(self) -> list[str]:
return list(MODELS)
@property
def model(self) -> str:
return self._model
@model.setter
def model(self, value: str):
self._model = value
def stream(self, message: str, context: SessionContext) -> Iterator[str]:
prompt = _build_prompt(message, context)
@@ -70,6 +90,7 @@ class ClaudeSDKProvider(AgentProvider):
async for msg in query(
prompt=prompt,
options=ClaudeAgentOptions(
model=self._model,
cwd=self._cwd or str(context.session_dir),
allowed_tools=["Read"],
system_prompt=SYSTEM_PROMPT,

View File

@@ -18,21 +18,35 @@ SYSTEM_PROMPT = """You are an assistant integrated into CHT, a screen recording
You help the user understand what happened during their recording session.
Be concise and specific. Focus on what's visible in the provided frames."""
# Default models per provider
_PROVIDER_DEFAULTS = {
"groq": ("https://api.groq.com/openai/v1", "meta-llama/llama-4-maverick-17b-128e-instruct"),
"openai": ("https://api.openai.com/v1", "gpt-4o"),
# Provider configs: (base_url, default_model, available_models)
_PROVIDER_CONFIGS = {
"groq": (
"https://api.groq.com/openai/v1",
"meta-llama/llama-4-maverick-17b-128e-instruct",
[
"meta-llama/llama-4-maverick-17b-128e-instruct",
"meta-llama/llama-4-scout-17b-16e-instruct",
"qwen/qwen-2.5-vl-72b-instruct",
],
),
"openai": (
"https://api.openai.com/v1",
"gpt-4o",
["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini"],
),
}
def _detect_provider() -> tuple[str, str, str] | None:
"""Returns (api_key, base_url, model) or None if no key found."""
def _detect_provider() -> tuple[str, str, str, list[str]] | None:
"""Returns (api_key, base_url, model, available_models) or None."""
if key := os.environ.get("GROQ_API_KEY"):
base_url, model = _PROVIDER_DEFAULTS["groq"]
return key, base_url, os.environ.get("CHT_MODEL", model)
base_url, default_model, models = _PROVIDER_CONFIGS["groq"]
model = os.environ.get("CHT_MODEL", default_model)
return key, base_url, model, models
if key := os.environ.get("OPENAI_API_KEY"):
base_url, model = _PROVIDER_DEFAULTS["openai"]
return key, base_url, os.environ.get("CHT_MODEL", model)
base_url, default_model, models = _PROVIDER_CONFIGS["openai"]
model = os.environ.get("CHT_MODEL", default_model)
return key, base_url, model, models
return None
@@ -54,7 +68,7 @@ class OpenAICompatProvider(AgentProvider):
raise RuntimeError(
"No API key found. Set GROQ_API_KEY or OPENAI_API_KEY."
)
self._api_key, self._base_url, self._model = detected
self._api_key, self._base_url, self._model, self._models = detected
@property
def name(self) -> str:
@@ -62,6 +76,18 @@ class OpenAICompatProvider(AgentProvider):
return f"groq/{self._model}"
return f"openai-compat/{self._model}"
@property
def available_models(self) -> list[str]:
return list(self._models)
@property
def model(self) -> str:
return self._model
@model.setter
def model(self, value: str):
self._model = value
def stream(self, message: str, context: SessionContext) -> Iterator[str]:
from openai import OpenAI

View File

@@ -19,12 +19,10 @@ from cht.agent.base import AgentProvider, FrameRef, SessionContext
log = logging.getLogger(__name__)
# Predefined actions sent as messages with a fixed prompt
# Predefined actions — label → verb prefix (frame ref appended by UI)
ACTIONS: dict[str, str] = {
"Summarize": "Summarize what happened in this recording so far. Look at the captured frames and describe the key content and any changes you notice.",
"What changed": "Compare the captured frames in order and describe what changed between them. Focus on meaningful transitions.",
"Key moments": "Identify the most important moments in the recording based on the frames. List them with timestamps.",
"Describe now": "Look at the most recent frame and describe exactly what is currently on screen.",
"Describe": "describe",
"Answer": "answer",
}
@@ -106,6 +104,24 @@ class AgentRunner:
except Exception:
return "unknown"
@property
def available_models(self) -> list[str]:
try:
return self._get_provider().available_models
except Exception:
return []
@property
def model(self) -> str:
try:
return self._get_provider().model
except Exception:
return ""
@model.setter
def model(self, value: str):
self._get_provider().model = value
def send(
self,
message: str,

View File

@@ -7,7 +7,7 @@ import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, Adw, Gio, GLib
from gi.repository import Gtk, Gdk, Adw, Gio, GLib
from cht.config import APP_ID, APP_NAME
from cht.window import ChtWindow
@@ -23,6 +23,15 @@ class ChtApp(Adw.Application):
def do_activate(self):
win = self.props.active_window
if not win:
css = Gtk.CssProvider()
css.load_from_string(
".frame-selected { border: 3px solid @accent_color; border-radius: 6px; }"
)
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(),
css,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
win = ChtWindow(application=self)
win.present()

View File

@@ -30,6 +30,8 @@ class ChtWindow(Adw.ApplicationWindow):
self._stream_mgr = None
self._tracker = None
self._known_frames = set()
self._selected_frame = None # currently selected frame ID
self._frame_widgets = {} # frame_id → outer Box widget
# Timeline is the central state machine
self._timeline = Timeline()
@@ -168,6 +170,8 @@ class ChtWindow(Adw.ApplicationWindow):
self._stream_mgr.stop_all()
self._stream_mgr = None
self._known_frames = set()
self._selected_frame = None
self._frame_widgets = {}
self._connect_btn.set_label("Connect")
self._connect_btn.remove_css_class("destructive-action")
@@ -349,13 +353,28 @@ class ChtWindow(Adw.ApplicationWindow):
outer.set_margin_top(4)
outer.set_margin_bottom(4)
# Quick action buttons
# Quick action buttons + model selector
actions_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
for label in ACTIONS:
for label, verb in ACTIONS.items():
btn = Gtk.Button(label=label)
btn.add_css_class("flat")
btn.connect("clicked", lambda b, l=label: self._send_message(ACTIONS[l]))
btn.connect("clicked", lambda b, v=verb: self._send_action(v))
actions_box.append(btn)
# Model dropdown (right-aligned)
spacer = Gtk.Box()
spacer.set_hexpand(True)
actions_box.append(spacer)
model_label = Gtk.Label(label="Model:")
model_label.add_css_class("dim-label")
actions_box.append(model_label)
self._model_dropdown = Gtk.DropDown.new_from_strings([])
self._model_dropdown.set_size_request(200, -1)
self._model_dropdown.connect("notify::selected", self._on_model_changed)
actions_box.append(self._model_dropdown)
outer.append(actions_box)
# Text entry + send
@@ -376,6 +395,27 @@ class ChtWindow(Adw.ApplicationWindow):
frame.set_child(outer)
return frame
def _send_action(self, verb: str):
"""Send a predefined action with the selected frame."""
if not self._selected_frame:
self._append_agent_output("Select a frame first.\n")
return
self._send_message(f"{verb} @{self._selected_frame}")
def _select_frame(self, frame_id: str):
"""Toggle selection of a frame thumbnail."""
# Deselect previous
if self._selected_frame and self._selected_frame in self._frame_widgets:
self._frame_widgets[self._selected_frame].remove_css_class("frame-selected")
if self._selected_frame == frame_id:
self._selected_frame = None
return
self._selected_frame = frame_id
if frame_id in self._frame_widgets:
self._frame_widgets[frame_id].add_css_class("frame-selected")
def _send_message(self, text: str | None = None):
if text is None:
text = self._input_entry.get_text().strip()
@@ -570,15 +610,37 @@ class ChtWindow(Adw.ApplicationWindow):
buf.apply_tag(tag, buf.get_iter_at_mark(mark), it)
buf.delete_mark(mark)
def _on_model_changed(self, dropdown, _pspec):
idx = dropdown.get_selected()
model = self._agent.available_models[idx] if idx < len(self._agent.available_models) else None
if model:
self._agent.model = model
log.info("Model switched to %s", model)
def _populate_model_dropdown(self):
models = self._agent.available_models
if not models:
return
string_list = Gtk.StringList.new(models)
self._model_dropdown.set_model(string_list)
# Select current model
current = self._agent.model
for i, m in enumerate(models):
if m == current:
self._model_dropdown.set_selected(i)
break
def _check_agent_auth(self):
import os
if os.environ.get("GROQ_API_KEY") or os.environ.get("OPENAI_API_KEY"):
return # using external provider, no CLI check needed
self._populate_model_dropdown()
return
err = check_claude_cli()
if err:
self._append_agent_output(f"{err}\n")
else:
self._append_agent_output(f"Agent ready ({self._agent.provider_name})\n")
self._populate_model_dropdown()
def _append_agent_output(self, text: str):
buf = self._agent_output_view.get_buffer()
@@ -636,12 +698,11 @@ class ChtWindow(Adw.ApplicationWindow):
label.set_ellipsize(Pango.EllipsizeMode.END)
box.append(label)
# Click to highlight — does NOT switch mode or seek
# (future: jump to timestamp in scrub bar without leaving live)
gesture = Gtk.GestureClick()
gesture.connect("released", lambda g, n, x, y, fid=frame_id: self._send_message(f"solve this @{fid}"))
gesture.connect("released", lambda g, n, x, y, fid=frame_id: self._select_frame(fid))
box.add_controller(gesture)
self._frame_widgets[frame_id] = box
self._frames_strip.append(box)
# Auto-scroll to show the latest frame