Commit graph

1946 commits

Author SHA1 Message Date
cogwheel
dda134cd0d chore: bump version to 4.2.0
Some checks failed
CI / Mobile app (analyze + test) (push) Has been cancelled
CI / Android native tests (push) Has been cancelled
CI / Dart packages (push) Has been cancelled
CI / Web-safe packages (push) Has been cancelled
CI / Localization (push) Has been cancelled
2026-10-13 12:11:17 +05:30
cogwheel
c356cd3193 Add 4.2.0 release notes in all languages 2026-10-13 12:10:49 +05:30
cogwheel
7e9ae1eaa5 Update files 2026-10-13 12:06:44 +05:30
cogwheel
9ee7b114ee
Talk with a realtime voice in calls on Open WebUI, Hermes and Direct (#842)
* Keep what a voice call said on its messages, and read Open WebUI's realtime call settings

Open WebUI 0.12 stores a realtime call's spoken transcripts in each
message's meta.voice and replays them to the chat model. A message
Conduit wrote again from its own state carried no meta, and the server
replaces each message whole, so the next push would have erased them.
Rows rewritten locally now keep the meta stored on them, the voice record
is read into the message's metadata, and the echo writes it back where
Open WebUI reads it. Another device's chat:message:voice refreshes the
chat.

Direct chats replay what a call spoke the way Open WebUI does: a reply
the voice gave by itself becomes its transcript, labeled as historical
voice speech, and what was spoken over a chat answer follows that answer.

The config's audio.realtime block and the chat.call permission are read,
ready for gating realtime calls; unreadable permissions count as unknown.

* Speak and listen through each chat's own server, including Hermes and Direct

The Server engines for speech to text and text to speech only worked
with an Open WebUI server, and in a Hermes or Direct chat they quietly
used whichever Open WebUI server was signed in. Server speech now
follows the selected model's backend: an Open WebUI model uses its
server, a Hermes chat on a Desktop Gateway uses the gateway's audio
relay, and a Direct or Apple chat uses a new Voice provider. A chat
whose backend has none uses the device instead; audio never goes to a
server the chat does not belong to.

Hermes speech goes through /api/audio/transcribe and /api/audio/speak
for the connection's profile, with the providers and keys configured on
the gateway; the voice-config route, which hands out provider keys, is
never called.

The Voice provider is one Direct connection that speaks the OpenAI API,
chosen in Audio settings with its transcription model, speech model and
voice. Requests go to its audio/transcriptions and audio/speech with
the connection's own key, headers, api-version and certificates. Picking
OpenAI fills in the models Open WebUI defaults to. The page is also in
the native Settings sheet on iOS.

The server voice picker shows only for Open WebUI, which lists voices;
Hermes and the Voice provider speak with their own.

* Talk to a realtime voice in calls, ready for Open WebUI and Direct

Open WebUI 0.12 can run calls through an OpenAI realtime voice that
answers small talk itself and hands real requests to the chat's model.
This adds the call side of that, shared by Open WebUI and Direct chats;
the audio engine it needs comes next, so calls stay as they are until
then.

The engine follows Open WebUI's web client: one request to the voice at
a time, made only while nobody is speaking; the chat's latest state sent
before each; speaking over the voice cancels its reply and tells it how
much was heard; a request the voice delegates runs as an ordinary chat
turn, and its outcome goes back to be spoken. Open WebUI's server
bridge carries a call for its chats. A Direct or Apple chat talks to the
Voice provider's Realtime API instead, with the bridge's rules checked
on the device, so one engine runs both; the device's bridge is tested
against the server's own rules, and the engine's tests hold every
command to them.

A delegated request is sent like a typed message, with the user's words
marked as spoken. A reply the voice gives itself is stored through the
same send as a finished answer under the voice model, and no model
runs; Open WebUI replays it to the chat model as past speech. What the
voice says about a delegated answer is merged into that answer's
meta.voice. Words nothing answered stay in the call's captions.

The call controller runs either engine: a call with a realtime voice
keeps CallKit, the audio session, background work, mute, speaker and
the call panel as before. Whether a call has one is decided when it
starts, and it falls back to a Standard call whenever anything is
unknown: Standard chosen in settings, a temporary chat, a server without
realtime calls, an account that may not call or whose permissions could
not be read, a sign-in with an API key, or a Voice provider without a
realtime model, voice and transcription model.

Direct connections now share their key, header and certificate setup
between HTTP and WebSocket, and Open WebUI's socket headers are built in
one place, as is the WebSocket that never follows redirects.

* Translate the Voice provider settings into Polish

Polish carries every English key, so the Voice provider's strings need
their Polish copies; without them polish_localization_test fails.

* Talk with a realtime voice in calls on iOS and Android

Calls in Direct and Apple chats with a Voice provider, and in Open WebUI
chats on a server with realtime calls, now talk with a realtime voice:
both sides can speak at once, and speaking over the voice stops it.

The voice needs the microphone and the speaker on one echo-cancelled
stream, so it does not hear itself. On iOS one audio engine runs with
voice processing on: the microphone is converted to 24 kHz frames, and
the voice plays from a queue that works like Open WebUI's web worklet,
starting once 80 ms are buffered and reporting how much of each part
was played when the user cuts in. It restarts after a route change. On
Android the microphone records as voice communication with the
platform's echo canceller, noise suppressor and gain control, and the
voice plays on the same audio session; what was heard comes from the
track's play position. A microphone that only offers 48 kHz is brought
down to 24 kHz. If the audio stops mid-call, the call ends and says so.

Audio settings gain Calls: Realtime, the default, or Standard, which
keeps every call turn by turn. The Voice provider takes a realtime
model, voice and optional instructions, prefilled for OpenAI. Both are
in the native Settings sheet on iOS too.

* Tell the user what a realtime call could not do, and mark what its voice said

A realtime call's problems that do not end it now show as messages in
the user's language: words that could not be made out, a reply cut
short, and a part of the call that could not be saved to the chat.

A reply the voice gave by itself shows its voice model with a waveform
and "spoken", even when the answer above came from the same model, and
offers no Regenerate: there is no chat model answer to run again.

Opening another chat, or none, ends a realtime call: its voice talks
about the chat it started in, and a request it handed over is answered
there. A call started without a chat follows the chat its first turn
creates, and that chat's server id.

* Talk with Hermes's GPT-Live voice in calls on a Desktop Gateway

A Hermes profile that runs voice calls through GPT-Live, and has an
OpenAI key for it, now gets realtime calls in Conduit too. The gateway
keeps the key: Conduit makes a WebRTC offer, Hermes opens the session
with OpenAI and returns the answer, and the voice's audio and events then
flow between the phone and OpenAI. Profiles in the chained voice mode, a
gateway that cannot say, and Responses API connections keep Standard
calls.

The call follows Hermes's own desktop client. GPT-Live takes its own
turns; a request it delegates carries no text, so Conduit sends the
user's latest words as an ordinary Hermes turn, marked as coming from
voice-live, with the spoken conversation of the last five minutes as
context only the model reads. While Hermes works the voice is told so
quietly; an approval waiting in the chat is mentioned once; the answer
is handed back in sentence-sized pieces for the voice to say. A newer
request stops the one still running, mute reaches the session, and
ending a call closes the session before the connection.

The WebRTC call comes from flutter_webrtc, behind the same port as the
other call audio, so the call controller, CallKit, background work and
the call panel are shared with every other call.

* Translate realtime calls and the Voice provider into every language

The Calls setting, the Voice provider page, the call's notices and the
"spoken" label were in English and Polish only, and the Localization check
requires every key in every language. They are now in all of them.

Every language also still said Server speech always goes to Open WebUI.
It now follows the chat's own server, so the descriptions name Open WebUI,
Hermes, or the Voice provider for Direct chats, as the English does.

* Keep the Voice provider page and the iOS call audio within the app's rules

The Voice provider page imported the Material and Cupertino libraries
directly, which the platform UI check allows only behind the shared
facade; it now takes its icons from the shared vocabulary. The package
boundary check also lists the new iOS call audio bridge, which reaches its
host only through ConduitBridgeHost.

* Fix what review found in realtime calls

- A call writes only to its own chat. What it said could land in the chat
  the user opened as the call ended, and a call started without a chat
  adopted any chat opened next. It now follows only the chat its own send
  creates and that chat's server id, writes nothing elsewhere, and merges a
  delegated answer's transcript into the call's chat even after a switch.
- A call that starts a new chat no longer seeds GPT-Live with the previous
  chat's messages, or names that chat to Open WebUI.
- Hanging up while a call is starting leaves no microphone, WebRTC peer or
  socket open. A WebSocket handshake that times out is stopped, and a
  socket that opens after it is closed.
- GPT-Live's microphone goes quiet before the session is asked to close,
  and an answer handed over as the call ends goes on in the chat unfollowed.
- Muting while speaking no longer holds a finished answer back until the
  user speaks again.
- Small talk held behind a delegated answer is saved after that answer
  when the call ends, so the answer stays last while it runs and can still
  be stopped. If it is still running a few seconds later, the small talk
  stays in the captions.
- A delegated answer whose send failed after it was placed ends as failed
  instead of working forever. Hermes approvals and questions now count as
  waiting for the user, so the voice says to answer them in the chat.
- GPT-Live's request is the user's latest words from the last five
  minutes, as in Hermes's own client; words from long before no longer
  join it.
- Read-aloud and dictation keep the server they started with, so text and
  recordings never go to another chat's server when the model changes.
- A voice reply's "spoken" label appears when its transcript arrives after
  the message, the native Voice provider choice is kept while connections
  reload, the call mode is saved on its own key, a Direct voice turn
  releases its database lease if saving it fails, and ending the call
  controller ends its realtime voice.

* Fix what review found in the realtime call audio on iOS and Android

- Android: a piece of the voice's speech taken just before the user cut in
  could still play, and count as heard. It is now taken with its clear
  generation, so a clear right after drops it. A stopped call's capture
  and playback threads stop with it, and an old thread sends nothing to
  the next call; the recorder is stopped before its thread is awaited, so
  a blocked read returns first.
- iOS: after a Bluetooth or headset route change, the microphone tap and
  converter are rebuilt for the new route's format. The playback queue no
  longer builds a string on the audio thread for every render.

* Fix what the second review found in realtime calls

- The first request in a new chat no longer ends the call: the call's own
  send counts as creating its chat until it is done, as the chat can open
  only after the answer is placed.
- A chat opened while the call connects ends it, and the voice never reads
  a chat other than the call's own.
- A reply cut off by hanging up waits behind a delegated answer still
  running, like the small talk held before it.
- A GPT-Live request replaced while the one before it was stopping is
  dropped instead of being sent to the chat.
- Resetting settings clears the Voice provider for good; it came back on
  the next launch.
- An earlier version of a voice reply is not labeled as spoken: versions
  keep no transcript to tell.
- The call's chat host closes even when the voice's ending fails, and
  chunkForCommentary takes only a positive limit.

* Follow only the chat a realtime call's own send created

A call started without a chat followed any chat that opened while its
first send was running, so a chat the user opened during that wait became
the call's: the voice read it, and later requests went there. The call now
remembers the messages its sends placed and follows a new chat only if it
holds them, which every send path's new chat does when it opens.

* Hand GPT-Live's newest request to the chat only once older answers stop

A request replaced while the one before it was still stopping skipped the
wait: the newest went to the chat while the first answer still ran, and
the chat put it off as busy. Stopping replaced requests now runs as one
chain, and a new request waits for all of it, including stops added while
it waited.

* Leave answers handed over as a realtime call ends running in the chat

- A request handed to the chat as the call ended was stopped by the bridge
  engine, though the chat should go on answering it. Both engines now stop
  only following such an answer, through a new DelegatedTurn.close that
  leaves the answer running, and its listener no longer outlives the call.
- A GPT-Live request still being handed to the chat when a newer one came
  is now stopped once the chat has it, before the newer request goes, so
  the chat never takes two at once.

* Name Apple chats where Server speech says it uses the Voice provider

The Voice provider page says Direct and Apple chats use it, but the
Server speech descriptions named only Direct chats. Both now say Direct
and Apple chats, in every language, worded as each language's Voice
provider description already words it.

* Keep realtime calls working alongside enforced temporary chats

Open WebUI 0.12 support (#843) made a new chat start temporary when the
server enforces it, and reading that now reaches the signed-in account and
its permissions. Three things follow for realtime calls:

- A call that starts a new chat stays Standard when the server will keep
  that chat temporary, as it already did for a temporary chat that is open.
- A realtime route that cannot be read keeps the call Standard instead of
  failing it, like anything else the route cannot tell.
- The Hermes route tests say the server enforces nothing, as they never
  sign in to Open WebUI.

* End a realtime call at once when it is hung up while connecting

- Hanging up while a realtime voice was still connecting waited for the
  start queued ahead of the stop, so the microphone and connection could
  stay up until the connection was made or timed out. The stop now ends a
  connecting voice right away, and the start it cuts short counts as
  cancelled, not failed.
- A voice reply's "spoken" label kept its old language after the app's
  language changed: the cached header now keys on the label as shown.

* Let every end of a realtime call wait for its one ending

A hang-up while the voice connects ends it at once, and the teardown that
follows ends it again. The second end returned as soon as the voice was
marked ended, so teardown could turn off the audio session before the
voice had stopped, and an error stopping it went unreported. Both engines
now keep their ending: every end waits for the same one, and teardown
reports what it throws.

* End a connecting realtime call at once when the system call UI hangs up

Hanging up from the CallKit screen queued the stop without the immediate
end a hang-up in the app gets, so a realtime voice still connecting kept
its microphone and connection until it connected or timed out, and could
connect after the call had ended. Both hang-ups now share that immediate
end, and the system's own end of the call is still not repeated.

* Fix what Greptile's full review found in realtime calls

- A chat opened while a call was still setting up kept the call going with
  the voice made for the chat the user left. The call now notes its chat
  when it starts, and a different chat open by the time it connects
  cancels it.
- Stopping a request the call replaces now waits for the chat's stop to
  settle, so the stop's late effects cannot reach the new request.
- An answer whose send failed after it was placed is marked failed in the
  chat, the way a typed send's is, instead of staying shown as running and
  keeping the chat busy.
- Small talk waits while the chat is answering, whoever asked; it used to
  wait only for answers the call had delegated, so a typed answer could
  stop being the chat's last message and could no longer be stopped.
- Pause holds the voice's answers until the call resumes, instead of only
  muting the microphone while finished answers went on being spoken.
- Speech to text set to Server uses the device in a chat whose backend
  cannot transcribe, as the Voice provider promised, instead of hiding
  voice input.
- Choosing the native Voice provider's connection, or editing its fields,
  now redraws that page, which iOS redraws only when it is the page the
  update is for.

* Hold a realtime call to the chat it was asked for, and unpause on unmute

- The chat a call is for is now noted when the call is asked for, before
  the setup that waits for the microphone and the route. A chat opened
  during that wait cancels the call instead of becoming its chat.
- Unmuting a paused realtime call, from the app or CallKit, now resumes it
  too; the voice stayed paused and kept its answers back while the call
  showed as live.

* Keep held small talk from silencing requests, and pause notices honest

- Small talk said while a typed answer runs waited on the call's own
  writes, which a request then waited behind for minutes, so the caller
  heard nothing instead of being told the chat was busy. It is now held
  apart: a request meanwhile hears at once that the chat is busy, and one
  made after the answer comes after the held words.
- A GPT-Live request that waited for approval while the call was paused,
  then finished, no longer has the voice say on resume that it still
  waits; its answer is still said.
- The native Voice provider page is patched alone and at once when it
  opens, rather than after the speech voices it does not show have loaded,
  so the patch cannot land over text the user is typing.

* Leave GPT-Live answers alone as a call closes, and drop replaced notices

- A request the voice handed over while the call was closing replaced the
  answer still running, which stopped it although an ended call leaves its
  answer to go on in the chat. Such a request now replaces nothing.
- A request replaced while the call was paused had its held approval
  notice said on resume, as if it still waited. Replacing a request now
  drops what was held about it.
2026-10-13 12:05:02 +05:30
cogwheel
4cb3c48fa2
Support Open WebUI 0.12 (#843)
Some checks failed
CI / Mobile app (analyze + test) (push) Has been cancelled
CI / Android native tests (push) Has been cancelled
CI / Dart packages (push) Has been cancelled
CI / Web-safe packages (push) Has been cancelled
CI / Localization (push) Has been cancelled
* Support Open WebUI 0.12.0 servers without the version warning

The vendored Open WebUI source now points at the v0.12.0 release, and the
newest server version Conduit accepts without a compatibility warning rises
from 0.11.4 to 0.12.0. Servers newer than 0.12.0 still see the warning, and
0.11 servers stay supported.

* Keep skill instructions when exporting and importing skills from Open WebUI 0.12

Open WebUI 0.12 exports each skill as a package of files, with the
instructions in its root SKILL.md, and no longer sends the top-level
content field. Conduit read only that field, so exported skills came out
with empty instructions, and importing a file exported by 0.12 created
empty skills.

Skills now take their instructions from the SKILL.md file when the server
sends files, as 0.12's own import does, and fall back to content for 0.11.
The export file carries both content and the package files, so 0.11 and
0.12 servers can each import it, and importing a 0.12 package passes its
files on so a 0.12 server keeps any supporting files.

* Offer folder sharing only to the folder's owner or an admin

Open WebUI 0.12 lets only a folder's owner or an admin change who it is
shared with; someone the folder was shared with for editing now gets a
403. Conduit still offered Share folder to them, so it failed when used.

The folder menu now offers Share folder for your own folders, and for
folders shared with you only when you are an admin, as Open WebUI's web
client does. Project settings stays with anyone who can edit the folder.

* Say why sign-in stops when Open WebUI asks for two-step verification

With two-step verification on, Open WebUI 0.12 answers a password or LDAP
sign-in with the next step to take (enroll, verify or recover) instead of
a session, and answers an account still awaiting approval with a pending
step. Conduit looked only for a session token, so sign-in failed with a
generic message, and signing in again with the saved password on launch
kept the credentials and tried again on every start, each attempt counting
toward the account's sign-in limit.

Sign-in now recognizes these answers. The sign-in page says the account
uses two-step verification, which Conduit does not support yet, and
suggests signing in with SSO, which goes through Open WebUI's own sign-in
page where the step can be finished, or with a session token from Open
WebUI on the web. An account awaiting approval is told so. Signing in
again with saved credentials treats either answer as final: it clears the
credentials it tried and lands on the sign-in page with the reason,
without retrying. Servers without two-step verification answer as before,
so pending accounts there behave as they did.

* Keep new chats temporary when the server requires temporary chats

Open WebUI 0.12 refuses to create a chat for a non-admin whose
permissions turn on both temporary chats and enforced temporary chats.
Conduit did not read that permission, so a new chat queued its creation,
the server refused it, and the message waited behind it without being
sent or showing an error.

Conduit now reads the permission the way Open WebUI's web client does.
While it is enforced, every new chat starts temporary, a chat not yet
saved becomes temporary as soon as the permissions arrive after sign-in,
and the temporary chat toggle and Save chat action are hidden. Chats that
already exist on the server go on as before, and admins are unaffected.

* Show the error Open WebUI 0.12 saved when a reply fails without a socket

When Conduit sends a message without a live socket, Open WebUI 0.11
answered a failed reply with a JSON null and saved the error on the
message, and Conduit then read the saved reply. Open WebUI 0.12 answers
the same failure with the provider's error status, or a 400, while still
saving the error. Conduit treated that as a failed send, so the saved
error never showed, and a queued send retried, only to be refused with
409 "Message already exists or has an invalid ID." because the reply had
already run.

When a completion for a saved chat fails, Conduit now checks the chat on
the server. If the reply is already there, finished, with an error or
with content, it reads that reply the way it already does after a JSON
null, so the saved error or answer shows and a retry stops instead of
repeating. A failure the server saved nothing for, such as an unknown
model, still fails the send with its reason, as before.

* Keep the session when a model's provider rejects a message with 401 or 403

A message that failed with any error mentioning 401 or 403 reloaded the
sign-in state. Open WebUI 0.12 passes a provider's own status through,
so a bad upstream API key on one model reloaded the user's session over a
problem that had nothing to do with it.

The failure now only marks the reply with its error. The auth interceptor
already decides when the session has ended, from the session endpoint,
as it does for every other request.

* Show which tool is asking for approval in chats that are not saved

With tool approval set to ask, Open WebUI 0.12 asks to approve a tool
call in a chat that is not saved through a socket confirmation that
carries only the call: its id, name and arguments. Conduit showed a
confirmation's title and message, which this request does not have, so
the approval appeared blank.

A confirmation for a tool call now shows the tool's name over its
arguments, formatted the same way as an approval in a saved chat.
Approving or declining still answers true or false, as the server
expects.

* Answer Open WebUI's terminal and MCP requests so replies are not held up

Open WebUI 0.12 asks the client for a terminal's AGENTS.md through
request:terminal and waits up to 5 seconds, and asks it to fill in an
MCP server's elicitation through request:elicitation, waiting up to 240
seconds. It also checks whether a user terminal is open through
request:terminal:state, waiting 2 seconds. Conduit answered none of
them, so each turn that triggered one sat until the server gave up.

Conduit now answers at once: no terminal data, no user terminal open,
and a cancelled elicitation, which the server treats as the user
declining so the tool goes on. The request:interaction:done note that
follows such a request is ignored.

* Sign in to Open WebUI accounts that use two-step verification

Open WebUI 0.12 can require two-step verification with an authenticator
app. A password or LDAP sign-in then answers with the step to take
instead of a session, and Conduit could only say it did not support it.

The sign-in page now takes that step itself, the way Open WebUI's web
client does:

- An account with an authenticator asks for its six-digit code, or for
  one of its recovery codes instead.
- An account signing in for the first time since two-step verification
  became required adds an authenticator. Conduit shows the setup key to
  copy, the server's QR code for an authenticator on another device, and
  a button that hands the key to an authenticator app on this device.
  Once a code confirms it, the account's new recovery codes are shown,
  with a way to copy them, before signing in, as the server never shows
  them again.
- An account whose authenticator an administrator reset takes the
  recovery token they gave, then adds a new authenticator.

A wrong code, an expired step and too many attempts each get their own
message, and Back returns to the sign-in form. Once the step succeeds,
the session is signed in to and remembered as the password or LDAP
sign-in would have been.

Signing in again in the background with a saved password still cannot
answer a second step, so it stops as before and the sign-in page now asks
the user to sign in again and enter their code. Changing the
authenticator or making new recovery codes from Settings stays on the
web for now.

* Wait for a running reply to finish before showing it after a refused retry

When a queued send was retried while the reply it had already started
was still running, Open WebUI 0.12 refused the retry with 409, and
Conduit read the saved reply once and closed it as finished. The user
was left with part of an answer marked complete.

Conduit now reads a saved reply in place of a refused request only once
the server has finished it, with its answer or its error. While it is
still running, the retry fails as before and tries again later, by when
the reply has finished.

* Check the session when Open WebUI refuses a message with 401 or 403

Open WebUI refuses a message with 401 or 403 both when the session has
ended and when a model's provider rejects its own API key, which 0.12
passes through. Since a refused message stopped reloading the sign-in
state, an ended session went unnoticed by a send.

A message refused with 401 or 403 now asks the session endpoint once.
The auth interceptor reports a refusal there as the session ending, as
it does for every other session check, while a provider's refusal only
marks the reply with its error.

* Let the two-step sign-in reach the server before there is a session

Conduit's API client refuses, without sending it, any request to an
endpoint that needs a session while it has none. The two-step sign-in
routes were treated as such endpoints, so every code, setup and recovery
token was refused locally, and the sign-in page said the code was wrong.

The four routes the sign-in uses (verify, enroll/start, enroll/confirm and
recover) are now public, like sign-in itself: they authenticate with the
challenge token in their body, and are sent without a session token.

* Ask to start again when a verified session is refused

If the server refused the session a correct second-step code had issued,
the sign-in page said the username or password was wrong. It now says
the step has expired and to sign in again, and the step is kept so the
sign-in can be tried once more.

The Czech and Slovak text for an administrator's reset now says reset
rather than restored, and the code field's listener is removed before the
field is disposed.

* Label a new authenticator with the account that signed in

The account name given to an authenticator app while setting up two-step
verification was read from the sign-in form once the server answered, so
a field edited meanwhile could label it with another name. It is now
taken when the sign-in is submitted.

* Sign in with the details as they were submitted

The sign-in page read the method, username and password again after
waiting to save the server and to confirm leaving the active account, so
a field changed during that wait could send different details from the
ones checked, and label a new authenticator with a third. It now takes
them once, when the form is submitted, and uses them throughout.
2026-10-11 19:46:31 +05:30
cogwheel
d04b0b7833
Put every account and connection on one Accounts page, and add them from one sheet (#828)
Some checks failed
CI / Mobile app (analyze + test) (push) Has been cancelled
CI / Android native tests (push) Has been cancelled
CI / Dart packages (push) Has been cancelled
CI / Web-safe packages (push) Has been cancelled
CI / Localization (push) Has been cancelled
* List every account and connection on one Accounts page, reached from a card atop Settings

Accounts now has a card for each Open WebUI server, its accounts under it
with the active one checked, then Hermes with its connections and Direct
with its providers; each card's top row opens its page, and + adds
another. A server's page lists its accounts to sign out of beside its
addresses, and goes once the last of them is gone.

Settings opens on a card naming who it is for, leading to Accounts, with
Add account under it. Profile joins the account's own group, the Hermes,
Direct and Connect rows leave for Accounts, and Sign out leaves only the
active account when there are several; signing out of all moved to
Accounts.

* Add and edit accounts and connections in one sheet, a tab for each kind

Add account opens a sheet with Open WebUI, Hermes and Direct tabs. Open
WebUI continues on a saved server or a new one. Hermes and Direct host
their editors: Connect tests a new Hermes connection, saves it, and puts
it in use with Hermes on; Direct tests and saves a provider. Each tab
keeps what was typed while another is open, and an editor's further
options open as pages inside the sheet, which grows to hold them.

A Direct provider on the Accounts page, or a Hermes connection held
there, opens the same sheet to edit it.

* Enter a new Open WebUI server in the account sheet, and sign in once it answers

The Open WebUI tab holds the connection form itself: the address, and
the self-signed, mutual TLS and header options. It checks the server in
the sheet, saying there when it does not answer; once it does, the sheet
hands the server to the connection page, which goes straight on to its
sign-in. A server behind a proxy goes on to the proxy's sign-in from
there, and a saved server's Continue connects on opening.

* Open Accounts inside the native Settings sheet, from a card naming whom it is for

On iOS, Settings now opens on the same account card as elsewhere: whom
it is for, where, and how many others, with Add account under it. The
card pushes a native Accounts page inside the sheet, a card for each
Open WebUI server with its accounts, then Hermes and Direct. The account
or connection in use is checked; tapping another switches to it once
the sheet has gone. A server's own row opens its page, a Direct provider
its editor, and + adds any kind. The account's own rows gather under
Account, and Sign out is single, as in the Flutter Settings.

Sheet rows can now show a round picture or initials, a check, and the
accent color, and a page can carry a bar button that closes the sheet
to act.

* Keep the Accounts page's + once its rows refresh, and drop chevrons from action rows

The native Accounts page refreshes its rows as it appears, and the
refresh rebuilt the page without its bar button: + never showed. A
patch now keeps it. The Settings root also honors a row's own
disclosure choice, so Add account, which opens no page, has no chevron.

The account sheet's close button is drawn in Flutter: the native glass
toolbar button rendered as a flat grey square over the sheet.

Found driving the app on an iOS simulator against two Open WebUI
servers, three accounts and a Direct provider.

* Open the account sheet the rest of the way when its form is dragged up

A drag on the form lifts the sheet before it scrolls anything, and the
sheet snaps to its nearest size when let go. A short drag ended nearer
the opening size than the top, so the sheet settled back where it began
and the form had not moved: scrolling seemed to do nothing. A drag that
lifted the sheet now carries it to the top, where the form scrolls;
dragging it down still settles or closes it as before.

* Keep the account sheet tests' saved Hermes connection out of the next file's tests

CI runs test files together in shards, sharing one isolate. The sheet
tests saved a Hermes connection in the preferences store and left it
there, and the Settings card counts Hermes connections in its "+N", so
the profile tests that followed read one too many. The sheet tests now
put the store back after each test, and the profile tests no longer
read Hermes connections from it.

* Keep the sheet's Hermes Connect until it finishes, and its handoffs to a page still there

- A Connect that saved the new Hermes connection but failed to put it in
  use, or to turn Hermes on, left the button as Save: the next press only
  saved, and the connection never came into use. The sheet's form stays
  the adding one, and Connect runs the whole sequence again.
- A server the sheet named that could not be read went on to connect with
  no address, asking for one over the reason it was empty.
- A proxy answer that came back after the sheet closed still handed the
  server on from the closed sheet.
- The Czech Continue with reads as continuing with a server, not going to
  one.

* Lay out the sheet's Hermes loading state, switch before enabling Hermes, and keep one Save

- Editing an inactive Hermes connection in the account sheet waits for it
  to load, and that wait, or its retry after a failed read, was a page of
  its own that the sheet's list could not lay out. It is plain content in
  the sheet.
- Picking a Hermes connection while Hermes was off turned Hermes on first,
  with the connection it was meant to leave; a switch that then failed
  left that one on and in use. It now switches first.
- Editing a Direct provider in the sheet showed two Save buttons outside
  iOS: the form's own and the sheet's. The sheet's stands in only for
  iOS's toolbar Save.
- With Direct primary, Hermes in use and no Open WebUI user, the native
  account card named the Hermes connection beside Direct's avatar. Its
  avatar now follows the same choice as its text.
2026-10-09 22:32:09 +05:30
cogwheel
1f321dac89
Keep the docked iPad sidebar visible to VoiceOver (#827)
* Keep the docked iPad sidebar visible to VoiceOver

The tablet layout paints the sidebar, then the detail pane, which is
go_router's nested shell navigator. Every route in that navigator puts a
modal barrier over its page, and the barrier's BlockSemantics drops all
semantics painted before it up to the nearest container. With no container
between the barrier and the layout's Row, the whole docked sidebar (chats,
notes, channels, tabs) was missing from VoiceOver and XCTest while it was
on screen; only the resize handle, painted after the Row, survived.

Wrap the content in a semantics container. With a navigator in the detail
pane, the sidebar now has the same semantics as with plain content: the
docked sidebar is exposed with its actions, and a dismissed one is still a
zero-size node with no actions. Phone is unaffected, because its drawer is
painted after the content.

The new widget test fails without the change (no sidebar node) and passes
with it.

* Check the docked sidebar row keeps its tap action

The semantics test only checked the row's label. Give the synthetic row a
tap handler and assert VoiceOver can still activate it beside the detail
navigator.
2026-10-09 21:04:12 +05:30
cogwheel
cea3518bab
Stop downloading whole chats again when they open (#826)
* Stop downloading whole chats again when they open

Opening an Open WebUI chat with no stored copy fetched it to show it, then fetched it again in the background to store it. The sync engine now fetches it once, stores it unless it belongs to another user's shared folder, and the open shows that copy. When the engine has nothing to give, the open falls back to the direct fetch as before.

Every open of a stored chat also downloaded the whole chat again in the background, changed or not. A chat with more than 64 KiB of stored messages and no unsent edits now starts a sync cycle instead, which fetches it only if the server's copy changed. Smaller chats still download directly, since that costs less than a cycle.

* Treat an outdated read as absent when a pulled chat isn't stored

A chat that pullChat declined to store was returned even when a bulk change, such as deleting every chat, finished while it was being read. The storing path already treats that read as absent. The unstored path now does the same, so the open falls back to a fresh fetch.

* Parse a large stored chat off the UI thread, however few messages it has

Rebuilding a stored chat moved to a worker only above 100 messages, so a chat with a few messages carrying megabytes of inline image data was parsed on the UI thread. Opening such a chat for the first time now goes through this path, where the direct fetch it replaced used a worker above 50 KiB. Assembly now also uses the worker when the stored message JSON passes 50 KiB.

* Decide whether to store a first-opened chat with the shared-chat rule

The first-open fetch decided whether to store a chat with its own copy of the rule in isReadOnlySharedConversation, while the fallback path used that function. Both now call isReadOnlySharedOwner, so the two paths can't drift apart.

* Test that reopening a large chat still gets server edits

The reopen tests only checked that a sync cycle was requested. With the real sync engine, an edit made on the server now has to reach the open chat through the cycle the open starts, and an unchanged large chat must not be downloaded again.

* Store a chat with large inline data off the UI thread

Splitting a fetched chat into rows for storage moved to a worker only above 100 messages, so a chat with a few messages carrying megabytes of inline image data was split on the UI thread. A first open now waits for that step. It also uses the worker once the messages' text passes 50 KiB, counted without encoding anything.
2026-10-09 19:50:09 +05:30
cogwheel
66cf579466
Merge pull request #816 from cogwheel0/hermes-multiple-connections
Some checks are pending
CI / Mobile app (analyze + test) (push) Waiting to run
CI / Android native tests (push) Waiting to run
CI / Dart packages (push) Waiting to run
CI / Web-safe packages (push) Waiting to run
CI / Localization (push) Waiting to run
Save several named Hermes connections and switch between them
2026-10-09 12:27:07 +05:30
cogwheel
8ec4ba0baa Move off a deleted Hermes connection that cannot be put back
Deleting the active connection saves the list without it, then clears its dashboard cookies. When the clear failed and saving the connection back failed too, the active id was pointed back at it, but it was gone from the list: the runtime kept serving it until a restart, which then found no connection. The deletion now stands: the runtime moves to the replacement and the connection's secrets are discarded, and the failed clear is still reported.
2026-10-09 12:16:05 +05:30
cogwheel
fc76e5a9a6 Keep a Hermes server URL the upgrade carries over, however long
A server URL saved before Hermes connections existed had no length limit. The upgrade copied a long one into the first saved connection, and the next launch read it back as damaged and dropped it, so the connection worked once and then lost its address. A saved URL is now read back as stored; only a new one is held to the limit.
2026-10-09 12:16:05 +05:30
cogwheel
2e4186b4e0 Merge remote-tracking branch 'origin/main' into hermes-multiple-connections
# Conflicts:
#	lib/core/services/native_sheet_hydration_service.dart
#	lib/core/utils/native_sheet_utils.dart
#	lib/features/navigation/widgets/sidebar_user_pill.dart
#	lib/l10n/app_cs.arb
#	lib/l10n/app_de.arb
#	lib/l10n/app_en.arb
#	lib/l10n/app_es.arb
#	lib/l10n/app_fr.arb
#	lib/l10n/app_it.arb
#	lib/l10n/app_ja.arb
#	lib/l10n/app_ko.arb
#	lib/l10n/app_nl.arb
#	lib/l10n/app_pl.arb
#	lib/l10n/app_ru.arb
#	lib/l10n/app_sk.arb
#	lib/l10n/app_zh.arb
#	lib/l10n/app_zh_Hant.arb
#	lib/main.dart
#	packages/conduit_core/lib/services/secure_credential_storage.dart
#	test/core/services/secure_credential_storage_test.dart
2026-10-09 11:52:08 +05:30
cogwheel
e0ed03f2ab
Merge pull request #820 from cogwheel0/multi-account/4-endpoints
Reach an Open WebUI server through whichever of its addresses answers (4/4)
2026-10-09 11:51:36 +05:30
cogwheel
e39e1390f7 Begin an addition in the tests of the guards that keep a pending account 2026-10-09 11:33:46 +05:30
cogwheel
f094a46ab0
Merge pull request #819 from cogwheel0/multi-account/3-accounts-ui
Switch, add and sign out of Open WebUI accounts from the profile (3/4)
2026-10-09 11:33:03 +05:30
cogwheel
fa8cbd306c
Merge pull request #818 from cogwheel0/multi-account/2-sessions
Keep several Open WebUI accounts signed in and switch between them (2/4)
2026-10-09 11:28:48 +05:30
cogwheel
7d99602052
Merge pull request #817 from cogwheel0/multi-account/1-registry
Store Open WebUI servers and accounts in one registry (1/4)
2026-10-09 11:28:33 +05:30
cogwheel
5b8ad67be6 Rebuild the clients after an address edit only when the active connection changed
Every save and removal of an address reloaded the saved servers, which
built a new client and ended a reply arriving through it, even for an
address not in use or of another server. The addresses shown and the
route check still follow every edit.
2026-10-09 04:42:49 +05:30
cogwheel
4b063fe575 Stop only the open chat's Open WebUI reply when the address in use changes
Agreeing went through the general stop, which also cancels Direct and
Hermes runs. An address change stops only what arrives through the
address.
2026-10-09 04:10:21 +05:30
cogwheel
939bcd7686 Ask about stopping a reply before an address change only for an Open WebUI one
The question counted every reply, and agreeing stopped the open chat's.
A Direct, Hermes or on-device reply does not come through the address,
and was stopped for nothing.
2026-10-09 04:10:21 +05:30
cogwheel
675f56e872 Ask before changing or removing the address in use stops a reply being written
Saving an edit to the address in use, or removing it, moves the clients
off it at once, and a reply arriving through them ended without a word.
Both now ask first, as switching accounts does, and stop the reply only
once the user agrees.
2026-10-09 04:10:21 +05:30
cogwheel
e8d6c92abf Keep a proxy sign-in on an edited address for the server's only account, and never save the address without it
With another server active and nothing of this one's to prove the address
with, the cookie was kept for no account and the address saved for its proxy
to refuse. The server's only account keeps it now; with several to choose
from, the editor asks for a sign-in first instead.
2026-10-09 04:10:21 +05:30
cogwheel
2eb2a600ce Report a gateway error to a request that takes every status as its answer 2026-10-09 04:10:21 +05:30
cogwheel
188c7ee18f Check a server's other addresses when its proxy answers with a gateway error 2026-10-09 04:10:21 +05:30
cogwheel
cbd66c6513 Keep session headers off an address, where a sign-out would not find them 2026-10-09 04:10:21 +05:30
cogwheel
33db650893 Clear the refusal of the route a renewed proxy cookie is saved to 2026-10-09 04:10:21 +05:30
cogwheel
8758538f39 Test that a checked address keeps the cookie it was checked with 2026-10-09 04:10:21 +05:30
cogwheel
aaa7886adf Save a checked address and its proxy cookie together, or neither 2026-10-09 04:10:21 +05:30
cogwheel
f1b5cb87cc Clear the refusal of the route a proxy sign-in renewed, not the one in use after it 2026-10-09 04:10:21 +05:30
cogwheel
8d072c99ba Drop the account list a sign-out's cookie scrub no longer reads 2026-10-09 04:10:21 +05:30
cogwheel
04def3f7bf Judge a page after an upgrade to HTTPS as one at the same address 2026-10-09 04:10:21 +05:30
cogwheel
bb8a6da3ae Hold an address's fields while it is checked 2026-10-09 04:10:21 +05:30
cogwheel
eb0df8480c Save a config to the route it was read on when routes share a URL 2026-10-09 04:10:21 +05:30
cogwheel
d20bd17de8 Report a file download a proxy redirected to its sign-in
A file's own content can be a page, so a page answering a file download after a followed redirect was left out of proxy refusals. A proxy redirecting the download to its sign-in page answered with that page, which was then taken for the file, and the route kept. Only a redirect that kept the file's path -- up to HTTPS, or a trailing slash -- now leaves its page out.
2026-10-09 04:10:21 +05:30
cogwheel
5a057aaac5 Leave only a file's download out of the page taken for a proxy sign-in
A page answering a request replayed after a same-origin redirect counts as a proxy's sign-in, except where the answer was not JSON. Many API answers are read as bytes or a stream -- a chat export among them -- and a proxy redirecting one of those to its sign-in was then not reported, and the unusable route kept. Only a file's own content, which can be a page of its own, is now left out.
2026-10-09 04:10:21 +05:30
cogwheel
7300444fc2 Read the saved servers again when retrying a server's addresses
Retry on the addresses page read the account list again, but the list is read from the saved servers, and that read, failed, stayed as it was: the list met the same failure, and the addresses never came back once storage did. Retry now reads the saved servers again too.
2026-10-09 04:10:21 +05:30
cogwheel
cf2c290650 Say when a server's addresses cannot be read, and read them again
The addresses page showed nothing -- no addresses, no way to add one -- when the saved accounts could not be read, a locked Keychain say, the same as for a server no longer saved. It now says something went wrong and offers to read them again.
2026-10-09 04:10:21 +05:30
cogwheel
97363da7f3 Forget only the refusals of the address renewed or saved
Signing in to a proxy again, or saving any server's addresses, forgot every route's refusals. A route whose proxy still refused requests but passed its health check was then moved back to, and refused again. Renewing a proxy session now forgets the refusals of the route in use, whose session it renewed, and saving an address forgets that address's, on the server in use; other routes' refusals stand.
2026-10-09 04:10:21 +05:30
cogwheel
359654bc44 Hold back a route upgrade for a reply that begins as it is selected
Moving to a better route waits while a reply is being written, and that was decided before the selection waited for storage. A reply begun meanwhile was cut off by the move once the selection went through, the route in use still answering. Storage now asks again as it makes the selection, and an upgrade held back there is put off as one held back before.
2026-10-09 04:10:21 +05:30
cogwheel
07fd5d38c2 Take a downloaded page for no proxy sign-in after an upgrade
A page answering a request replayed after a same-origin redirect was taken for a proxy's sign-in, and the route in use was kept from use for five minutes. A file downloaded through an address upgraded to HTTPS can be a page of its own -- an HTML attachment. Only a request expecting an API answer now counts.
2026-10-09 04:10:21 +05:30
cogwheel
553483529d Open a server's addresses without an Open WebUI session
With Direct or Hermes usable and the Open WebUI account signed out, Manage accounts stays open and shows a server's Addresses, but the router kept those pages to signed-in accounts and sent the tap to chat. They are on the device, like the saved accounts, and now open in that state too.
2026-10-09 04:10:21 +05:30
cogwheel
69e438a175 Forget a route's refusals when its proxy session is renewed
Signing in to a proxy again from the connection issue keeps a new cookie for the route, but its refusals, of the session replaced, were still remembered: checks skipped the renewed route for five minutes, and a fallback failing meanwhile was kept instead of it. Renewing a proxy session now forgets them.
2026-10-09 04:10:21 +05:30
cogwheel
3c5538edb6 Report no proxy refusal from a retired API client
The app's client is rebuilt when its session changes, and the client it replaces lets its requests finish. A proxy refusing one of those, sent with the session since replaced, was reported as the route refusing the current one, when its connection and cookie were the same, and the route was kept from use for five minutes. A retired client no longer reports.
2026-10-09 04:10:21 +05:30
cogwheel
bc5ac10293 Leave a route a sign-in selected while a check of the routes ran
A check of the routes selects the route it chose once its probes answer. Waiting for storage behind a sign-in, which selects the address it was checked with, it then replaced that address, and the sign-in page, finding another connection, refused to go on. The check now selects only while the server is still reached through the route it began from; otherwise the route selected since stands, and the routes are checked again from it.
2026-10-09 04:10:21 +05:30
cogwheel
9e3b36ce69 Hold a route probe and its redirects to one deadline
Each redirect a route probe followed started a full timeout of its own, so a probe with the default four seconds could take nearly sixteen, holding up the choice of a route that had already answered. The probe and its redirects now share one deadline.
2026-10-09 04:10:21 +05:30
cogwheel
9e99008ada Keep a route's headers on the HTTPS upgrade its probe follows
A route probe follows an upgrade to HTTPS on the server's host, as the API client does, but the health client dropped the route's own headers -- an access token for the proxy in front, say -- from any request away from the route's origin, that upgrade included. The server answered the probe's second request with its sign-in, and a working route was taken not to answer. The headers now go where the API client sends them: the route's origin, and that upgrade on its host. The logout fence still keeps the cookie off.
2026-10-09 04:10:21 +05:30
cogwheel
4edc41d9e6 Leave an address unsaved when its editor is closed during the check
Saving an address checks, with the server's sessions, that it reaches the same server. Left while that check ran, the editor still saved the address once it passed, changing the address every account on the server uses though the user had backed out. Back now abandons the edit there too.
2026-10-09 04:10:21 +05:30
cogwheel
7d1d28103c Follow a redirect a route probe's server makes on its own address
A route probe took any redirect for a route not answering. An address saved as http:// whose server upgrades it to HTTPS on the same host, or a server that adds a trailing slash, was then skipped, though the API client follows those redirects and the route works; with the address in use gone, the resolver kept to it instead of moving there. The probe now follows a redirect the API client would -- on the same host and port, or up to HTTPS there -- a few times at most, and still takes a sign-in page it lands on, or a redirect anywhere else, for not answering.
2026-10-09 04:10:21 +05:30
cogwheel
3524ce891d Stop an address save once its editor has closed
Saving an address checks it first. Left while that check ran, the editor still went on to save, which works through the page's context: it read the gone page's localizations and container, failed, and the failure was swallowed. The editor now stops there, on both the direct and the proxy path.
2026-10-09 04:10:21 +05:30
cogwheel
849c47a802 Keep a proxy sign-in's cookie out of an address save's error
Saving an address checked through a proxy sign-in could fail -- a Keychain error, the server no longer saved -- and the error reached the page's general handler, which redacts only the headers from before the sign-in. The captured cookie and token were not among them. The save's error is now shown here, with what the sign-in captured redacted, as a failed check of the address already is.
2026-10-09 04:10:21 +05:30