Commit graph

1076 commits

Author SHA1 Message Date
cogwheel
a95a2d8acf Pair secondary containers with their text and floor inverse primary
- 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.
2026-09-28 13:11:18 +05:30
cogwheel
be776e52e8 Address review: contrast floors, back to chat, router coverage
- 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.
2026-09-28 12:54:17 +05:30
cogwheel
acdda47c61 Return to chat after signing in from the settings sheet
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.
2026-09-28 11:53:37 +05:30
cogwheel
d51ba8dcb5 Make toolbar icons and settings surfaces follow the active palette
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.
2026-09-28 11:53:29 +05:30
cogwheel
0b3ba91e9d Retain early task events and isolate PDF preview caches 2026-09-28 00:08:46 +05:30
cogwheel
ceb1439265 Preserve newer checklist events and honor cleared metadata 2026-09-28 00:00:17 +05:30
cogwheel
863729b273 Persist checklist events and close streaming UI review gaps 2026-09-27 23:44:16 +05:30
cogwheel
6e9e866840 Handle remaining Open WebUI streaming events and task state 2026-09-27 23:20:13 +05:30
cogwheel
28428bfda6 Serialize shared audio configuration across replacement calls 2026-09-27 21:13:45 +05:30
cogwheel
51b9da0aee Preserve pending and incomplete tool display states 2026-09-27 20:56:46 +05:30
cogwheel
a1d9a73953 Stop old Bluetooth teardown after a replacement call takes ownership 2026-09-27 20:46:01 +05:30
cogwheel
d9df561fc6 Show failed and rejected tool outcomes without a success indicator 2026-09-27 20:36:47 +05:30
cogwheel
4a7e15993e Match Open WebUI streaming state through rendering and persistence
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.
2026-09-27 20:25:45 +05:30
cogwheel
548fd5b28a Show PNGs whose metadata embeds an SVG icon
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.
2026-09-27 14:36:56 +05:30
cogwheel
7a31955bef Hand the Android call route to a replacement call
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.
2026-09-27 00:04:30 +05:30
cogwheel
ab85ab6b00 Address review feedback on streaming, tool blocks, and speaker routing
- 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.
2026-09-26 22:46:17 +05:30
cogwheel
fa75223158 Render Open WebUI tool-call blocks in answer text instead of a raw wall
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.
2026-09-26 22:03:22 +05:30
cogwheel
28521e6553 Keep voice calls on the speaker and hand audio back after a call
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.
2026-09-26 22:03:02 +05:30
cogwheel
b5c2ab6f50 Open chat before attaching an assistant screenshot 2026-09-26 17:07:34 +05:30
cogwheel
4291cb0bd5 Wait for chat readiness on cold launches and release unused recognizers
- 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.
2026-09-26 16:38:43 +05:30
cogwheel
e7fa71e8c1 Fix Android entry points, shares, folders, STT, and keyboard insets
- 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).
2026-09-26 14:25:16 +05:30
cogwheel
c86b431557 Restore the header new-chat action in the sidebar
Remove the floating new-chat button and its list-inset reservation; the
Chats and Hermes tabs create from the header action again.
2026-09-26 12:50:36 +05:30
cogwheel
9782eb9ad8 Keep the new-chat pill on screen at large text sizes
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.
2026-09-26 12:03:50 +05:30
cogwheel
64e5db8f6e Search DMs by the participant names the channel list shows
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.
2026-09-26 11:45:51 +05:30
cogwheel
1ce9461270 Address review feedback on accessibility and layout edge cases
- 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.
2026-09-26 02:12:02 +05:30
cogwheel
174f894e08 Refine chat, composer, sidebar, and settings UI
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.
2026-09-25 21:59:07 +05:30
cogwheel
35e8ea7db6 Fix issues found in a simulator verification pass
- 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.
2026-09-25 20:20:33 +05:30
cogwheel
d7c0971310 Drop internal tracking tags from comments
The comments keep what they explain.
2026-09-24 14:15:59 +00:00
cogwheel
ac0bec3623 Extract shared packages from the mobile app
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.
2026-09-24 10:05:58 +00:00
cogwheel
90bfe62fdd
Fix native dropdown crash, composer overflow artifact, and stale streaming state (#749)
* 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.
2026-09-22 22:05:37 +05:30
cogwheel
d82087fc79
Support Open WebUI 0.11.4 and fix user-settings clears (#748)
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.
2026-09-22 13:09:36 +05:30
cogwheel
57da9186a3
Add iOS native Barge in toggle (#737)
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
2026-09-17 19:02:10 +05:30
cogwheel
446975413b
Keep chat scrollbar thumb stable and inset it from the composer (#734)
* 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
2026-09-16 20:59:50 +05:30
cogwheel
c552008a3c
Fix attachments, STT, TTS, citations, proxy re-auth, and Hermes probe (#732)
Some checks failed
L10n / l10n (push) Has been cancelled
* 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
2026-09-16 15:44:46 +05:30
cogwheel
8fe6f782f6
Collapse raw reasoning tags client-side and stop re-escaping stored content (#731)
* 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
&lt; 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.
2026-09-16 15:16:11 +05:30
cogwheel
1dd1d624d8
Upgrade all packages, add notification settings link and Ollama thinking replay (#727)
* 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
2026-09-15 10:18:53 +05:30
cogwheel
915bfb5f24
Support Open WebUI shared folders (#726)
* 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
2026-09-15 08:55:55 +05:30
cogwheel
4e73c9a0ea
Fix ten reported issues and vendor Hermes Agent as an API reference (#725)
Some checks are pending
L10n / l10n (push) Waiting to run
* 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.
2026-09-15 00:48:03 +05:30
cogwheel
6c6e02f824
Fix native toolbar, Apple gating, streaming cadence, and wakelock regressions (#718)
* 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 #694
Fixes #692
Fixes #688
Fixes #686
Fixes #681
Fixes #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>
2026-09-14 19:53:35 +05:30
cogwheel
30b9a43ab6
Support Open WebUI v0.11.3 (#696)
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.
2026-09-01 23:00:14 +05:30
cogwheel
df667870bf
feat: add direct MCP servers to Direct models (#676)
* feat: add direct MCP server management

* feat: run direct models with approved MCP tools

* fix: settle direct MCP release gates

* fix: harden direct MCP runtime bounds

* feat: support direct MCP in Responses API

* fix: stop cancelled Responses rounds

* test: strengthen Responses replay limit coverage

* fix: host direct MCP editor material controls

* feat: add direct MCP OAuth credential schema

* feat: add direct MCP OAuth lifecycle

* fix: bind MCP OAuth tokens to resource paths

* fix: preserve OAuth expiry diagnostics

* test: harden direct MCP OAuth fixtures

* fix: stabilize direct MCP OAuth editor lifecycle

* feat: remember direct MCP tool approvals

* feat: insert MCP prompts and text resources

* feat: pin MCP Apps protocol boundary

* feat: add MCP Apps containment harness

* fix: harden direct MCP review boundaries

* fix: address direct MCP review follow-ups

* fix: close MCP OAuth review races

* fix: reject malformed MCP payloads

* fix: close direct MCP review gaps

* fix: settle remaining direct MCP reviews

* fix: harden MCP credential and sign-out boundaries

* fix: recover MCP writes after incomplete sign-out

* fix: close MCP OAuth sign-out races

* fix: settle MCP review edge cases

* fix: ignore chat frames after completion

* fix: stop cancelled chat tool rounds

* fix: apply CodeRabbit review fixes

* fix: address remaining MCP review feedback

* fix: normalize Direct MCP send features

* fix: bind MCP credentials to exact endpoints

* test: strengthen MCP review coverage

* feat: enable MCP tools for Apple models

* fix: confirm MCP OAuth endpoint transfers

* fix: preserve Pigeon Flutter error details

* feat: unify direct MCP approval prompts

* test: isolate MCP approval fingerprint inputs
2026-08-29 00:51:28 +05:30
cogwheel
c564acb830
Add Mac desktop UX and chat scrollbars (#680)
* Add Mac desktop UX and chat scrollbars

* Prevent repeated Mac create shortcuts
2026-08-28 23:15:02 +05:30
cogwheel
ee4ab6ec86
Fix Hermes approvals, tool-call markup, and restored running state (#679)
* Fix Hermes approval binding and attached tool-call rendering

* Restore running Hermes session indicators

* Address PR review feedback
2026-08-28 21:51:34 +05:30
cogwheel
3401173dd6
Fix Markdown blockquote rendering (#673)
Some checks failed
L10n / l10n (push) Has been cancelled
* fix markdown blockquote rendering

* handle blockquote escaping edge cases
2026-08-26 23:57:39 +05:30
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
8b94d30495
chore(dependencies): replace sharing intent plugin (#669) 2026-08-26 22:33:33 +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