mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Let the command go to Codex or OpenRouter, not only Claude Code
Everything the last commit built assumed one agent was installed, which is a poor assumption to bake into a dictation tool. So the provider is a setting, and what it selects is one of three quite different things. Claude Code and Codex are the same shape: a CLI, streaming JSONL, a session id to resume, tools that reach the machine and whatever is connected to it. They share the runner. What differs is spelled out where it differs, which is more than the flag names: Codex has no system prompt to append, so the instruction rides in front of the command with a rule between them; it confines its commands in a sandbox rather than asking about them, so the permission setting is a sandbox mode; and `-s` is not accepted by `exec resume`, so both settings go through `-c` overrides, which are. OpenRouter is the odd one and is meant to be. No tools, no files, no calendar: it can say what the capital of Peru is and not what is in your diary, and the settings box says so rather than letting it be discovered. It also has no session to resume, so the conversation is kept here and resent, capped at 24 messages. A stored conversation names the provider that made it, and is ignored by any other: none of them can pick up another's thread, and a stale id would otherwise fail every command until the timeout cleared it. The interface calls the thing by its name, which in Turkish means the suffix has to agree with it: Claude'a but Codex'e, Claude'u but Codex'i. A name dropped into a sentence through t() cannot be inflected by that sentence, so it arrives inflected, from a small table in i18n. English takes the name as it is and keeps the preposition in the sentence.
This commit is contained in:
@@ -87,9 +87,11 @@ elapsed time, then the stage it is on. It never takes focus. Pressing
|
|||||||
what comes of it: the answer, or a sentence saying what was done. It is the
|
what comes of it: the answer, or a sentence saying what was done. It is the
|
||||||
session you would have opened yourself, so your skills and connected services
|
session you would have opened yourself, so your skills and connected services
|
||||||
are there, which is what makes "put that in my calendar on Thursday at three"
|
are there, which is what makes "put that in my calendar on Thursday at three"
|
||||||
a thing you can say to a window that is not Claude. Model, permissions and
|
a thing you can say to a window that is not Claude. Codex (`codex exec`) runs
|
||||||
working directory are under Settings → Claude, and commands close together
|
the same way, and OpenRouter is there as a plain question-and-answer fallback
|
||||||
stay in one conversation.
|
for a machine with neither CLI on it. Provider, model, permissions and working
|
||||||
|
directory are under Settings → Claude, and commands close together stay in one
|
||||||
|
conversation.
|
||||||
- **Meetings** are recorded from the microphone and the speaker output at the
|
- **Meetings** are recorded from the microphone and the speaker output at the
|
||||||
same time, which settles who said what by the channel a voice arrived on
|
same time, which settles who said what by the channel a voice arrived on
|
||||||
instead of guessing at it. The two sides are transcribed separately and
|
instead of guessing at it. The two sides are transcribed separately and
|
||||||
@@ -121,7 +123,7 @@ needs your user in the `input` group: `sudo usermod -aG input $USER`.
|
|||||||
dikte.py entry point, tray icon, state machine, IPC
|
dikte.py entry point, tray icon, state machine, IPC
|
||||||
audio.py PCM capture: pw-record for dictation, ffmpeg for a meeting
|
audio.py PCM capture: pw-record for dictation, ffmpeg for a meeting
|
||||||
meeting.py channel split, speaker labelling, cleanup, minutes
|
meeting.py channel split, speaker labelling, cleanup, minutes
|
||||||
assistant.py running a dictation through Claude Code and reading it back
|
assistant.py running a dictation through Claude Code, Codex or OpenRouter
|
||||||
api.py transcription on either provider, OpenRouter cleanup (stdlib only)
|
api.py transcription on either provider, OpenRouter cleanup (stdlib only)
|
||||||
worker.py transcribe → clean up → clipboard → paste
|
worker.py transcribe → clean up → clipboard → paste
|
||||||
vad.py deciding whether a recording holds speech at all
|
vad.py deciding whether a recording holds speech at all
|
||||||
|
|||||||
+4
-2
@@ -86,7 +86,9 @@ süreyi, ardından hangi aşamada olduğunu gösterir. Odak almaz. Dikte çalı
|
|||||||
yapıştırır: cevabı ya da ne yapıldığını söyleyen bir cümle. Kendi açacağın
|
yapıştırır: cevabı ya da ne yapıldığını söyleyen bir cümle. Kendi açacağın
|
||||||
oturumun aynısıdır, yani skill'lerin ve bağlı servislerin oradadır; "bunu
|
oturumun aynısıdır, yani skill'lerin ve bağlı servislerin oradadır; "bunu
|
||||||
perşembe üçe takvime koy" cümlesini Claude olmayan bir pencerede söyleyebilir
|
perşembe üçe takvime koy" cümlesini Claude olmayan bir pencerede söyleyebilir
|
||||||
olmanı sağlayan da budur. Model, izinler ve çalışma dizini Ayarlar → Claude
|
olmanı sağlayan da budur. Codex (`codex exec`) da aynı şekilde çalışır;
|
||||||
|
OpenRouter ise ikisi de kurulu olmayan bir makinede düz soru cevap için
|
||||||
|
duruyor. Sağlayıcı, model, izinler ve çalışma dizini Ayarlar → Claude
|
||||||
sekmesinde; arka arkaya verilen komutlar tek bir konuşmada kalır.
|
sekmesinde; arka arkaya verilen komutlar tek bir konuşmada kalır.
|
||||||
- **Toplantılar** mikrofonla hoparlör çıkışından aynı anda kaydedilir; kimin ne
|
- **Toplantılar** mikrofonla hoparlör çıkışından aynı anda kaydedilir; kimin ne
|
||||||
dediği tahmin edilmez, sesin hangi kanaldan geldiğiyle belli olur. İki taraf
|
dediği tahmin edilmez, sesin hangi kanaldan geldiğiyle belli olur. İki taraf
|
||||||
@@ -118,7 +120,7 @@ grubunda olmasını gerektirir: `sudo usermod -aG input $USER`.
|
|||||||
dikte.py giriş noktası, tepsi simgesi, durum makinesi, IPC
|
dikte.py giriş noktası, tepsi simgesi, durum makinesi, IPC
|
||||||
audio.py PCM kaydı: diktede pw-record, toplantıda ffmpeg
|
audio.py PCM kaydı: diktede pw-record, toplantıda ffmpeg
|
||||||
meeting.py kanal ayırma, konuşmacı etiketi, temizleme, tutanak
|
meeting.py kanal ayırma, konuşmacı etiketi, temizleme, tutanak
|
||||||
assistant.py dikteyi Claude Code'dan geçirip cevabı geri okuma
|
assistant.py dikteyi Claude Code, Codex ya da OpenRouter'dan geçirme
|
||||||
api.py iki sağlayıcıda transkript + OpenRouter temizleme (yalnız stdlib)
|
api.py iki sağlayıcıda transkript + OpenRouter temizleme (yalnız stdlib)
|
||||||
worker.py transkript → temizleme → pano → yapıştırma
|
worker.py transkript → temizleme → pano → yapıştırma
|
||||||
vad.py kayıtta gerçekten konuşma var mı kararı
|
vad.py kayıtta gerçekten konuşma var mı kararı
|
||||||
|
|||||||
@@ -209,6 +209,38 @@ 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,
|
||||||
|
timeout=180):
|
||||||
|
"""A conversation, rather than one transcript rewritten.
|
||||||
|
|
||||||
|
The messages are the whole history and come back unchanged; the caller keeps
|
||||||
|
them, because there is no session on OpenRouter's side to resume.
|
||||||
|
"""
|
||||||
|
if not api_key:
|
||||||
|
raise ApiError(t("{service} API key is empty. Add it in Settings.",
|
||||||
|
service="OpenRouter"))
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": [{"role": "system", "content": system_prompt}] + list(messages),
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
data = _request(
|
||||||
|
f"{base_url.rstrip('/')}/chat/completions",
|
||||||
|
json.dumps(payload).encode("utf-8"),
|
||||||
|
_headers("openrouter", api_key, "application/json"),
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
except ApiError as exc:
|
||||||
|
raise explain(exc, "OpenRouter") from None
|
||||||
|
choices = data.get("choices") or []
|
||||||
|
if not choices:
|
||||||
|
raise ApiError(_extract_error(json.dumps(data)))
|
||||||
|
content = ((choices[0].get("message") or {}).get("content") or "").strip()
|
||||||
|
if not content:
|
||||||
|
raise ApiError(t("The model returned an empty reply."))
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
def _get_json(url, headers, timeout=20):
|
def _get_json(url, headers, timeout=20):
|
||||||
req = urllib.request.Request(url, headers=headers)
|
req = urllib.request.Request(url, headers=headers)
|
||||||
try:
|
try:
|
||||||
|
|||||||
+266
-105
@@ -1,14 +1,25 @@
|
|||||||
"""Handing a dictation to Claude Code as a command, and pasting back its answer.
|
"""Handing a dictation to an agent as a command, and pasting back its answer.
|
||||||
|
|
||||||
`claude -p` runs the same session an interactive window would open: the same
|
Three of them, because not everyone has the same one installed:
|
||||||
skills, the same MCP servers, the same account. So a dictation does not have to
|
|
||||||
end as text on the screen. It can be a question to answer or a job to carry out,
|
|
||||||
and what comes back is pasted exactly where the transcript would have been.
|
|
||||||
|
|
||||||
Its output is read as it arrives rather than waited out. A command that reaches
|
Claude Code `claude -p`, the session you would have opened yourself
|
||||||
for the calendar or the web takes long enough that a still indicator is
|
Codex `codex exec`, the same idea from the other shop
|
||||||
indistinguishable from a hang, so every tool it picks up is named in the corner
|
OpenRouter a plain chat request, over the key that is already configured
|
||||||
while it works.
|
|
||||||
|
The first two are the whole machine: they run commands, read files, and reach
|
||||||
|
whatever skills and services you have connected, which is what makes "put that
|
||||||
|
in my calendar on Thursday" a thing you can say. OpenRouter cannot touch any of
|
||||||
|
that, and is there so that a question still gets an answer on a machine with
|
||||||
|
neither CLI installed.
|
||||||
|
|
||||||
|
Whichever it is, the reply is pasted exactly where the transcript would have
|
||||||
|
been, and the conversation carries across dictations so that "and move that to
|
||||||
|
Friday" knows what "that" is.
|
||||||
|
|
||||||
|
The two CLIs are read as they stream rather than waited out. A command that
|
||||||
|
reaches for the calendar or the web takes long enough that a still indicator is
|
||||||
|
indistinguishable from a hang, so every tool they pick up is named in the corner
|
||||||
|
while they work.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -18,15 +29,22 @@ import subprocess
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import api
|
||||||
import config as cfg
|
import config as cfg
|
||||||
from i18n import t
|
from i18n import t
|
||||||
|
|
||||||
SESSION_FILE = cfg.DATA_DIR / "assistant.json"
|
SESSION_FILE = cfg.DATA_DIR / "assistant.json"
|
||||||
|
PROVIDERS = ("claude", "codex", "openrouter")
|
||||||
|
|
||||||
# What to say in the indicator for a tool, keyed by name. Anything unlisted is
|
# How many messages of an OpenRouter conversation are carried forward. The two
|
||||||
# named as it comes, which is better than a generic "working" for the tools that
|
# CLIs keep their own history and need no such number; here every turn is resent
|
||||||
# arrive from an MCP server nobody wrote this table for.
|
# in full, so the window has to end somewhere.
|
||||||
TOOL_LABELS = {
|
MAX_HISTORY = 24
|
||||||
|
|
||||||
|
# What to say in the indicator for a tool, keyed by the name the CLI uses.
|
||||||
|
# Anything unlisted is named as it comes, which beats a generic "working" for
|
||||||
|
# tools arriving from an MCP server nobody wrote this table for.
|
||||||
|
CLAUDE_TOOLS = {
|
||||||
"Bash": "Running a command…",
|
"Bash": "Running a command…",
|
||||||
"BashOutput": "Running a command…",
|
"BashOutput": "Running a command…",
|
||||||
"Read": "Reading…",
|
"Read": "Reading…",
|
||||||
@@ -40,6 +58,14 @@ TOOL_LABELS = {
|
|||||||
"Task": "Handing it to a subagent…",
|
"Task": "Handing it to a subagent…",
|
||||||
"TodoWrite": "Planning…",
|
"TodoWrite": "Planning…",
|
||||||
}
|
}
|
||||||
|
CODEX_ITEMS = {
|
||||||
|
"command_execution": "Running a command…",
|
||||||
|
"reasoning": "Thinking…",
|
||||||
|
"web_search": "Searching the web…",
|
||||||
|
"file_change": "Editing a file…",
|
||||||
|
"patch_apply": "Editing a file…",
|
||||||
|
"todo_list": "Planning…",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AssistantError(Exception):
|
class AssistantError(Exception):
|
||||||
@@ -50,34 +76,61 @@ class Cancelled(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def provider(conf):
|
||||||
|
chosen = conf["assistant_provider"]
|
||||||
|
return chosen if chosen in PROVIDERS else "claude"
|
||||||
|
|
||||||
|
|
||||||
|
def executable(name):
|
||||||
|
"""The CLI a provider runs, or "" when it needs none."""
|
||||||
|
return {"claude": "claude", "codex": "codex"}.get(name, "")
|
||||||
|
|
||||||
|
|
||||||
|
def display_name(conf):
|
||||||
|
"""What to call the thing being asked, in the tray and in the corner."""
|
||||||
|
return {"claude": "Claude", "codex": "Codex"}.get(provider(conf), "OpenRouter")
|
||||||
|
|
||||||
|
|
||||||
# --- the conversation -----------------------------------------------------
|
# --- the conversation -----------------------------------------------------
|
||||||
#
|
#
|
||||||
# One conversation is kept across dictations, so "and move that to tomorrow"
|
# One conversation is kept across dictations, so "and move that to tomorrow"
|
||||||
# means something. It is dropped once it has sat unused for long enough: an
|
# means something. It is dropped once it has sat unused for long enough: an hour
|
||||||
# hour later the next command is almost certainly a new subject, and dragging
|
# later the next command is almost certainly a new subject, and dragging the old
|
||||||
# the old one along costs tokens and invites the model to answer the wrong
|
# one along costs tokens and invites an answer to the wrong question. Switching
|
||||||
# question.
|
# provider drops it too, since none of them can pick up another's thread.
|
||||||
|
|
||||||
def read_session(max_age_seconds):
|
def _read_row(name, max_age_seconds):
|
||||||
"""The session to continue, or "" when there is none worth continuing."""
|
|
||||||
try:
|
try:
|
||||||
with open(SESSION_FILE, encoding="utf-8") as fh:
|
with open(SESSION_FILE, encoding="utf-8") as fh:
|
||||||
row = json.load(fh)
|
row = json.load(fh)
|
||||||
except (OSError, json.JSONDecodeError, ValueError):
|
except (OSError, json.JSONDecodeError, ValueError):
|
||||||
return ""
|
return {}
|
||||||
session = str(row.get("session", ""))
|
if not isinstance(row, dict) or row.get("provider") != name:
|
||||||
if not session:
|
return {}
|
||||||
return ""
|
|
||||||
if max_age_seconds and time.time() - row.get("ts", 0) > max_age_seconds:
|
if max_age_seconds and time.time() - row.get("ts", 0) > max_age_seconds:
|
||||||
return ""
|
return {}
|
||||||
return session
|
return row
|
||||||
|
|
||||||
|
|
||||||
def write_session(session):
|
def read_session(name, max_age_seconds):
|
||||||
|
"""The id to resume, for the providers that keep their own history."""
|
||||||
|
return str(_read_row(name, max_age_seconds).get("session", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def read_messages(name, max_age_seconds):
|
||||||
|
"""The conversation so far, for the provider that does not."""
|
||||||
|
messages = _read_row(name, max_age_seconds).get("messages")
|
||||||
|
return messages if isinstance(messages, list) else []
|
||||||
|
|
||||||
|
|
||||||
|
def write_session(name, session="", messages=None):
|
||||||
|
row = {"provider": name, "session": session, "ts": time.time()}
|
||||||
|
if messages is not None:
|
||||||
|
row["messages"] = messages[-MAX_HISTORY:]
|
||||||
try:
|
try:
|
||||||
cfg.DATA_DIR.mkdir(parents=True, exist_ok=True)
|
cfg.DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
with open(SESSION_FILE, "w", encoding="utf-8") as fh:
|
with open(SESSION_FILE, "w", encoding="utf-8") as fh:
|
||||||
json.dump({"session": session, "ts": time.time()}, fh)
|
json.dump(row, fh, ensure_ascii=False)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -96,7 +149,9 @@ def session_age():
|
|||||||
row = json.load(fh)
|
row = json.load(fh)
|
||||||
except (OSError, json.JSONDecodeError, ValueError):
|
except (OSError, json.JSONDecodeError, ValueError):
|
||||||
return None
|
return None
|
||||||
return time.time() - row.get("ts", 0) if row.get("session") else None
|
if not isinstance(row, dict) or not (row.get("session") or row.get("messages")):
|
||||||
|
return None
|
||||||
|
return time.time() - row.get("ts", 0)
|
||||||
|
|
||||||
|
|
||||||
# --- the call -------------------------------------------------------------
|
# --- the call -------------------------------------------------------------
|
||||||
@@ -109,34 +164,42 @@ def working_dir(conf):
|
|||||||
|
|
||||||
|
|
||||||
def ask(prompt, conf, on_stage=None, should_stop=None):
|
def ask(prompt, conf, on_stage=None, should_stop=None):
|
||||||
"""Run the prompt through Claude Code. Returns (answer, warning).
|
"""Run the prompt through the configured agent. Returns (answer, warning).
|
||||||
|
|
||||||
`warning` is set when the answer arrived but something about the run should
|
`warning` is set when the answer arrived but something about the run should
|
||||||
be seen anyway, a denied tool above all: the reply still reads like a normal
|
be seen anyway, a denied tool above all: the reply still reads like a normal
|
||||||
one, and only the denial explains why it did not do what it was asked to.
|
one, and only the denial explains why it did not do what it was asked to.
|
||||||
"""
|
"""
|
||||||
if not shutil.which("claude"):
|
name = provider(conf)
|
||||||
|
if name == "openrouter":
|
||||||
|
return _ask_openrouter(prompt, conf, on_stage)
|
||||||
|
|
||||||
|
binary = executable(name)
|
||||||
|
if not shutil.which(binary):
|
||||||
raise AssistantError(t(
|
raise AssistantError(t(
|
||||||
"claude not found. Install Claude Code and make sure `claude` is on "
|
"{binary} not found. Install it, or pick another provider under "
|
||||||
"your PATH."
|
"Settings → Claude.", binary=binary,
|
||||||
))
|
))
|
||||||
|
|
||||||
session = read_session(conf["assistant_session_minutes"] * 60)
|
run = _ask_claude if name == "claude" else _ask_codex
|
||||||
|
session = read_session(name, conf["assistant_session_minutes"] * 60)
|
||||||
try:
|
try:
|
||||||
return _run(prompt, conf, session, on_stage, should_stop)
|
return run(prompt, conf, session, on_stage, should_stop)
|
||||||
except _SessionGone:
|
except _SessionGone:
|
||||||
# The conversation it pointed at is not there any more: the history was
|
# The conversation it pointed at is not there any more: the history was
|
||||||
# cleared, or it was started somewhere else. Say nothing and start over,
|
# cleared, or it was started somewhere else. Say nothing and start over,
|
||||||
# because from the outside this is just the first command of the day.
|
# because from the outside this is just the first command of the day.
|
||||||
clear_session()
|
clear_session()
|
||||||
return _run(prompt, conf, "", on_stage, should_stop)
|
return run(prompt, conf, "", on_stage, should_stop)
|
||||||
|
|
||||||
|
|
||||||
class _SessionGone(Exception):
|
class _SessionGone(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _run(prompt, conf, session, on_stage, should_stop):
|
# --- Claude Code ----------------------------------------------------------
|
||||||
|
|
||||||
|
def _ask_claude(prompt, conf, session, on_stage, should_stop):
|
||||||
cmd = [
|
cmd = [
|
||||||
"claude", "-p", prompt,
|
"claude", "-p", prompt,
|
||||||
"--output-format", "stream-json", "--verbose",
|
"--output-format", "stream-json", "--verbose",
|
||||||
@@ -147,6 +210,137 @@ def _run(prompt, conf, session, on_stage, should_stop):
|
|||||||
if session:
|
if session:
|
||||||
cmd += ["--resume", session]
|
cmd += ["--resume", session]
|
||||||
|
|
||||||
|
found = {"answer": "", "warning": "", "session": "", "failure": ""}
|
||||||
|
|
||||||
|
def on_event(event):
|
||||||
|
kind = event.get("type")
|
||||||
|
if kind == "system" and event.get("subtype") == "init":
|
||||||
|
found["session"] = event.get("session_id") or found["session"]
|
||||||
|
elif kind == "assistant" and on_stage:
|
||||||
|
for block in event.get("message", {}).get("content", []) or []:
|
||||||
|
if isinstance(block, dict) and block.get("type") == "tool_use":
|
||||||
|
on_stage(_claude_label(block))
|
||||||
|
elif kind == "result":
|
||||||
|
found["session"] = event.get("session_id") or found["session"]
|
||||||
|
answer = (event.get("result") or "").strip()
|
||||||
|
if event.get("is_error"):
|
||||||
|
found["failure"] = answer or t("Claude ended with an error.")
|
||||||
|
else:
|
||||||
|
found["answer"] = answer
|
||||||
|
found["warning"] = _denial_warning(event)
|
||||||
|
|
||||||
|
code, stderr = _stream(cmd, conf, on_event, should_stop)
|
||||||
|
return _conclude(found, code, stderr, session, "Claude")
|
||||||
|
|
||||||
|
|
||||||
|
def _claude_label(block):
|
||||||
|
name = block.get("name", "")
|
||||||
|
if name in CLAUDE_TOOLS:
|
||||||
|
return t(CLAUDE_TOOLS[name])
|
||||||
|
if name == "Skill":
|
||||||
|
return t("Using {name}…", name=(block.get("input") or {}).get("skill") or "a skill")
|
||||||
|
if name.startswith("mcp__"):
|
||||||
|
parts = name.split("__")
|
||||||
|
return t("Using {name}…", name=parts[1] if len(parts) > 1 else name)
|
||||||
|
return t("Using {name}…", name=name or "a tool")
|
||||||
|
|
||||||
|
|
||||||
|
def _denial_warning(event):
|
||||||
|
denials = event.get("permission_denials") or []
|
||||||
|
names = []
|
||||||
|
for denial in denials:
|
||||||
|
name = denial.get("tool_name") if isinstance(denial, dict) else str(denial)
|
||||||
|
if name and name not in names:
|
||||||
|
names.append(name)
|
||||||
|
return t("It was not allowed to use: {tools}", tools=", ".join(names)) if names else ""
|
||||||
|
|
||||||
|
|
||||||
|
# --- Codex ----------------------------------------------------------------
|
||||||
|
|
||||||
|
def _ask_codex(prompt, conf, session, on_stage, should_stop):
|
||||||
|
# Codex takes no system prompt of its own, so the instruction rides along in
|
||||||
|
# front of the command, kept apart from it so the two are not read as one.
|
||||||
|
body = f"{conf.assistant_prompt()}\n\n---\n\n{prompt}"
|
||||||
|
settings = [
|
||||||
|
"-c", f'sandbox_mode="{conf["assistant_codex_sandbox"]}"',
|
||||||
|
"-c", 'approval_policy="never"', # there is nobody here to approve
|
||||||
|
"--skip-git-repo-check",
|
||||||
|
"--json",
|
||||||
|
]
|
||||||
|
if conf["assistant_codex_model"].strip():
|
||||||
|
settings += ["-m", conf["assistant_codex_model"].strip()]
|
||||||
|
|
||||||
|
cmd = (["codex", "exec", "resume", session] if session else ["codex", "exec"])
|
||||||
|
cmd += settings + [body]
|
||||||
|
|
||||||
|
found = {"answer": "", "warning": "", "session": "", "failure": ""}
|
||||||
|
|
||||||
|
def on_event(event):
|
||||||
|
kind = event.get("type")
|
||||||
|
if kind == "thread.started":
|
||||||
|
found["session"] = event.get("thread_id") or found["session"]
|
||||||
|
elif kind in ("item.started", "item.completed"):
|
||||||
|
item = event.get("item") or {}
|
||||||
|
item_type = item.get("type")
|
||||||
|
# Every message is kept rather than only the last: a run can say
|
||||||
|
# something, use a tool and speak again, and the closing one is the
|
||||||
|
# answer.
|
||||||
|
if item_type == "agent_message" and kind == "item.completed":
|
||||||
|
found["answer"] = (item.get("text") or "").strip() or found["answer"]
|
||||||
|
elif on_stage and kind == "item.started":
|
||||||
|
on_stage(_codex_label(item))
|
||||||
|
elif kind in ("turn.failed", "error"):
|
||||||
|
error = event.get("error") or {}
|
||||||
|
found["failure"] = (error.get("message") if isinstance(error, dict)
|
||||||
|
else str(error)) or t("Codex ended with an error.")
|
||||||
|
|
||||||
|
code, stderr = _stream(cmd, conf, on_event, should_stop)
|
||||||
|
return _conclude(found, code, stderr, session, "Codex")
|
||||||
|
|
||||||
|
|
||||||
|
def _codex_label(item):
|
||||||
|
item_type = item.get("type", "")
|
||||||
|
if item_type in CODEX_ITEMS:
|
||||||
|
return t(CODEX_ITEMS[item_type])
|
||||||
|
if item_type == "mcp_tool_call":
|
||||||
|
return t("Using {name}…", name=item.get("server") or item.get("tool") or "a tool")
|
||||||
|
return t("Using {name}…", name=item_type or "a tool")
|
||||||
|
|
||||||
|
|
||||||
|
# --- OpenRouter -----------------------------------------------------------
|
||||||
|
|
||||||
|
def _ask_openrouter(prompt, conf, on_stage):
|
||||||
|
"""No tools, no files, no calendar: a question and an answer.
|
||||||
|
|
||||||
|
It is the fallback for a machine with neither CLI on it, so it says what it
|
||||||
|
knows and nothing else. The conversation is ours to keep here, since there
|
||||||
|
is no session on the other end to resume.
|
||||||
|
"""
|
||||||
|
if on_stage:
|
||||||
|
on_stage(t("Thinking…"))
|
||||||
|
history = read_messages("openrouter", conf["assistant_session_minutes"] * 60)
|
||||||
|
messages = history + [{"role": "user", "content": prompt}]
|
||||||
|
try:
|
||||||
|
answer = api.chat(
|
||||||
|
messages, conf.openrouter_key(), conf["assistant_openrouter_model"],
|
||||||
|
conf.assistant_prompt(), base_url=conf["openrouter_base_url"],
|
||||||
|
timeout=conf["assistant_timeout"],
|
||||||
|
)
|
||||||
|
except api.ApiError as exc:
|
||||||
|
raise AssistantError(str(exc)) from exc
|
||||||
|
write_session("openrouter",
|
||||||
|
messages=messages + [{"role": "assistant", "content": answer}])
|
||||||
|
return answer, ""
|
||||||
|
|
||||||
|
|
||||||
|
# --- running a CLI --------------------------------------------------------
|
||||||
|
|
||||||
|
def _stream(cmd, conf, on_event, should_stop):
|
||||||
|
"""Run cmd, hand every JSON line it prints to on_event.
|
||||||
|
|
||||||
|
Returns (exit code, stderr). Raises Cancelled when the stop was asked for,
|
||||||
|
and AssistantError when the clock ran out.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
cmd, cwd=working_dir(conf), stdin=subprocess.DEVNULL,
|
cmd, cwd=working_dir(conf), stdin=subprocess.DEVNULL,
|
||||||
@@ -154,9 +348,9 @@ def _run(prompt, conf, session, on_stage, should_stop):
|
|||||||
text=True, encoding="utf-8", errors="replace", bufsize=1,
|
text=True, encoding="utf-8", errors="replace", bufsize=1,
|
||||||
)
|
)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
raise AssistantError(t("Could not run claude: {error}", error=exc)) from exc
|
raise AssistantError(t("Could not run {binary}: {error}",
|
||||||
|
binary=cmd[0], error=exc)) from exc
|
||||||
|
|
||||||
answer, warning, new_session, failure = "", "", "", ""
|
|
||||||
# Reading the stream blocks between lines, and a model that thinks for a
|
# Reading the stream blocks between lines, and a model that thinks for a
|
||||||
# minute sends none. So the clock and the stop button are watched from the
|
# minute sends none. So the clock and the stop button are watched from the
|
||||||
# side, and they end the run by killing the process: that closes the stream
|
# side, and they end the run by killing the process: that closes the stream
|
||||||
@@ -171,23 +365,16 @@ def _run(prompt, conf, session, on_stage, should_stop):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
|
line = line.strip()
|
||||||
|
# Both CLIs print the odd unstructured line among the JSON.
|
||||||
|
if not line.startswith("{"):
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
event = json.loads(line)
|
event = json.loads(line)
|
||||||
except (json.JSONDecodeError, ValueError):
|
except (json.JSONDecodeError, ValueError):
|
||||||
continue
|
continue
|
||||||
kind = event.get("type")
|
if isinstance(event, dict):
|
||||||
if kind == "system" and event.get("subtype") == "init":
|
on_event(event)
|
||||||
new_session = event.get("session_id", "") or new_session
|
|
||||||
elif kind == "assistant" and on_stage:
|
|
||||||
for label in _labels(event):
|
|
||||||
on_stage(label)
|
|
||||||
elif kind == "result":
|
|
||||||
new_session = event.get("session_id", "") or new_session
|
|
||||||
answer = (event.get("result") or "").strip()
|
|
||||||
if event.get("is_error"):
|
|
||||||
failure = answer or t("Claude ended with an error.")
|
|
||||||
answer = ""
|
|
||||||
warning = _denial_warning(event)
|
|
||||||
finally:
|
finally:
|
||||||
stderr = _finish(proc)
|
stderr = _finish(proc)
|
||||||
watchdog.join(timeout=1)
|
watchdog.join(timeout=1)
|
||||||
@@ -195,24 +382,33 @@ def _run(prompt, conf, session, on_stage, should_stop):
|
|||||||
if ended["cancelled"]:
|
if ended["cancelled"]:
|
||||||
raise Cancelled()
|
raise Cancelled()
|
||||||
if ended["timed_out"]:
|
if ended["timed_out"]:
|
||||||
raise AssistantError(t(
|
raise AssistantError(t("It did not finish within {seconds} seconds.",
|
||||||
"Claude did not finish within {seconds} seconds.",
|
seconds=conf["assistant_timeout"]))
|
||||||
seconds=conf["assistant_timeout"],
|
return proc.returncode, stderr
|
||||||
))
|
|
||||||
if proc.returncode != 0 and not answer:
|
|
||||||
|
def _conclude(found, code, stderr, session, service):
|
||||||
|
"""Turn what the stream said into an answer, or into the reason there is none."""
|
||||||
|
if code != 0 and not found["answer"]:
|
||||||
if session and _session_missing(stderr):
|
if session and _session_missing(stderr):
|
||||||
raise _SessionGone()
|
raise _SessionGone()
|
||||||
raise AssistantError(_first_line(stderr) or failure or t(
|
raise AssistantError(_last_line(stderr) or found["failure"] or t(
|
||||||
"claude exited with code {code}.", code=proc.returncode
|
"{service} exited with code {code}.", service=service, code=code))
|
||||||
))
|
if found["failure"] and not found["answer"]:
|
||||||
if failure:
|
raise AssistantError(found["failure"])
|
||||||
raise AssistantError(failure)
|
if not found["answer"]:
|
||||||
if not answer:
|
raise AssistantError(t("{service} answered with nothing.", service=service))
|
||||||
raise AssistantError(t("Claude answered with nothing."))
|
if found["session"]:
|
||||||
|
write_session("claude" if service == "Claude" else "codex", found["session"])
|
||||||
|
return found["answer"], found["warning"]
|
||||||
|
|
||||||
if new_session:
|
|
||||||
write_session(new_session)
|
def _session_missing(stderr):
|
||||||
return answer, warning
|
lowered = (stderr or "").lower()
|
||||||
|
if "session" in lowered or "thread" in lowered or "conversation" in lowered:
|
||||||
|
return any(word in lowered for word in ("not found", "no such", "unknown",
|
||||||
|
"does not exist", "no conversation"))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _watch(proc, deadline, should_stop, ended):
|
def _watch(proc, deadline, should_stop, ended):
|
||||||
@@ -228,49 +424,14 @@ def _watch(proc, deadline, should_stop, ended):
|
|||||||
_kill(proc)
|
_kill(proc)
|
||||||
|
|
||||||
|
|
||||||
def _labels(event):
|
|
||||||
"""The indicator lines for one assistant message, in the order they happen."""
|
|
||||||
out = []
|
|
||||||
for block in event.get("message", {}).get("content", []) or []:
|
|
||||||
if not isinstance(block, dict) or block.get("type") != "tool_use":
|
|
||||||
continue
|
|
||||||
name = block.get("name", "")
|
|
||||||
if name in TOOL_LABELS:
|
|
||||||
out.append(t(TOOL_LABELS[name]))
|
|
||||||
elif name == "Skill":
|
|
||||||
skill = (block.get("input") or {}).get("skill", "")
|
|
||||||
out.append(t("Using {name}…", name=skill or "a skill"))
|
|
||||||
elif name.startswith("mcp__"):
|
|
||||||
parts = name.split("__")
|
|
||||||
out.append(t("Using {name}…", name=parts[1] if len(parts) > 1 else name))
|
|
||||||
elif name:
|
|
||||||
out.append(t("Using {name}…", name=name))
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _denial_warning(event):
|
|
||||||
denials = event.get("permission_denials") or []
|
|
||||||
if not denials:
|
|
||||||
return ""
|
|
||||||
names = []
|
|
||||||
for denial in denials:
|
|
||||||
name = denial.get("tool_name") if isinstance(denial, dict) else str(denial)
|
|
||||||
if name and name not in names:
|
|
||||||
names.append(name)
|
|
||||||
return t("Claude was not allowed to use: {tools}", tools=", ".join(names))
|
|
||||||
|
|
||||||
|
|
||||||
def _session_missing(stderr):
|
|
||||||
lowered = stderr.lower()
|
|
||||||
return "session" in lowered and ("not found" in lowered or "no conversation" in lowered)
|
|
||||||
|
|
||||||
|
|
||||||
def _kill(proc):
|
def _kill(proc):
|
||||||
proc.terminate()
|
|
||||||
try:
|
try:
|
||||||
|
proc.terminate()
|
||||||
proc.wait(timeout=3)
|
proc.wait(timeout=3)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
proc.kill()
|
proc.kill()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _finish(proc):
|
def _finish(proc):
|
||||||
@@ -290,6 +451,6 @@ def _finish(proc):
|
|||||||
return stderr
|
return stderr
|
||||||
|
|
||||||
|
|
||||||
def _first_line(text):
|
def _last_line(text):
|
||||||
lines = [line for line in (text or "").splitlines() if line.strip()]
|
lines = [line for line in (text or "").splitlines() if line.strip()]
|
||||||
return lines[-1].strip() if lines else ""
|
return lines[-1].strip() if lines else ""
|
||||||
|
|||||||
@@ -313,10 +313,14 @@ DEFAULTS = {
|
|||||||
"meeting_keep_audio": False, # a failed run keeps its audio regardless
|
"meeting_keep_audio": False, # a failed run keeps its audio regardless
|
||||||
"meeting_shortcut": "", # empty -> tray only
|
"meeting_shortcut": "", # empty -> tray only
|
||||||
|
|
||||||
# --- asking Claude Code -----------------------------------------------
|
# --- speaking a command to an agent -------------------------------------
|
||||||
"assistant_shortcut": "", # empty -> tray only
|
"assistant_shortcut": "", # empty -> tray only
|
||||||
"assistant_model": "sonnet", # an alias, or a full model id
|
"assistant_provider": "claude", # claude | codex | openrouter
|
||||||
|
"assistant_model": "sonnet", # Claude Code: an alias, or a full model id
|
||||||
"assistant_permission_mode": "auto",
|
"assistant_permission_mode": "auto",
|
||||||
|
"assistant_codex_model": "", # empty -> whatever Codex is set to
|
||||||
|
"assistant_codex_sandbox": "workspace-write",
|
||||||
|
"assistant_openrouter_model": "google/gemini-3.5-flash",
|
||||||
"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
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import assistant # noqa: E402
|
|||||||
import audio # noqa: E402
|
import audio # noqa: E402
|
||||||
import config as cfg # noqa: E402
|
import config as cfg # noqa: E402
|
||||||
import hotkey # noqa: E402
|
import hotkey # noqa: E402
|
||||||
|
import i18n # noqa: E402
|
||||||
import meeting # noqa: E402
|
import meeting # noqa: E402
|
||||||
from i18n import t # noqa: E402
|
from i18n import t # noqa: E402
|
||||||
from meeting import MeetingPipeline # noqa: E402
|
from meeting import MeetingPipeline # noqa: E402
|
||||||
@@ -192,6 +193,7 @@ class Dikte:
|
|||||||
BUSY: ("Working…", "view-refresh", "Dikte: working"),
|
BUSY: ("Working…", "view-refresh", "Dikte: working"),
|
||||||
}
|
}
|
||||||
label, icon, tip = labels[self.state]
|
label, icon, tip = labels[self.state]
|
||||||
|
agent = assistant.display_name(self.conf)
|
||||||
if self.ask_mode:
|
if self.ask_mode:
|
||||||
tip = "Dikte: talking to Claude"
|
tip = "Dikte: talking to Claude"
|
||||||
if self.state == RECORDING:
|
if self.state == RECORDING:
|
||||||
@@ -202,9 +204,11 @@ class Dikte:
|
|||||||
self.toggle_action.setEnabled(
|
self.toggle_action.setEnabled(
|
||||||
self.state == IDLE or (self.state == RECORDING and not self.ask_mode)
|
self.state == IDLE or (self.state == RECORDING and not self.ask_mode)
|
||||||
)
|
)
|
||||||
|
asked = i18n.name(agent, "dative")
|
||||||
self.ask_action.setText(
|
self.ask_action.setText(
|
||||||
t("Stop and ask Claude") if self.state == RECORDING and self.ask_mode
|
t("Stop and ask {name}", name=asked)
|
||||||
else t("Ask Claude")
|
if self.state == RECORDING and self.ask_mode
|
||||||
|
else t("Ask {name}", name=asked)
|
||||||
)
|
)
|
||||||
self.ask_action.setEnabled(
|
self.ask_action.setEnabled(
|
||||||
self.state == IDLE or (self.state == RECORDING and self.ask_mode)
|
self.state == IDLE or (self.state == RECORDING and self.ask_mode)
|
||||||
@@ -213,7 +217,8 @@ class Dikte:
|
|||||||
# A Claude command is the one job long enough to be worth calling off
|
# A Claude command is the one job long enough to be worth calling off
|
||||||
# once it is already running.
|
# once it is already running.
|
||||||
self.cancel_action.setText(
|
self.cancel_action.setText(
|
||||||
t("Stop Claude") if self.state == BUSY and self.ask_mode
|
t("Stop {name}", name=i18n.name(agent, "accusative"))
|
||||||
|
if self.state == BUSY and self.ask_mode
|
||||||
else t("Cancel recording")
|
else t("Cancel recording")
|
||||||
)
|
)
|
||||||
self.cancel_action.setEnabled(
|
self.cancel_action.setEnabled(
|
||||||
@@ -351,7 +356,10 @@ class Dikte:
|
|||||||
"""Drop the thread Claude has been following, so the next command starts
|
"""Drop the thread Claude has been following, so the next command starts
|
||||||
a conversation of its own."""
|
a conversation of its own."""
|
||||||
assistant.clear_session()
|
assistant.clear_session()
|
||||||
self.overlay.show_done(t("Claude starts fresh next time."), 2500)
|
self.overlay.show_done(
|
||||||
|
t("{name} starts fresh next time.",
|
||||||
|
name=assistant.display_name(self.conf)), 2500
|
||||||
|
)
|
||||||
|
|
||||||
def _tick(self):
|
def _tick(self):
|
||||||
seconds = self.elapsed.elapsed() / 1000.0
|
seconds = self.elapsed.elapsed() / 1000.0
|
||||||
@@ -493,17 +501,19 @@ class Dikte:
|
|||||||
|
|
||||||
def _on_finished(self, _raw, text, warning):
|
def _on_finished(self, _raw, text, warning):
|
||||||
asked = self.ask_mode
|
asked = self.ask_mode
|
||||||
|
agent = assistant.display_name(self.conf)
|
||||||
if warning:
|
if warning:
|
||||||
# The text was still pasted, but something on the way did not run.
|
# The text was still pasted, but something on the way did not run.
|
||||||
# Say so loudly: a rejected key, or a tool Claude was not allowed to
|
# Say so loudly: a rejected key, or a tool Claude was not allowed to
|
||||||
# touch, otherwise looks exactly like a job that worked.
|
# touch, otherwise looks exactly like a job that worked.
|
||||||
first_line = warning.splitlines()[0]
|
first_line = warning.splitlines()[0]
|
||||||
self.overlay.show_warning(
|
self.overlay.show_warning(
|
||||||
t("Claude answered, but: {error}", error=first_line) if asked
|
t("{name} answered, but: {error}", name=agent, error=first_line)
|
||||||
|
if asked
|
||||||
else t("Pasted raw, cleanup failed: {error}", error=first_line)
|
else t("Pasted raw, cleanup failed: {error}", error=first_line)
|
||||||
)
|
)
|
||||||
self.tray.showMessage(
|
self.tray.showMessage(
|
||||||
t("Dikte: Claude could not do all of it") if asked
|
t("Dikte: {name} could not do all of it", name=agent) if asked
|
||||||
else t("Dikte: cleanup failed"),
|
else t("Dikte: cleanup failed"),
|
||||||
f"{warning}\n\n{text}" if asked else warning,
|
f"{warning}\n\n{text}" if asked else warning,
|
||||||
QSystemTrayIcon.MessageIcon.Warning, 10000,
|
QSystemTrayIcon.MessageIcon.Warning, 10000,
|
||||||
@@ -514,7 +524,9 @@ class Dikte:
|
|||||||
if asked:
|
if asked:
|
||||||
# Longer than a dictation's flash: this one is an answer, and
|
# Longer than a dictation's flash: this one is an answer, and
|
||||||
# it is worth being able to read the start of it in the corner.
|
# it is worth being able to read the start of it in the corner.
|
||||||
self.overlay.show_done(t("Claude: {preview}", preview=preview), 6000)
|
self.overlay.show_done(
|
||||||
|
t("{name}: {preview}", name=agent, preview=preview), 6000
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
action = t("Pasted") if self.conf["auto_paste"] else t("Copied")
|
action = t("Pasted") if self.conf["auto_paste"] else t("Copied")
|
||||||
self.overlay.show_done(
|
self.overlay.show_done(
|
||||||
|
|||||||
@@ -32,6 +32,22 @@ def t(text, **kwargs):
|
|||||||
return out.format(**kwargs) if kwargs else out
|
return out.format(**kwargs) if kwargs else out
|
||||||
|
|
||||||
|
|
||||||
|
# Turkish suffixes follow the vowels of the word they attach to, so "Claude'a"
|
||||||
|
# but "Codex'e". A name dropped into a sentence through t() cannot be inflected
|
||||||
|
# by the sentence, so it arrives already inflected. English takes the name as it
|
||||||
|
# is and puts the preposition in the sentence, where it belongs.
|
||||||
|
_TR_CASES = {
|
||||||
|
"dative": {"Claude": "Claude'a", "Codex": "Codex'e", "OpenRouter": "OpenRouter'a"},
|
||||||
|
"accusative": {"Claude": "Claude'u", "Codex": "Codex'i", "OpenRouter": "OpenRouter'ı"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def name(text, case=""):
|
||||||
|
if _lang != "tr" or not case:
|
||||||
|
return text
|
||||||
|
return _TR_CASES.get(case, {}).get(text, text)
|
||||||
|
|
||||||
|
|
||||||
TR = {
|
TR = {
|
||||||
# --- tray ---------------------------------------------------------
|
# --- tray ---------------------------------------------------------
|
||||||
"Start recording": "Kaydı başlat",
|
"Start recording": "Kaydı başlat",
|
||||||
@@ -288,22 +304,20 @@ TR = {
|
|||||||
"Geçmişin tamamı silinsin mi? Bu geri alınamaz.",
|
"Geçmişin tamamı silinsin mi? Bu geri alınamaz.",
|
||||||
|
|
||||||
# --- asking Claude Code -------------------------------------------------
|
# --- asking Claude Code -------------------------------------------------
|
||||||
"Ask Claude": "Claude'a sor",
|
"Ask {name}": "{name} sor",
|
||||||
"Stop and ask Claude": "Kaydı bitir ve Claude'a sor",
|
"Stop and ask {name}": "Kaydı bitir ve {name} sor",
|
||||||
"Start a new conversation": "Yeni konuşma başlat",
|
"Start a new conversation": "Yeni konuşma başlat",
|
||||||
"Start a new conversation now": "Şimdi yeni konuşma başlat",
|
"Start a new conversation now": "Şimdi yeni konuşma başlat",
|
||||||
"Stop Claude": "Claude'u durdur",
|
"Stop {name}": "{name} durdur",
|
||||||
"Stopping…": "Durduruluyor…",
|
"Stopping…": "Durduruluyor…",
|
||||||
"Stopped.": "Durduruldu.",
|
"Stopped.": "Durduruldu.",
|
||||||
"Claude starts fresh next time.": "Claude bir sonrakine sıfırdan başlayacak.",
|
"{name} starts fresh next time.": "{name} bir sonrakine sıfırdan başlayacak.",
|
||||||
"Dikte: talking to Claude": "Dikte: Claude ile konuşuyor",
|
"Dikte: talking to Claude": "Dikte: ajanla konuşuyor",
|
||||||
"Dikte: recording for Claude": "Dikte: Claude için kaydediyor",
|
"Dikte: recording for Claude": "Dikte: ajan için kaydediyor",
|
||||||
"Asking Claude…": "Claude'a soruluyor…",
|
"Asking {name}…": "{name} soruluyor…",
|
||||||
"Claude: {preview}": "Claude: {preview}",
|
"{name}: {preview}": "{name}: {preview}",
|
||||||
"Claude answered, but: {error}": "Claude cevapladı, ama: {error}",
|
"{name} answered, but: {error}": "{name} cevapladı, ama: {error}",
|
||||||
"Dikte: Claude could not do all of it": "Dikte: Claude her şeyi yapamadı",
|
"Dikte: {name} could not do all of it": "Dikte: {name} her şeyi yapamadı",
|
||||||
"Claude was not allowed to use: {tools}":
|
|
||||||
"Claude şunları kullanamadı: {tools}",
|
|
||||||
"Running a command…": "Komut çalıştırıyor…",
|
"Running a command…": "Komut çalıştırıyor…",
|
||||||
"Reading…": "Okuyor…",
|
"Reading…": "Okuyor…",
|
||||||
"Looking through files…": "Dosyalara bakıyor…",
|
"Looking through files…": "Dosyalara bakıyor…",
|
||||||
@@ -315,36 +329,63 @@ TR = {
|
|||||||
"Handing it to a subagent…": "Alt ajana devrediyor…",
|
"Handing it to a subagent…": "Alt ajana devrediyor…",
|
||||||
"Planning…": "Planlıyor…",
|
"Planning…": "Planlıyor…",
|
||||||
"Using {name}…": "{name} kullanıyor…",
|
"Using {name}…": "{name} kullanıyor…",
|
||||||
"claude not found. Install Claude Code and make sure `claude` is on your PATH.":
|
"Thinking…": "Düşünüyor…",
|
||||||
"claude bulunamadı. Claude Code'u kur ve `claude` komutunun PATH'te "
|
"{binary} not found. Install it, or pick another provider under "
|
||||||
"olduğundan emin ol.",
|
"Settings → Claude.":
|
||||||
"Could not run claude: {error}": "claude çalıştırılamadı: {error}",
|
"{binary} bulunamadı. Kur ya da Ayarlar → Claude sekmesinden başka bir "
|
||||||
"claude exited with code {code}.": "claude {code} koduyla çıktı.",
|
"sağlayıcı seç.",
|
||||||
"Claude did not finish within {seconds} seconds.":
|
"Could not run {binary}: {error}": "{binary} çalıştırılamadı: {error}",
|
||||||
"Claude {seconds} saniye içinde bitirmedi.",
|
"{service} exited with code {code}.": "{service} {code} koduyla çıktı.",
|
||||||
|
"It did not finish within {seconds} seconds.":
|
||||||
|
"{seconds} saniye içinde bitmedi.",
|
||||||
"Claude ended with an error.": "Claude bir hatayla sonlandı.",
|
"Claude ended with an error.": "Claude bir hatayla sonlandı.",
|
||||||
"Claude answered with nothing.": "Claude boş cevap verdi.",
|
"Codex ended with an error.": "Codex bir hatayla sonlandı.",
|
||||||
|
"{service} answered with nothing.": "{service} boş cevap verdi.",
|
||||||
|
"It was not allowed to use: {tools}": "Şunları kullanmasına izin yoktu: {tools}",
|
||||||
|
"The model returned an empty reply.": "Model boş cevap döndürdü.",
|
||||||
|
|
||||||
# --- settings: Claude ---------------------------------------------------
|
# --- settings: Claude ---------------------------------------------------
|
||||||
"Claude": "Claude",
|
"Claude": "Claude",
|
||||||
"This shortcut records the same way dictation does, but the transcript is "
|
"This shortcut records the same way dictation does, but the transcript is "
|
||||||
"not what gets pasted. It goes to Claude Code as a command, and what comes "
|
"not what gets pasted. It goes to an agent as a command, and what comes "
|
||||||
"back is pasted instead: the answer to a question, or a sentence saying "
|
"back is pasted instead: the answer to a question, or a sentence saying "
|
||||||
"what was done. It runs as the session you would have opened yourself, "
|
"what was done. Claude Code and Codex run as the session you would have "
|
||||||
"with your skills, your connected services and your account.":
|
"opened yourself, with your skills, your connected services and your "
|
||||||
|
"account.":
|
||||||
"Bu kısayol dikte ile aynı şekilde kaydeder, ama yapıştırılan şey "
|
"Bu kısayol dikte ile aynı şekilde kaydeder, ama yapıştırılan şey "
|
||||||
"transkript değildir. Transkript Claude Code'a komut olarak gider ve "
|
"transkript değildir. Transkript bir ajana komut olarak gider ve yerine "
|
||||||
"yerine oradan döneni yapıştırılır: bir sorunun cevabı ya da ne "
|
"oradan döneni yapıştırılır: bir sorunun cevabı ya da ne yapıldığını "
|
||||||
"yapıldığını söyleyen bir cümle. Kendi açacağın oturumun aynısı olarak "
|
"söyleyen bir cümle. Claude Code ve Codex, kendi açacağın oturumun "
|
||||||
"çalışır: skill'lerinle, bağlı servislerinle ve kendi hesabınla.",
|
"aynısı olarak çalışır: skill'lerinle, bağlı servislerinle ve kendi "
|
||||||
|
"hesabınla.",
|
||||||
"How it runs": "Nasıl çalışıyor",
|
"How it runs": "Nasıl çalışıyor",
|
||||||
|
"Runs on": "Şunun üstünde çalışır",
|
||||||
|
"Claude Code": "Claude Code",
|
||||||
|
"Codex": "Codex",
|
||||||
|
"Codex's own default": "Codex'in kendi varsayılanı",
|
||||||
|
"Sandbox": "Kum havuzu",
|
||||||
|
"Read anything, write in the working directory":
|
||||||
|
"Her şeyi okusun, çalışma dizinine yazsın",
|
||||||
|
"Read only": "Yalnızca okusun",
|
||||||
|
"No sandbox at all": "Kum havuzu hiç olmasın",
|
||||||
|
"A plain question and a plain answer, over the OpenRouter key you already "
|
||||||
|
"have. It runs no commands, opens no files and reaches none of your "
|
||||||
|
"services, so it can tell you what the capital of Peru is but not what is "
|
||||||
|
"in your calendar. Working directory and permissions above mean nothing "
|
||||||
|
"here.":
|
||||||
|
"Elindeki OpenRouter anahtarı üzerinden düz bir soru ve düz bir cevap. "
|
||||||
|
"Komut çalıştırmaz, dosya açmaz, servislerinin hiçbirine erişmez; yani "
|
||||||
|
"Peru'nun başkentini söyler ama takviminde ne olduğunu söyleyemez. "
|
||||||
|
"Yukarıdaki çalışma dizini ve izinler burada bir şey ifade etmez.",
|
||||||
|
"Needs no program installed, only the OpenRouter key.":
|
||||||
|
"Kurulu bir programa değil, yalnızca OpenRouter anahtarına ihtiyaç duyar.",
|
||||||
|
"{binary} is not on your PATH, so this cannot run yet. Install it, or pick "
|
||||||
|
"another one above.":
|
||||||
|
"{binary} PATH'te değil, dolayısıyla bu henüz çalışamaz. Kur ya da "
|
||||||
|
"yukarıdan başka birini seç.",
|
||||||
"The conversation": "Konuşma",
|
"The conversation": "Konuşma",
|
||||||
"The answer": "Cevap",
|
"The answer": "Cevap",
|
||||||
"Found: {path}": "Bulundu: {path}",
|
"Found: {path}": "Bulundu: {path}",
|
||||||
"claude is not on your PATH, so this cannot run yet. Install Claude Code "
|
|
||||||
"first.":
|
|
||||||
"claude PATH'te değil, dolayısıyla bu henüz çalışamaz. Önce Claude "
|
|
||||||
"Code'u kur.",
|
|
||||||
"No KDE shortcut installed. The tray menu asks Claude too.":
|
"No KDE shortcut installed. The tray menu asks Claude too.":
|
||||||
"Kurulu KDE kısayolu yok. Tepsi menüsünden de sorulabilir.",
|
"Kurulu KDE kısayolu yok. Tepsi menüsünden de sorulabilir.",
|
||||||
"A name like “sonnet” always means the newest model of that line. Opus "
|
"A name like “sonnet” always means the newest model of that line. Opus "
|
||||||
@@ -388,9 +429,9 @@ TR = {
|
|||||||
"and cleanup costs an API call and a second or two.":
|
"and cleanup costs an API call and a second or two.":
|
||||||
"Varsayılan olarak kapalı: Claude “eee” ve “hani”yi yardımsız da okur, "
|
"Varsayılan olarak kapalı: Claude “eee” ve “hani”yi yardımsız da okur, "
|
||||||
"temizlik ise bir API çağrısına ve bir iki saniyeye mal olur.",
|
"temizlik ise bir API çağrısına ve bir iki saniyeye mal olur.",
|
||||||
"Told to Claude alongside every command, on top of whatever your own "
|
"Told to the agent alongside every command, on top of whatever your own "
|
||||||
"configuration already says.":
|
"configuration already says.":
|
||||||
"Her komutla birlikte Claude'a söylenir, kendi yapılandırmanın zaten "
|
"Her komutla birlikte ajana söylenir, kendi yapılandırmanın zaten "
|
||||||
"söylediklerinin üstüne eklenir.",
|
"söylediklerinin üstüne eklenir.",
|
||||||
" · asked Claude: {question}": " · Claude'a soruldu: {question}",
|
" · asked Claude: {question}": " · Claude'a soruldu: {question}",
|
||||||
|
|
||||||
|
|||||||
+115
-23
@@ -52,9 +52,18 @@ MEETING_MODELS = [
|
|||||||
"google/gemini-3.5-flash", "google/gemini-3.1-pro-preview",
|
"google/gemini-3.5-flash", "google/gemini-3.1-pro-preview",
|
||||||
"anthropic/claude-sonnet-5", "openai/gpt-5.4", "x-ai/grok-4.5",
|
"anthropic/claude-sonnet-5", "openai/gpt-5.4", "x-ai/grok-4.5",
|
||||||
]
|
]
|
||||||
|
ASSISTANT_PROVIDERS = [
|
||||||
|
("Claude Code", "claude"), ("Codex", "codex"), ("OpenRouter", "openrouter"),
|
||||||
|
]
|
||||||
# Aliases resolve to the newest model of that name, so they age better than an
|
# Aliases resolve to the newest model of that name, so they age better than an
|
||||||
# id does; a full id can be typed in when a particular one is wanted.
|
# id does; a full id can be typed in when a particular one is wanted.
|
||||||
ASSISTANT_MODELS = ["sonnet", "opus", "haiku", "fable"]
|
ASSISTANT_MODELS = ["sonnet", "opus", "haiku", "fable"]
|
||||||
|
CODEX_MODELS = ["gpt-5.4-codex", "gpt-5.4", "o4-mini"]
|
||||||
|
# Starting points only; the box is editable and OpenRouter has hundreds.
|
||||||
|
ASSISTANT_OR_MODELS = [
|
||||||
|
"google/gemini-3.5-flash", "anthropic/claude-sonnet-5", "openai/gpt-5.4",
|
||||||
|
"x-ai/grok-4.5", "google/gemini-3.1-pro-preview",
|
||||||
|
]
|
||||||
# What Claude Code may do without being able to ask. It cannot ask: there is no
|
# What Claude Code may do without being able to ask. It cannot ask: there is no
|
||||||
# window to answer in, so a mode that would have prompted denies instead.
|
# window to answer in, so a mode that would have prompted denies instead.
|
||||||
PERMISSION_MODES = [
|
PERMISSION_MODES = [
|
||||||
@@ -62,6 +71,12 @@ PERMISSION_MODES = [
|
|||||||
("Allow everything", "bypassPermissions"),
|
("Allow everything", "bypassPermissions"),
|
||||||
("Only what needs no permission", "manual"),
|
("Only what needs no permission", "manual"),
|
||||||
]
|
]
|
||||||
|
# Codex confines the commands it runs instead of asking about them.
|
||||||
|
CODEX_SANDBOXES = [
|
||||||
|
("Read anything, write in the working directory", "workspace-write"),
|
||||||
|
("Read only", "read-only"),
|
||||||
|
("No sandbox at all", "danger-full-access"),
|
||||||
|
]
|
||||||
MEETING_STATUS = {
|
MEETING_STATUS = {
|
||||||
"recorded": "waiting to be written up",
|
"recorded": "waiting to be written up",
|
||||||
"transcribed": "transcript ready, minutes missing",
|
"transcribed": "transcript ready, minutes missing",
|
||||||
@@ -323,11 +338,11 @@ class SettingsWindow(QDialog):
|
|||||||
layout = QVBoxLayout(page)
|
layout = QVBoxLayout(page)
|
||||||
intro = QLabel(t(
|
intro = QLabel(t(
|
||||||
"This shortcut records the same way dictation does, but the "
|
"This shortcut records the same way dictation does, but the "
|
||||||
"transcript is not what gets pasted. It goes to Claude Code as a "
|
"transcript is not what gets pasted. It goes to an agent as a "
|
||||||
"command, and what comes back is pasted instead: the answer to a "
|
"command, and what comes back is pasted instead: the answer to a "
|
||||||
"question, or a sentence saying what was done. It runs as the "
|
"question, or a sentence saying what was done. Claude Code and "
|
||||||
"session you would have opened yourself, with your skills, your "
|
"Codex run as the session you would have opened yourself, with your "
|
||||||
"connected services and your account."
|
"skills, your connected services and your account."
|
||||||
))
|
))
|
||||||
intro.setWordWrap(True)
|
intro.setWordWrap(True)
|
||||||
layout.addWidget(intro)
|
layout.addWidget(intro)
|
||||||
@@ -350,20 +365,13 @@ class SettingsWindow(QDialog):
|
|||||||
self.assistant_shortcut_status.setWordWrap(True)
|
self.assistant_shortcut_status.setWordWrap(True)
|
||||||
how_form.addRow(self.assistant_shortcut_status)
|
how_form.addRow(self.assistant_shortcut_status)
|
||||||
|
|
||||||
self.assistant_model = QComboBox()
|
self.assistant_provider = QComboBox()
|
||||||
self.assistant_model.setEditable(True)
|
for label, value in ASSISTANT_PROVIDERS:
|
||||||
self.assistant_model.addItems(ASSISTANT_MODELS)
|
self.assistant_provider.addItem(t(label), value)
|
||||||
self.assistant_model.setToolTip(t(
|
self.assistant_provider.currentIndexChanged.connect(
|
||||||
"A name like “sonnet” always means the newest model of that line. "
|
self._assistant_provider_changed
|
||||||
"Opus thinks harder and answers slower, which is felt here more "
|
)
|
||||||
"than anywhere else: you are standing in front of the screen."
|
how_form.addRow(t("Runs on"), self.assistant_provider)
|
||||||
))
|
|
||||||
how_form.addRow(t("Model"), self.assistant_model)
|
|
||||||
|
|
||||||
self.assistant_permission = QComboBox()
|
|
||||||
for label, value in PERMISSION_MODES:
|
|
||||||
self.assistant_permission.addItem(t(label), value)
|
|
||||||
how_form.addRow(t("Permissions"), self.assistant_permission)
|
|
||||||
|
|
||||||
self.assistant_dir = QLineEdit()
|
self.assistant_dir = QLineEdit()
|
||||||
self.assistant_dir.setPlaceholderText(os.path.expanduser("~"))
|
self.assistant_dir.setPlaceholderText(os.path.expanduser("~"))
|
||||||
@@ -389,6 +397,57 @@ class SettingsWindow(QDialog):
|
|||||||
how_form.addRow(t("Give up after"), self.assistant_timeout)
|
how_form.addRow(t("Give up after"), self.assistant_timeout)
|
||||||
layout.addWidget(how)
|
layout.addWidget(how)
|
||||||
|
|
||||||
|
# One box per provider, only the chosen one on screen: they have nothing
|
||||||
|
# in common past the model, and three sets of half-relevant fields would
|
||||||
|
# be worse than none.
|
||||||
|
self.claude_box = QGroupBox(t("Claude Code"))
|
||||||
|
claude_form = QFormLayout(self.claude_box)
|
||||||
|
self.assistant_model = QComboBox()
|
||||||
|
self.assistant_model.setEditable(True)
|
||||||
|
self.assistant_model.addItems(ASSISTANT_MODELS)
|
||||||
|
self.assistant_model.setToolTip(t(
|
||||||
|
"A name like “sonnet” always means the newest model of that line. "
|
||||||
|
"Opus thinks harder and answers slower, which is felt here more "
|
||||||
|
"than anywhere else: you are standing in front of the screen."
|
||||||
|
))
|
||||||
|
claude_form.addRow(t("Model"), self.assistant_model)
|
||||||
|
self.assistant_permission = QComboBox()
|
||||||
|
for label, value in PERMISSION_MODES:
|
||||||
|
self.assistant_permission.addItem(t(label), value)
|
||||||
|
claude_form.addRow(t("Permissions"), self.assistant_permission)
|
||||||
|
layout.addWidget(self.claude_box)
|
||||||
|
|
||||||
|
self.codex_box = QGroupBox(t("Codex"))
|
||||||
|
codex_form = QFormLayout(self.codex_box)
|
||||||
|
self.assistant_codex_model = QComboBox()
|
||||||
|
self.assistant_codex_model.setEditable(True)
|
||||||
|
self.assistant_codex_model.addItem(t("Codex's own default"), "")
|
||||||
|
for name in CODEX_MODELS:
|
||||||
|
self.assistant_codex_model.addItem(name, name)
|
||||||
|
codex_form.addRow(t("Model"), self.assistant_codex_model)
|
||||||
|
self.assistant_codex_sandbox = QComboBox()
|
||||||
|
for label, value in CODEX_SANDBOXES:
|
||||||
|
self.assistant_codex_sandbox.addItem(t(label), value)
|
||||||
|
codex_form.addRow(t("Sandbox"), self.assistant_codex_sandbox)
|
||||||
|
layout.addWidget(self.codex_box)
|
||||||
|
|
||||||
|
self.openrouter_box = QGroupBox("OpenRouter")
|
||||||
|
or_form = QFormLayout(self.openrouter_box)
|
||||||
|
self.assistant_openrouter_model = QComboBox()
|
||||||
|
self.assistant_openrouter_model.setEditable(True)
|
||||||
|
self.assistant_openrouter_model.addItems(ASSISTANT_OR_MODELS)
|
||||||
|
or_form.addRow(t("Model"), self.assistant_openrouter_model)
|
||||||
|
or_note = QLabel(t(
|
||||||
|
"A plain question and a plain answer, over the OpenRouter key you "
|
||||||
|
"already have. It runs no commands, opens no files and reaches none "
|
||||||
|
"of your services, so it can tell you what the capital of Peru is "
|
||||||
|
"but not what is in your calendar. Working directory and permissions "
|
||||||
|
"above mean nothing here."
|
||||||
|
))
|
||||||
|
or_note.setWordWrap(True)
|
||||||
|
or_form.addRow(or_note)
|
||||||
|
layout.addWidget(self.openrouter_box)
|
||||||
|
|
||||||
thread = QGroupBox(t("The conversation"))
|
thread = QGroupBox(t("The conversation"))
|
||||||
thread_form = QFormLayout(thread)
|
thread_form = QFormLayout(thread)
|
||||||
self.assistant_session_minutes = QSpinBox()
|
self.assistant_session_minutes = QSpinBox()
|
||||||
@@ -426,7 +485,7 @@ class SettingsWindow(QDialog):
|
|||||||
layout.addWidget(answer)
|
layout.addWidget(answer)
|
||||||
|
|
||||||
prompt_label = QLabel(t(
|
prompt_label = QLabel(t(
|
||||||
"Told to Claude alongside every command, on top of whatever your "
|
"Told to the agent alongside every command, on top of whatever your "
|
||||||
"own configuration already says."
|
"own configuration already says."
|
||||||
))
|
))
|
||||||
prompt_label.setWordWrap(True)
|
prompt_label.setWordWrap(True)
|
||||||
@@ -820,8 +879,13 @@ class SettingsWindow(QDialog):
|
|||||||
self.transcribe_prompt.setPlainText(conf["transcribe_prompt"])
|
self.transcribe_prompt.setPlainText(conf["transcribe_prompt"])
|
||||||
|
|
||||||
self.assistant_shortcut.setText(conf["assistant_shortcut"])
|
self.assistant_shortcut.setText(conf["assistant_shortcut"])
|
||||||
|
self._select_data(self.assistant_provider, conf["assistant_provider"])
|
||||||
self.assistant_model.setCurrentText(conf["assistant_model"])
|
self.assistant_model.setCurrentText(conf["assistant_model"])
|
||||||
self._select_data(self.assistant_permission, conf["assistant_permission_mode"])
|
self._select_data(self.assistant_permission, conf["assistant_permission_mode"])
|
||||||
|
self.assistant_codex_model.setCurrentText(conf["assistant_codex_model"])
|
||||||
|
self._select_data(self.assistant_codex_sandbox, conf["assistant_codex_sandbox"])
|
||||||
|
self.assistant_openrouter_model.setCurrentText(conf["assistant_openrouter_model"])
|
||||||
|
self._assistant_provider_changed() # selecting index 0 fires no signal
|
||||||
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"]))
|
||||||
@@ -899,10 +963,23 @@ class SettingsWindow(QDialog):
|
|||||||
conf["transcribe_prompt"] = self.transcribe_prompt.toPlainText().strip()
|
conf["transcribe_prompt"] = self.transcribe_prompt.toPlainText().strip()
|
||||||
|
|
||||||
conf["assistant_shortcut"] = self.assistant_shortcut.text().strip()
|
conf["assistant_shortcut"] = self.assistant_shortcut.text().strip()
|
||||||
|
conf["assistant_provider"] = self.assistant_provider.currentData() or "claude"
|
||||||
conf["assistant_model"] = (self.assistant_model.currentText().strip()
|
conf["assistant_model"] = (self.assistant_model.currentText().strip()
|
||||||
or cfg.DEFAULTS["assistant_model"])
|
or cfg.DEFAULTS["assistant_model"])
|
||||||
conf["assistant_permission_mode"] = (self.assistant_permission.currentData()
|
conf["assistant_permission_mode"] = (self.assistant_permission.currentData()
|
||||||
or "auto")
|
or "auto")
|
||||||
|
# The editable box shows a label for "no choice", which must not be
|
||||||
|
# stored as if it were a model id.
|
||||||
|
codex_model = self.assistant_codex_model.currentText().strip()
|
||||||
|
conf["assistant_codex_model"] = (
|
||||||
|
"" if codex_model == t("Codex's own default") else codex_model
|
||||||
|
)
|
||||||
|
conf["assistant_codex_sandbox"] = (self.assistant_codex_sandbox.currentData()
|
||||||
|
or "workspace-write")
|
||||||
|
conf["assistant_openrouter_model"] = (
|
||||||
|
self.assistant_openrouter_model.currentText().strip()
|
||||||
|
or cfg.DEFAULTS["assistant_openrouter_model"]
|
||||||
|
)
|
||||||
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()
|
||||||
@@ -1242,13 +1319,28 @@ class SettingsWindow(QDialog):
|
|||||||
else t("No KDE shortcut installed. The tray menu asks Claude too.")
|
else t("No KDE shortcut installed. The tray menu asks Claude too.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _assistant_provider_changed(self):
|
||||||
|
provider = self.assistant_provider.currentData() or "claude"
|
||||||
|
self.claude_box.setVisible(provider == "claude")
|
||||||
|
self.codex_box.setVisible(provider == "codex")
|
||||||
|
self.openrouter_box.setVisible(provider == "openrouter")
|
||||||
|
self._refresh_assistant_status()
|
||||||
|
|
||||||
def _refresh_assistant_status(self):
|
def _refresh_assistant_status(self):
|
||||||
found = shutil.which("claude")
|
provider = self.assistant_provider.currentData() or "claude"
|
||||||
|
binary = assistant.executable(provider)
|
||||||
|
found = shutil.which(binary) if binary else ""
|
||||||
|
if not binary:
|
||||||
self.assistant_found.setText(
|
self.assistant_found.setText(
|
||||||
t("Found: {path}", path=found) if found else
|
t("Needs no program installed, only the OpenRouter key.")
|
||||||
t("claude is not on your PATH, so this cannot run yet. Install "
|
|
||||||
"Claude Code first.")
|
|
||||||
)
|
)
|
||||||
|
elif found:
|
||||||
|
self.assistant_found.setText(t("Found: {path}", path=found))
|
||||||
|
else:
|
||||||
|
self.assistant_found.setText(t(
|
||||||
|
"{binary} is not on your PATH, so this cannot run yet. Install "
|
||||||
|
"it, or pick another one above.", binary=binary,
|
||||||
|
))
|
||||||
age = assistant.session_age()
|
age = assistant.session_age()
|
||||||
if age is None:
|
if age is None:
|
||||||
self.assistant_session_status.setText(t("No conversation going."))
|
self.assistant_session_status.setText(t("No conversation going."))
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import api
|
|||||||
import assistant
|
import assistant
|
||||||
import audio
|
import audio
|
||||||
import config as cfg
|
import config as cfg
|
||||||
|
import i18n
|
||||||
import paste
|
import paste
|
||||||
import vad
|
import vad
|
||||||
from i18n import t
|
from i18n import t
|
||||||
@@ -119,7 +120,8 @@ class Pipeline(QObject):
|
|||||||
question = ""
|
question = ""
|
||||||
if ask:
|
if ask:
|
||||||
question = text
|
question = text
|
||||||
self.stage.emit(t("Asking Claude…"))
|
self.stage.emit(t("Asking {name}…", name=i18n.name(
|
||||||
|
assistant.display_name(conf), "dative")))
|
||||||
text, denied = assistant.ask(
|
text, denied = assistant.ask(
|
||||||
question, conf,
|
question, conf,
|
||||||
on_stage=self.stage.emit,
|
on_stage=self.stage.emit,
|
||||||
|
|||||||
Reference in New Issue
Block a user