- Active Mermaid controls draw onSecondaryContainer on
secondaryContainer instead of onSurface, which nearly vanished on
Claude dark's near-white secondary.
- inversePrimary is the palette primary with lightness adjusted to reach
4.5:1 on the inverse surface.
- The badge contrast test also covers the folder page surface.
- Error color reaches 4.5:1 on every palette, adjusting lightness to keep
the preset's hue; non-red presets still fall back to a red.
- Status on-colors (snackbar text) pick a readable palette text color;
banner status icons reach 3:1 against the banner, keeping their hue.
- Error container text uses the page text color.
- Sidebar tint legibility is checked at the badge's actual fill, and
badge counts use full-opacity sidebar text.
- Terminal cards on the plain sidebar page use a grouped fill that never
matches that page.
- Server setup's back button returns to chat when an Apple, Direct, or
Hermes backend already works; first-time setup still goes to the
backend chooser.
- The popup menu's icon color reaches the Flutter fallbacks.
- Integration test drives the app router through the native-sheet
connect flow and a completed sign-in back to chat.
The native settings sheet's Connect to Open WebUI row pushed the connect
flow over /chat. go_router keeps the base location for pushed routes, so
the redirect still saw /chat after login and never left the Sign in
page. Go to the flow instead, as the chooser and profile page do.
Toolbar icons rendered three different ways under Liquid Glass. They now
all color the SF Symbol with the exact theme text color, and the sidebar
glass group rebuilds when brightness changes.
Settings screens take palette-derived grouped page and card colors on
iOS and Android, shared with the native UIKit sheets. Material container
roles, snackbars, form fields, and native sheet titles and bar buttons
now come from the palette instead of system or seed colors.
Tweakcn presets now match upstream exactly. The app keeps error text
legible where a preset's destructive color is meant only as a fill, and
softens saturated sidebar accents under sidebar text.
Keep structured output authoritative across text updates, recovery, cancellation,
outlet filters and reload. Match the upstream response reducer and tool states,
and stop older canonical widget rebuilds from replacing the current stream.
Verify against upstream TypeScript and captured DeepSeek traffic, then exercise
multi-tool generation and reopening in the production iOS simulator app.
OpenRouter-generated PNGs carry a C2PA manifest whose icon is an SVG
within the first kilobyte, so searching the header for `<svg` sent them
to the SVG renderer and they showed "Failed to decode image" (#768).
Only a document that starts with markup (after a byte order mark and
whitespace) can be SVG now.
A call placed while the previous one was still hanging up could lose its
route: the old teardown cleared the communication device and put the
phone back in normal mode under the new call. The replacement also saved
the old call's communication mode as the state to restore, so the phone
stayed in call mode after it ended.
The coordinator configuring a call now claims the Android route and
inherits the pre-call state from any coordinator still holding it, and a
teardown stops restoring as soon as it no longer owns the route.
Also send a cumulative snapshot through the full path when its suffix
completes a tag the previous snapshot left open, such as a split
`</details`, which that path strips from the plain text.
- Tail cursor: a delta with no backtick or escapable character extends
the line's rendering as is, and fenced lines never re-render, so an
unmatched backtick or a long code line no longer costs work per delta
proportional to the line. The rare re-render is capped, and the
cursor keeps only rendered lengths.
- Snapshots: an entity split at the boundary (`&quo` + `t;`) takes the
full path; any raw delta ends the snapshot basis, since the splitter
can change state without emitting; a snapshot that closes one
reasoning block and opens another restarts the timer. Reasoning
timing and refresh pacing use an injectable clock in tests.
- Semantic blocks: a block nested in a generic <details> wrapper stays
escaped, and a fence left open before Open WebUI text that carries a
block is closed.
- iOS speaker toggle: a change counts as applied only when the route read
back matches the request.
Tool calls showed up as a wall of raw `<details type="tool_calls">`
text (#677). Three separate causes:
- Pipes (for example the Anthropic manifold pipe) put complete semantic
<details> blocks in the text of an output message item, and Conduit
escaped them. Open WebUI's own renderer shows them as sections, so
SemanticTextBlock.openWebUI now passes complete, well-formed
reasoning/tool_calls/code_interpreter blocks through; anything inside
code or malformed stays escaped. Plain SemanticTextBlock keeps escaping
everything (#549), and Direct connections use it.
- LaTeX extraction ran inside tool-call attributes, where escaped JSON
looks like `\[...\]`. Its placeholder split the opening tag. Attributes
are now skipped.
- package:markdown is quadratic on long unbroken runs (about 4 s for
10 KB). LongAlphanumericRunSyntax and a guarded autolink syntax keep
the same output in linear time.
Also closes a fence a text block leaves open before a details block,
drops a semantic opening tag cut off at the end of a saved message, and
shows tool results made of text parts as their joined text, as Open
WebUI's getToolResultText does.
Voice calls could stay on the earpiece, the speaker button only toggled
one way, and on iOS every sound after a call came out of the earpiece
(#716).
- Teardown restores the session's idle configuration and clears the iOS
speaker override, so read-aloud, server TTS and the notes player play
normally once a call ends.
- Turning the speaker off is read back like turning it on, and every
configure pass reports the route the platform actually took, so the
button follows what people hear and never sticks.
- iOS adds .defaultToSpeaker while the loudspeaker is chosen, so CallKit
and route changes cannot drop it; it stays off otherwise (#643).
- On Android 12+, device TTS plays from the engine's own process, which
does not keep Conduit's call route alive. A silent voice-communication
track in Conduit's process runs while device TTS speaks in a call.
- Siri send actions, the Android assistant, and widget camera/photos wait
briefly for Direct profiles, auth, or model discovery to finish loading
instead of dropping the request or skipping the fresh chat.
- Destroy the language-switch recognizer on every exit that does not hand
it to the caller, including cancellation.
- Let shares, the home widget, app shortcuts, Siri, and the Android
assistant open chat for Direct- or Hermes-only installs (#760, #765).
- Keep the original file name for shared attachments (#763).
- Hide folders when no Open WebUI account is signed in (#764).
- Listen on the recognizer that ran the language-support check so
automatic STT no longer fails with ERROR_SERVER_DISCONNECTED (#762).
- Re-apply real window insets when a keyboard inset survives a hidden
IME after backgrounding mid-animation (#758).
The pill was positioned by its trailing edge only, so a large text scale
grew it past the sidebar's leading edge (-98pt at 3x on a 390pt phone).
Bound it to the sidebar width, keep it pinned to the trailing corner,
and ellipsize the label.
A DM with no channel name is titled by its participants, but the list
filter only matched channel.name, so searching a visible participant hid
the DM. Match the channel name or the displayed title.
- Screen readers can activate the new-chat pill, composer toggle rows,
and attach tiles: each labelled node now carries the tap action.
- A reply after a versioned row always shows its model label, since that
row may display a historical version from another model.
- The new-chat pill sits on the lists' own bottom inset, and the lists
reserve its height so the last rows scroll clear of it.
- Grouped native sheets pick page and card colors by luminance, so
palettes that already tint the page (Catppuccin, Tangerine) keep it.
- Attach tiles widen for long localized labels and grow with text size;
the hidden checkmark's width no longer conflicts with the stack.
- iOS form-row labels wrap and always leave room for the value field.
- DM titles fall back to "Unknown" when neither name nor members exist.
- The What's new title wraps instead of clipping; the ARB note describes
the full version.
- Remove ModelListTile.surfaceColor, which no longer had an effect.
Chat thread: tail-less borderless user bubble, model label only when the
responding model changes, outline reply actions with the overflow inline.
Composer: softer expanded corner; the attach panel uses compact tiles and
flat checkmark rows (native iOS and Flutter), and the native panel now
matches the keyboard height it replaces.
Sidebar: floating new-chat button on chat tabs, regular-weight titles,
plain pin glyph, neutral count pills.
Settings and forms: single-line rows with plain monochrome glyphs, outline
SF Symbols in native sheets, grouped light-mode sheet palette, centered
profile header, outlined form fields, natural-width iOS row labels.
Fixes: DM channel header showed "#" with no name; symmetric padding in
the model and channel title pills; What's new tile shows the full version.
- Ask once when deleting on iOS 26. ThemedDialogs.confirm already presents
a native confirmation sheet; the bridge stacked a second alert on top of
its destructive action.
- Treat a zero Apple context size as unreported. The simulator reports 0,
which showed as "Context limit: 0M" and was passed on as context_length.
- Pluralize the note editor's word and character counts ("1 word").
- Give VoiceOver real labels: native sheet rows read their titles instead
of SF Symbol names, the sidebar toolbar button reads "Sidebar" instead of
"drag", the profile avatar is one labelled control, and Direct settings
switches read their titles instead of "0"/"1".
- Show the provider's explanation for rejected Direct requests, e.g.
"HTTP 403: Free tier users do not have access to this model."
- Localize the default Direct connection name, and let the API key field's
Done key dismiss the keyboard.
The mobile app's non-UI code moves into pure-Dart packages in a pub
workspace, so a second front-end can share it. The app keeps working
from the repo root with no extra flags, and behaves as before.
- packages/conduit_core: auth, database and sync, services, providers
and the feature logic (direct connections, Hermes, MCP, terminal,
workspace), with platform code behind ports implemented in lib/platform.
- packages/conduit_markdown: the markdown, citation, reasoning,
tool-call and speech-text parsing, web-safe.
- packages/conduit_theme: the tweakcn palette registry as plain ARGB,
which lib/shared/theme wraps in Color.
Most files are moves; the tools that did the mechanical part
(split_into_parts, split_class_into_mixins, migrate_core_tests and the
listing helpers) are in tool/, and tool/check_package_boundaries.dart
keeps the packages free of Flutter. Fixes made along the way are
included, as are missing translations for existing mobile strings.
CI gains a workflow for the app, each package on its own, the web-safe
markdown package under dart2js, and the ARB checks.
* Keep completed replies idle after a refresh (#744)
Open WebUI merges server-side finalizations into the stored message
(`{**existing, **update}`), so a backend that writes `done: true` leaves
an earlier client `isStreaming: true` in the same message. The parser
copied that flag verbatim, so adopting the snapshot during a background
refresh restored a finished reply as streaming and put the Stop button
back on an idle chat.
Let the completion marker win, reading `done` from the message or its
history entry. Messages with `done: false` or no marker keep streaming.
* Paint the overflow cue only when a row overflows (#745)
HorizontalOverflowFade painted its trailing gradient unconditionally and
in surfaceBackground, which is #000000 in the dark theme, over the
composer shell's surfaceContainerHighest (#141414). With a single quick
pill nothing overflows, so the cue was a 28pt black rectangle sitting
next to the mic. The attachments strip and the composer overflow panel
had the same latent artifact, and no solid colour is correct under the
native glass composer.
Gate the cue on real trailing overflow and mask the row's own pixels
with BlendMode.dstIn, so it needs no surface colour at all. Toggling the
mask reparents the row, so hold the child with a GlobalKey; otherwise
reaching the trailing edge rebuilds the row and snaps it back to its
start.
* Stop using private UIAlertController APIs for native dropdowns (#747)
Every AdaptiveSingleChoiceTrigger row crashed on iOS 18 — Provider type,
Authentication, and any other native selectable list. The dropdown
bridge relied on two patterns UIKit does not support on
UIAlertController, whose view hierarchy is documented as private and
unmodifiable:
- NativeSheetTheme.apply(to:) read `view` before presentation and then
walked the alert's internals, setting colours and calling reloadData
on UIKit-owned table and collection views.
- The action icon went through private KVC, setValue(_:forKey: "image"),
which raises NSUnknownKeyException on a runtime that does not
implement the key. That is an Objective-C exception Swift cannot
catch, so it terminates the app.
Alerts now take only overrideUserInterfaceStyle before presentation and
their accent from applyAccent(to:) once UIKit has loaded the view for
the presentation it belongs to. The image setter is probed with
instancesRespond(to: setImage:) and falls back to the "✓ " title prefix
the other native action sheets already use. Setting the presentation
controller's delegate also moves after present(_:animated:), so nothing
builds a presentation controller outside the presenting trait
environment.
* Address review feedback on the dropdown, parser, and overflow cue
- Drop the private `UIAlertAction` image path entirely. Probing the
undocumented setter avoided the crash, but every option now carries
its selection marker in the title, so the action sheet uses no private
API at all. Options lose their decorative SF Symbols, which UIKit
never offered a public way to draw.
- Treat either completion marker as authoritative. `??` only falls
through on null, so a stale `done: false` in the flat message list
masked `done: true` in the history entry it projects. That envelope is
reachable: a payload with `history.messages` but no `currentId` builds
its message list from `chat.messages` while still reading fields from
history.
- Follow the scroll axis when masking. A right-to-left or reversed row
resolves to `AxisDirection.left`, so its clipped content sits at the
physical left and the gradient has to start from the right.
* Build the iOS 27 SDK path and keep Private Cloud Compute from trapping
`flutter build ios` failed on every Xcode 26 / iOS 27 SDK toolchain:
PccBridge referenced iOS-27-only `LanguageModelError` from inside an
`@available(iOS 26.0, *)` extension, and PccSdk.xcconfig defines
CONDUIT_PCC_SDK for any SDK major above 26. The on-device catch now
checks availability before reading the new type and keeps the iOS 26
mapping, which the older runtime still throws, so one binary reports
specific errors on both.
With the path compiled, Private Cloud Compute turned out to kill the
app: FoundationModels does not report a missing
com.apple.developer.private-cloud-compute entitlement through
`availability` — it reports `.available` and then traps on the first
request, which no `catch` can recover. Apple grants that entitlement on
request, so a build without it must not offer the model. The entitlement
is a property of the signed binary, so a compile-time flag gates the
live calls and the bridge reports the model unsupported until
PccSdk.xcconfig switches it on.
* Ship Private Cloud Compute with its entitlement
The signing identity carries com.apple.developer.private-cloud-compute,
so claim it in Runner.entitlements and switch the bridge's live path on.
Apple Private Cloud Compute now appears in the model picker and answers.
The flag stays the kill switch: FoundationModels reports `.available`
and then traps on the first request when the process is not entitled, so
clearing CONDUIT_PCC_ENTITLEMENT_FLAG is what keeps the model hidden if
the grant is ever lost.
* Give Apple's models Apple's own mark
Apple On-Device and Apple Private Cloud Compute fell through to the
generic avatar: a lettered plate in the pickers and Conduit's sparkle in
the assistant header. They now carry `apple.intelligence`, Apple's own
symbol for the Foundation Models they run.
UIKit draws the glyph. SF Symbols may be rendered by the system but not
redistributed, and the Apple logo is not ours to use, so nothing is
bundled: `resolveModelIconUrlForModel` returns a `symbol:` url next to
the existing `asset:` one, a small bridge rasterizes the symbol on
demand, and the result is cached per name, point size, and pixel ratio
so a list of avatars never waits on the platform twice. The native model
sheet gets the symbol name instead, since UIKit already draws its rows.
Provenance comes from the trusted direct binding, never from a
server-supplied id or metadata field: an Open WebUI server controls every
field of a model response and must not be able to borrow Apple's mark.
Anything the platform cannot draw — Android, or a system without the
symbol — keeps Conduit's sparkle on the same plate.
* Ignore a glyph that arrives for a symbol the avatar left
The scale-only guard let an in-flight request settle onto the avatar
after it moved to another symbol or size: reusing the widget resets the
requested scale, the new request sets it back to the same value, and the
older completion then passed the check and painted the wrong mark until
its replacement arrived. The completion now has to match the symbol and
point size it was made for.
Bump the max supported server version to 0.11.4 and move the
openwebui-src reference to the v0.11.4 tag.
Open WebUI 0.11.4 patches the user `ui` settings map per key: an
omitted key keeps its old value and only an explicit null resets it.
Conduit cleared the system prompt and the default model by removing
the key, which now silently no-ops. Send an explicit null instead.
Older servers store the null literally, which the reader already
treats as unset.
Drift audit of v0.11.3..v0.11.4 found no other consumed contract
changes. Verified the clear against live 0.11.3 and 0.11.4 servers.
Native voice sheet was missing the barge-in toggle that the Android
Audio settings already expose. Add the toggle to
buildNativeAudioSheetParts and handle voice-barge-in control changes
by calling setVoiceBargeInEnabled.
Fixes#735
* Keep chat scrollbar thumb stable and inset it from the composer
Estimate unbuilt row extents from heights recorded while rows were
mounted instead of the built-row average, so a long response no longer
makes the thumb jump. Give the platform scrollbar toolbar and composer
insets while rows keep the real window padding.
* Address review: median fallback, extent invalidation, safe-area padding
Estimate never-measured rows from the median known height so one tall
response cannot move the estimate as short rows mount. Drop remembered
extents when the viewport width or text scale changes. Keep horizontal
safe-area padding on the scrollbar track.
* Forget row extents when row content or builder changes
* Forget row extents when horizontal padding changes
* Estimate unmeasured chat rows from their text length
Right after a chat opens only the rows near the bottom are measured, so
a long latest response made the median guess huge for every older row
and the thumb jumped as short rows mounted. The chat page now supplies a
per-row estimate from message text length, which depends on nothing but
the row itself.
* Scale row extent estimates by the text scale factor
* Test the row estimate against a row that is never built
* Remember measured row heights and estimate by row kind
Measured row heights now go into a process-wide LRU keyed by message id,
content length, viewport width, and text scale, so a chat scrolled before
reopens with exact extents. The text-length estimate gains flat terms for
user bubbles, attachments, images, and follow-ups, mirroring paseo's
per-kind estimates.
* Key remembered row extents on a full layout signature
Content hash plus follow-up, attachment, file, code-execution, source,
and version counts, so a same-length edit or new follow-ups never reuse
a stale measured height.
* Include streaming, status, and archived state in the extent key
* Fix attachments, STT, TTS, citations, proxy re-auth, and Hermes probe
- #729: put resolved attachment files on the in-memory user message so the
completion runner sends top-level `files`
- #721: Android device STT falls back to any installed recognizer with
EXTRA_PREFER_OFFLINE when no system on-device recognizer exists
- #715: add "Show citation page titles" setting for inline citation badges
- #709: stop previous TTS playback before assigning the new message id, no
terminal error before a successful device fallback, footer watches state
- #707: show "Transcribing..." in the composer while server STT runs
- #698/#690: detect proxy redirects on the session endpoint and add an
in-place "Sign in through proxy again" action on the connection issue page
- #683: probe Hermes desktop gateway with a throwaway service during
onboarding instead of throwing; log probe failures
* Address review: gate Hermes probe writer, VAD transcribing state, TTS log
- Persist native token rotations from a draft probe only when the draft is
the saved gateway, so testing another draft cannot overwrite or clear the
saved credentials
- Expose VoiceInputService.transcribing so the composer shows
"Transcribing..." for VAD-driven stops as well as manual ones
- Log the server TTS failure before falling back to device TTS
* Await in-flight transcription before disposing the notifier
* Track every in-flight transcription for the transcribing notifier
* Collapse raw reasoning tags client-side and stop re-escaping stored content
Fixes#728.
Raw <think>-style tags reached the plain-text append paths verbatim whenever
the server did not convert them (SSE fallback, pipes, tag-emitting backends),
so reasoning streamed inline and only collapsed once a final snapshot arrived.
A StreamingReasoningTagSplitter now routes every model-text delta into the
existing reasoning-details path, holding back partial tags across chunks.
Stored assistant content that carries Conduit's own rendered <details> was
escaped a second time when merged with server output items, showing literal
< entities; the longer escaped text also shifted the offsets tool and
reasoning blocks were spliced back at, splitting words. Unescape that content
before merging.
* Address review: attributed tags, event order, code-aware unescape
- Match attributed XML opening tags (<think source="x">) with the same
grammar the server and ReasoningParser use; hold an unterminated
attributed tag back, capped at 256 characters.
- Flush held-back partial tag text before tool status tiles and explicit
reasoning deltas so interleaved events keep stream order.
- Decode stored once-rendered content with unescapeRenderedAnswerText,
which skips fenced, indented, and inline code plus autolinks, so stored
code keeps literal entities.
* Address review: share the code-region walk and reset tag state on snapshots
- _escapeText and unescapeRenderedAnswerText now run the same
_transformAnswerText walk over parser-confirmed fences, indented code,
multiline code spans, and autolinks, so decoding cannot touch code the
escaper left alone.
- Server content snapshots (chat:message, replace, chat:completion content,
JSON completion, recovery adoption) reset the raw reasoning tag splitter so
later deltas do not inherit a held-back fragment or an open block.
* Collapse raw reasoning tags inside cumulative content snapshots
The server's non-streaming handler never converts reasoning tags, and pipes
that emit chat:message/replace stream their own cumulative text, so raw
<think> blocks reached replaceVisibleAssistantSnapshot verbatim. Run the
snapshot through the tag splitter and render reasoning details in place,
keeping answer text verbatim and an unterminated block as still thinking.
* Collapse empty and just-opened reasoning tags in snapshots
An empty <think></think> or a freshly opened <think> yields no reasoning
text event, so the snapshot gate returned the raw tags. Gate on any tag
event or an open block instead; drop a completed block without text like
the server does.
* Continue an unterminated snapshot reasoning block across later deltas
When a cumulative snapshot ends inside an open <think> block, re-arm the
tag splitter with the snapshot and seed the reasoning accumulators so a
following delta extends and closes that block instead of starting a
second one or rendering the closing tag as text.
* Hold tag fragments split at a snapshot boundary
Render a content snapshot from the stream splitter's events without
flushing, so a fragment such as <thi or </thi stays held for the next
delta or the terminal flush instead of being shown verbatim or leaking
into the reasoning body.
* Keep a held closing-tag fragment across interleaved events
Flushing held text before a tool tile or explicit reasoning delta is only
right outside a reasoning block. Inside one the fragment is a possible
closer; flushing it as reasoning text stopped the later remainder from
closing the block and swallowed the answer into it.
* Upgrade all packages, add notification settings link and Ollama thinking replay
- flutter pub upgrade --major-versions: go_router 18, flutter_secure_storage 11,
openai_dart 8, permission_handler 13, file_picker 13, freezed 4.0.1, riverpod
4.0.x, plus 130 transitive bumps.
- flutter_secure_storage 11 removed sharedPreferencesName; pass the same name as
storageNamespace so the plugin's LegacyNamespaceKeyRecovery keeps existing
Android data readable.
- file_picker 13 returns List<PlatformFile>; update the three call sites.
- Pin objective_c 9.6.0: 9.6.1's build hook references Architecture.arm64e,
which no published code_assets defines, breaking native-assets builds.
- Notification settings: add a row that opens the OS notification settings via
flutter_local_notifications 22.3's openAppNotificationSettings().
- Ollama: persist raw reasoning for completed direct assistants and replay it as
the `thinking` field on later turns (ollama_dart 2.6), gated on the same
terminal direct provenance as raw content.
* Use a dedicated message when system notification settings cannot open
* Support Open WebUI shared folders (#710)
Fetch GET /api/v1/folders/shared alongside owned folders in the pull seam and
merge both into the folders table (shared entries tagged with owner name and
permission). List shared-folder chats from /folders/{id}/shared/chats, keep
another user's chats out of the local sync store, and render them read-only
(composer replaced, context menus hidden). Persist the chat owner id in the
chats table (schema v10) so the read-only gate survives DB-first opens.
Write-grant shared folders keep the folder-page composer for new chats.
* Address review: fail folder pull on shared-route error, gate owner-only actions
- getFoldersRaw now propagates a shared-route failure instead of returning an
owned-only list; replaceServerFolders purges rows absent from its input, so
a partial list would drop cached shared folders (404 still yields owned).
- _loadConversation fails closed: skip local materialization when the owner
is known but the signed-in user id is still null.
- Folder page hides Edit Folder / System Prompt for shared folders and the
new-chat button for read grants; chat page hides overflow conversation
actions on another user's chat.
- Migration test asserts chats.user_id exists after upgrade.
* Gate native folder toolbar actions for shared folders
Mirror the Flutter toolbar guard on the iOS native action list: no Edit
Folder / System Prompt menu for shared folders and no new-chat action for
read grants.
* Shared folders: page chat lists, owner view, read-only message actions, write-grant targets
- Walk /folders/{id}/shared/chats?page=N until has_more is false (bounded)
instead of the unpaged 60-chat cap.
- Owned folders union local rows with chats other users created under a
write grant, matching the web sidebar; shared folders stay network-only.
- Another user's chat hides regenerate/delete/edit and follow-up prompts
(assistant + user message widgets take a readOnly flag).
- Move-to-folder offers only writable folders; a write-grant shared folder
gains a 'New folder' action; own folders never re-parent under a shared one.
* Fail closed in isReadOnlySharedConversation while the user id hydrates
* Hide follow-up suggestions and block their fallback send in read-only chats
* Drawer: show a Shared badge on shared folders instead of owner name and eye icon
* Fix ten reported issues and vendor Hermes Agent as an API reference
Closes#693, #695, #699, #700, #701, #703, #708, #712, #716, #723.
- Hermes (#693, #699): load transcripts over REST even when session.resume
fails, page at 200 messages and shrink on oversized responses, and show
distinct errors for too-large, not-found, and RPC failures. Add the
hermes-src submodule (NousResearch/hermes-agent v2026.9.11) as the RPC and
REST contract reference.
- Server version banner (#695): dismissible, remembered per server and version.
- Model selector (#700): overflow fade and selection tint use the card surface.
- OpenRouter (#701): skip Batch API-only ":batch" model variants.
- Persisted chat content (#703): store plain output text instead of rendered
<details> markup when structured output exists, matching the web client.
- Direct connections (#708): probes report their real failure instead of a
generic message, the Open WebUI logout fence no longer blocks Direct setup,
and 401/403 under the api-key header mode hints at Bearer auth.
- Reasoning effort (#712): per-server, per-model user picks override the
model's configured default and are sent explicitly in the request.
- Voice calls (#716): stop the record plugin from clearing the Android
communication device and verify the loudspeaker route before reporting it.
- Voice mode (#723): finalize speech on the responseDone marker.
- Add the five new strings to every locale.
* Address review: keep incomplete-clear block, pass effort on sends, keep all dismissals
- Direct profiles stay hidden and unusable after an incomplete full app-data
clear, in memory and across restarts, until the logout fence is cleared by a
completed cleanup or a new authenticated session. Ordinary failed sign-outs
still do not block Direct setup.
- The primary Open WebUI send path now passes the resolved per-model reasoning
effort like regeneration and queued completions already did.
- Server version warning dismissals are stored as a bounded set so dismissing
one server no longer forgets another.
* Address review: release rejected speaker device, speak settled text on responseDone
- When Android accepts the built-in speaker but the read-back disagrees,
clear that selection before the legacy setSpeakerphoneOn fallback, which is
ignored while a communication device stays selected.
- Voice mode takes the final text from the message once responseDone is set;
the streaming content provider can lag behind the flushed message.
* Address review: await the incomplete-clear restart marker before returning
The marker is written through the app-data-clear barrier with a checked write
and awaited by the clear coordinator, so a process death right after an
incomplete clear cannot reload the surviving Direct profiles on restart.
* Address review: arm the restart fence before the wipe and propagate marker failures
The incomplete-clear marker is now armed before any data is wiped, disarmed
only when the clear completes or yields ownership, re-asserted on an
incomplete outcome, and a failed checked write aborts the clear instead of
being swallowed. The in-memory block is unchanged.
* Do not raise an error banner for response.incomplete
Open WebUI forwards the provider's `response.incomplete` event over the
socket but treats it as a no-op: the output is kept and the turn still ends
with a normal `done`. Raising an error on that event flashed "The response
stopped before it was complete" under the answer until `done` cleared it.
`response.failed` still surfaces the provider error.
* Fix raw <details> blocks whose opening tag spans multiple lines
Some models (GLM 5.3 Flash / DeepSeek-series via Open WebUI) emit a
tool-call <details> block whose quoted attribute values contain real
newlines (legal in HTML). The per-line DetailsBlockSyntax parser never
matches such a spanning opening tag, so the whole block renders as raw
text and persists across app restarts.
Two repairs in ConduitMarkdownPreprocessor.normalize, both operating
outside code spans/fences:
- join an unterminated <details opening tag across newlines until its
quote-balanced closing > (capped at 256 KB)
- escape raw < / > inside quoted attribute values of single-line tags
so the tag regex reaches the real end-of-tag and keeps the result
attribute
The transforms run before DetailsBlockSyntax parses, fixing live
rendering, streaming, and retroactively healing saved messages.
Closes#677
* Address review: join before escaping, case-insensitive tag matching
- Reorder normalize passes so _joinSpanningDetailsOpenTag runs before
_escapeRawAngleBracketsInTag. Escaping first only matched tags already
on a single line, so a spanning tag with a raw <br> in a quoted value
would still truncate at the <br>'s > when joined.
- Case-insensitive <details marker matching in the join scan to match
DetailsBlockSyntax behavior.
- Regression tests: spanning tag + raw <br> combined case, mixed-case
spanning tag.
Fixes CodeRabbit major finding and Greptile P1/P2 findings.
* Fix multi-angle truncation: single quote-aware scan for tag repair
Greptile P1: the escaper consumed _detailsOpenTagSingleLine matches,
which stop at the first raw >, so with multiple raw <br> in quoted
values only the first was escaped and the parser still truncated the
attribute list after the second.
Replace the regex-match + escape-then-join sequence with one quote-aware
state-machine scan (_normalizeDetailsOpenTags) that, per <details
opening tag (case-insensitive), locates the true end-of-tag as the first
> outside quotes, folds newlines for spanning tags, and escapes every
raw < > inside quoted values in the same pass.
New regression test covers result="a<br>b<br>c" other="x".
* Fix single-quoted attributes and tilde-fence masking (review round 3)
- Quote-aware scans (end-of-tag finder + angle escaper) now track the
active delimiter for both ' and " values; a quote only opens a value
immediately after '=', so prose apostrophes cannot corrupt quote state
(CodeRabbit + Greptile P1; the naive always-toggle fix would break on
It's in surrounding prose)
- _codeSpanOrFence now also masks tilde fences (~~~), so literal
<details> examples inside them are no longer rewritten before parsing;
also covers _replaceMatchesOutsideCode (sanitize/clipboard paths)
- 8 regression tests: single-quoted escape, single-quoted spanning join,
nested delimiters both ways, prose-apostrophe guard, 3 tilde-fence
masking cases, post-fence normalization
Verified in pure Dart (SDK 3.13.3, markdown 7.3.1 = repo lock): full
test file 54/54 green; DetailsBlockSyntax compile-through and streaming
incremental-normalize checks pass.
* Round 4: whitespace-tolerant quotes, scan budget, CommonMark tilde fences
- A quote now opens a value after '=' with optional HTML whitespace
between them (result = "a<b>c", result= '...'), in both the tag-end
scan and the angle escaper (CodeRabbit Major + Greptile P1)
- Aggregate scan budget across all <details markers in one normalize()
call; on exhaustion the remainder is copied verbatim, bounding
synchronous work for pathological inputs
- Tilde-fence mask tightened to CommonMark: 0-3 leading spaces, info
string, closing run >= opening run via backreference
- _attachedToolCallDetailsOpen lookahead bounded to one line; measured
normalize() on 8000 unterminated markers: 9235ms -> 33ms (linear)
- 6 new regressions incl. the perf-bound check
Verified: full repo test file 60/60 green (pure Dart, SDK 3.13.3,
markdown 7.3.1); DetailsBlockSyntax compile-through + streaming
incremental-normalize harness 42/42 green
* fix: address native toolbar, Apple gating, streaming cadence, and wakelock regressions
- Hide Apple On-Device and Private Cloud Compute rows on non-iOS platforms
and answer status probes with unsupported instead of hitting the channel
- Render iOS 26 toolbar actions as separate glass controls; the merged
CNGlassButtonGroup pinches adjacent buttons into a dumbbell
- Use a fixed 100 ms streaming render cadence instead of scaling with
response length
- Keep the screen awake while an assistant response is streaming
- Show a loading state and error feedback when opening the model selector,
and present it without waiting on reasoning-effort hydration
- Fix a stale checks call in the cherry-picked tool-call details tests
Fixes#694Fixes#692Fixes#688Fixes#686Fixes#681Fixes#677
* fix: follow process-owned generations for the wakelock and tighten details parsing
- Count Direct, Hermes, and outbox generations so the screen stays awake
after the user switches chats mid-response
- Accept CommonMark tilde fences closed by a longer run
- Match only exact <details> tags and a whitespace-delimited type attribute
* perf: restore masked code spans in a single pass
Replace the per-span replaceAll loop with one replaceAllMapped over the
placeholder pattern so normalize() stays linear in the number of code spans.
* test: assert code spans are restored in order
* fix: read provider reasoning keys on the Open WebUI SSE path
Open WebUI relays provider chunks unchanged, so gpt-oss via OpenRouter-style
gateways streams reasoning as delta.reasoning and Ollama as delta.thinking.
Conduit only read reasoning_content, so the pending reasoning block never
appeared mid-stream and 'Thought for' only showed after the server refresh.
Accept the same three keys the upstream middleware does.
* feat: apply Open WebUI response:completion events while streaming
Open WebUI 0.11 streams socket-bound completions token by token as
Responses-style events (response.reasoning_text.delta,
response.output_text.delta, output_item.added/done, response.completed)
and only sends the cumulative output snapshot on the terminal frame. Conduit
had no handler for the response:completion event, so reasoning and text
reached the message only through recovery polling or the final frame.
Port the web client's applyResponseStreamEvent reducer, rebuild the output
list locally, and project it through the existing structured-output path.
Treat a reasoning item followed by another item as finished, matching
upstream, since the per-token stream never sends the status flip.
* test: deep-copy the reducer immutability snapshot
* test: type the deep-copied snapshot for deepEquals
* fix: mirror Open WebUI reasoning header timing and duration
Upstream's Collapsible only reads "Thought for…" once a reasoning block is
done AND carries a duration; a done block without one keeps reading
"Thinking…". Conduit rendered every closed block as "Thought for less than
a second" because both transports injected duration="0".
Record when reasoning starts and stamp the elapsed whole seconds when the
answer begins, the same int(ended_at - started_at) the server computes, on
both the SSE path and the response:completion reducer. Expose whether a
duration attribute exists on the compiled details data and apply the
upstream header rule in the details widget.
* fix: show Responses-API reasoning while it streams and keep its measured duration
Providers that speak the OpenAI Responses API (Kimi and GPT models on Azure)
stream typed response.* events over SSE, which the parser discarded, and add
the reasoning item long before its summary text arrives. Both left the
reasoning block invisible until the answer, and the server never records
how long such a model thought, so the label always fell back to zero.
- Route response.* SSE frames through the shared response stream reducer
- Render a pending reasoning item even before it has text, as upstream does
- Preserve client-measured timing when output_item.done replaces the item
and when cumulative snapshots arrive; derive a duration from ended_at and
the message timestamp for persisted items that carry only ended_at
- Keep the local render when a server copy of the same answer lacks the
reasoning duration, and never invent a zero duration
- Label a finished block with no timing 'Thoughts' instead of a fake time
* fix: address review on Responses reasoning handling
- Drop the timestamp-derived reasoning duration; it included queueing,
tool, and provider startup delays. Only measured or server-provided
timing is shown; otherwise the block reads 'Thoughts'
- Apply the output carried by response.failed and response.incomplete and
surface response.failed as a message error
- Require exact attribute-name boundaries in the semantic details patterns
so data-type and data-duration never match
- Use the seconds from a legacy 'Thought for N seconds' summary instead of
the compiler's zero
* fix: surface incomplete responses and harden semantic attribute matching
- Report response.incomplete as a message error carrying the server's
reason so a cut-off answer is not presented as a clean success
- Keep the local output list when a terminal event carries an empty output;
it is not authoritative over items that already streamed
- Require whitespace before type/duration in the semantic details patterns
so any qualified attribute name (data-, data:, data.) is rejected
* fix: parse semantic details openers with a quote-aware attribute scanner
Regexes over raw tag text let a quoted value such as title=' type="reasoning"'
classify an ordinary <details> as a semantic wrapper, which the strip and
truncate helpers would then remove from visible content. Parse the opening
tag's attributes properly instead, and read attributes correctly past quoted
values that contain '>'.
* fix: advance past parsed details openers when scanning
Restarting one character after an opener rescanned the quoted attribute
values the parser had already consumed, so a literal <details ...> inside a
value was treated as a real opener.
---------
Co-authored-by: foegra <foegra@users.noreply.github.com>
Update the Open WebUI reference to v0.11.3, raise the supported-version ceiling after the drift audit, and keep the unsupported-server warning on the new-chat greeting only.