The indicator asked QCursor.pos() which screen to appear on, and Wayland tells a client where the pointer is only while it is over one of that client's own windows. The indicator is never under the pointer, so the answer came back stale, or at the origin when the pointer had never been over a window of ours. Measured on Plasma 6: the origin under the Wayland platform, and a point frozen for the whole run through XWayland, which is the platform Dikte actually uses. Every indicator therefore landed in the corner of whichever screen holds 0,0, which on a two monitor desk is the wrong screen most of the time.
KWin knows, and answers for it over D-Bus with activeOutputName, naming outputs the way Qt names screens: by connector, natively and through XWayland alike. That answer now comes before the pointer, and the pointer still decides everywhere else, which is right on X11 and no worse than before on other Wayland desktops. It is the active output and not the pointer's, so on Plasma the two are the same screen only where the active screen is set to follow the mouse, and otherwise it is the focused window that decides, which is where the typing is going anyway. Nothing in the settings window promises the pointer any more.
Deciding the screen once, when the indicator appears, leaves it behind when the work moves to another monitor mid-recording, so overlay_follows_pointer keeps it up to date while it is up. Off by default: a ribbon that changes desks mid-sentence is one more thing moving while you are trying to talk. The compositor is asked four times a second rather than at the ribbon's 33 ms, because a hand moving a mouse across a desk is slower than that, and the call is given a 200 ms timeout so a wedged compositor cannot freeze the indicator with it.
One indicator stacking on another takes that one's screen and never asks for its own. Asked for itself it would answer where the session is now, which is not where the ribbon underneath was put a minute ago, and the pair would end up a monitor apart with the top one raised over nothing. That one is checked every tick, since its answer costs nothing.
A timestamped run on OpenRouter always asked openai/whisper-1 for the
segments, whatever model was picked for plain transcription. Not every
model there returns segment times, so the one to use is now its own
setting, openrouter_subtitle_model, shown in the speech-to-text box only
when OpenRouter is the provider. Empty keeps the old whisper-1 fallback.
Target carries the choice as subtitle_model and timestamp_model() reads
it; the other providers are unchanged.
Master grew Google AI Studio and Antigravity as providers, a doctor that
names each provider's own key, and settings that fetch every hosted
model list as the window opens. OpenCode Go is folded into each: its
name joins SERVICES and the doctor's key table, its key row sits beside
Google's, its Fetch button follows the per-provider pattern Google's
uses, and _load_hosted_models fetches its catalog at open when a key is
on file, filling the cleanup and agent boxes alike.
Codex now asks itself for its model list, doctor grew ready flags and a
per-provider line, and the Groq key joined the masked ones; the OpenCode
additions are folded into each. The no-CLI cleanup test follows the
fake_run to fake_cli rename.
Both sides rewrote the doctor: master rebuilt it around ready flags so a
fully local setup stops reading as broken, while this branch taught it
that Google has a key of its own and that the local model has neither a
key nor a program. Kept master's structure and folded the branch in: the
two hosted providers answer for their keys, a CLI for its program, and
the JSON keeps both the provider-aware "key" and master's "ready".
SECRET_KEYS gained groq on master and gemini here; the resolution keeps
all four. _conclude was also changed by both: master stopped matching
stderr wording and asks _API_TROUBLE instead, this branch renamed its
last parameter to the provider's short name so a session is stored under
the name it is read back by; the rename now rides on master's body.
codex_models() and the settings loaders landed beside the new Gemini
ones, so both stay. The new cleanup tests called the CLI fake by its old
name, fake_run, which master had renamed to fake_cli.
The paste block was rewritten by both sides: master taught press() to
put the remembered application back in front (focus), this branch moved
the history write ahead of the paste and stopped restoring the old
clipboard over a transcript the key press refused. Kept this branch's
order and error handling, and handed press() the focus it now takes.
OpenRouter's free tier rate-limits and carries no free Gemini model, and
cleaning up through Claude Code costs a fixed few seconds because it opens
a whole CLI session to drop three "uh"s. Google's own free tier suits a
short, frequent request, and its OpenAI-compatible endpoint answers
/chat/completions, so cleanup there is one request and the same code path
OpenRouter already takes.
The one thing that is not shared is the thinking level. Google reads
OpenAI's flat reasoning_effort rather than OpenRouter's object, and "none"
is how thinking is turned off, so it is sent rather than skipped: a Flash
model left to think spends exactly the second this provider was chosen to
save. Its top two rungs land on "high", which is as far as Google goes.
Speech to text stays where it was. That endpoint has no
/audio/transcriptions behind it, audio only goes in as base64 inside a
chat message, and what comes back has none of the segment times a subtitle
file or a meeting transcript is built out of.
Antigravity joins as well, on cleanup and as an agent. It is a CLI like the
other two and costs the same session, so it is here for people who already
pay for it rather than as an answer to the speed. It takes neither an empty
tool list nor a read-only sandbox, and cleanup.py now says so plainly
instead of implying parity; what it gets is a project of its own, the home
directory, and its slash commands off.
Three things were already wrong and are fixed on the way past, because the
new providers walk the same paths: doctor raised KeyError on the local
model, whose executable is ""; the history recorded Claude's model whoever
answered; and every agent row read "asked Claude".
Co-Authored-By: Claude Opus 5 <[email protected]>
OpenCode Go serves open coding models from an OpenAI-compatible endpoint. It cannot transcribe, so it joins
the two LLM jobs: transcript cleanup and the plain-chat agent. The key
falls back to OPENCODE_API_KEY, and api.chat() stops hardcoding the
OpenRouter name so the same call serves both.
history.jsonl and meetings.jsonl were rewritten read-modify-replace from
two threads with nothing between them, so a dictation landing during a
trim, or a meeting status landing during a delete, was silently gone; one
lock now wraps every touch of either file. The config rename gets a flush
and fsync in front of it, because a rename that survives a power loss
ahead of its data is an empty settings file, and a corrupt file is set
aside as config.json.broken instead of being quietly replaced by defaults
on the next save, API keys and all. Every replace retries briefly on
Windows, where an antivirus or a sync tool holds a fresh file for a beat
and one PermissionError out of a Qt slot takes the whole application
down.
Co-Authored-By: Claude Fable 5 <[email protected]>
A check on the releases page once a day: at start, on a timer while Dikte
runs, from the General tab on demand, and from `dikte update` at a terminal.
What it finds goes in the tray menu and in one notification per version, and
opens the release page.
Nothing is downloaded and nothing is installed. The four downloads are
installed four different ways and three of those belong to the platform: a
Mac bundle cannot rewrite itself while it is running, the Windows setup has
an uninstall entry of its own, an AppImage is a file kept wherever its owner
keeps it, and a checkout is updated with git. Being wrong about any one of
them means an installation somebody has to repair by hand.
Versions are compared by their numbers alone. A build off master carries the
released number with its commit after it, and that build is ahead of the
release it names rather than behind it; read as a version suffix, every
nightly would be told to go back to a release it had already passed.
The clock lives in its own file rather than in the settings, since a check
runs while the settings window may be open and a background write into
config.json is what would overwrite whatever it holds.
The cleanup prompt is picked by interface language, so a Turkish
interface hands a Turkish system prompt to the model and English
dictation comes back translated. The rule against that was the fifth
line of a DO NOT list, which is not where a model looks when the whole
prompt around it is written in one language.
Move it up under the job, in both cleanup prompts, and say plainly that
the language the rules are written in decides nothing.
Twenty-two files at the top of the tree was the first thing anybody saw of
this repository. They are one package now, imported relatively, and the three
scripts that are not the front door moved under scripts/. install.sh stays
where the README has always said it is.
What starts the application is dikte/__main__.py: python3 -m dikte runs it,
and so does naming the file, which is what the launcher symlink, both .desktop
files, the macOS bundle and every registered shortcut do. Run by path there is
no package around it, so it puts the checkout on sys.path itself.
The installers now keep the keys you chose when they are given none, which is
what an update is: update.sh no longer has to read them out and pass them back.
An updater from before this commit cannot read them at all, so the one thing it
can say, the default key with an empty discard key, is read as "nothing was
asked for" rather than obeyed. That guard can go once nobody is updating across
this commit.