Commit Graph
150 Commits
Author SHA1 Message Date
yusufipek f44609378b Clarify model labels and style compact controls for each theme 2026-09-09 14:38:13 +03:00
yusufipek a71545a88a Center wide task pages and group related actions in one row 2026-09-09 14:31:06 +03:00
yusufipek e5e9ae66e8 Redesign desktop workspace with compact task views and themes 2026-09-09 14:12:23 +03:00
yusufipek 6b4e590a12 Merge current master into local model status 2026-09-09 11:31:09 +03:00
yusufipek b06a1cd4d1 Merge master and correct local acceleration reporting
Reject failed Whisper GPU attempts, identify Llama devices from model buffers, and keep historical diagnostics independent of current settings. Report loaded backends without inferring build support, with regression coverage for each case.
2026-09-09 11:28:18 +03:00
Yusuf İpek 072812b6df Merge pull request #81 from catrobe/custom-binary-in-settings
Describe the binary the settings point at, not the one Dikte found
2026-09-09 11:26:50 +03:00
Yusuf İpek 5381631034 Merge pull request #65 from dumbovita/fix/agent-doctor-and-wheel
Fix doctor CLI check, inactive wheel focus, and agent shortcut label
2026-09-09 11:21:47 +03:00
yusufipek afa53934c2 Test remembered wheel focus in inactive windows 2026-09-09 11:19:03 +03:00
Yusuf İpek a6c0710a3f Merge pull request #63 from sudoeren/auto-language-detect
Detect the spoken language instead of fixing one
2026-09-09 11:13:54 +03:00
yusufipek 665902b546 Merge current local master and preserve automatic language detection 2026-09-09 11:09:02 +03:00
Yusuf İpek 4f2b2a91d4 Merge pull request #64 from dumbovita/master
Cap local model thread count by available CPU threads
2026-09-09 11:08:06 +03:00
yusufipek 71a2e08fa8 Merge master and retain only editable field width fix
Keep the current overlay implementation and adapt the form growth policy and regression coverage to the current settings fields.
2026-09-09 10:27:39 +03:00
yusufipek e85622aefb Build cues out of word times where a model marks no segments
Not every model behind /audio/transcriptions marks segments the way
whisper does. microsoft/mai-transcribe-2 answers a fourteen minute video
with three of them, one per paragraph, and to_srt turns each into a cue
that stays up for minutes. The model is worth keeping for what it hears,
so the times are taken from somewhere else instead: the same request now
asks for word timestamps too, and where the segments come back too long
to be cues, the cues are cut out of the words.

A cue ends where a sentence does, and failing that where it has grown
too long to read or to leave up. A full stop too early in a cue is not
the end of a sentence but a list marker or a shortened word, and one
that ends up short anyway is held on screen until the next needs the
space. Whisper still answers with its own segments and nothing on that
path changes; the local server is not asked for words it was never
asked for, and a hosted model that refuses the field falls back to the
request it used to answer.

A cue is short enough now that two can begin in the same second, so
to_srt hands out every timing a second holds rather than the first.
2026-09-07 11:56:19 +03:00
M. Ömer Okyar b46181e001 Describe the binary the settings point at, not the one Dikte found
program_path takes the custom path as its second argument, and config.py
passes it, but the settings window never did. Happened on this machine:
Compiled my own whisper.cpp and llama.cpp with CUDA and pointed the
settings at them. The box with no downloaded copy said "Not installed."
The box with one said "Downloaded, version b4938." naming a processor
build while the CUDA one did the transcribing.

LocalModelBox is now handed a callable, so the setting is read when
the label is drawn rather than frozen when the window is built.

The label reflects that too. A path set by hand now says so, instead of
claiming Dikte downloaded something it didn't.
2026-09-05 16:10:07 +03:00
yusufipek 2af5ec671c Merge master into the idle unload
Two conflicts, both where the local cleanup grew a second thing at once.

cleanup._local now passes the server's context alongside the timeout, and this
branch wrapped that same call in the busy() hold; the call takes both.

