some changes
This commit is contained in:
@@ -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):
|
||||
...
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
11
cht/app.py
11
cht/app.py
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user