mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Wave the agent's progress away, and say how hard it should think
Two things a real command turned up. A job can run for ten minutes. "Report on the front page" is not a question with an answer a second later, and having the corner narrate every tool it touches for those ten minutes is worse than saying nothing. So that indicator can be clicked away. It is the only one that can: a dictation is over in seconds, and an indicator that swallows a click meant for the window underneath has to earn it. The work carries on; what is muted is the progress, not the outcome, which shows up whether or not the box was sent away. A faint cross on the right says the box can be clicked, because a feature nobody can see is not one. The next run starts visible again. Muting leaves the state alone rather than setting it to something hidden, so no later repaint puts the box back on the screen behind its own back. Thinking effort is a setting now, offered once rather than three times: how hard to think is one thing to want, and only the rungs differ. Claude takes it as --effort, Codex as a model_reasoning_effort override, OpenRouter in the reasoning field it already understood for cleanup. A level a provider does not have lands on the nearest one it does, so "maximum" is xhigh on Claude and high on Codex rather than an error or a silent drop. Left alone, nothing is sent and each model does what it would have done.
This commit is contained in:
@@ -209,8 +209,8 @@ def cleanup(text, api_key, model, system_prompt, reasoning="",
|
|||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
def chat(messages, api_key, model, system_prompt, base_url=OPENROUTER_URL,
|
def chat(messages, api_key, model, system_prompt, reasoning="",
|
||||||
timeout=180):
|
base_url=OPENROUTER_URL, timeout=180):
|
||||||
"""A conversation, rather than one transcript rewritten.
|
"""A conversation, rather than one transcript rewritten.
|
||||||
|
|
||||||
The messages are the whole history and come back unchanged; the caller keeps
|
The messages are the whole history and come back unchanged; the caller keeps
|
||||||
@@ -223,6 +223,8 @@ def chat(messages, api_key, model, system_prompt, base_url=OPENROUTER_URL,
|
|||||||
"model": model,
|
"model": model,
|
||||||
"messages": [{"role": "system", "content": system_prompt}] + list(messages),
|
"messages": [{"role": "system", "content": system_prompt}] + list(messages),
|
||||||
}
|
}
|
||||||
|
if reasoning:
|
||||||
|
payload["reasoning"] = {"effort": reasoning, "exclude": True}
|
||||||
try:
|
try:
|
||||||
data = _request(
|
data = _request(
|
||||||
f"{base_url.rstrip('/')}/chat/completions",
|
f"{base_url.rstrip('/')}/chat/completions",
|
||||||
|
|||||||
+20
-1
@@ -68,6 +68,18 @@ CODEX_ITEMS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# How hard to think, in each provider's own vocabulary. The setting is one
|
||||||
|
# scale, offered once, because "think harder" is one thing to want; what differs
|
||||||
|
# is only which rungs a provider has. A level it does not have lands on the
|
||||||
|
# nearest one it does rather than being dropped.
|
||||||
|
CLAUDE_EFFORT = {"none": "low", "minimal": "low", "low": "low",
|
||||||
|
"medium": "medium", "high": "high", "xhigh": "xhigh",
|
||||||
|
"max": "max"}
|
||||||
|
CODEX_EFFORT = {"none": "minimal", "minimal": "minimal", "low": "low",
|
||||||
|
"medium": "medium", "high": "high", "xhigh": "high",
|
||||||
|
"max": "high"}
|
||||||
|
|
||||||
|
|
||||||
class AssistantError(Exception):
|
class AssistantError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -207,6 +219,9 @@ def _ask_claude(prompt, conf, session, on_stage, should_stop):
|
|||||||
"--permission-mode", conf["assistant_permission_mode"],
|
"--permission-mode", conf["assistant_permission_mode"],
|
||||||
"--append-system-prompt", conf.assistant_prompt(),
|
"--append-system-prompt", conf.assistant_prompt(),
|
||||||
]
|
]
|
||||||
|
effort = CLAUDE_EFFORT.get(conf["assistant_reasoning"], "")
|
||||||
|
if effort:
|
||||||
|
cmd += ["--effort", effort]
|
||||||
if session:
|
if session:
|
||||||
cmd += ["--resume", session]
|
cmd += ["--resume", session]
|
||||||
|
|
||||||
@@ -269,6 +284,9 @@ def _ask_codex(prompt, conf, session, on_stage, should_stop):
|
|||||||
]
|
]
|
||||||
if conf["assistant_codex_model"].strip():
|
if conf["assistant_codex_model"].strip():
|
||||||
settings += ["-m", conf["assistant_codex_model"].strip()]
|
settings += ["-m", conf["assistant_codex_model"].strip()]
|
||||||
|
effort = CODEX_EFFORT.get(conf["assistant_reasoning"], "")
|
||||||
|
if effort:
|
||||||
|
settings += ["-c", f'model_reasoning_effort="{effort}"']
|
||||||
|
|
||||||
cmd = (["codex", "exec", "resume", session] if session else ["codex", "exec"])
|
cmd = (["codex", "exec", "resume", session] if session else ["codex", "exec"])
|
||||||
cmd += settings + [body]
|
cmd += settings + [body]
|
||||||
@@ -323,7 +341,8 @@ def _ask_openrouter(prompt, conf, on_stage):
|
|||||||
try:
|
try:
|
||||||
answer = api.chat(
|
answer = api.chat(
|
||||||
messages, conf.openrouter_key(), conf["assistant_openrouter_model"],
|
messages, conf.openrouter_key(), conf["assistant_openrouter_model"],
|
||||||
conf.assistant_prompt(), base_url=conf["openrouter_base_url"],
|
conf.assistant_prompt(), reasoning=conf["assistant_reasoning"],
|
||||||
|
base_url=conf["openrouter_base_url"],
|
||||||
timeout=conf["assistant_timeout"],
|
timeout=conf["assistant_timeout"],
|
||||||
)
|
)
|
||||||
except api.ApiError as exc:
|
except api.ApiError as exc:
|
||||||
|
|||||||
@@ -340,6 +340,7 @@ DEFAULTS = {
|
|||||||
"assistant_codex_model": "", # empty -> whatever Codex is set to
|
"assistant_codex_model": "", # empty -> whatever Codex is set to
|
||||||
"assistant_codex_sandbox": "workspace-write",
|
"assistant_codex_sandbox": "workspace-write",
|
||||||
"assistant_openrouter_model": "google/gemini-3.5-flash",
|
"assistant_openrouter_model": "google/gemini-3.5-flash",
|
||||||
|
"assistant_reasoning": "", # empty -> the model's own default
|
||||||
"assistant_dir": "", # empty -> the home directory
|
"assistant_dir": "", # empty -> the home directory
|
||||||
"assistant_prompt": "", # empty -> language-specific default
|
"assistant_prompt": "", # empty -> language-specific default
|
||||||
"assistant_cleanup": False, # the model reads through filler words fine
|
"assistant_cleanup": False, # the model reads through filler words fine
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ class Dikte:
|
|||||||
self.overlay = Overlay(self.conf["overlay_corner"])
|
self.overlay = Overlay(self.conf["overlay_corner"])
|
||||||
# The agent's indicator sits on top of the dictation one when both are
|
# The agent's indicator sits on top of the dictation one when both are
|
||||||
# up, and drops into the corner when it is alone there.
|
# up, and drops into the corner when it is alone there.
|
||||||
self.ask_overlay = Overlay(self.conf["overlay_corner"], below=self.overlay)
|
self.ask_overlay = Overlay(self.conf["overlay_corner"], below=self.overlay,
|
||||||
|
dismissable=True)
|
||||||
self.recorder = audio.Recorder()
|
self.recorder = audio.Recorder()
|
||||||
self.pipeline = Pipeline(self.conf)
|
self.pipeline = Pipeline(self.conf)
|
||||||
self.ask_pipeline = Pipeline(self.conf)
|
self.ask_pipeline = Pipeline(self.conf)
|
||||||
|
|||||||
@@ -360,6 +360,11 @@ TR = {
|
|||||||
"hesabınla.",
|
"hesabınla.",
|
||||||
"How it runs": "Nasıl çalışıyor",
|
"How it runs": "Nasıl çalışıyor",
|
||||||
"Runs on": "Şunun üstünde çalışır",
|
"Runs on": "Şunun üstünde çalışır",
|
||||||
|
"More thinking is slower, and you are standing in front of the screen while "
|
||||||
|
"it happens. Worth it for a job that has to be worked out rather than "
|
||||||
|
"looked up.":
|
||||||
|
"Daha çok düşünmek daha yavaştır ve bu sırada ekranın başında bekliyor "
|
||||||
|
"olursun. Bakılıp bulunacak değil, çözülmesi gereken işler için değer.",
|
||||||
"Claude Code": "Claude Code",
|
"Claude Code": "Claude Code",
|
||||||
"Codex": "Codex",
|
"Codex": "Codex",
|
||||||
"Codex's own default": "Codex'in kendi varsayılanı",
|
"Codex's own default": "Codex'in kendi varsayılanı",
|
||||||
|
|||||||
+59
-13
@@ -36,10 +36,15 @@ class Overlay(QWidget):
|
|||||||
of covering it, which is what lets a dictation and a command to the agent be
|
of covering it, which is what lets a dictation and a command to the agent be
|
||||||
under way at the same time and still both be visible."""
|
under way at the same time and still both be visible."""
|
||||||
|
|
||||||
def __init__(self, corner="bottom-left", below=None):
|
def __init__(self, corner="bottom-left", below=None, dismissable=False):
|
||||||
super().__init__(None)
|
super().__init__(None)
|
||||||
self.corner = corner
|
self.corner = corner
|
||||||
self.below = below
|
self.below = below
|
||||||
|
# A job that can run for ten minutes should not have to be watched for
|
||||||
|
# ten minutes. Clicking such an indicator puts the progress away; the
|
||||||
|
# work carries on and its result still shows up.
|
||||||
|
self.dismissable = dismissable
|
||||||
|
self.muted = False
|
||||||
self._stacked = False
|
self._stacked = False
|
||||||
self.state = "idle"
|
self.state = "idle"
|
||||||
self.message = ""
|
self.message = ""
|
||||||
@@ -58,7 +63,13 @@ class Overlay(QWidget):
|
|||||||
)
|
)
|
||||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating)
|
self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating)
|
||||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
# One that can be clicked away has to receive the click, which means it
|
||||||
|
# also swallows one aimed at whatever is underneath it. The rest stay
|
||||||
|
# transparent to the mouse, as an indicator should be.
|
||||||
|
if dismissable:
|
||||||
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
else:
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||||||
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||||
self.resize(MIN_WIDTH, HEIGHT)
|
self.resize(MIN_WIDTH, HEIGHT)
|
||||||
|
|
||||||
@@ -79,6 +90,7 @@ class Overlay(QWidget):
|
|||||||
self.message = ""
|
self.message = ""
|
||||||
self.seconds = 0.0
|
self.seconds = 0.0
|
||||||
self.levels = [0.0] * BARS
|
self.levels = [0.0] * BARS
|
||||||
|
self.muted = False # a new run starts visible, whatever the last one did
|
||||||
self._hide_timer.stop()
|
self._hide_timer.stop()
|
||||||
self._appear()
|
self._appear()
|
||||||
|
|
||||||
@@ -93,26 +105,31 @@ class Overlay(QWidget):
|
|||||||
self._appear()
|
self._appear()
|
||||||
|
|
||||||
def show_busy(self, message):
|
def show_busy(self, message):
|
||||||
|
# Muted: the stages keep arriving and are simply not drawn. The state is
|
||||||
|
# left alone as well, so nothing repaints the box back onto the screen.
|
||||||
|
if self.muted:
|
||||||
|
self.message = message
|
||||||
|
return
|
||||||
self.state = "busy"
|
self.state = "busy"
|
||||||
self.message = message
|
self.message = message
|
||||||
self._hide_timer.stop()
|
self._hide_timer.stop()
|
||||||
self._appear()
|
self._appear()
|
||||||
|
|
||||||
def show_done(self, message="", msec=2000):
|
def show_done(self, message="", msec=2000):
|
||||||
self.state = "done"
|
self._finish("done", message, msec)
|
||||||
self.message = message
|
|
||||||
self._appear()
|
|
||||||
self._hide_timer.start(msec)
|
|
||||||
|
|
||||||
def show_warning(self, message, msec=9000):
|
def show_warning(self, message, msec=9000):
|
||||||
"""Finished, but something the user should know about went wrong."""
|
"""Finished, but something the user should know about went wrong."""
|
||||||
self.state = "warning"
|
self._finish("warning", message, msec)
|
||||||
self.message = message
|
|
||||||
self._appear()
|
|
||||||
self._hide_timer.start(msec)
|
|
||||||
|
|
||||||
def show_error(self, message, msec=6000):
|
def show_error(self, message, msec=6000):
|
||||||
self.state = "error"
|
self._finish("error", message, msec)
|
||||||
|
|
||||||
|
def _finish(self, state, message, msec):
|
||||||
|
"""An outcome always shows, even one that was told to be quiet: waving
|
||||||
|
the progress away asks not to be watched, not to be kept in the dark."""
|
||||||
|
self.muted = False
|
||||||
|
self.state = state
|
||||||
self.message = message
|
self.message = message
|
||||||
self._appear()
|
self._appear()
|
||||||
self._hide_timer.start(msec)
|
self._hide_timer.start(msec)
|
||||||
@@ -121,6 +138,14 @@ class Overlay(QWidget):
|
|||||||
self._hide_timer.stop()
|
self._hide_timer.stop()
|
||||||
self._conceal()
|
self._conceal()
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
# Only a job in progress can be waved away. A recording is short and
|
||||||
|
# ending it by accident would cost the words; an outcome goes on its own.
|
||||||
|
if self.dismissable and self.state == "busy":
|
||||||
|
self.muted = True
|
||||||
|
self.dismiss()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def showing(self):
|
def showing(self):
|
||||||
"""Mapped and actually painting something. The window stays mapped while
|
"""Mapped and actually painting something. The window stays mapped while
|
||||||
@@ -172,7 +197,9 @@ class Overlay(QWidget):
|
|||||||
width = MIN_WIDTH + (24 if self.state == "meeting" else 0)
|
width = MIN_WIDTH + (24 if self.state == "meeting" else 0)
|
||||||
else:
|
else:
|
||||||
metrics = QFontMetrics(self._label_font())
|
metrics = QFontMetrics(self._label_font())
|
||||||
width = max(MIN_WIDTH, min(MAX_WIDTH, metrics.horizontalAdvance(self.message) + 76))
|
extra = 76 + (18 if self._can_dismiss else 0)
|
||||||
|
width = max(MIN_WIDTH,
|
||||||
|
min(MAX_WIDTH, metrics.horizontalAdvance(self.message) + extra))
|
||||||
self.resize(width, HEIGHT)
|
self.resize(width, HEIGHT)
|
||||||
|
|
||||||
def _reposition(self):
|
def _reposition(self):
|
||||||
@@ -232,6 +259,8 @@ class Overlay(QWidget):
|
|||||||
self._draw_time(painter)
|
self._draw_time(painter)
|
||||||
else:
|
else:
|
||||||
self._draw_message(painter)
|
self._draw_message(painter)
|
||||||
|
if self._can_dismiss:
|
||||||
|
self._draw_dismiss(painter)
|
||||||
|
|
||||||
def _draw_indicator(self, painter, accent):
|
def _draw_indicator(self, painter, accent):
|
||||||
cx, cy = 26.0, self.height() / 2
|
cx, cy = 26.0, self.height() / 2
|
||||||
@@ -340,10 +369,27 @@ class Overlay(QWidget):
|
|||||||
text,
|
text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _can_dismiss(self):
|
||||||
|
return self.dismissable and self.state == "busy"
|
||||||
|
|
||||||
|
def _draw_dismiss(self, painter):
|
||||||
|
"""A faint cross on the right: without it there is nothing to say the
|
||||||
|
box can be clicked away, and a feature nobody can see is not one."""
|
||||||
|
cx, cy = self.width() - 18.0, self.height() / 2
|
||||||
|
pen = QPen(QColor(MUTED), 1.6)
|
||||||
|
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||||
|
painter.setPen(pen)
|
||||||
|
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||||
|
painter.drawLine(QPointF(cx - 4, cy - 4), QPointF(cx + 4, cy + 4))
|
||||||
|
painter.drawLine(QPointF(cx + 4, cy - 4), QPointF(cx - 4, cy + 4))
|
||||||
|
|
||||||
def _draw_message(self, painter):
|
def _draw_message(self, painter):
|
||||||
painter.setFont(self._label_font())
|
painter.setFont(self._label_font())
|
||||||
painter.setPen({"error": ERR, "warning": WARN}.get(self.state, TEXT))
|
painter.setPen({"error": ERR, "warning": WARN}.get(self.state, TEXT))
|
||||||
box = QRectF(46, 0, self.width() - 60, self.height())
|
# Leave the cross its corner rather than running the text under it.
|
||||||
|
box = QRectF(46, 0, self.width() - 60 - (18 if self._can_dismiss else 0),
|
||||||
|
self.height())
|
||||||
metrics = QFontMetrics(self._label_font())
|
metrics = QFontMetrics(self._label_font())
|
||||||
text = metrics.elidedText(self.message, Qt.TextElideMode.ElideRight, int(box.width()))
|
text = metrics.elidedText(self.message, Qt.TextElideMode.ElideRight, int(box.width()))
|
||||||
painter.drawText(
|
painter.drawText(
|
||||||
|
|||||||
@@ -395,6 +395,18 @@ class SettingsWindow(QDialog):
|
|||||||
dir_note.setWordWrap(True)
|
dir_note.setWordWrap(True)
|
||||||
how_form.addRow(dir_note)
|
how_form.addRow(dir_note)
|
||||||
|
|
||||||
|
# One scale for all three: how hard to think is one thing to want, and
|
||||||
|
# each provider is handed the nearest rung it actually has.
|
||||||
|
self.assistant_reasoning = QComboBox()
|
||||||
|
for label, value in REASONING_LEVELS:
|
||||||
|
self.assistant_reasoning.addItem(t(label), value)
|
||||||
|
self.assistant_reasoning.setToolTip(t(
|
||||||
|
"More thinking is slower, and you are standing in front of the "
|
||||||
|
"screen while it happens. Worth it for a job that has to be worked "
|
||||||
|
"out rather than looked up."
|
||||||
|
))
|
||||||
|
how_form.addRow(t("Thinking"), self.assistant_reasoning)
|
||||||
|
|
||||||
self.assistant_timeout = QSpinBox()
|
self.assistant_timeout = QSpinBox()
|
||||||
self.assistant_timeout.setRange(15, 3600)
|
self.assistant_timeout.setRange(15, 3600)
|
||||||
self.assistant_timeout.setSuffix(t(" s"))
|
self.assistant_timeout.setSuffix(t(" s"))
|
||||||
@@ -903,6 +915,7 @@ class SettingsWindow(QDialog):
|
|||||||
self._select_data(self.assistant_codex_sandbox, conf["assistant_codex_sandbox"])
|
self._select_data(self.assistant_codex_sandbox, conf["assistant_codex_sandbox"])
|
||||||
self.assistant_openrouter_model.setCurrentText(conf["assistant_openrouter_model"])
|
self.assistant_openrouter_model.setCurrentText(conf["assistant_openrouter_model"])
|
||||||
self._assistant_provider_changed() # selecting index 0 fires no signal
|
self._assistant_provider_changed() # selecting index 0 fires no signal
|
||||||
|
self._select_data(self.assistant_reasoning, conf["assistant_reasoning"])
|
||||||
self.assistant_dir.setText(conf["assistant_dir"])
|
self.assistant_dir.setText(conf["assistant_dir"])
|
||||||
self.assistant_timeout.setValue(int(conf["assistant_timeout"]))
|
self.assistant_timeout.setValue(int(conf["assistant_timeout"]))
|
||||||
self.assistant_session_minutes.setValue(int(conf["assistant_session_minutes"]))
|
self.assistant_session_minutes.setValue(int(conf["assistant_session_minutes"]))
|
||||||
@@ -997,6 +1010,7 @@ class SettingsWindow(QDialog):
|
|||||||
self.assistant_openrouter_model.currentText().strip()
|
self.assistant_openrouter_model.currentText().strip()
|
||||||
or cfg.DEFAULTS["assistant_openrouter_model"]
|
or cfg.DEFAULTS["assistant_openrouter_model"]
|
||||||
)
|
)
|
||||||
|
conf["assistant_reasoning"] = self.assistant_reasoning.currentData() or ""
|
||||||
conf["assistant_dir"] = self.assistant_dir.text().strip()
|
conf["assistant_dir"] = self.assistant_dir.text().strip()
|
||||||
conf["assistant_timeout"] = self.assistant_timeout.value()
|
conf["assistant_timeout"] = self.assistant_timeout.value()
|
||||||
conf["assistant_session_minutes"] = self.assistant_session_minutes.value()
|
conf["assistant_session_minutes"] = self.assistant_session_minutes.value()
|
||||||
|
|||||||
Reference in New Issue
Block a user