FakeServer gained a `context` attribute on master and a `held` counter here.
2026-09-05 12:18:50 +03:00
yusufipek 825f089fe9 Give the memory back when a local model has been sitting unused
A whisper.cpp or llama.cpp server started for one dictation stayed loaded
until Dikte quit. On this machine that is 1.3 GB of VRAM for large-v3 plus
whatever the cleanup LLM takes, held all day between dictations that last
seconds.

Each Server now carries an idle window. A watcher thread per launch stops the
server once nothing has asked it anything for that long, and the next request
loads it again through serve(), which already starts what is not running.
Settings has one checkbox and one number for both servers, on by default at ten
minutes, and it only appears for a machine that runs a model here. The tray menu
says which models are loaded and offers to unload them now.

Two things the clock alone gets wrong, both held off by a count of requests in
flight:

  * A file or a meeting is one address lookup and then minutes of work, which
    to a clock started at the lookup looks exactly like a model nobody wants.
    api.py and cleanup.py hold the count for the length of the request.

  * The count must survive the start it triggered. cleanup._local takes the
    hold and only then asks for the address, so a cold start happens inside it;
    neither serve() nor _stop_now() resets the count any more.

Unloading by hand runs on the interface's thread, so it asks for the start lock
rather than waiting on it: a model still being read in is refused, the way one
in the middle of a request is, instead of freezing the window for as long as
the load takes.
2026-09-05 12:15:14 +03:00
Yusuf İpek 5a4ae8c315 Merge pull request #79 from yusufipk/claude/bazen-publisher-change-blank-c6832e
Measure a wrapped label against a width it actually has
2026-09-05 12:12:46 +03:00
yusufipek 22d2a40341 Count the lines off the font, not off this machine's font
The new test pinned the wrapped height at four lines, which is four lines
on a Linux runner and four and a half on a Windows one, where the same
sentence in the same 400 pixels needs 54 of the box's 48. What the test is
actually about is that the height comes from the width the label has now
rather than the eight pixels it had while the window was being built, so it
measures that width itself and compares against the answer.
2026-09-05 12:10:58 +03:00
yusufipek 44db26c459 Measure a wrapped label against a width it actually has
The publisher note is written while the settings window is still being
built, when its label is eight pixels wide. Wrapped against that width the
sentence came out a hundred and twenty lines tall, and the minimum taken
from it did not stay a minimum: QLabel folds the widget's minimum size into
its own cached size hints and clears that cache only when the text changes.
So the row stood two thousand pixels tall, carrying the model box, the
status line and the options under it off the bottom of the window, and
picking another publisher was what brought them back.

Nothing to measure against yet means nothing to claim yet. The show and the
resize come back for it once there is a real width.
2026-09-05 12:07:13 +03:00
yusufipek 70bc4c16fa Give a local model room to think without spending the answer on it
llama.cpp counts the thinking towards max_tokens along with the answer it
precedes, and the local ceiling was sized for the answer alone. Turning
Thinking up therefore came out of the reply rather than being added to
it, and on a short dictation the 512 floor is the whole budget, so the
model spent it in the think block and came back with nothing to paste.

Each rung of the ladder now carries its own budget, doubling from 256 at
"minimal" to 8192 at "maximum", added on top of the answer's share
rather than taken out of it. The rungs are small because cleanup is
punctuation and locally every one of these tokens is also a second of
somebody standing in front of the screen. "Off" keeps the old tight
ceiling untouched, and an empty setting is given a middling amount,
since a template that can think thinks by default and there is no way to
ask which kind of model this is.

The ceiling is also held under what the server was started with. Above
the context it is not a ceiling at all: the runaway it exists to stop
would run to the end of the context instead, which on CPU is minutes of
waiting. The prompt keeps its share at two characters to the token,
which is under any tokeniser's rate for natural language and so reserves
too much rather than promising room that is not there.

