Commit graph

778 commits

Author SHA1 Message Date
cogwheel
0fbb7a148a
fix: enable Apple models for existing backends (#671) 2026-08-26 22:44:17 +05:30
cogwheel
695b5493aa
fix(hermes): allow continued dashboard chats (#670) 2026-08-26 22:41:51 +05:30
cogwheel
f80f79352e
feat(openwebui): support v0.11.1 (#667) 2026-08-26 18:41:29 +05:30
cogwheel
d8bed975c8 Harden voice call audio handoff and response recovery 2026-08-26 15:36:56 +05:30
cogwheel
3b4817138c Preserve voice mode and handle completion errors 2026-08-25 20:55:18 +05:30
cogwheel
8aa3826458
Fix citation links, note checklists, and Hermes PDF input (#665)
* Fix citations, note checklists, and Hermes PDFs

* Address Hermes attachment review feedback

* Wait for Hermes PDF capability discovery

* Handle mixed Hermes attachments during discovery

* Reject replaying Hermes PDF attachments
2026-08-25 19:47:30 +05:30
cogwheel
3b9a327710
fix(chat): add skill mention autocomplete (#664)
Some checks failed
L10n / l10n (push) Has been cancelled
* fix(chat): add skill mention autocomplete

* fix(chat): handle unavailable skill suggestions

* fix(chat): invalidate edited mentions

* fix(chat): preserve mentions after delimiter deletion

* fix(chat): preserve mentions across delimiter edits
2026-08-25 15:29:57 +05:30
Christian Ulstrup
0f0dc1cc15
fix(hermes): recover completed runs after idle timeout (#663)
Co-authored-by: Christian Ulstrup <1883384+culstrup@users.noreply.github.com>
2026-08-25 13:23:08 +05:30
cogwheel
3e7e3c0aaa
Fix voice call speech and default new calls to the speakerphone (#650)
* fix(voice): keep reasoning blocks out of speech and stop skipping the answer

Voice mode speaks the assistant content as it streams, and since the
streaming merge started preserving local <details> wrappers, TTS only
stripped blocks that were already closed. An open reasoning or tool_calls
wrapper was read aloud, and once its </details> landed the block vanished
from the split, shifting every later chunk left past the monotonic chunk
cursor. The answer itself was then never spoken.

TTS now sanitizes through the shared semantic_details helpers and treats an
unterminated semantic opener as a hard stop, so the speakable text only ever
grows. The streaming feed re-anchors on the text of the last chunk it handed
to playback instead of trusting a bare index.

* feat(voice): start calls on the speakerphone when nothing is plugged in

A voice call runs the audio session in communication mode, so a phone with
no accessory attached routes playback to the earpiece. Held like a call it
is fine, held like a speakerphone it is barely audible.

The coordinator now scans the attached audio devices at call start and
engages the loudspeaker only when it finds nothing to play through. A failed
scan leaves the route alone, since blaring an answer over someone's headset
is worse than a quiet earpiece. It keeps watching for the rest of the call,
so pulling headphones out moves playback to the speaker and connecting a
headset takes it back off. Pressing the speaker button ends the automatic
switching for that call.

* fix(voice): serialize audio route and TTS feed changes

Route changes are several platform calls deep, so two of them running at
once interleaved and the slower one got the last word. A headset pulled
out during a reroute could leave the call on the earpiece, and an
automatic reroute could land after the user pressed the speaker button or
after the call ended.

Queue route changes one at a time and re-read the state each one assumed
before it applies, so a stale reroute stands down instead of overwriting
a newer decision. Deactivation drains the queue before tearing the route
down.

Streaming TTS feeds had the same shape of problem: a second feed could
append its chunks in between the ones an earlier feed was still handing
to playback, so sentences could be spoken out of order. Chain the feeds.

* fix(voice): catch uppercase semantic details wrappers

The complete-block pattern in the TTS sanitizer already ignored case, but
the shared opener and block patterns did not, so an uppercase
`<DETAILS TYPE="reasoning">` that had not closed yet was read aloud.
Tag and attribute names are case-insensitive in HTML and this is model
output, so match either case everywhere.

* fix(voice): ignore a default route scan that came back too late

The accessory scan at the start of a call can still be out when the user
presses the speaker button or hangs up. Its result would then resubscribe
to device changes and push the call onto the loudspeaker after teardown.
Stamp the scan with a call generation and drop the answer if the call it
belongs to is over or the user has since chosen a route.

* fix(voice): only treat routes a call can actually use as accessories

A voice call runs the session in communication mode, which cannot route
to A2DP or AirPlay, so a device offering only those is not somewhere the
call can play and should not suppress the loudspeaker default. Headsets
that also speak HFP still register as bluetoothSco.

Accessory detection also required only a matching device type, so a
plugged-in microphone counted as somewhere to play. Require an output.

Manual speaker toggles are rejected once teardown has started, where
honouring them would put communication mode back after the route was
handed back.

* fix(voice): anchor the speech cursor on spoken text, not one chunk

The cursor was re-anchored by searching the fresh split for the text of
the last chunk handed to playback. When an answer repeats a sentence, that
search can match the wrong copy and skip everything in between, and when
the server rewrites the answer the anchor disappears and the cursor stops
moving at all.

Carry the text already spoken instead and walk the new split against it.
The match is positional, so repeated sentences are unambiguous, and a
rewrite resumes at the point the two versions stop agreeing rather than
replaying or skipping.

* fix(tts): strip nested details blocks before speaking

A non-greedy `</details>` match stops at the first close tag, so a
wrapper nested inside another left the outer block's tail in the text
handed to TTS: `<details><details>x</details>secret</details>` spoke
`secret`. Replace the pattern with a depth-counting walk shared from
`semantic_details.dart`, which also keeps the existing behaviour of
withholding the tail of a semantic wrapper that has not closed yet.

* fix(voice): re-check the route owner before publishing a reroute

The speaker button can be pressed, or the call can end, while the
platform calls behind an automatic reroute are still in flight.
Publishing afterwards left the speaker control showing a route nobody
chose.

* fix(tts): keep scanning inside an unclosed ordinary details

An ordinary `<details>` still waiting for its close tag kept the whole
suffix, so a reasoning wrapper opened inside it reached TTS. Keep the
tag and carry on from just past it: nested complete blocks are stripped
and a nested open wrapper still truncates.

* fix(voice): let a newer device event own the published route

A device event that lands while an earlier reroute is mid-flight claims
_accessoryAttached before queueing its own work. The earlier reroute
then published its now-stale value, so the speaker control showed the
old route until the newer operation caught up.

* fix(tts): speak on the call route during a voice call

Android routes voice-communication and media output separately. Device
TTS spoke as USAGE_MEDIA while the call held focus as
USAGE_VOICE_COMMUNICATION in MODE_IN_COMMUNICATION, so it ignored the
call's speakerphone choice and went silent once the app was backgrounded
mid-call.

The engine now gets voice-communication audio attributes (and the
matching legacy stream param) while a call is up, and goes back to the
media stream for read-aloud.

* fix(voice): only report a route change the platform took

A refused reroute still published to speakerphoneRouteChanges and stuck
in _speakerphoneEnabled, so the speaker button pointed at a route nobody
was hearing and the enabled == _speakerphoneEnabled guard dropped the
next identical device event as already handled.

The route calls now answer whether they landed, the flag rolls back when
they did not, and neither the device-change path nor the user toggle
publishes without a successful move.

* fix(tts): hand the engine back to read aloud after a call

Disposing the voice-mode provider mid-call left TtsManager in voice-call
mode, so a later read-aloud spoke on the call route: earpiece, at call
volume. Provider disposal now clears the flag, and so does reset().

reset() also stops rewinding _sessionCounter. A feed queued on the
previous chain only checks the active session's id, so reusing an id let
it append its old text to the next session, and the id names the server
chunk temp dir and background lease that the same stale work tears
down.

* fix(voice): light the speaker button only on a confirmed route

The default route is picked before the audio session exists, so the move
to the loudspeaker happens on the next configureFor* pass. The snapshot
was set from the pick, not the move, so a refused reroute still lit the
speaker button while the call stayed on the earpiece.

applyDefaultSpeakerphoneRoute now only sets the preference. The
configure pass reports whether the platform took it and announces it on
speakerphoneRouteChanges, which is already the one path the snapshot
follows. A refused move puts the flag back so the next device event can
try again.

* fix(voice): queue configure-pass routing behind the other reroutes

The listening, speaking and barge-in passes made the same platform route
calls as the button and device-change reroutes, off the same
_speakerphoneEnabled flag, without going through _routeSerial. A pass
that started before a headset was pulled out could finish after the
reroute and put the call back on the route it had just left.

They now queue with everything else, so each one reads the route
decision when it runs rather than when it was scheduled, and the default
route is only announced while the loudspeaker is still the current
choice.

* test(tts): assert with package:checks

The rest of the suite asserts with package:checks; this file was the odd
one out on expect(), which a review flagged while reading the new voice
call tests.

* fix(tts): stop a rewrite replaying the sentences it left alone

The cursor resumes at the point the old and new splits stop agreeing, so
a server that revises one sentence in the middle of an answer queues
everything after it a second time. The listener hears the tail twice.

Skip forward through the text playback already heard, in order, and only
speak the chunks that are not in it. Scanning forward rather than
searching the whole string keeps a sentence that genuinely repeats later
in the answer spoken once for each time it appears.

* fix(tts): hold the session open until the last feed lands

finishStreaming marks the response finalized before its own feed reaches
the serial chain. If the engine finishes the chunk it is speaking in that
window, playback sees a finalized session with nothing left queued, ends
it, and the feed carrying the rest of the answer finds no session to
append to. The answer stops mid-sentence.

Count the feeds still queued or running and treat playback as waiting
while any remain.

* fix(voice): keep automatic routing after a refused speaker press

The speaker button claimed the route before the platform had taken the
move, and kept the claim even when the move was refused. Nothing had
changed, but the call ignored every accessory event from then on: plug a
headset in afterwards and it stayed on the old route.

Count presses that are queued or on the wire so an automatic reroute
behind one still stands down, and only make the override permanent once
the platform reports the move applied.

* fix(voice): release the call route when stopping tts throws

The engine is shared with read-aloud, and the hand-back sat after the
stop call in the same teardown step. A stop that threw skipped it and
left read-aloud speaking on the call route. Give it its own step.

* fix(voice): hand the route back when the coordinator is disposed first

Riverpod gives no order to provider disposal, so the coordinator can go
before the controller that would have called deactivate. Its dispose only
cancelled the device watch, leaving the phone in communication mode with
the call's route still selected.

Disposal now runs the same teardown as hanging up, and that teardown is
safe to run twice. Session activation also joins the route queue, so a
configure pass already in flight cannot reactivate the session after
teardown drained the queue, and a default-route scan started during a
teardown is rejected instead of riding its generation bump through.

* fix(voice): keep the route shut once the coordinator is disposed

Teardown lifts the shutter again so the next call can route. A disposed
coordinator has no next call, so a deactivate arriving behind disposal
left the speaker button able to put the phone back into communication
mode after the coordinator was gone.

* fix(voice): retry an accessory move the platform refused

The transition is claimed in the accessory snapshot before the route
operation starts, so the burst of events a single headset sends collapses
into one move. A refused move left the claim standing, and since the
hardware never moved there is no fresh transition to come: the next
notification about the same headset matched the snapshot and was dropped
as old news, stranding the call on its previous route.

* fix(voice): keep a stale configure pass and overlapping teardowns apart

Session configuration is awaited before the activation step joins the
route queue, so a teardown that both starts and finishes inside that
window puts the shutter back up and the queued step reactivates a call
that is over. Each pass now carries the call generation it was started
for and stands down when it no longer matches.

Overlapping teardowns had the same shape: hanging up and disposal each
ran their own finally, and whichever finished first lifted the shutter
while the other was still restoring the platform route. The shutter now
waits for the last one out.

* fix(tts): keep heard sentences the held-back chunk hides

Mid stream the trailing chunk stays put until finalization, so a rewrite
arriving then leaves the sentences behind it out of the returned spoken
text. Finalization has nothing left to match them against and queues them
again. Carry the unconsumed heard history along instead.

* fix(voice): retry a default route the platform refused

A bare phone repeats the same device list rather than announcing a
transition, so the snapshot the refused default was picked from makes the
next notification look like old news and the call stays on the earpiece.

* fix(tts): stop a stale feed holding the next session open

A feed already inside the engine's speak call outlives the session that
queued it. Its count carried over to the next session, which then waited
for a chunk that was never coming and never finished speaking.

* fix(voice): drop a speaker press the teardown overtakes

A press queued before the call ended still reached the platform behind the
teardown, and answered the caller that the route had moved.

* fix(tts): hold server completion for a feed still on its way

finishStreaming marks the response finalized before its own feed reaches
the chain. The device path already waits that window out; the server path
ended the session there instead and dropped the rest of the answer.

* fix(tts): keep a stale fetch off the next session's bookkeeping

A fetch that outlives its session cleared the marker the next session had
put down for the same chunk index.

* fix(tts): keep a dead session's fetch error to itself

A failed fetch from a session that is over was reported as an error
against whatever is playing now.
2026-08-25 11:50:36 +05:30
cogwheel
1371c352d5 Improve scroll performance and sync efficiency 2026-08-25 00:09:19 +05:30
cogwheel
2ee24a5817 Cache markdown extents and defer code highlighting 2026-08-23 20:14:48 +05:30
cogwheel
f4d76fd411
Improve chat transcript scrolling performance (#652)
* perf: smooth chat transcript scrolling

* fix: address scrolling review feedback
2026-08-23 14:59:56 +05:30
cogwheel
3891dde3f1
Add Apple on-device support and improve direct chats (#651)
Some checks failed
L10n / l10n (push) Has been cancelled
* feat: add Apple model backends and context compaction

* Add Apple on-device and PCC direct providers

* dev: show all onboarding backends in debug builds

* Fix short response pin-to-top settlement

* Polish Direct Connections context settings

* Fix onboarding selection row spacing

* Address PR review feedback

* Harden context and native request bounds

* Strengthen compaction regression coverage

* Close final context and image review gaps

* Validate structured array bounds
2026-08-22 23:47:51 +05:30
cogwheel
f656f8d5d6 chore(release-notes): update to version 4.1.0 with new features and improvements
- Introduced Hermes Bot Mode with Desktop Gateway integration.
- Redesigned lists and panels for better usability.
- Fixed various long-standing bugs.
- Enhanced streaming performance and LaTeX rendering.
- Added Polish language support to the language list.
2026-08-20 22:32:07 +05:30
Kamil Maciąg
a376e617ce
feat(l10n): add Polish localization (#617)
* feat(l10n): add Polish localization

- add complete Polish Flutter localization (app_pl.arb) mirroring app_en.arb
- add pl to preferred-supported-locales, language selector, and native language sheets
- add Polish iOS native resources (pl.lproj) and Xcode project registration
- add Polish release notes and language count documentation
- add focused tests for locale resolution, resource completeness, and Polish plurals

* fix(l10n): address Polish localization review feedback

- localize the 'polish' language name per catalog (Polish/Polnisch/Polonais/Poľština/...)
- label the credentials auth mode 'E-mail i hasło' instead of 'Hasło'
- pluralize the active-schedule count in hermesSchedulesSummary (1 aktywny / 2 aktywne / 5 aktywnych)
- use feminine 'Poprzednia' for the previous-version action label
- replace literal 'szybkie pigułki' with 'szybkie akcje'
- reword server-downgrade instruction, shared-chat notice, and direct-connection delete message
- use the official product name 'Open Web UI' in user-facing values
- make the placeholder parity test robust to Dart identity-based set/list equality

* fix(l10n): address remaining Polish review feedback

- use a username-inclusive label 'Dane logowania' for the credentials auth mode
- reword the prompt version delete confirmation to 'Ta wersja zostanie usunięta'
- keep channelMembers plural forms unchanged (with digits, '2 członków' is correct, not '2 członkowie')

Assisted-by: Open WebUI

* fix(l10n): rebase Polish catalog onto current English keys

Polish was complete against an older template; fill the 134 strings added on main (Hermes jobs/sessions, voice barge-in, sidebar) so locale validation can pass after merge.

---------

Co-authored-by: Kamil Maciąg <6450912+Dragonk@users.noreply.github.com>
Co-authored-by: cogwheel <172976095+cogwheel0@users.noreply.github.com>
2026-08-20 22:12:00 +05:30
cogwheel
f79ccf9c51 Use system fonts and preserve streaming state 2026-08-20 22:03:38 +05:30
cogwheel
929e5ca2bd fix: close remaining post-merge review gaps
Some checks are pending
L10n / l10n (push) Waiting to run
2026-08-20 18:19:18 +05:30
cogwheel
9e9de70f3c fix: address post-merge review findings 2026-08-20 17:50:30 +05:30
cogwheel
6a55f2d85d fix: resolve post-merge regressions 2026-08-20 17:31:55 +05:30
cogwheel
fdc32758b5 fix: hermes bot mode 2026-08-20 16:19:49 +05:30
cogwheel
bd7f4a87d1 fix: ui 2026-08-20 13:29:51 +05:30
cogwheel
34bd29a160 fix: ui consistencies 2026-08-19 23:51:19 +05:30
cogwheel
cea5b10e80
Hermes Bot Mode roster, grouped assistant responses, and a turn-state fix (#644)
Some checks failed
L10n / l10n (push) Has been cancelled
* fix: repair Hermes turn state keyed by stored session id

Every Hermes prompt after the first failed with "Hermes run failed.",
recoverable only by hitting retry.

`session.create` does not return a `running` field (verified against a live
0.20.1 gateway: absent both top-level and under `info`). That makes
`_applyAuthoritativeRunning` park the session at `unsupportedGateway` under
its STORED id. The first turn still works because `_freshSessionIds` waives
the safety gate, and that flag is then cleared.

The global `session.info` listener only ever updated the RUNTIME id, so the
stale stored-id entry was never repaired. From the second turn on, the gate
in `_runtimeStreamDesktopResponse` sees `unsupportedGateway` and throws a
StateError, which `_friendlyError` renders as the generic "Hermes run
failed.". Retry worked because `session.resume` does return `running`.

Update both keys from the one listener every session-state change routes
through, rather than patching the second-turn path alone.

Tests drive the real service against a gateway stub replying without
`running`; both fail with `unsupportedGateway` if the fix is reverted.

* feat: show Hermes Bot Mode roster in the sidebar

Adds a collapsible "Bots" section at the top of the Hermes sidebar tab.
Upstream, a bot IS a Hermes profile, so this is a UI over primitives that
already exist rather than a new concept:

- profiles.list  -> roster rows + the bot_mode_protocol capability flag
- profiles.get_asset -> avatars
- session.resume / session.create -> the canonical "Bot Chat" forever-chat

The section is gated on `bot_mode_protocol: true`, which Hermes only reports
from 0.20.3 (v2026.8.16.2). Older gateways and the Responses backend return
an empty roster and render nothing, leaving today's UI untouched.

Bot Mode needs per-session profile scoping, so RPC default params became a
fallback that callers can override (`{...defaults, ...params}`); no existing
call site passes `profile`, so this is inert until a bot chat is opened.
Session-scoped profiles are tracked by stored id and applied to
session.resume, transcript pages, and DELETE.

Profile names are validated with `HermesConfig.isValidDesktopProfile` before
becoming an RPC `profile`, and pinned chat ids go through
`validateHermesOpaqueIdentifier`, so a hostile roster row cannot redirect a
request. The `Bot Chat` title lookup key is evicted after resolution so two
bots cannot alias one another's binding.

Also backfills every locale ARB: the 47 pre-existing untranslated Hermes
keys plus the new one, taking all 12 locales to zero untranslated messages.

* feat: group consecutive assistant responses under one header

A single Hermes turn lands as several assistant messages, which repeated the
same avatar and model name down the transcript. Consecutive assistant rows
resolving to the same model now present as one grouped response, with only
the first row carrying the identity header.

Decided in the layout metadata that already computes per-row model
presentation, as a single forward pass carrying the open group:

- a user turn closes the group, so the next response always shows its header
- archived variants are skipped, not treated as breaks; they render as
  zero-size placeholders and would otherwise re-show a header mid-response
- a blank/unknown model name never groups, since absent identity is not
  evidence of a shared speaker

Fixing this in shared layout rather than special-casing the Hermes transport
means OpenWebUI and Direct multi-message turns get it too, with one code
path. `showModelHeader` defaults to true, so every other call site is
unchanged.

* fix: address review feedback

- Copy the bot roster before sorting. The Bot-Mode-off path returns
  `const []`, which sorts in place with an UnsupportedError that the
  provider's catch masked as "no bots" — the path every pre-0.20.3 gateway
  takes. Extracted as `sortHermesBotsByRecency` so it is directly testable.

- Scope `session.branch` and `session.close` to the session's profile, and
  copy the profile mapping onto the forked stored id. Forking or deleting a
  Bot Chat otherwise targeted the connection's configured profile.

- Keep the model header when a selected historical version resolves to a
  different model than the group header announced. Suppression now requires
  the active identity to match what was already shown above.

- Use "ensemble d'outils" in hermesToolsetUpdateFailed for consistency with
  the adjacent hermesNoToolsets French wording.

* fix: scope bot-chat turn transcript reads to the bot profile

`_matchingPromptMarkers` fetched `/api/sessions/{stored}/messages` without a
profile scope. The REST layer fills an absent `profile` with the connection's
configured profile (`putIfAbsent` in hermes_desktop_auth_rest.dart), so for a
bot chat this read resolved against a DIFFERENT profile's conversation that
happens to share the session id.

That read is the prompt-delivery baseline behind steer, queue, and ambiguous
prompt-submit recovery: a wrong-profile transcript can both leak another
profile's message content into the comparison and mis-decide whether the
user's prompt was accepted, so a retry can double-send or silently drop.

Every other desktop RPC in the turn path carries a runtime session id, which
the gateway already bound to its profile at resume; this REST call is the one
place that takes a stored id and therefore has to carry the scope itself.

The regression test asserts the query param on every bot transcript read.
Reverting the scope makes it fail with the connection profile ('default')
instead of the bot's ('researcher').

* fix: bind every bot-chat runtime request to the bot profile

The RPC transport injects the connection's configured profile into every
frame as a default. `_sessionScope` returned an EMPTY map for sessions with
no recorded profile, so "no scope" did not mean "let the server decide" — it
meant the frame travelled naming the CONNECTION's profile. Any handler that
consults `params['profile']` then resolved against the wrong profile.

The concrete leak was `_applySessionOptions`, which runs on the first turn of
every chat: `config.get` read reasoning effort from, and `config.set` with
`scope: global` PERSISTED it to, the connection profile's config.yaml rather
than the bot's — a cross-profile config write driven by opening a Bot Chat.

- `_sessionScope` now always resolves a profile explicitly, falling back to
  the connection's, so a scope can never be silently inherited.
- Added `_runtimeScope`, which maps a runtime id back to its stored id, for
  the helpers that only carry a runtime id (slash/command dispatch,
  image/pdf/file attach + detach, approval and decision responses).
- Scoped the remaining turn-path RPCs: prompt.submit, session.interrupt,
  session.steer, and the queued submit.

The regression test asserts that every frame carrying a session_id names the
bot's profile. Reverting any single scope fails it with 'default'.

* fix: scope MCP and rename RPCs to the bot profile

Completes the profile-scoping sweep with the paths outside the turn runtime:

- `reload.mcp` iterates EVERY bound session, which includes bot chats, and
  sent each under the connection profile.
- `mcp.setup.respond` answered a bot session's setup request unscoped.
- `session.title` (rename) renamed against the connection profile.

A programmatic audit now reports zero `_rpc.request` calls carrying a
session_id without a scope across the turn runtime, live runtime,
administration, and bots files. The one remaining unscoped REST call is
`GET /api/sessions` (the sidebar list), which is connection-wide by design
and correctly inherits the configured profile.

Test coverage extended to rename, approval response, decision response, and
reload.mcp; it now asserts the profile on 8 distinct RPC methods. Reverting
the reload.mcp scope fails it with 'default'.

* fix: persist bot profile with pending decisions across restarts

`_sessionProfiles` is in-memory only, but pending decisions are durable. On
restart the map is empty while a bot chat's approval/clarification is still
outstanding, so `_runtimeScope` fell back to the connection profile and the
restored response targeted a same-id decision in the WRONG profile. Nothing
re-seeds the map on that path either: answering a restored decision does not
require reopening the bot from the roster.

- `HermesPendingDesktopDecision` now carries the owning profile, written by
  all four upsert sites and validated on read with the same rule the RPC
  layer applies, so a tampered store cannot redirect a request.
- An update that omits the profile keeps the previously recorded one, so a
  refresh cannot silently drop a bot chat back to the connection profile.
- `pendingDecisionsForSession` re-seeds `_sessionProfiles` from the durable
  record before resuming, restoring the scope for every later call.

Also adds the mcp.setup.respond coverage requested in review: a DENIED setup
skips the install/enable/authorize branches and goes straight to the respond
RPC. Reverting either that scope or reload.mcp's fails with 'default'.

* fix: preserve the bot profile when a pending decision is rebound

`rebindSession` rebuilt each record field by field but omitted `profile`,
silently dropping it. That runs on every resume whose stored id changes
(compaction lineage), so a bot chat's pending decision lost its scope and the
response would target the connection profile — the same class of bug the
persisted profile was added to prevent.

Copy `record.profile` through the rebind. Tests cover both the rebind and a
profile-less refresh; reverting either preservation fails with null.
2026-08-19 01:33:12 +05:30
cogwheel
43182d7282
fix: resolve reported rendering, voice, terminal, and connection bugs (#643)
Some checks are pending
L10n / l10n (push) Waiting to run
* fix: resolve reported rendering, voice, terminal, and connection bugs

LaTeX (#642, #533): render complex MathJax SVGs through the compatible
renderer so underbrace/boxed/pmatrix no longer glitch or leak `amp`, and
center block equations horizontally.

Fonts (#501): rebuild the bundled Geist faces, which mapped lowercase
Greek omega to the uppercase glyph.

Hermes (#637): connection tests now exercise authenticated capability and
toolset discovery instead of the unauthenticated health endpoint, so a
server that only answers /health no longer reports "connected".

Voice (#632): request microphone permission before starting the Android
foreground service, which crashed when permission had never been granted.

Voice (#630): stop local STT while TTS plays so the model no longer hears
itself. Barge-in is off by default and opt-in from Audio settings, and the
voice overlay gains a speakerphone toggle.

Transport (#629): drop the plaintext-HTTP restriction for Hermes and
direct connections. Tailscale/Headscale CGNAT addresses and other private
networks were rejected outright. Custom headers and the self-signed
certificate toggle are now shared by all backends via a single Hermes
transport configurator.

Terminal (#626): download now opens the native save dialog instead of the
share sheet.

Terminal (#608): route the `terminal:display_file` event into the Terminal
files panel rather than dropping it in the chat event handler.

Auth (#606): drive reverse-proxy login through the real WebView identity
and cookie store so Authelia and similar providers finish sign-in.

Also fixes pre-existing test failures on main: three assertions still
expected the single-newline block join replaced in #641, and six expected
the "Delete connection" button label renamed to "Delete" in #628.

* fix: route Android speakerphone and address review feedback

Speakerphone on Android: setSpeakerphoneOn is deprecated and no-ops on
Android 12+ once a communication device is selected, which the voice
session does for Bluetooth SCO. The toggle rendered but never changed the
route. Select the built-in speaker as the communication device instead,
keeping the legacy call as the pre-31 fallback, and tear down the active
SCO route before re-routing so a headset cannot keep owning playback.

Voice: treat TtsError as a terminal assistant-speech event. With barge-in
disabled the recognizer is stopped for playback and only TtsCompleted
restarted it, so a failed TTS request left voice mode active with no way
to accept the next utterance.

Hermes: cancel active runs when certificate trust changes. A trust-only
edit rebuilt the transport while old cancellation tokens stayed live.

Terminal: sanitize the download file name, which comes from the server's
Content-Disposition header, before passing it to the save dialog.

Hermes settings: use the existing headerName/headerValue/addHeader/
removeHeader localizations instead of hardcoded English.

Adds coverage for the speakerphone toggle, TTS-error recovery, and
HttpClient forwarding through the desktop RPC channel factory.

* fix: fall back for dot-only download file names

`.` and `..` survive character sanitization but name a directory rather
than a file, so writing them throws instead of producing a download. Use
the timestamp fallback for those, and cover the sanitizer directly.

Windows reserved device names are not handled: this app ships only iOS and
Android targets.
2026-08-18 15:25:53 +05:30
cogwheel
ea82ee164e
perf: chat scrolling and markdown rendering (#640)
* perf: stop shell rebuilds cascading into rows and cut per-flush markdown cost

Scrolling:
- Give the timeline slivers a delegate with a real shouldRebuild keyed on
  rowBuilder/entries identity and centerIndex, so ChatPage setState (drag
  start, keyboard insets, composer resize, pin transitions) rebuilds only
  the shell, not every mounted row.
- Memoize the transcript window, ChatTimelineRenderModel, and rowBuilder by
  identity in ChatPage; the fresh window list per build also defeated the
  stable-layout cache's identity fast path.
- Replace full MediaQuery.of dependencies with scoped paddingOf/sizeOf.
- Raise the streaming cacheExtent from 120px to 600px; the small extent
  evicted rows that then remounted with a synchronous markdown compile.

Markdown:
- Memoize buildMarkdownDisplayParts by compiled-document identity; it
  re-derived one sub-document per block with deep compares on every build.
- Add identical() fast paths to CompiledMarkdownDocument and
  PreparedMarkdownText equality; compare rope segments instead of
  materializing both sides.
- Cache per-message structure-signature fragments by ChatMessage identity
  instead of rebuilding O(messages x versions) strings per emission.
- Gate incremental preparation and the reference-definition strip on one
  shared line-anchored predicate; a bare "]:" substring no longer forces
  full re-preparation of the whole message every flush.
- Make the streaming split's unsafe-line detection fence-aware and cap
  freezing at the first raw HTML block instead of keeping the entire
  document mutable; reuse the fence-close helper instead of compiling a
  RegExp per fenced block.
- Skip the four LaTeX extraction regex passes when content has no $ or
  backslash; memoize the error-heuristic content scans in the assistant
  footer; gate profiler map allocations to profiling builds; memoize the
  code-block line split and render >50k-char code plain.

* fix: detect reference definitions past raw HTML blocks in streaming split

The single-pass unsafe-line scan returned at the first raw HTML line, so a
reference definition appearing after that block was never seen and blocks
containing its links could be frozen with the link unresolved. Scan the whole
region for definitions and only record the first raw HTML offset as the
freeze cap.

* fix: distrust fence state after a raw HTML block starts in unsafe-line scan

Backtick lines inside raw HTML are content, not fences; an odd count left the
fence tracker 'inside' a fence and skipped a later real reference definition,
freezing earlier blocks with unresolved links. After the first raw HTML start,
check every line for a definition regardless of fence state — at worst more
conservative than the whole-document check this replaced.

* fix: long-response truncation at completion and follow-ups never arriving

Truncation: the streamed buffer is never periodically folded into message
state, so /api/chat/completed was built from a stale prefix of the response
and the server's echo of that payload truncated the full content when merged
back (worst on the HTTP/SSE transport, which reached completion without any
terminal flush). Flush the buffer before building the completed payload, and
guard the three unguarded overwrite paths (completed echo, replay-gap
authoritative recovery, cumulative chat:completion content snapshots) so a
strict prefix of already-streamed content is never adopted.

Follow-ups: the server emits chat:message:follow_ups only after
chat:completion {done:true}, and the per-stream socket subscription is
disposed synchronously by that done event, so the streaming handler for
follow-ups was unreachable. The passive conversation subscription is the
surviving delivery path; apply the pushed payload directly to the target
message there instead of relying on a debounced refetch that races the
server's own persistence of the suggestions.

* fix: address review feedback on follow-ups delivery and splitter

- Fall through to the debounced refetch when a pushed follow-ups payload
  targets a message id not present in local state.
- Split the follow-ups envelope parser into a private implementation with a
  visibleForTesting wrapper, matching file convention.
- Detect reference-definition labels containing escaped brackets in the
  streaming splitter's unsafe-line scan.
- Use package:checks in the new follow-ups parser test.

* fix: deferred structured-output projections dropping response content

The structured-output projector defers full re-projections geometrically
(next re-render only at 2x the last projected length) and permanently
disables its plain-append fast path once the text contains a backtick, so
the visible content can trail the logical content by up to half the
response. Two consequences fixed here:

- A plain content delta arriving after a deferred projection appended onto
  the short stale render and flipped structuredOutputIsLatest, which also
  made the terminal projector finalize bail — permanently dropping the
  deferred middle of the response on screen and in the persisted echo.
  appendVisibleAssistantChunk now materializes the full projection (new
  StructuredOutputStreamingProjector.syncProjectionToLatest) before
  switching the content basis to plain appends.
- handleCompletionDone flushed the buffer before building the completed
  payload but did not finalize the projector first, so the payload (and the
  outlet-filter echo derived from it) could carry the stale short render.

Also fold the un-flushed streaming buffer into state in _cancelMessageStream
(conversation switch / message deletion mid-stream discarded the entire
un-synced tail), skipped during provider dispose where state is untouchable.

* fix: harden remaining content-adoption paths against divergent server bodies

Local and server renders of the same turn wrap reasoning/tool sections in
semantic <details> blocks with different attributes (locally injected
duration=\"0\" vs the server's real duration), so every raw startsWith/length
guard was dead on reasoning turns. Content comparisons now strip rendered
semantic details and compare answer bodies:

- applyServerContent adopts only when the server's answer body is at least
  as long as the local one; a snapshot whose raw length grew (long reasoning
  block) while the answer shrank no longer replaces a complete local answer.
- _shouldPreserveLocalAssistantContent (all snapshot adoptions including the
  reopened-stream reconcile and its buffer rebase) compares stripped bodies.
- The completed-echo, replay-gap recovery, and cumulative content-snapshot
  guards compare stripped bodies, and an echo differing only by details
  wrappers is a no-op instead of an adoption.

Also:
- Hermes: a terminal/recovered output that is a strict prefix of the
  streamed text no longer replaces it (lagging aggregate or incomplete
  recovery would truncate delivered content).
- The local turn echo payload now carries output, files, embeds, usage,
  sources, statusHistory, followUps, and error: the sync outbox rebuilds the
  chat blob from these rows and the server merge replaces message objects
  wholesale, so omitted fields were wiped from the server copy on push.
- A stale settled markdown refresh no longer leaves the preparation flag set
  when nothing newer is queued (indefinite loading skeleton).

* fix: address review findings on the unsafe-line scan and echo payload

- Remove the backslash overlap in the reference-definition label pattern;
  the overlapping alternation could backtrack exponentially on long
  malformed labels, on the UI isolate.
- Track <details> bodies opaquely (open/close depth) in the unsafe-line
  scan, mirroring the block scanner: an unmatched backtick line inside a
  details body no longer opens a phantom outer fence that hid later
  reference definitions.
- Persist codeExecutions in the local turn-echo payload alongside the other
  durable server-shape fields.

* fix: live-tail freeze/duplication, follow-up persistence, scroll-down jank

Live tail (regression from the projection-sync fix): syncProjectionToLatest
re-armed the projector's geometric backoff to 2x the full content length
while the plain-append transition disabled the append path, so subsequent
output snapshots all deferred and the visible tail froze for the rest of the
turn. The sync now preserves the backoff threshold. Same-frame handling now
also matches the upstream client contract (Chat.svelte): a frame carrying an
output snapshot supersedes its own choices delta / content field — Conduit
applied the delta first and the snapshot second, duplicating text the
snapshot already contained.

Follow-ups: pushed suggestions were applied to in-memory state only; the
turn echo had been persisted at completion before the event fired, so a
conversation switch reloaded the message without them. The passive handler
now re-persists the message row after applying the payload.

Scroll-down jank: three down-only per-frame costs while returning toward the
bottom — the bottom-anchor recompute re-armed a full layout-maintenance pass
(row-rect snapshot + pin geometry) on every metrics tick once anchored (now
only on anchored-state transitions); pin geometry re-measured three global
rects per frame mid-scroll (now skipped until motion settles once reported);
and UserScrollNotification(idle) was treated as drag end even though Flutter
publishes it at ballistic START, running mode flips and jump-to-latest
arming mid-fling (drag end now comes from ScrollEndNotification, which fires
at actual rest).

* fix: streamed word drops and quote/entity rendering defects

Quotes/entities:
- Answer text no longer escapes double quotes (element-mode escaping; tags
  are still neutralized). &quot; escaped into a context the markdown decoder
  skips — immediately after a backquote, or inside code via the streaming
  fragment path — surfaced literally on screen. Attribute-mode escaping
  stays for <details> attribute values.
- The plain streaming accumulator was seeded/refilled from the RENDERED
  (already-escaped) body on reopen/reasoning sync; the next full render
  escaped it a second time (&amp;quot; decoding once back to a visible
  &quot;). Plain-content derivation now strips semantic details AND
  unescapes entities.
- Clipboard copy and TTS decode presentation entities back to literal text.
  API replay deliberately does not (it cannot distinguish model-typed
  entities from presentation escaping, and the direct bridge has trusted
  raw replay for fidelity).

Missing words:
- Whitespace-only deltas were discarded on one transport (trim() guard),
  gluing words together and losing paragraph breaks.
- Whitespace-only semantic text blocks were dropped from full renders and
  the streaming append delta never re-emits the swallowed prefix — the
  blank line between a reasoning section and the answer vanished.
- Once a backtick/tilde disabled the projector's append path for the turn,
  geometric backoff left the visible tail up to 50% behind until
  completion; renders now use a bounded additive step when appends are
  unavailable.
- The SSE parser now mutes same-frame deltas only when the output snapshot
  parses into renderable blocks, matching the socket path — an output whose
  items all parse away no longer mutes the delta while rendering nothing.

* fix: address review feedback on plain-content whitespace and coverage

- The semantic-details strip in plain-content derivation now consumes only
  the wrapper's own trailing newline instead of \s* plus trim, preserving
  answer whitespace such as a leading indented code block's indentation.
- Regression tests: the additive re-projection schedule for code-bearing
  streams, and a non-renderable output snapshot not muting the same-frame
  delta.

* fix: match the details parser's exact close token in streaming scans

The streaming details trackers accepted '</details >' as a close while the
details parser recognizes only the literal '</details>'. A close lookalike
inside a streamed details body exited details tracking early, let a body
backtick open a phantom outer fence, and hid a valid reference definition
after the real close — freezing an earlier reference-style link unresolved.
The preparation engine's checkpoint scanner had the same loose pattern and
could split prepared content mid-block. Both now match the parser exactly,
with a regression test verified to fail against the loose pattern.

* fix: reconcile deferred snapshots at terminal finalize and audit lengths

finalizeStructuredOutputProjection bailed whenever a plain chunk was the
last content-affecting operation; if output snapshots after that chunk had
deferred under the re-projection threshold, the deferred tail was dropped
from the final content. The finalize now adopts the terminal render unless
the accumulated visible text is longer (matching upstream's output-replaces-
content contract while preserving delta-only hybrid streams).

The done-signal log now records message/rendered/plain lengths so a
truncation report can be pinpointed from a single log line: message shorter
than rendered points at a lost flush; rendered shorter than plain points at
an unrepaired deferred projection.

* fix: reconcile terminal projection when plain chunks ended the stream

finalizeStructuredOutputProjection bailed whenever a plain chunk was the
last content-affecting operation. The terminal snapshot render is
authoritative upstream (output replaces content wholesale in Chat.svelte);
adopt it unless the accumulated visible text is longer, preserving
delta-only hybrid streams.

The done-signal log now records message/rendered/plain lengths so a
truncation report can be pinpointed from one log line.

* fix: render output[] in poll recovery when persisted content is empty

OWUI 0.11 never persists a flat content string for a normal completion —
the durable body is the output[] item array, so a reasoning turn's raw
content is ''. pollServerForMessage ignored output[] entirely: whenever the
live socket missed the final frames (buffer caps on long reasoning
streams, reconnects), every recovery path polled the server, extracted an
empty string, adopted nothing, saw done=true, and finished the turn with
the partial local text — permanent tail truncation, reasoning models only.
Recovery now renders output[] with the same renderer the snapshot parser
uses when flat content is empty. Regression test verified to fail without
the fix.

* revert: drop the speculative terminal-finalize reconciliation

The longer-wins adoption added in 349bb708 was based on a wrong premise:
structuredOutputIsLatest is set back to true by every output snapshot,
applied or deferred, so the bail it targeted only holds when a plain chunk
was the very last content-affecting operation — where the accumulated
visible text is already the right terminal value (syncProjectionToLatest
keeps it complete). Its regression test passes with and without the change,
and adopting the terminal snapshot in that narrow case could drop
equal-length delta-only text. Restore the original bail, with a comment
explaining why it is correct.

* fix: keep the settle transition extent-neutral

Measured the streaming-to-settled swap (typing-indicator footer 16+28+4 vs
in-card action row 16+32): it is extent-neutral by design for plain,
reasoning, and completed-status turns. The residual jump came from status
rows whose updates never reported done — the settle filter emptied the list
and dropped the entire row (~30px shift on the bottom-anchored viewport),
also losing the only description of what the turn did. The last update now
stays visible (without the pending spinner) when filtering would otherwise
empty the row.

A new widget test measures both sides of the swap and asserts
extent-neutrality for the plain, scaled, versioned, pending-status, and
reasoning cases. Settle-only additions (sources row, a text-scaled version
chip) legitimately grow the card; an AnimatedSize wrapper was tried and
reverted — RenderAnimatedSize asserts when mutated during this tree's
layout pass.

* fix: count inline nested details opens like the parser; tighten projector test

The streaming split scanner counted details opens with the line-anchored
pattern, but the details parser counts complete opening tags anywhere in a
line. A nested inline open inside a streamed details body made the first
closing tag exit scanner details mode one level early, so a body backtick
poisoned fence state and hid later reference definitions from the fallback
scan. Depth counting now uses the parser's complete-tag pattern (still
crediting a line-leading partial tag so unterminated opens stay mutable);
block entry detection stays line-anchored, matching the parser.

Also assert the exact terminal projection content in the additive-schedule
projector test instead of suffix/length checks that a truncated result
could satisfy.

* fix: address thermo audit and outside-diff review findings

- Persist local-echo and direct/Hermes message payloads in the server
  shape: citation-shaped sources, snake_case code_executions via the
  shared converters, and null-stripped files. Client-model shapes broke
  the OpenWebUI web client's code-execution panels and citations for any
  chat synced from Conduit. The converters move from api_service.dart to
  core/utils/openwebui_message_payload.dart, and localEchoRowForMessage
  is now top-level with a test pinning payload completeness and shape.
- Bail out of syncProjectionToLatest when the projector never owned the
  visible basis (observe-only path): materializing the snapshot there
  shrank visible content that was deliberately kept as a superset.
- Copy user messages verbatim: the assistant clipboard sanitizer decoded
  entities the user actually typed and stripped definition-shaped lines.
- Route replaceVisibleAssistantContent's fallback through
  initialPlainStreamingContent so cumulative content frames cannot leak
  details wrappers into the plain accumulator and re-escape later.
- Stop treating definition-shaped lines inside a details body as
  reference definitions in the unsafe-line scan: the parser lifts details
  bodies into body_markdown compiled as its own document, so they cannot
  couple frozen and mutable segments (CodeRabbit outside-diff finding).
- Deduplicate the semantic-details strip and stale-prefix guards into
  core/utils/semantic_details.dart; five divergent regex copies and five
  hand-rolled prefix checks now share one definition.
- Extract the follow-ups socket-event parser to
  features/chat/utils/follow_ups_socket_event.dart and drop the
  ForTesting shims.
- Drop the dead streaming parameter on the chat cache-extent helper,
  hoist hot-loop regexes in the raw-boundary checkpoint scan, re-resolve
  the message index before the passive follow-ups re-persist.

* style: complete the dart format migration repo-wide

The Dart 3.13 formatter had been applied piecemeal to files this branch
touched, leaving the repo half-migrated and smearing mechanical churn
through functional diffs. One-shot migration of the remaining 214 files;
no semantic changes.

* fix: count details depth on the raw line, not the dedented candidate

The details parser counts open/close tags on the raw line regardless of
indentation inside a block, but both scanner depth-tracking sites routed
counting through the block-starter candidate, which is null for lines
indented four or more columns. An indented nested open was silently
dropped, so the first close exited scanner details mode a level early
and a body backtick opened a phantom fence that hid later reference
definitions. Depth now counts on the raw line (the partial-open carve-out
dedents before its anchored check so an indented incomplete tag still
holds the block open).

* fix: count only complete details tags; keep incomplete entry tags mutable

Crediting a partial line-leading <details as an open (added so an
unterminated streamed entry tag would not freeze) left scanner depth
permanently stale when a body contained a literal <details that never
completed: the parser counts only complete tags, so it closed the block
while the scanner stayed inside it and swallowed the document-level
reference definition after the block. Depth counting is now parser-exact
(complete tags only), and the unterminated-entry case is handled
structurally: an entry line whose tag has no closing > keeps the tail
mutable instead of faking a depth of one.
2026-08-18 01:20:40 +05:30
cogwheel
8d626d0f93
feat: add Hermes Desktop Gateway backend (#636)
Some checks failed
L10n / l10n (push) Has been cancelled
* feat: add Hermes Desktop Gateway backend

* fix: address Hermes Desktop review feedback

* fix: harden Hermes review edge cases

* fix: disambiguate restored decision IDs

* fix: apply remaining review feedback

* fix: restore partially written Hermes principal

* test: assert Hermes runtime rollback

* fix: add contrast to Hermes model avatar

* fix: adapt Hermes avatar to theme

* fix: surface Hermes Desktop models

* fix: show Hermes avatar in model suggestions

* fix: keep Hermes Stop enabled during local streams

* fix: correct Hermes model selectors

* fix: refresh Hermes model selector state

* fix: simplify Hermes agent model name
2026-08-16 19:51:36 +05:30
cogwheel
332f0c75bb
feat: unify app surfaces with native grouped design (#628)
Some checks failed
L10n / l10n (push) Has been cancelled
Squash-merge the reviewed native UI, sidebar, onboarding, direct connection, Hermes, workspace, theming, haptics, and consistency updates.
2026-08-15 18:37:46 +05:30
cogwheel
af7bb8a655
Migrate adaptive UI to native iOS glass (#622)
* Migrate adaptive UI to native iOS glass

* fix(sidebar): unify chat and folder gutters

The alignment regression persisted because nested folder rows combined their hierarchy depth with an additional fixed outer inset. Share the tile surface geometry, keep only the tree-depth offset, and lock the header, root-row, tint, and nested-row coordinates with widget tests.

* fix: harden adaptive iOS UI migration

* address greptile review feedback (greploop iteration 1)

* address review feedback (greploop iteration 2)

* address review feedback (greploop iteration 3)

* address review feedback (greploop iteration 4)

* address greptile review feedback (greploop iteration 1)

* address greptile review feedback (greploop iteration 2)

* address greptile review feedback (greploop iteration 3)

* refine iOS native chrome and composer
2026-08-09 12:05:01 +05:30
cogwheel
13692901ef fix: complete iOS system image paste 2026-08-05 20:20:30 +05:30
cogwheel
4b66d1a759 fix: restore image paste in iOS composer 2026-08-05 16:33:13 +05:30
cogwheel
47ac04c0d9
fix(auth): finish proxy login after iOS redirects (#615)
* fix(auth): finish proxy login after iOS redirects

* fix(auth): fence proxy capture to committed document

* fix(auth): centralize proxy document checks

* fix(auth): fall back to load-stop document commits

* fix(auth): reject stale document commit callbacks

* fix(auth): correlate proxy navigation callbacks

* fix(auth): fence history inspection results

* fix(auth): serialize proxy history updates

* fix(auth): contain proxy history drain errors

* fix(auth): track proxy history fragments
2026-08-05 10:24:53 +05:30
cogwheel
4b66a1c4e1
fix(notes): keep context-menu scrolling attached (#614) 2026-08-04 23:56:23 +05:30
cogwheel
f6ea3f6719
Fix reasoning effort persistence for unsupported models (#613)
* fix: gate reasoning effort by model support

Automatic previously omitted the params key, so OpenWebUI's shallow settings merge retained the old reasoning_effort value. Explicitly replace the params map when clearing and fail closed at the request boundary for models that do not advertise support.

* address reasoning effort review feedback

* fix: hydrate workspace reasoning effort details

The earlier fix trusted the OpenWebUI model catalog, but current OpenWebUI deliberately strips workspace model params from /api/models. Fetch and cache the selected workspace model detail so its configured custom effort wins over the user-level fallback and reaches the native selector.

* fix: avoid workspace effort hydration race

Do not fall back to the user-level effort while private workspace model params are still loading. Omitting the override during that window lets OpenWebUI apply its model configuration, and the held-response regression test prevents the first-send race from returning.

* refactor: generate workspace effort provider

* fix: retry workspace effort after failures

* fix: handle unavailable workspace effort

* fix: hide unhydrated native effort controls

* fix: progressively hydrate native reasoning effort

* fix: guard native effort callback rollback

* fix: serialize native effort updates

* test: distinguish native effort update callbacks
2026-08-04 22:47:31 +05:30
cogwheel
a13c6fdf79
fix: polish iOS 26 native glass controls (#610)
* fix: polish iOS 26 native glass controls

The platform-view optimization reused Flutter glyph sizing and native label defaults, causing oversized symbols, wrapped model names, and stale theme colors. Use explicit optical sizes, bounded native labels, and color-keyed view identity so the regressions remain fixed.

* perf: avoid unused native selector subtree

* fix iOS native toolbar polish

* use native model selector chevron

* shrink native model chevron

* fix iOS toolbar grouping and selection menus

Keep paired actions in one UIKit toolbar surface and delegate editable selection to the system menu. Avoid rebuilding EditableText from overlay visibility, which caused the menu and handles to cycle.

* fix: address iOS toolbar review feedback

* fix: preserve composer loading surface

* fix: enlarge native toolbar icons

* fix: balance native toolbar symbols

* fix: standardize native toolbar icon sizing

* fix: group channel and folder toolbar actions

* fix: preserve full model selector semantics

* test: cover native model selector semantics payload
2026-08-04 16:48:25 +05:30
cogwheel
3561f55de5 fix: release notes 2026-08-01 13:58:12 +05:30
cogwheel
76412a748b
Fix voice calls for Direct-only setups (#605)
* fix(voice): support accountless call entry points

* fix(voice): address review feedback

* fix(voice): preserve startup ownership

* fix(voice): make startup cancellation-safe

* fix(voice): preserve first-turn ownership

* fix(voice): preserve launch ownership

* fix(voice): cancel stale startup promptly

* fix(carplay): preserve concurrent call ownership

* fix(carplay): scope disconnect ownership cleanup
2026-08-01 12:01:42 +05:30
cogwheel
e8b519ee90
Enable inline interactive HTML tool embeds (#603)
* feat: render interactive tool embeds inline

* fix: stabilize composer controls across layout states

Measure multiline expansion from the rendered compact width instead of focus or character count. Keep shared control sizes, text gutters, and modal close theming consistent across platforms.

* fix: address embed and composer review feedback

* fix: require real embed navigation gestures

* fix: align embed fragments and composer measurement

* fix: handle Android embed popups safely

* fix: finalize embed popup review

* fix: sandbox remote tool embeds

* fix: restore remote embed bootstrap

* fix: surface remote embed load failures

* fix: isolate remote embed bootstrap

* perf: avoid duplicate embed observers

* fix(chat): guard timeline geometry during route transitions

* fix(markdown): defer embeds until tool completion
2026-07-31 22:45:57 +05:30
cogwheel
677a20e2ba
Redesign the Conduit 4.0 release announcement (#602)
* feat: bundled JSON release notes for 4.0, polished changelog sheet, remove IAP

- Move release-note content from ARB to assets/release_notes/<locale>.json
  with locale-fallback repository and schema-checked validator
- Polish the release notes sheet (staggered reveal, version badge,
  feature glyph bullets, action cards) and add the 4.0 note in 13 locales
- Prune pre-4.0 notes
- Remove in_app_purchase and the tip jar entirely; keep the Buy Me a
  Coffee donation link on all platforms

* Refresh release notes sheet and localized copy

* Add release notes banner and native review support

* Redesign release announcement sheet

* Target release announcement to 4.0.1

* Address release announcement review feedback

* Tighten release note locale handling
2026-07-31 21:54:47 +05:30
cogwheel
b2f725a425
Polish chat composer and unify cross-platform typography (#598)
* Polish chat composer layout and platform styling

* Unify product typography across platforms

* Address automated review feedback
2026-07-29 11:42:22 +05:30
cogwheel
6a335c1883
fix: reconcile remote deletions and generated titles (#596)
* fix: reconcile remote deletions and generated titles

* fix: address synchronization review feedback

* fix: recover note drafts after remote deletion

* fix: fence recovered notes to the active session

* fix: preserve recovered note retry state

* fix: serialize recovered audio migration

* fix: journal recovered audio moves

* fix: serialize concurrent audio rebinders

* fix: own audio rebind lifecycle

* fix: retain failed audio rebinds

* fix: preserve refresh and rebind failures

* fix: exclude concurrent audio removal

* fix: retain dirty note on failed save

* polish chat pull-to-refresh layout

* refactor: reuse motion preference

* fix: complete adaptive chat refresh sync

Recover title-only server changes during the full-list reconcile because Open WebUI title generation does not advance updated_at. Keep the global sync status visible through reconciliation, use platform-native pull feedback, and auto-collapse its completion state.

The regression guards cover the missed-watermark title path, pending local renames, sync progress lifecycle, adaptive indicators, and automatic gutter cleanup.
2026-07-29 11:00:34 +05:30
cogwheel
f2c3a718f7
fix: reconcile streaming chats after reopen (#593)
* fix: reconcile streaming chat state after reopen

* fix: harden reopened state reconciliation

* fix: address state recovery review feedback

* test: verify knowledge cache LRU identity

* fix: fence knowledge files across provider rebuilds

* fix: address outside-diff review feedback

* fix: arm reopened monitor before socket attach

* fix: wait for authoritative reopened completion

* refactor: address final review feedback

* fix: clear channel state on owner change

* fix: fence channel operations across owner changes

* fix: clear active channel during route changes

* fix: fence channel reaction picker ownership

* fix: complete destructive sign-out and auth routing

Purge the direct-local chat database after a committed full-data clear, and distinguish durable local cleanup from best-effort WebView cleanup so a cookie failure cannot leave on-device chats behind. Strip credential-bearing headers and mTLS material when preserving server details, and route completed optional OpenWebUI authentication back to chat even while server state refreshes. Regression tests cover the durable purge and both Direct/Hermes route shapes.

* fix: avoid authenticated router redirect loop

Keep the connection-issue route stable when active server resolution fails while authenticated. The regression exercises auth-to-chat completion, chat-to-error routing, and terminal error-page behavior.

* fix: keep destructive cleanup fail closed

Do not reopen Direct write or run admission when the on-device chat purge fails. Fence reaction pickers with the channel operation generation to reject A-B-A owner cycles, and extend regression coverage for both boundaries.

* fix: fence channel actions across owner ABA

Capture the channel operation generation across sends, attachment selection and upload, edits, deletes, pinning, channel dialogs, and member loading so A-B-A owner cycles cannot revive stale continuations. Keep app-data-clear write resumption centralized in the fail-closed finalizer.

* perf: reduce streaming UI and recovery activity

* perf: window chat transcripts with positioned scrolling

* perf: bound raster media decoding

* fix: bound pre-handler socket event buffering

* fix: address performance review feedback

* test: share transcript chain fixtures

* fix: recover transcripts from dangling tips

* fix: address outside-diff performance feedback

* fix: settle terminal replay snapshots

* fix: unify image preview bounds

* fix: stabilize reversed chat anchoring

The positioned-list migration treated item zero's trailing edge as the latest edge even though reversed lists report the latest boundary at leading edge zero. That kept reissuing streaming follow animations and delayed explicit detachment detection. Pinning also began before its synthetic spacer had real row measurements, so the target geometry changed during the animation.\n\nUse the reversed leading-edge invariant, track manual detachment separately, wait for measured pin rows, and hold the newest item at a constant minimum extent while the assistant consumes the remaining viewport. Regression tests cover the package edge semantics, initial settle, button gating, and stable streaming growth.

* fix: address chat viewport review feedback

Fence delayed anchor restores to their conversation, separate structural overflow from scroll-button eligibility, and restore viewport-aware markdown prewarming. Keep every image preview state on a stable bounded geometry and guard late pin measurements after disposal.

Regression coverage verifies reversed-list overflow classification, immediate detached-button eligibility, conversation restore fencing, visible/fallback prewarm windows, and shared preview dimensions.

* fix: align chat viewport bounds

Compute scrollability from the rendered transcript window, centralize anchor recomputation, and size raster decodes from the stable preview box so unbounded layouts do not decode at the inline cap.

The unbounded 3x preview regression now verifies a 900 by 900 decode target instead of 1536 by 1536.

* fix: align windowed chat navigation

* fix: fence deferred chat state updates

* fix: scope chat send admission ownership

* fix: keep pinned streaming viewport stable

The previous fix still revealed the first turn before user-row measurement and handed the active pin to tail-follow after spacer exhaustion. Settle first turns before reveal, keep active pins fixed through overflow, and expose latest navigation only once content is scrollable.

* fix: harden initial transcript settlement

Keep the hidden first-turn transcript out of hit testing and semantics until its positioned jump completes. Limit initial list seeding to the first settlement and bound the streaming stability test's frame drain.

* fix: preserve transcript state while settling

Keep the positioned transcript under a stable semantics and pointer wrapper while the first-turn pin settles. Toggle only wrapper properties so reveal cannot recreate the reversed list or reset its initial index.

* fix: stabilize pinned streaming scroll

The regression recurred because scroll-to-latest discarded measured pin geometry while streaming growth launched overlapping eased corrections. Keep the anchor through reattachment, animate measured end space once, and fence non-animated live maintenance behind the explicit navigation generation.

* fix: enforce exclusive streaming scroll ownership

The regressions recurred because pin completion and stale latest navigation could both reposition the list, while a detached multi-viewport assistant row continued rebuilding beneath the viewport. Settle measured pin geometry without a second item jump, fence competing bottom actions, and freeze detached tail presentation until an explicit latest action. Regression tests cover post-transition stability, responseDone settlement, and detached presentation.

* fix: anchor chat scrolling with forward slivers

Replace reversed item-position scrolling with a chronological, centered
CustomScrollView so streamed row growth and history prepends preserve exact
pixels.

The regression recurred because item-position tests bypassed the real
streaming subscription, while scrollable_positioned_list cannot preserve an
intra-row pixel offset as a growing row relayouts.

* fix: address final performance review

* fix: address hosted review gates

* fix: retain undispatched screen context

* fix: retry undispatched screen context

* fix: bound screen context retries

* fix: stabilize active streaming navigation

The regression recurred because the latest action restored pin state while still targeting the footer, and ChatPage duplicated the viewport's post-layout streaming corrections as the same assistant row grew.

Route active pinned turns back to their measured user row while pin space remains, leave layout maintenance to the sliver viewport, and replace the linear dots with Conduit's low-frequency painted orbit.

* fix: keep pinned streaming turns in stable slivers

The regression persisted because the forward-sliver port still coupled the
live footer and a shrinking pin spacer to streamed layout. Real markdown
growth changed maxScrollExtent between frames, so Flutter corrected the
viewport even without a follow-latest command; the earlier item tests did not
exercise that runtime subscription and layout sequence.

Render the live footer as its own sliver, keep fixed viewport-sized support
for the active pin, exclude that support from logical latest metrics, and
retain the pinned prompt as the semantic latest target across lazy unmounts
and manual detachment.

* fix: detach timeline follow for pointer scrolling

Mouse-wheel and trackpad input has no drag details, so it previously left the
timeline in automatic follow ownership during a live response. Treat non-idle
user-scroll notifications as manual ownership while keeping driven latest
animations excluded, and cover both paths with a pointer-signal regression.

* fix: omit absent timeline slivers

Use null-check patterns for optional footer and trailing content so the sliver
tree contains no empty adapters when those widgets are absent. Add a regression
that verifies removing the live footer removes its sliver as well.

* fix: retire pinned chat viewport ownership

The regression kept recurring because the previous tests encoded pinned-row navigation as the desired latest action and exercised item geometry without covering the terminal ownership transition used by real streaming updates.

Retire pin support on completion, failure, drag, or explicit latest; remove the spacer before the single real-footer navigation; fence stale callbacks; and clip earlier turns at the app-bar content boundary. Add red-to-green lifecycle, footer, streaming-growth, and clipping guards.

* fix: fence deferred pin release from drags

Prevent either deferred pin-release continuation from reclaiming latest ownership after real user interaction. Remove the constant-only latest-state helper and cover the shared production guard directly.

* fix: release orphaned pinned turns

Treat a missing pinned assistant as a terminal lifecycle transition so edit, regeneration, or reconciliation cannot retain synthetic pin support for a row that no longer exists.

* fix: restore glass-safe pinned chat geometry

The regression recurred because clipping hid the overlapped rows instead of removing their geometry, which also deprived native glass of backdrop content. Earlier tests rebuilt simplified rows and missed the direct streaming subscription's repeated metrics notifications. Reserve real sliver clearance for the pinned prompt and stop maintaining settled pin geometry on every streamed extent update. Regression coverage now drives mounted streaming growth and a real completed assistant row.

* test: restore chat row extent regressions

Cover archived variants at their real zero-sized production placeholder and drive completion content growth through the mounted AssistantMessageWidget row. This replaces the obsolete estimated-extent assertions removed with the forward-sliver viewport.

* fix: keep completed turns anchored below chat chrome

The regression recurred because per-pin top clearance disappeared at completion, while simplified initial-pin tests never exercised the established-chat transition. Latest-button visibility also duplicated scroll ownership in a bottom-anchor flag that stale post-pin metrics could clear.

Keep toolbar clearance at the oldest transcript edge, preserve the active prompt position when lifecycle pin support retires, and make free-scrolling mode authoritative for latest-button visibility. Add established-chat completion and stale-metrics regressions that were observed failing before the fix.

Fresh SimDeck validation held the second prompt at logical y=126 through thinking, streaming, and completion. Real overflow exposed latest at y=736, and one manual tap reached the actual footer without terminal auto-scroll.

* feat: support native iOS transcript scroll to top

The chat viewport used a private centered sliver controller, so Flutter’s native iOS status-bar event had no correct route to the transcript; offset zero is also not the oldest edge once saved anchors or older pages move the center.

Handle the native event only for the current, ticker-enabled viewport, transfer ownership to free scrolling, and navigate to the exact minimum extent with bounded post-layout correction. This avoids the generic message seeker, whose viewport-count budget failed across a single very tall assistant row.

Regression coverage observes red with the callback disabled and with the old bounded seeker, then green for a 20,000-pixel assistant row. The focused 102-test chat suite, full 4,926-test suite, analyzer, diff check, and iOS simulator build pass.

* test: harden native transcript navigation

* fix: stabilize first-turn streaming scroll

Preserve the initial turn pin when a newly created local conversation receives its first ID, and maintain the attached trailing edge during layout so streamed row growth never paints an intermediate jump.

* perf: reduce streaming render and markdown work

Keep high-frequency content updates inside the assistant body, materialize coalesced reasoning snapshots only at publication, and prevent transient Markdown revisions from filling settled caches.

Retire idle Markdown workers, sample debug-only diagnostics, and cover rebuild, cache, and lifecycle bounds with deterministic regression tests.

* fix: fence markdown cache eviction

Advance a global cache epoch before memory-pressure eviction and reject late writes from single compiles, shared followers, and batches that began before the clear.

Add deterministic delayed single and batch regressions covering post-eviction followers and subsequent current-epoch caching.

* fix: fence disposed markdown followers

Prevent single and batch follower continuations from repopulating the shared compiled cache after their MarkdownCompileService has been disposed.

Add deterministic delayed disposal regressions for both follower paths.
2026-07-28 13:34:54 +05:30
cogwheel
4539a7d5ab
Support Open WebUI 0.11 (#594)
* feat: support Open WebUI 0.11

Treat both 404 and 405 as the removed active-chat route because v0.11 deployments can surface the missing POST as Method Not Allowed. Use the active field from paginated chat lists and render the compatibility warning as an undecorated greeting card.

* fix: address Open WebUI 0.11 review feedback

* fix: keep empty-state scroll viewport accessible
2026-07-27 22:18:45 +05:30
cogwheel
bdeb1a0057
feat: add first-party OpenRouter support (#589)
* feat: add first-party OpenRouter support

* fix: address OpenRouter review feedback

* fix: preserve OpenRouter server auth options

* fix: localize reviewed catalog entries

* feat: normalize OpenRouter reasoning effort

* fix: validate raw OpenRouter PDF filenames

* fix: refine connection setup and reasoning defaults

* fix: keep selected models visible in picker

* fix: restore OpenRouter image generation defaults

A review fix conflated native image output modalities with OpenRouter server-tool eligibility. Restore the image tool for every device-owned Chat Completions model, localize the provider label, and add a persisted Personalization override for the image model.

* fix: preserve image model during settings hydration

Wait for the pending preferences load before applying the OpenRouter image-model override so a startup write cannot be replaced by stale persisted state.

* fix: localize OpenRouter image settings

Translate the new image-model preference in every supported locale and complete the German, Korean, and Dutch OpenRouter settings strings identified during review.

* fix: correct Dutch OpenRouter description

* fix: expose image model in native Chats sheet

The setting was added to an unreachable legacy Personalization detail while the live iOS profile menu routes model defaults through Chats. Build the visibility row from one tested helper and include it in the active Chats detail.

* fix: buffer OpenRouter image tool completions

Image server-tool child jobs can succeed before the parent streaming continuation fails, and a committed SSE response cannot fail over. Buffer only image-tool requests so OpenRouter returns the complete markdown image result while all other completions remain streamed.

* fix: decode OpenRouter generated image results

The buffered image-tool fix still assumed generated images lived in message content, so successful message.images payloads were rejected and nested choice errors were masked. Normalize bounded image outputs into markdown, surface choice-level errors, and require the captured direct route to remain selected across preparation awaits.

* fix: keep OpenRouter images on continuation errors

The earlier decoder fix still prioritized choices[].error before message.images, and Gemini server tools can return both after generating successfully. Treat usable OpenRouter images as completion while preserving top-level and image-less choice failures.

* fix: retain direct route ownership through preflight

The earlier route fix covered initial preparation but the later dispatch guard still accepted any live captured binding. Require the captured model to remain selected after attachment/message preflight, with a regression that keeps both bindings live while selection changes.

* fix: restore direct answer when regeneration preflight stops

A late model switch could cancel regeneration after its empty same-id placeholder had replaced the completed answer. Restore and persist the captured previous assistant on preflight cancellation, with a regression covering the UI and durable timeline.

* feat: use OpenRouter dedicated image API

Generate and persist image assets independently from parent narration. Prompt refinement and acknowledgement are best-effort, while dedicated Image API output remains authoritative across continuation and transport failures.

* fix: preserve image and route progress

Exclude immediate image durability writes from provider stream timeouts, and retain trusted binding authority across metadata-only catalog refreshes while still revoking edited or removed profiles.

* fix: harden OpenRouter image responses

* refactor: remove legacy OpenRouter image parsing

* fix: preserve completed answer across chained regeneration

A superseding direct regeneration could capture the first run's empty same-ID preflight placeholder, so a later cancellation restored and persisted an empty answer. Normalize pristine streaming placeholders back to their last completed version before building the next placeholder, while retaining any partial or generated output. Add a deterministic two-preflight regression covering visible and durable restoration.

* fix: stabilize OpenRouter follow-ups and source favicons

* fix: preserve follow-up timeline state across remounts

Keep the assistant shell stable when a live tail enters history so generated images do not collapse and re-expand above the pinned prompt. Remember the running haptic per message so list virtualization cannot replay feedback.

* fix: apply OpenRouter review feedback
2026-07-26 10:50:00 +05:30
cogwheel
633e10b790
Add sign-out data retention options (#588)
Adds sign-out retention choices, clears local preferences and Direct/Hermes connections by default, and fixes the iOS post-sign-out provider lifecycle hang.
2026-07-24 23:45:46 +05:30
cogwheel
c5e4998e9a
feat: expand native Ollama and Direct connection support (#587)
* feat: add native Ollama Cloud support

* feat: unify model and backend settings UX

* feat: polish direct backend model and composer UX

* fix: gate attachments by backend capabilities

* feat: add trusted direct document attachments

* feat: show Ollama web search sources

* fix: address PR review feedback

* fix: address follow-up review feedback

* test: use checks for reasoning effort coverage

* fix: harden model effort selection

* fix: surface effort persistence failures
2026-07-24 19:03:55 +05:30
cogwheel
c71af21118
Render Mermaid diagrams natively (#585)
* feat: render Mermaid diagrams natively

* fix: apply CodeRabbit auto-fixes

* ci: harden submodule checkout credentials

* fix: hide persistent glass controls under sheets

Route-level scroll controls were not subscribed to tracked sheet coverage, so their UIKit compositor layers could bleed through modal routes. Centralize pre-presentation native chrome suppression and add a regression guard.

* fix: respect Dynamic Type in chat chrome

Cupertino navigation bars suppress inherited scaling and the composer used fixed control extents. Restore the route text scaler, bound control geometry to 1.5x, and add a regression guard.

* fix: honor accessibility settings across custom chrome

Match native Bold Text for Flutter icon glyphs and route every custom Cupertino toolbar through the shared Dynamic Type-aware shell. Keep route content and refresh offsets aligned with the scaled toolbar height.

* fix: scale sidebar navigation with Dynamic Type

The native iOS 26 tab bar applies Bold Text but keeps fixed label and symbol metrics. Retain it at normal size and use the bounded, scalable Flutter navigation bar for enlarged accessibility categories.

* Revert "fix: scale sidebar navigation with Dynamic Type"

This reverts commit 048e9de00b.

* fix: address accessibility review feedback

Use adaptive note editor insets, hide the workspace native section menu beneath tracked sheets, and align the new accessibility assertions with the test suite's checks convention.

* test: use checks for workspace sheet coverage
2026-07-22 22:53:45 +05:30
cogwheel
d159e82a65
Stabilize adaptive navigation and account sync (#584)
* Update adaptive platform UI

* Upgrade project dependencies

* Stabilize OpenWebUI conversation selection after auth changes

- Keep the bearer token mirrored across apiService rebuilds
- Add account-storage certification and selection ownership checks
- Kick off a post-certification sync when storage isolation settles

* Fix reauthentication sync and show sidebar progress

* Scope sidebar sync progress rebuilds
2026-07-21 21:39:50 +05:30
cogwheel
d5c6b11b28
Optimize performance and harden lifecycle handling (#581)
* Optimize performance and harden lifecycle handling

* Address PR review feedback

* Clean up duplicate App Intent image retries

* Fix follow-up pin-to-top routing

* Restore no-jump pin dismissal on user scroll

A later timeline optimization kept the synthetic pin spacer active through every manual scroll, undoing the guarded dismissal from #560 and allowing iOS range correction to snap the viewport. Restore the phantom-free range guard and cover both unsafe and safe dismissal offsets so the regression cannot recur silently.

* Rebuild chat turn anchoring like T3 Code

Create the anchor from the exact optimistic user-message ID, replace the full-screen phantom range and capped physics with measured anchored end space, and use item-level layout correction until real content fills the viewport. Cancel automatic corrections on the first user gesture so streaming growth cannot reintroduce scroll jumps.

* Fix Android cookie-clear verification and allow https-upgrade capture origins

The verified cookie clear treated Android's unimplemented getAllCookies as
failure, blocking SSO sign-in on empty stores and permanently arming the
incomplete-logout fence. Exact-origin capture checks silently dropped token
capture for http-configured servers upgraded to https by their proxy; capture
now also trusts the default-port https upgrade of the configured origin.
Also stabilize the background-validation sanitization test's poll deadline.

* Restore same-origin redirect recovery, pool warmup, and native fixes

- Replay credential-safe 3xx hops (same origin or default-port https
  upgrade) for idempotent methods on the shared API client; cross-origin
  hops still surface to the caller.
- Warm the completion client's actual connection pool at startup again;
  checkHealth's request-scoped probe no longer touches it.
- Graceful ApiService dispose so provider rebuilds cannot abort in-flight
  SSE streams; cap connectivity failure backoff at the healthy interval.
- iOS: thread the trusted origin into native sheet avatar loads so auth
  headers are attached again; let oversized STT tap buffers fall back to
  one-off copies instead of being dropped.

* Harden PR re-application: auth, upload/share, and provider regression fixes

Auth: logout preserves connection prerequisites (custom headers, mTLS)
while still revoking session credentials (legacy apiKey, captured proxy
Cookie headers); config-header edits and legacy apiKey migration no
longer sign the user out; cold-start background validation retries for
~7s to cover slow tunnels; interactively reissued byte-identical tokens
are accepted after logout; SSO button failures surface visibly.

Uploads/share: native-share durable keys derive from payload id +
ordinal instead of mutable content checksums; Hermes/direct-model
shares route through the local composer path instead of retrying
forever; pre-connection network failures defer instead of failing
terminally; orphaned receipt-held rows are garbage collected once
native storage is confirmed drained; disposed-queue persistence reports
failure so staged files survive; legacy staging roots are reclaimable.

Providers/UI: queued-completion banner watches every ownership-fence
input so retry/cancel cannot silently no-op; drawer keeps previous rows
during pagination reloads; authenticated image cache keys derive from a
stable server+token digest so the disk cache survives restarts while
accounts stay isolated.

* Make share staging indeterminate-ownership test hermetic under concurrency

The test snapshotted the process-global staging temp root, so files staged
or cleaned by concurrently running suites broke exact set equality. Assert
only that this test's own artifact never appears.

* Stabilize streaming UI and pending share persistence

Persist Android pending-share state atomically with migration coverage. Keep prompt anchoring and streaming haptics stable across row remounts, and reduce markdown streaming churn while hardening placeholder cleanup.

* Make streaming Markdown preparation incremental

* Reuse stable Markdown render inputs

* Avoid cumulative structured stream rebuilds

* Instrument and streamline structured output

* Hide dismissed sidebar native chrome

* Reduce streaming platform view retention
2026-07-20 21:04:28 +05:30
cogwheel
ff630dbad5 Update files 2026-07-18 11:32:35 +05:30
cogwheel
eb99937f09
Fix chat title generation, copying, and note markdown styling (#579)
* fix: restore chat metadata and rich content handling

* fix: apply CodeRabbit auto-fixes

* fix: harden markdown code masking
2026-07-18 09:27:57 +05:30