Separately, a reply cut off at somebody's ceiling was returned as if it
were whole. Half a sentence looks like a cleaned-up transcript and is
not one, so finish_reason is now read in both cleanup and chat. The
callers already keep the transcript they started with, which is the
better of the two. This one is not local-only: a hosted provider
stopping at its own output limit was silently pasted the same way.
2026-09-05 12:02:28 +03:00
yusufipek e282e6b0cf Say the download started before the first byte arrives
Opening the connection takes ten or twenty seconds, and the byte counts
under the model box only start after it. Until then the line read "X has
not been downloaded yet" beside a button that had just turned into Stop,
so a download that was running looked like a click that had not landed.

The stop had the same gap the other way round: should_stop is read
between blocks, and the wait for the server to answer is not between
blocks, so pressing Stop during it changed nothing on screen either.
2026-09-05 11:37:19 +03:00
Yusuf İpek 06e578d901 Merge pull request #76 from yusufipk/claude/model-selection-ui-organization-62dc90
Group the model lists and say which row this machine should take
2026-09-05 11:33:03 +03:00
yusufipek fedb4fe5c0 Let the new tests run on a Windows box and on a small machine
Two ways the tests were standing on this machine rather than on the one
they meant to describe.

Windows has no os.sysconf at all, and mock.patch.object insists the
attribute exists before it will replace it, so four tests failed at the
patch rather than in the body. `create=True` is what lets them stand
somewhere that has no such function, which is the case the code under test
already handles two lines down and which is checked on its own.

And the publisher order follows the memory on purpose: a runner with 7 GB
in it puts the two Gemma 4 rows last and is right to. The three tests that
read an order now say which machine they are standing on instead of
assuming the one that ran them has room for everything.
2026-09-05 11:31:03 +03:00
yusufipek 74e17cbd15 Do not let a shrugging sysconf turn a workstation into a tiny machine
Three from the review of the change before this one.

sysconf answers -1 for a limit it holds to be indeterminate, and CPython
hands that back rather than raising, so the page count times the page size
came out negative. A negative is truthy, so it went past the check for a
machine nothing could be read from and floored at half a gigabyte: a 64 GB
workstation was told every model past 512 MB was too big for it, the
suggestion dropped to small-q5_1, and the machine line read "Memory:
-4096 B". Anything not positive is now the unknown machine it always was.

The memory is read once and kept. It does not change while Dikte runs, and
a thirty row list asked seventy times per draw, which on the Mac path is
seventy processes started on the interface thread every time a download
finished, a model was deleted or a publisher changed.

And "test-" is matched as a plain substring, so it was also inside
"Latest-" and dropped a publisher nothing is wrong with. Anchored the way
every other mark in that list already is.
2026-09-05 11:22:12 +03:00
yusufipek f36348d536 Put the indicator on the screen the session is actually on
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.
2026-09-05 11:19:02 +03:00
yusufipek 08fc2e4a9d Group the model lists and say which row this machine should take
The two local model boxes handed over a flat list sorted by size and left
every choice in it to the reader. For whisper that interleaved the models:
large-v3-turbo-q5_0 landed between the two medium quantisations, half a
screen from the turbo model it is a copy of. For cleanup it was forty
repository ids, half of which answer with nothing at all because what they
publish is split across files or larger than the cap, and an empty box read
as though the click had not registered.

Now each box says what the machine is, groups the list by model, and marks
the row to take:

- whisper rows are grouped by model, with the quantisations and the
  English-only files under the model they are a copy of, and every row says
  its bit depth rather than leaving q5_1 and Q4_K_M and BF16 to be decoded.
- the recommendation follows the machine. Under 4 GB it is small-q5_1;
  with a graphics interface and 15 GB it is large-v3-q5_0, which is worth
  about two and a half points of word error in the languages that are not
  English; in between it is turbo, and a processor build where the Vulkan
  one belongs is not counted as a card.
- a row larger than half the memory less a gigabyte says it is too big.
- the publisher box holds the five suggestions until the switch beside it
  is turned on, and a line under it says in words what the chosen one is.
- a publisher that answers with nothing says why instead of going blank.
- the draft heads (dflash, dspark, eagle3) are no longer offered as models,
  and neither are the base models that sit beside their tuned twin.
2026-09-05 11:12:42 +03:00
yusufipek 4ae44720c8 Merge master, and let one function pick the archive
master grew _pick_asset() while this branch grew a second answer to the
same question inside install_program(). Two functions deciding which
archive this machine wants is one too many, so the managed build is
asked for at the top of the picker: Dikte's own Vulkan whisper-server
first where the machine is one it is built for, then upstream's newest,
then the nightly pointer, then the newest release that carries a build.

install_program() is back to master's three lines and reads which of the
two landed off the asset name.
2026-09-05 10:06:38 +03:00
Yusuf İpek 74c37c0112 Merge pull request #73 from yusufipk/llama-nightly-and-model-box
Find the llama.cpp nightly builds, and keep the model box honest
2026-09-05 09:51:24 +03:00
yusufipek fb80d85332 Stop two tests failing on what is around them
The macOS path test set XDG_CONFIG_HOME to "/c" and asserted that "/c" was nowhere in the answer, but the home these run under is a mkdtemp path, so any TMPDIR with a "/c" in it failed the test. macOS is exactly where that happens: its temporary directories are /var/folders/<two letters>/, and one letter in thirty-six starts with c. The needle is now a word no directory can be called.

The other is the `ask` verb reading what was piped into it. Nothing was, but the runner's own stdin is not nothing either, so the verb read whatever the runner left there. It now reads an empty stream, whichever runner is in use.
2026-09-05 09:47:50 +03:00
yusufipek fff9cd1c55 Keep the publisher and the model boxes saying the same thing
Changing the publisher left the model box untouched: the old selection was carried over, added back as "not downloaded" and selected again, so a model the new repository does not publish could be saved against it. The selection is now only carried within the publisher it was made in, every keystroke in the publisher box no longer starts its own request, and a list that comes back for a publisher that is no longer chosen is dropped rather than answering the wrong one.

The status line grew two things it could not say before. A row rebuilt from a name alone carries no file to fetch, and the Download button stayed lit over it doing nothing; those rows now say the publisher does not offer the model, and the button is out. A model that is here while the program above it is not no longer reads "Ready", which is what had people asking why nothing transcribed.
2026-09-05 09:47:42 +03:00
yusufipek cfeed2af8c Find the llama.cpp builds where they are actually published
"latest" for llama.cpp is a version marker carrying one file, nightly-tag.txt, and the archives it names hang off a prerelease that "latest" never points at. Reading only the latest release meant Dikte offered no llama-server for any machine, so _pick_asset now follows that pointer, and when there is none it walks the recent releases and takes the newest one that does carry a build for this machine. hub grows releases() for the listing and text() for the pointer file; the pointer is read rather than cached, because what it carries is a few bytes on the way to a download that is checksummed in full.

The extra lookups are best effort: whatever goes wrong in them leaves the caller's own message standing, but a first release that could not be fetched at all is kept and re-raised when nothing else turns up, so an unreachable GitHub still reads as an unreachable GitHub rather than as a machine nobody publishes for.
2026-09-05 09:47:22 +03:00
yusufipek 5f6e4ad782 Make a version bump possible without editing the workflow first
The digest was a literal in the workflow while the version was an input,
so a dispatch for anything but 1.9.3 built the archive and then failed
its own gate. The digest of a version nobody has reviewed cannot be
known before it is built, which made the inputs unusable for the one job
they exist for.

An empty expected_sha256 now reports the digest of what was built and
refuses to publish; a digest handed in is checked the way the literal
was. The input shapes are also checked before the checkout that uses
them rather than after it.
2026-09-05 09:45:48 +03:00
yusufipek 10ef4e62a9 Smoke-test the bundle the way Dikte starts it
The CPU run passed -ng, which tells whisper.cpp not to look for a
backend at all, so the claim the bundle rests on, that it starts and
transcribes where Vulkan cannot, was never tested. Dikte passes -ng only
when its GPU setting is off; every other run goes through the backend
registry.

Both runs lose the flag, and a third one is added between them: the
loader installed, nothing behind it. That is a real machine, libvulkan
pulled in by something else with no driver to go with it, and it is the
one whose answer nobody knew.
2026-09-05 09:45:19 +03:00
yusufipek 00a5283adb Let a downloaded program be downloaded again
The button disappeared the moment anything landed, and nothing else on
the window asks for that download. So a machine that got the processor
build before its graphics driver was installed can never be given the
Vulkan one, and nobody can pick up a newer whisper.cpp either: both of
those are the same missing control.

It reads "Download again" once a copy is here, and stays hidden while a
system one is on the PATH, because that is the copy that would run.
2026-09-05 09:45:00 +03:00
yusufipek c3bf328eef Build the bundle only for what the bundle is built from
The trigger listed dikte/ggml.py, both test files and the two READMEs,
which are among the files that change most often. A typo fix in the
README started a run that builds a container image, compiles every
Vulkan shader and starts two more containers, with a 45 minute timeout
on it because that is roughly what it costs.

Nothing is lost. What ties ggml.py to the release, the tag, version,
commit and reviewed digest, is asserted in tests/test_packaging.py, and
that file already runs on every pull request in milliseconds.
2026-09-05 09:37:08 +03:00
yusufipek 59180b8eff Say when the processor build landed instead of the Vulkan one
The install falls back to upstream's CPU archive whenever Dikte's own
release, the file in it, or its reviewed digest is not there, and until
the package is published by hand that is every download. It happened
without a word: the window said "Downloaded, version v1.9.3." either
way, and a graphics card sitting idle looks exactly like one being used.

The install record now carries which of the two builds landed, written
only where both were on offer, and the settings window says so on the
line that already reports the version.
2026-09-05 09:16:59 +03:00
yusufipek 956c3eaf3c Leave the tar extraction fallback where it was
Refusing the install on a Python without the extraction filters is a
change to how every archive on every platform is unpacked, and it has
nothing to do with shipping a Vulkan whisper-server. On those Pythons
the download stops working altogether, which is a worse answer than the
one that was there.

Worth doing on its own terms, in its own change, where the versions it
turns away can be argued about without a backend release riding on it.
2026-09-05 09:16:49 +03:00
nomoreshow 1f57455a34 Fix cross-platform test assumptions 2026-09-04 01:04:36 +03:00
nomoreshow e0eae4d8fe Ship a managed Vulkan whisper-server for Linux x64 2026-09-04 00:34:55 +03:00
yusufipek eda1398a2b Call the OpenRouter subtitle model the audio file model
Timestamps are a file transcription option, so the model box is named
after the file rather than the format it ends up in.
2026-09-02 12:43:07 +03:00
yusufipek 6e307bd8d0 Let OpenRouter subtitles use a chosen model instead of whisper-1
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.
2026-09-02 12:41:43 +03:00
oztturkandClaude Opus 5 e56515d032 Name the card that ran, and say what to install
Two things the first pass got wrong.

The card was named out of whichever listing came first, so a machine
with both a CUDA build and a Vulkan loader could have its card named
from the wrong one. Worse, whisper numbers every device it can see in
one sequence while the handle carries the backend's own index: a
processor-only build lent a Vulkan backend through GGML_BACKEND_PATH
reports "device 1: Vulkan0", and reading that listing by the handle's
digit names whatever sat in slot zero. The backend's own enumeration is
asked first now, because it is the only one indexed the way the handle
is, and whisper's listing is a fallback taken only when it names
exactly one card.

And "this build carries no graphics backend" left the reader with
nowhere to go. It is the one case with a fix worth naming: whisper.cpp
publishes no build that reaches a card on Linux, and program_path runs
a copy from the system ahead of the downloaded one, so installing one
is the whole remedy. Server.state() now says which copy is running,
and the line says so only when it is Dikte's own download; a system
build that cannot reach the card gets the shorter sentence, since
installing it again would change nothing.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019zqCqhmeaNT6m1GPp8ZWPp
2026-08-28 16:38:11 +03:00
oztturkandClaude Opus 5 840e70463a Say what the local models are running on
"Use the graphics card" was a flag and nothing else: whisper got -ng
when it was off, llama got -ngl 99 or 0, and nobody looked at what
happened next. A build without a GPU backend runs on the processor
while the box stays ticked, which is what the release archives for
Linux do, every time. Nothing anywhere reported whether the local
server was even up.

Both servers already say where the model went, in the log Dikte
captures. It is read back once the server reports ready and turned
into a backend, a card and, for llama, the layers it offloaded. The
verdict comes from what whisper committed to -- "using X backend" and
the model buffer -- rather than from the devices it merely listed: a
card that is found and then fails to initialise sends it back to the
processor, and the listing alone would have called that a graphics
card. A log that says nothing stays "could not tell" instead of being
guessed at; a hand-built macOS whisper has Metal compiled in and
prints no backend line at all.

The state is then somewhere to be seen. Server.state() is a snapshot
of the process and what it settled on, and it reaches `dikte status`,
`dikte doctor` and a line under each local model box in the settings
window. doctor reads the log from disk when no instance is running, so
it still answers on a machine where Dikte is closed, and it says which
of the three it is: what the last run used, that the last run said
nothing, or that none ever ran here. Where the card was asked for and
not obtained, the line says which of the two it is -- none was found,
or this build carries none -- because only the second is worth
replacing a download over.

The tests grew a second isolation. They read ggml's own data
directory, which is the real one on the machine running them, and
program_path prefers a whisper-server on the PATH, so the suite
answered from whatever the developer happened to have installed. Both
are now the test's own.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019zqCqhmeaNT6m1GPp8ZWPp
2026-08-28 16:16:45 +03:00
kemal 21e28f621e Allow wheel events on focused boxes when window is inactive 2026-08-28 02:13:51 +03:00
kemal cd419558fd Do not require claude on PATH when assistant uses a hosted provider 2026-08-28 02:13:51 +03:00
kemal b30ee55241 Cap local model thread count by available CPU threads 2026-08-28 01:28:53 +03:00
sudoeren 90ae1690ab Fix the cleanup prompt language and harden detection parsing
Review found the detected language was only switching the glossary rule, not
the base prompt: a detected Turkish recording with an English interface got the
English cleanup prompt with a Turkish glossary footnote. Both now follow the
spoken language, and the detected-language value is guarded against a server
that returns something other than a string. The record reply is covered by a
CLI test.
2026-08-27 21:40:09 +03:00
sudoeren 7da871c567 Make auto the default speech language and carry what was detected through
New installs start detecting instead of being locked to one language; a stored
value from before this default still wins. The dictation chain asks
transcribe_detected() in auto mode, records the detected code in history as
speech_language, hands it to the cleanup prompt (a detected Turkish recording
gets the Turkish prompt and glossary rule), and reports it on the socket reply.
The stale comment claiming whisper.cpp's -l auto does not detect is corrected.
2026-08-27 21:40:09 +03:00
sudoeren 1bb5c9ebbc Ask whisper.cpp for the detected language when auto mode runs it
The spoken language is only knowable when the transcription model reports it,
and only whisper.cpp does: the hosted endpoints accept auto but never say what
they heard. transcribe_detected() asks for detection exactly where it can be
answered, the local server in auto mode, by requesting verbose_json with
no_language_probabilities switched back on for that one request (the server
runs with -nlp, which keeps the sweep off everything else).
2026-08-27 21:40:09 +03:00
yusufipek 22a73bd939 Merge master into the OpenCode Go branch
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.
2026-08-27 16:27:57 +03:00