mirror of
https://github.com/cogwheel0/conduit.git
synced 2026-08-27 19:41:48 +00:00
233 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f80f79352e
|
feat(openwebui): support v0.11.1 (#667) | ||
|
|
e5b5b388e2
|
fix(markdown): align list bullet markers vertically with text (#660)
Co-authored-by: t0lot <t0lot@users.noreply.github.com> |
||
|
|
1926af4b7f | Harden sync, interaction, and markdown rendering | ||
|
|
1371c352d5 | Improve scroll performance and sync efficiency | ||
|
|
2ee24a5817 | Cache markdown extents and defer code highlighting | ||
|
|
f4d76fd411
|
Improve chat transcript scrolling performance (#652)
* perf: smooth chat transcript scrolling * fix: address scrolling review feedback |
||
|
|
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 |
||
|
|
f79ccf9c51 | Use system fonts and preserve streaming state | ||
|
|
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. |
||
|
|
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). " 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 (&quot; decoding once back to a visible
"). 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
|
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
3561f55de5 | fix: release notes | ||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
66308acf46
|
Optimize chat timeline, streaming, Hermes, and native navigation (#578)
Refactor chat timeline anchoring and streaming rendering, extend Hermes session and history support, and preserve stable native navigation and Liquid Glass controls. Validated with flutter analyze and flutter test (4,012 passed; 2 skipped). |
||
|
|
63218a655c
|
Add a versioned Conduit User-Agent (#572)
Adds a versioned Conduit identity across Open WebUI REST, health, Socket.IO, system terminal, and protected-media paths while preserving third-party boundaries. Addresses the User-Agent portion of #542. HTTP/2 support remains separate. |
||
|
|
df8eaa1ce3
|
feat: add direct OpenAI-compatible and Ollama connections (#567)
* feat: add direct provider connections * fix: harden direct connection workflows * fix: stabilize direct connection state * fix: ignore failed direct image attachments * fix: serialize direct profile reloads * fix: recheck queued media upload routes * fix: prune stale direct models during refresh * feat: polish adaptive backend onboarding * fix: restore explicit backend onboarding back paths Hermes and Direct onboarding are entered with replacement routes, so they cannot rely on an implicit navigator pop. Give both flows explicit destinations and align their setup screens with the adaptive auth shell. * fix: harden direct onboarding state Re-read profiles after asynchronous confirmation, guard discovery writes after disposal, and keep disabled segments unselected. Consolidate the shared onboarding shell and header-security resets to prevent drift. * fix: keep local backends independent after logout Logout intentionally retains OpenWebUI server state, so routing and model selection now key off resolved backend usability, terminal auth, trusted Direct bindings, and auth-session identity across loading, error, retained-value, cold-start, and backend-switch races. * fix: preserve chat storage boundaries on recovery Treat OpenWebUI cache reads as best-effort only when ownership is explicit, retain ambiguity and Direct-local failures instead of crossing providers, and contain asynchronous default-model cache write errors. Regression tests cover all three paths. * fix: retain legacy OpenWebUI chat ownership When the merged chat list is still loading, an explicitly OpenWebUI-scoped active summary now restores ownership for legacy raw IDs without trusting Direct-local or unannotated rows. This prevents same-ID local chats from making a valid OpenWebUI conversation ambiguous. * feat: migrate direct providers to typed SDKs * fix: isolate direct transport and reject blank streams * fix: hide stale direct model bindings * fix: apply CodeRabbit auto-fixes * feat: support Hermes attachments via Responses API * fix: address direct and Hermes review findings * fix: avoid OpenWebUI settings load in direct composer * fix: preserve images during direct regeneration * fix: preserve repeated direct images across turns * fix: settle direct reasoning and Android chrome * fix: harden multi-backend streaming ownership * fix: harden multi-backend recovery lifecycles * fix: close adversarial multi-backend edge cases * fix: separate Responses reasoning items * fix: preserve streamed reasoning boundaries * fix: address final multi-backend review findings * fix: close remaining security review findings * fix: address PR review findings |
||
|
|
cb32a22ba3
|
Fix full-width drawer gesture arbitration (#568)
Fix drawer gesture ownership for wide Markdown content while preserving selection, horizontal scrolling, and embedded gesture owners. Closes #556. |
||
|
|
d59a19f571
|
feat(hermes): Hermes Agent backend + Hermes-only mode (#521)
* feat(hermes): integrate Hermes Agent as an optional direct backend
Add Nous Research's self-hosted Hermes Agent as a second backend, separate
from the Open WebUI client, surfaced as a synthetic "Hermes Agent" model and a
dedicated sidebar tab.
Core
- Direct Hermes API client (own Dio + bearer/X-Hermes-* auth), runs transport
with inline tool progress, human-approval gates, persistent-memory headers,
and dropped-stream recovery via GET /v1/runs/{id}.
- Shared SSE frame scanner extracted from the OpenWebUI parser; defensive
Hermes runs-event parser validated against real server traffic.
- Send routing branches to Hermes in durableSend/_sendMessageInternal; chats
are local/ephemeral on the OWUI side, persisted server-side via /api/sessions.
Tiers 1 & 2
- Discovery: /v1/capabilities (feature-gates approval/skills/jobs),
/v1/toolsets, /health/detailed surfaced in settings.
- Sessions: dedicated Hermes tab (open/fork/rename/delete) backed by
/api/sessions; tiles mirror chat tiles with in-progress + selected states and
a long-press context menu.
- Scheduled Agents (/api/jobs): CRUD + pause/resume/run, read-only when the
server reports jobs_admin=false; also a collapsible section in the tab.
- Slash commands sourced from /v1/skills.
UX
- Settings reachable on iOS via the native settings sheet; Hermes model hidden
from the normal picker and the header dropdown suppressed for single-agent
chats. Real Hermes logo as the tab icon (theme-aware alpha template).
Verified end-to-end on an iOS simulator against a live Hermes server.
flutter analyze clean; flutter test (2200) passing.
* fix(hermes): match Chats tab top/bottom safe-area insets in Hermes tab
The Hermes sessions tab used a bare RefreshIndicator + CustomScrollView with
no inset handling, so content ran under the native sidebar chrome and bottom
tab bar. Mirror the Chats tab: top/bottom spacer slivers via
sidebarTabContentTopPadding/BottomPadding and a ConduitRefreshIndicator with
sidebarRefreshIndicatorEdgeOffset.
* fix(hermes): address Greptile review (greploop iteration 1)
- openHermesSession: set isManualModelSelectionProvider so default-model
restoration can't overwrite the Hermes model on session open (P1).
- Hermes approval: null-check hermesApiServiceProvider before resolving so a
disabled-between-display-and-tap approval doesn't show success while the
server-side run stays blocked; log both null + failure paths (P1).
- _isTruthyError: treat numeric 0 as a non-error (Python servers send int 0) (P2).
- Session context-menu Fork/Rename/Delete: await + catch + user feedback instead
of dropping the futures silently (P2).
* fix(hermes): address CodeRabbit/Macroscope/Greptile review feedback
Error surfacing (stream parser):
- run.failed / response.failed now emit a HermesRunError (response.failed
previously surfaced nothing); falsy error markers ("False"/0) fall back to a
generic message instead of showing "False".
- Failed/cancelled/stopped tool events are marked terminal so the tool row
stops spinning. + regression tests.
Robustness / lifecycle:
- Opening a session that fails to load now shows an error and aborts instead of
silently opening an empty transcript.
- Hermes settings "Test connection" wraps health() so the spinner always clears.
- Jobs page create/edit/delete and the approval resolver guard context.mounted /
mounted after their awaits (StateError on disposed widget).
- Run transport recovers the missing suffix when an SSE stream drops mid-content
(previously recovered output was ignored once any delta had streamed).
API / parsing:
- Scope the Dio no-receive-timeout to the SSE stream request; regular endpoints
now have a finite 60s receive timeout so they can't hang forever.
- SSE field parsing strips only a single optional leading space (spec), no longer
over-trimming payload whitespace/indentation.
- HermesConfig.copyWith uses a sentinel so secrets can be explicitly cleared.
- Toolbar pill reserves chevron width only when a chevron is shown.
flutter analyze clean; flutter test (2312) passing.
* fix(hermes): remove stray NUL byte from the _keep sentinel string
A literal NUL had crept into `_keep = '\x00__hermes_keep__'`, which made git
treat hermes_providers.dart as binary (breaking 3-way merges/diffs). The value
is only ever compared with identical(), so dropping the byte is behavior-neutral.
* feat(hermes): Hermes-only mode — run with no Open WebUI server
Make Open WebUI optional so a user can install the app and use a self-hosted
Hermes Agent exclusively. The mode is derived from what's configured and is
switchable; first run shows a backend chooser.
- Mode signals: a synchronous persisted `preferredBackend` (unset|owui|hermes)
for boot-deterministic routing, plus a derived `hermesOnlyMode` for UI gating
(mirrors the reviewer-mode precedent).
- Router: Hermes-only short-circuits to chat with no OWUI server/auth; first run
routes to a new backend chooser; holds splash while Hermes secrets load.
- Onboarding: BackendChooserPage + HermesSettingsPage(isOnboarding) with a
Finish button that sets preferredBackend=hermes and enters the app.
- Models/defaultModel surface + auto-select the synthetic Hermes model when
there's no OWUI API; send guard relaxed for Hermes models.
- UI gating: hide chats/notes/terminal/channels (3 synced sites); Hermes is the
home tab. preferredBackend=owui set on OWUI connect.
Verified end-to-end on an erased iOS simulator (fresh install): chooser →
Hermes setup → Finish → Hermes-only chat → only the Hermes tab → relaunch boots
straight to chat → send creates a server session with no OWUI.
flutter analyze clean; flutter test (2200) passing.
* feat(hermes): bidirectional switching, composer + native-sheet gating, tests
Address the Hermes-only follow-ups:
- Composer: hide OWUI affordances (the "+" overflow button, quick pills, and the
iOS keyboard-accessory actions) when a Hermes model is selected — Hermes has no
OWUI tools/web-search/image-gen/attachments and uses its own `/` skills.
- iOS native settings sheet: gate the OWUI account sections (profile header,
AI memory, data connection, password + profile detail sheets, sign-out) on
Hermes-only, and make the About detail skip the server lookup so it no longer
errors with no server.
- Bidirectional switching: add a "Connect to Open WebUI" entry in both the native
sheet (routed via a control event in main.dart) and the Flutter profile page;
let the router reach the OWUI connect/auth routes for a Hermes-only user; reset
preferredBackend to unset when a Hermes-only backend is disabled.
- Tests: preferredBackend parse/persistence round-trip and hermesOnlyMode
derivation (usable/no-server, server-present, disabled, incomplete, reviewer
precedence).
flutter analyze clean; flutter test (2211) passing.
* feat(hermes): branded native header + Models/defaultModel/send-guard tests
Close out the last Hermes-only follow-ups:
- iOS native settings sheet: render a Hermes-branded profile header in
Hermes-only mode ("Hermes Agent" + the agent host + the hermes_agent.png
avatar). Fully data-driven from Dart — no Swift change; the avatar bytes are
pre-loaded (cached) before the sync config builder runs.
- Extract the inline send/regenerate guard into a pure isSendBlocked() helper
used at both sites, removing the duplicated condition and making the
Hermes-only relaxation unit-testable.
- Tests: isSendBlocked (null model, OWUI-no-api, api present, reviewer, Hermes
relaxation); modelsProvider surfaces only the synthetic Hermes model when
unauthed + usable Hermes; defaultModelProvider auto-selects + writes through
the Hermes model when api == null.
flutter analyze clean; flutter test (2218) passing.
* fix(hermes): address adversarial review findings
* fix(hermes): address Macroscope review
* fix(hermes): surface secret hydration failures
* fix(hermes): harden recovery and credential boundaries
* fix(hermes): preserve credentials across setup failures
* fix(streaming): handle empty SSE lifecycle frames
* fix(hermes): keep connection probes non-destructive
* feat(hermes): move scheduled agents into adaptive sheet
* fix(hermes): address direct Macroscope review
* fix(hermes): address CodeRabbit follow-up
* fix(hermes): close final review gaps
* fix(hermes): finalize terminal state handling
* fix(hermes): preserve completed tool history
* fix(hermes): bind completion to owning chat
|
||
|
|
a5a8e5d1c2
|
Warn instead of blocking newer Open WebUI servers (#564)
* fix: warn instead of blocking newer servers * fix: keep compatibility warning across app routes * fix: retain verified server compatibility state * fix: avoid stale compatibility config writes |
||
|
|
f843481092
|
Polish phase-aware chat streaming and markdown rendering (#537)
* Polish chat streaming timeline * Refactor streaming markdown into display parts * Keep streaming markdown part metadata local * Address review round 1 on chat streaming timeline Correctness: - chatTurnPhaseForMessage: treat metadata['responseDone'] as completed so the typing footer hides and the action row appears during the responseDone gap - timeline tail detection excludes archivedVariant assistants (history sliver already hides them; prevents a stale archived answer flashing as the live tail) - markdown display parts: image-only blocks keep non-empty normalizedContent so ConduitMarkdownWidget no longer drops standalone/base64 images - compose(): recompute mutableBlockStartIndex after tool_calls details merge across the frozen/tail boundary so the streaming tail keeps its fade; align the segment filter with isEmpty so blocks-only segments survive - bottom anchor: release the sticky latch on the final correction attempt so the scroll-to-bottom button isn't suppressed when a correction never lands - chat_page: suppress mount fade for a completed assistant migrating into history Simplification (behavior-preserving): - drop redundant streaming-preservation guards duplicated in the stale-echo check - remove dead carried state: ChatTurnFooterHost.sourceIndex/phase, MarkdownDisplayPart.kind/sourceBlockIndex/sourceBlockId, historyIndexByMessageId, completedFooterHost, and the pass-through bottom-anchor wrapper Tests: add coverage for the failed/responseDone/archived tail phases, the stale-echo completion fields + server-advance/non-tail preservation branches, the bottom-anchor state transitions + latch release, the image-block render path, display-part id uniqueness, and the tool_calls compose realignment + frozen tail. * Address review round 2 on chat streaming timeline Simplification: - extract the shared assistant-row wiring used by the history and live-tail slivers into _buildAssistantMessageRowContent, removing ~35 lines of duplication - gate animateOnMount on the settled (completed/failed) phase instead of responseDone alone, applied uniformly to both slivers — suppresses the mount fade for a completed tail on first load and on tail->history migration even when responseDone was never written, and removes the prior asymmetry - drop the redundant tailAssistantSourceIndex null check when building footerHost - collapse the unreachable non-ChatMessage fallbacks in _responseCompleted / _uiTreatsAsStreaming to the shared phase rule Tests: add coverage for the scroll-to-bottom hysteresis (currentlyShowing:true) and the non-scrollable re-anchor path; multi-row historyIndexByMessageKey; the no-mutable-metadata streaming display-part fallback; details-block display-part documents without a root node; compose() single-segment and empty-segment branches; co-firing streaming-state/content/modelName preservation; the StreamingTurnFooter running-haptic (fire-once, re-arm, suppression); and the errored-streaming action-footer phase at the widget level. * Address review round 3 on chat streaming timeline Correctness / consistency: - _activeStreamingAssistantId uses the chatTurnPhaseForMessage running phase so the scroll-keepalive agrees with the footer/pin logic across the responseDone gap - _correctStickyBottomAnchor clears the sticky latch when a correction is abandoned (widget gone / scroll not yet attached), instead of leaving it latched and wrongly suppressing the scroll-to-bottom button; generation / user-interaction aborts still bail without touching the latch Simplification: - ChatTurnFooterHost stores just the messageId (the only field ever read) - extract the duplicated sticky-latch predicate into _stickyLatchHolds - document the details-block id-rebasing invariant in _rebaseCompiledMarkdownBlock Tests: cover the sources/codeExecutions stale-echo retirement branches, the empty-message-list timeline, the completed-footer predicate across all phases, the empty-document and details-group display-part branches, and the streaming display-part cache reuse/eviction path across a shrinking part list. * Address review round 4 on chat streaming timeline Correctness / simplification: - _correctStickyBottomAnchor checks correction-generation ownership before the abandoned-correction latch clear, so a stale generation can't drop a newer correction's sticky latch - inline the unconditional footerSwitchDuration (Duration.zero) at its two AnimatedSwitcher call sites Tests: cover the trimLastBlockBottomPadding=false branch (trailing-block spacing) and its base-render cache invalidation; the StreamingTurnFooter hidden/empty state and the haptic re-arm on hide-then-reshow of the same id; the verifyStickyCorrection nearBottom-over-isFinalAttempt precedence; sticky-latch suppression while the user interacts; and display-part id dedup for blocks that share a block id. * Address review round 5 on chat streaming timeline All round-5 findings were test-coverage gaps (no code defects found). - add a @visibleForTesting debugShouldCleanupStreamingFromServer accessor and a seam test pinning the stale-echo guard vs. real (responseDone/error) completions in _shouldCleanupStreamingFromServer independently of the merge path - cover the boundToTail (socket-resumed foreign server id) streaming-state preservation path - cover shouldShowScrollToBottom with the sticky latch held AND currentlyShowing - verify the isMutableTail -> ConduitMarkdownWidget.enableStreamingTextFade gate end-to-end (only the mutable tail part fades) Not done: extracting the _correctStickyBottomAnchor generation counter into the controller purely to unit-test it. The reviewer confirmed no bug exists there and the generation-ownership ordering is already covered by the round-4 guard fix; a test-only production refactor was judged not worthwhile. * Lock chatTurnPhaseForMessage null / non-assistant branch Final convergence pass: rounds 5 and 6 surfaced no code defects, only test-coverage observations. Add a direct test for the one remaining trivial untested public-function branch (null / non-assistant -> ChatTurnPhase.none). * Fix bottom-anchor latch detach + cross-conversation leak (review) Two Medium findings from Macroscope on the bottom-anchor logic: - updateAnchor() no longer force-detaches mid-drag: while a sticky content change is pending it keeps the view anchored regardless of isUserInteractingWithScroll, so only shouldDetachForUserScrollAway (which honors userScrollAwayThreshold) breaks the latch. Previously a sub-threshold accidental drag during streaming dropped bottom anchoring. - _handleConversationChanged() clears the sticky latch on conversation switch so a new conversation doesn't inherit a stale anchored state. The clear lives here rather than in _cancelPendingStickyBottomCorrection() because that method is also called on drag-start, where the latch must survive to gate the scroll-away threshold. Rewrote the interaction controller test to lock the corrected small-drag-keeps- anchor / threshold-drag-detaches behavior. * Fix responseDone/failed phase staleness (CodeRabbit review) - Include responseDone in the chat message structure signature so the list shell rebuilds on the responseDone transition (running footer host / pin-to-top would otherwise stay stale through the responseDone gap until the transport isStreaming flag cleared). - AssistantMessageWidget: re-run the action-row settle when message.error changes, and settle on the failed phase, so a turn that fails in place while isStreaming is still true reveals the action row instead of staying stuck. Added a mid-stream-fail regression widget test. - CompiledMarkdownDocument.isMutableRootBlock bounds-checks the index against blocks.length so an out-of-range query can't be misclassified as mutable. Not changed: CodeRabbit's suggestion to clear the latch in _cancelPendingStickyBottomCorrection (would break the drag-start threshold; the conversation-change leak is already handled in 2d4a08a). * Smooth streaming bottom-follow + fix long-response scroll jump Two streaming/scroll UX issues (root-caused with adversarial verification): Bug 1 — streamed text stepped UP from the bottom instead of fading in place: bottom-stickiness was a per-chunk instant jumpTo (_scrollToBottom(smooth:false)), so each token batch yanked the viewport up a frame after the taller content was painted, overpowering the in-place suffix fade. _handleLiveTurnSizeChange now requests a delta-aware smooth follow: for small per-chunk growth (<= 48px from bottom) it glides to the bottom over 140ms and settles the sticky latch directly (verifyStickyCorrection(nearBottom:true)) without the synchronous-distance read or per-frame recursion the jump path uses; larger growth and all non-live corrections (initial settle, extent-invalidation) still jumpTo. The latch/verify logic and the _isUserInteractingWithScroll guard are otherwise untouched. Bug 2 — scrolling up through a long response jumped past the prompt to the first response: _estimateChatMessageExtent hard-clamped to 2400px, far below a real multi-thousand-pixel response, so a never-yet-measured long history row (e.g. one that migrated out of the live-tail sliver) produced a large SuperSliverList scroll-offset correction on first reveal. Raised the ceiling to 20000, added code-block/image structure terms, and excluded base64 data-uri payloads from the line estimate so a generated image can't over-estimate. Added estimate tests. Note: Bug 1 is animation behavior (not unit-testable); verify in-app with a fast streaming response that text fades in place, the scroll-to-bottom button never flashes mid-stream, and stickiness still converges. * Estimate rendered height for raw standalone base64 image lines (review) Macroscope: the row-extent estimate stripped data-uri payloads from the line count but only added a per-image term for markdown ![] images, so a raw standalone base64 image line (the common generated-image form, converted to an image at display time with no markdown wrapper) under-estimated to ~one line and re-introduced the scroll jump. Now count each rendered image once: markdown ![] images plus raw standalone data-uri lines (a data-uri inside a markdown image is not at line start, so it isn't double-counted). Added a regression test. * Use idiomatic string generation in estimate tests (review) CodeRabbit flagged 'A' * 20000. It compiled/ran via a transitively-imported String operator* extension, but that's a fragile dependency; switch to List.filled(20000, 'A').join() for an unambiguous, self-contained payload. * Exclude fenced-code-block markup from the image extent heuristic (review) Macroscope: image/data-uri markup inside a fenced code block renders verbatim (as text), so counting it as a rendered image (+220px) and stripping its data-uri payload from the line count corrupted the estimate — a code sample displaying a base64 blob would under-estimate and re-introduce the scroll jump. Now separate fenced code blocks: count their content in full for line height and apply the image-term / data-uri-strip logic only to prose outside them. Added a test. * Harden #540 fix: clear stale content on scope change + unique scopeless PageStorage keys (review) Two Macroscope findings on the #540 fix area: - Cross-message/version stale content: with 'always render a non-null doc', a StreamingMarkdownWidget reused across a stateScopeId change (a version/message switch) briefly showed the previous scope's content while the new body compiled. Fix without re-introducing the #540 flash: thread a clearStaleDocument signal so a scope change clears the stale doc (skeleton covers the gap), while same-scope content growth keeps its document. Added a regression test (fails before: stale 'First version' lingers on scope switch). - PageStorage key collision: _stateScopeIdForPart fell back to the raw partId when no stateScopeId was given, and block ids repeat across documents, so two scope-less StreamingMarkdownWidgets on a route cross-restored details expansion state. Use a unique per-instance scopeless fallback prefix. * Retire stale streaming echo once server moves past the turn (#537 review) Greptile: _shouldCleanupStreamingFromServer treated an empty non-streaming server echo of the in-flight assistant as 'keep streaming' even when the server snapshot also carried newer messages after it. The stale-echo early return ran before the serverMessages.length > state.length cleanup check, so the old assistant stayed marked as the active stream after the server had already moved past that turn — leaving the streaming footer/task state pinned to a no-longer-tail message. Gate the stale-echo return on the server NOT having additional messages, the same serverHasAdditionalMessages guard the sibling _shouldPreserveLocalAssistantStreamingState already uses, so the cleanup and preserve paths agree. Adds a regression case to the existing cleanup test. * Finalize the stream when a metadata-only server snapshot arrives (#537 review) _isStaleStreamingAssistantEcho classified any content-empty server snapshot as a stale streaming echo, ignoring statusHistory, versions, and usage. A final metadata-only snapshot (e.g. a closing statusHistory entry with empty content) was therefore misclassified as stale, which forced isStreaming back to true via _preserveFreshLocalAssistantState and kept _shouldCleanupStreamingFromServer from finalizing — leaving the typing indicator stuck until a later refresh. Require statusHistory/versions/usage to also be empty before treating a snapshot as a stale echo, and extend the parameterized completion-field seam test with statusHistory, versions, and usage cases. * Keep the stream alive for metadata-only status echoes (#537 review) Reverts the statusHistory/versions/usage check added in de7da9b1. Those fields are populated on the assistant message *during* streaming — the server pushes status/usage updates as content-empty, non-streaming snapshots before the answer tokens arrive (streaming_helper) — so treating their presence as a completed update retired the active stream prematurely and dropped the typing footer mid-turn (Greptile P1). Real completion is already proven by responseDone/error or by non-empty content/output/files/embeds/followUps/sources/codeExecutions, so a finished turn is never a metadata-only echo. Document why those fields are excluded and replace the completion-field test cases with a regression test that a content-empty, in-progress status-only echo keeps the stream alive. * Preserve resume-bound remote id through stale server adopts (#537 review) Dropping transport before preserve cleared `_boundRemoteMessageId`, so a foreign-id stale echo could retire the streaming tail early. Also skip DB-watch adopts while resume streaming is active. * Cancel the message stream before dropping transport on adopt cleanup CodeRabbit: `_dropStreamingTransportState` nulls `_messageStream` without canceling the controller, so cancel must run first while it is still attached. * Address post-rebase review findings on streaming timeline polish Fix blank details shrink, sticky detach on scroll-toward-bottom, queued typing footer, pin-to-top stuck after drag, and foreign-id stream cleanup. * Add staggered follow-up animation and fix pin-to-top scroll jump * fix(chat): preserve streaming anchor on drag start Non-update scroll notifications were mapped to a synthetic threshold-sized delta after the rebase, bypassing the small-drag guard. Pass no delta until a real ScrollUpdate arrives and cover that boundary. Also remove the stale-scope test duplicated from main so the shared markdown cache no longer makes the full suite fail. * fix(chat): detach streaming anchor for pointer scrolls Wheel and trackpad updates carry no drag details, so the touch-only gate skipped their delta after the directional notification. Treat null-detail updates as user-driven only while a user interaction is active, preserving programmatic scroll corrections. Add classifier coverage for touch, pointer, and programmatic paths so this input-path regression cannot recur unnoticed. * fix(chat): cancel retired streaming controllers Server adoption could release a tracked StreamingResponseController without cancelling its subscription, allowing late chunks or terminal callbacks to run against an adopted snapshot. Make the shared stale-transport teardown cancel the controller before dropping its reference. Add provider coverage that adopts a replacement snapshot and proves the retired controller is inactive and ignores a late chunk. * test(chat): cover streaming cleanup cancellation Exercise server adoption with a live streaming tail and a real tracked controller so the needsCleanup path is guarded directly. Assert responseDone settles the message, cancels the controller, and suppresses late chunks. * fix(chat): remove post-response settle motion Completion reused two independently added motion paths: follow-up suggestions ran a staggered entrance while every measured live-tail resize requested the streaming smooth bottom-follow. That chained a settle animation with an animated scroll after the response. Render follow-ups immediately and allow smooth bottom correction only while the turn phase is running. responseDone and settled follow-up layout changes now preserve the bottom anchor instantly, with regressions covering both decisions. * fix(chat): polish motion and reduced motion * fix(chat): address motion review feedback * fix(chat): keep archived versions settled |
||
|
|
fdf89a8149
|
Fix streaming body strobing between content and loading skeleton (#540) (#541)
* Fix streaming body strobing between content and loading skeleton (#540) Photosensitivity/seizure hazard: during a response the assistant body flashed at ~13Hz between the rendered markdown and the MarkdownLoadingSkeleton. Root cause: StreamingMarkdownWidget let the loading skeleton (and a second SizedBox.shrink blank path) REPLACE already-rendered content. When the turn phase latches to completed mid-response (responseDone gap / status+tool phases), the body is treated as non-streaming while answer tokens keep arriving; each non-streaming content growth left the compiled document stale-but-valid during the async recompile, and the gate regressed to the skeleton instead of keeping the rendered content. Make the skeleton/blank strictly a first-paint state: once a compiled document exists it is always rendered (a stale doc lags at most one frame and is superseded by _applyCompiledDocumentState), so the skeleton can only appear when there is no renderable content yet. Removed the now-dead _preserveStaleCompiledDocumentUntilFreshFinal field. Adds a deterministic regression test (delayed compile service) reproducing the non-streaming content-growth stale window; fails before the fix (skeleton over rendered content), passes after. * Clear stale content on scope change so a version switch can't show the old body (#541 review) Macroscope + Greptile flagged that the #540 fix ('always render a non-null doc') lets a reused StreamingMarkdownWidget show the previous message/version's content while the new body compiles asynchronously. Fix without re-introducing the #540 strobe: thread a clearStaleDocument signal so a stateScopeId change (a new message/version) clears the stale document — the skeleton covers the gap — while same-scope content growth keeps its document. Added a regression test (fails before: the previous version's text lingers on a scope switch). * Don't make a stale streaming document selectable (#541 review) Macroscope: the #540 fix removed the blank path that previously prevented a stale (non-streaming, not-yet-fresh) document from being wrapped in SelectionArea. During the responseDone gap the body is non-streaming while content still grows, so a stale doc was made selectable — re-enabling the concurrent-modification crash in Flutter's selection system. Gate SelectionArea on hasFreshCompiledDocument so a stale doc renders (no #540 flash) but isn't selectable until the compile settles. Extended the regression test to assert it. * Clear stale streaming document on scope change (#541 review) Greptile: the scope-change clear only ran on the non-streaming path. A scope change while streaming (e.g. switching from an old version back to the live, still-streaming current) kept the previous scope's compiled document visible until the deferred refresh landed, flashing a prior message under the new scope. Extend the existing clearDocumentWhenAsync mechanism to resolveStreamingPrepared (same async-only clear, so cached/sync compiles never flash), thread it through _resolveCompiledDocument's streaming branch, and carry the scope change into the deferred streaming refresh via _pendingClearStaleDocument so only the first post-scope-change refresh clears (same-scope growth still keeps its doc, #540). Adds a regression test for the deferred streaming scope-change path. (cherry picked from commit 11d75779fcab4c0fb3e338f8d4d4b96c7d950448) * Clear the stale streaming document immediately on scope change (#541 review) CodeRabbit + Greptile: the deferred streaming scope-change path only armed a pending clear (_pendingClearStaleDocument), consumed later by the scheduled refresh. build() now keeps rendering any existing compiled document while streaming (#540), so the previous scope painted for one frame under the new stateScopeId before the deferred refresh landed — exactly the flash this guards. Clear synchronously in didUpdateWidget instead: add MarkdownDocumentController.clearDocument() (cancels in-flight resolves, nulls the document, notifies) and call it on the deferred-path scope change; the scheduled refresh then compiles the new content. Removes the _pendingClearStaleDocument flag machinery entirely. The sync streaming and non-streaming paths already clear synchronously via clearDocumentWhenAsync, so all scope-change clears now happen within didUpdateWidget — no first-frame stale paint on any path. clearDocument() always invalidates pending async resolves first (even when no document is rendered yet) so a stale compile started under the old scope can't land after the clear. Test asserts the FIRST post-switch frame is already clear, that the new body stays absent until the compile is released (proving a genuine async gap), and pins the harness key so the switch exercises didUpdateWidget reuse rather than a remount. Verified: the first-frame assertion fails with the deferred behaviour, passes with the immediate clear. (cherry picked from commit 2798fe542d9477fe1fc3a4f518a2aac549282390) |
||
|
|
2468f7143e
|
Open WebUI streaming parity: modelName, optimistic state, fade caching, socket resume (#523)
Brings the chat streaming experience closer to upstream Open WebUI. - Model name: extract/display Open WebUI modelName (camel+snake), thread through the durable payload, preserve across streaming-error handling and server-snapshot adoption (incl. empty placeholders and empty-server-value edges). - Optimistic state: preserve locally-streamed assistant content/follow-ups when adopting a server snapshot, scoped to the streaming tail; metadata-union keeps local-only fields; consolidated streaming-error teardown (fail-by-message-id). - Fade caching: cache the opacity-1 span tree per content change and fade only the suffix via AnimatedBuilder (no per-frame markdown rebuild); widget spans only fade when fully past the boundary; surrogate/LaTeX/code offset alignment preserved. - Sidebar indicator: socket-driven active-chat set, optimistic setActive on task_ids with last-task-aware + token-guarded setInactive (incl. delayed done-recovery path), clear on logout. - Socket resume: reopening an in-flight chat streams token-by-token over the shared Socket.IO events envelope (REPLACE/APPEND, foreign message_id binding, nested-id extraction, chat-scope guard), with the 1s task poll demoted to a fallback that resolves the server message by the bound foreign id. Verification: build_runner + flutter analyze clean; affected suites green (new coverage for fade caching, active-chats sync, resume binding/finalize races, metadata preservation). Reviewed via Greptile (iterated) and CodeRabbit; Macroscope correctness check passing. |
||
|
|
a24f993e66
|
Offline-first persistence with Drift (#508)
* feat(persistence): CDT-RFC-001 Phase 0 — drift foundations, blob mapper, golden fixtures, fake server harness - Add drift 2.34 / drift_flutter 0.3 / drift_dev; scaffold AppDatabase with per-server open seam (D-08) and the sync_meta table (§6) - ChatBlobMapper (pure Dart) with the §6.1 round-trip invariant: payload/ rawExtra preservation, presence sentinels, unmappable-entry passthrough, deriveChildrenIds, treeIsConsistent - 12 golden chat-blob fixtures authored verbatim from openwebui-src shapes (branched trees, files, web-search sources, code execution, usage, embeds, error/annotation/arena/merged, legacy no-history, timestamp ties) - FakeOpenWebUiServer test harness mirroring vendored list/create/get/update/ delete semantics incl. the update route's shallow top-level merge and output→content re-derivation (serialize_output port) - 137 new tests; full suite 1577 passing; analyze clean for all new files - RFC amended to rev 3: §3.iii is a shallow top-level merge, not blob replace; pushes must always send the complete reconstructed blob * feat(persistence): CDT-RFC-001 Phase 1 — read path, DB as source of truth Schema/DAOs/manager: - chats/messages/folders/outbox tables (schema v2) with hand-rolled DESC and partial indexes (§10) and FK cascade; ChatsDao/MessagesDao/FoldersDao/ SyncMetaDao; per-server DatabaseManager + scoped provider (D-04) - conversation_assembler: MessageRowData.payload -> ChatMessage / Conversation Sync engine (pull only this phase): - chat_locks per-chat async mutex; pull_sync watermark delta (§7.1: 5s overlap, pool of 4, one transaction per chat merge, watermark advances only on full success); SyncApiClient seam over api_service / FakeOpenWebUiServer - sync_triggers (§7.6): once-only start gate, auth/connectivity edges, foreground + 5-min periodic timer with offline skip, single debounced funnel - sync_engine orchestrator; §9.3 legacy Hive purge gated on first fully- successful pull, idempotent via hive_cache_purged flag Provider inversion (read path): - conversation list renders from ChatsDao.watchChatList() (no message bodies); chat page from MessagesDao.watchForChat(); streaming overlay untouched - D-07 stream-completion + app-pause echo into DB under the chat lock - all Hive local_conversations/local_folders reads removed Tests: DAO/manager/pull/triggers suites; offline cold-start acceptance; new D-07 notifier-level persistence test; sync_triggers timing fixes (fakeAsync timer flush + redirect materialization). Full suite 1670 green, analyze clean. * feat(persistence): CDT-RFC-001 Phase 2 foundation — outbox, ID remap, push, migrator (dormant) Data-layer write path, built and tested in isolation; not yet wired into production (task_queue.dart remains the live path until the cutover commit). - OutboxDao: transactional enqueue (no inner txn), per-chat-FIFO claim respecting nextAttemptAt + parked predecessors, coalescing collapse (updateChat→newest, create+updates→create, delete annihilates), N=5 requestCompletion parking, stranded-inFlight recovery (§7.2) - OutboxDrainer: pool-of-2, seq order per chat, backoff 2s→5min full jitter (injectable clock+jitter), offline-deferred reschedule (no attempt burn) - IdRemapper: single-tx local:<uuid>→serverId rewrite of chats/messages/ outbox; content-hash recorded on createChat op (§7.3) - pull_sync crash-heal: pendingCreateForHash matches a pulled chat to its pending createChat op and remaps instead of duplicating - push_sync: createChat/updateChat/deleteChat reconstruct the FULL blob via rowsToBlob (§3.iii), serverUpdatedAt/dirty-clear rule; SyncApiClient extended (fake server mirrors shallow-merge + toggle semantics) - ChatsDao *WithOutbox mutation methods (row + op in one tx, caller holds lock) - OutboxTaskQueueMigrator: idempotent, flag-gated, content-hash dedupe, abort-without-flag on failure (§9.2) Full suite 1767 green, analyze clean. Cutover (drainer/migrator invocation, send-path + mutation + remapEvents wiring, task_queue retirement) follows. * feat(persistence): CDT-RFC-001 Phase 2 cutover — outbox is the live write path Activates the dormant outbox in production and retires the legacy Hive task queue. Phase 2 acceptance (§11) now met. Activation: - ChatRequestCompletionRunner (features/chat) implements the core RequestCompletionRunner seam, re-entering the EXISTING streaming pipeline (runQueuedCompletion) — no second streaming impl; guards: defer when a live stream owns the chat, no-op when the turn already completed; shared assistantMessageId guarantees one assistant row per turn (R8) - Provider layer: idRemapper/pushSync/outboxDrainer/requestCompletionRunner + migrator, all reusing the engine's chatLocks/folderLocks/clock/remapper - SyncEngine._runOnce: after pull, run migrateIfNeeded() once then drain(); single shared OutboxDrainer instance (drain + drainNow) — fixes a concurrent-instance resetInFlightToPending double-send (review high finding) - drainNow() on connectivity-regained; remapRouteSyncProvider swaps the open chat/folder id in place on remap (no nav, no stale-route window) Site swaps (15) + retirement: - send/retry/folder-mutation/peripheral sites → outbox *WithOutbox methods (row + op in one tx under ChatLocks); optimistic UI preserved - media uploads folded into media_upload_controller.dart (not an outbox op) - deleted task_queue.dart / task_worker.dart / outbound_task.dart Tradeoff (documented for Phase 3): queued completions use Option A — a drained background completion makes that chat active. Option B (headless UI-free streaming sink) deferred to Phase 3. §11 acceptance tests: offline-compose→force-quit→reconnect→create+send+remap; queued-Hive-task migration→drain; crash-between-create-and-remap heals without duplicate. Full suite 1776 green, analyze clean. * feat(persistence): CDT-RFC-001 Phase 3 — three-way merge, deletion reconcile, folder LWW Conflict merge (§7.4) — replaces the Phase-2 fast-forward-only stub: - chat_merger.dart pure three-way: union messages by id (local-dirty wins, remote-deleted clean drops, dirty kept), childrenIds always derived from parentId, currentId local-if-dirty, envelope LWW, rawExtra server-wholesale - idempotent (B unchanged on three-way; re-pull hits noRemoteChange); never drops a dirty message; ancestor-reachability re-parents a surviving dirty descendant when a clean ancestor is remotely deleted (no dangling parentId) - mergeServerChat treats a never-synced stub (serverUpdatedAt==null) as a plain server write, not a body-dropping three-way Deletion reconcile (§7.5): - full-ID enumeration diffed against local server-keyed chats; purge ONLY on a confirmed-gone probe; 24h throttle (background) + manual pull-to-refresh - safety valve aborts if >50% of local chats are candidates (token-expiry guard); a watermark-0 pull records last_full_reconcile_at (it already fully enumerated) so the first cycle does no redundant re-enumeration - session-liveness re-check before the first purge: the vendored GET returns 401 for genuine deletes AND for an expired token, so a one-shot authed ping distinguishes them and aborts without purging if the session is dead - wired into SyncEngine._runOnce (background) + reconcileNow() (manual) Folders (§7.6): LWW pull, tombstones, folderUpsert orders before dependent chat ops; folder delete uses delete_contents=false. Review findings (fix phase died on infra) applied inline: orphan re-parenting, session-liveness guard, reconcile wiring + regression tests. Full suite 1823 green, analyze clean. * feat(persistence): CDT-RFC-001 Phase 4 — FTS5 offline search, lazy list, perf budgets FTS5 search: - standalone chat_fts vtable (unicode61, remove_diacritics) over message content + chat titles, created via schema migration; triggers keep it consistent across every message/title write path (insert/update/delete) and an explicit chats-AFTER-DELETE trigger purges on FK-cascade (recursive triggers off) and tombstone-filtered out of results - populated AFTER first full sync (unawaited, idempotent, flag-gated) so it never blocks first interactive render; onUpgrade backfills existing installs - SearchDao: bm25-ranked, grouped one row per chat with snippet; fts_query sanitizer turns arbitrary user text into a safe parameterized MATCH (quotes, operators, empty/unicode/injection all neutralized — no crash) - offline search wired into the existing serverSearchProvider: ranked FTS when offline/reviewer, FTS fallback when server search unavailable - id_remapper now repoints message FTS rows on local:->serverId remap (was a no-op TODO that dropped the index for offline-composed chats — review high) Lazy list + perf: - paged conversation-list reads from the DB (no load-all), Conversations provider API stable - §10 budgets measured on a 1000-chat file-backed fixture: cold-start→list 21-104ms (≤400), append-to-500-msg ~0ms steady (≤10), one list emission per merge tx (zero jank), FTS population off the render path fts-build db-closed teardown race logged at debug (expected on server switch), not error. Full suite 1883 green, analyze clean. * feat(persistence): CDT-RFC-001 Phase 5 — notes as a synced entity Notes sync through the SAME infra as chats (outbox/drainer/id-remap/FTS/locks), extracted behind a SyncEntityAdapter seam now that two real adapters exist. - notes table + NotesDao (field-LWW upsert, *WithOutbox row+op in one tx, tombstone); note_mapper with the §6.1 round-trip invariant (access_grants + unknown keys preserved verbatim via rawExtra) - note_sync: nanosecond notes_pull_watermark in its OWN sync_meta key, never compared to the chat (seconds) watermark — distinct overlap constants (D-11, R-09); list-ordered-updated_at-DESC pull mirroring PullSync - note_conflict (pure): field-level LWW resolving title and data independently; a concurrent data edit spawns a conflict copy (local data preserved, never silently dropped) with no infinite-copy chain (canonical goes clean + base advances) - OutboxKind noteCreate/noteUpdate/noteDelete/notePin with patch-map coalescing; pushNoteUpdate always carries title (router requires it); pin via the dedicated toggle endpoint reconciled against server state - IdRemapper entityKind='note'; FTS extended over note title+text (kind discriminator partitions note rows from chat rows in the shared vtable) - offline notes UI (list + editor) wired into navigation; search returns note hits alongside chat hits SyncEntityAdapter seam (chat + note adapters); SyncEngine drains both kinds and pulls both with separate watermarks. Stabilized after the build agents died mid-run (sustained API outage): fixed test API drift (drainer adapters param, RecordingSyncApiClient note methods), and wrote the RFC-mandated acceptance tests the dead agents never reached — R-09 watermark isolation, D-11 conflict-copy/field-LWW, §6.1 round-trip. Full suite 1899 green, analyze clean. Known follow-ups (low): notes have no deletion-reconcile yet (server-side note deletes not purged locally); notes_dao/note_sync integration unit tests pending (core conflict resolver + wiring covered). * feat(persistence): CDT-RFC-001 follow-ups — non-disruptive completions, note reconcile, note sync tests (1) Option B — non-disruptive queued completions: - request_completion_runner now DEFERS (throws CompletionBusyException → op stays pending) when a DIFFERENT chat is foregrounded, so a drained background completion never yanks the user to another chat. It drives live (Option A's proven pipeline) only when its own chat is active or none is. - SyncEngine.drainOutbox() (plain drain, no backoff reset) + a SyncTriggers listener on activeConversationProvider so a deferred completion runs the moment the user opens its chat. - (Chosen over a from-scratch headless streaming consumer, which would re-implement ~370 lines of subtle stream-accumulation logic that can't be validated without a live server — strictly riskier than this for an edge-case UX win. A true background-streaming sink remains an optional follow-up.) (2) Note deletion-reconcile (§7.5 for notes): - NoteDeletionReconcile mirrors the chat reconcile over the note list/probe endpoints: purge only on a confirmed-gone probe, 50% safety valve, session- liveness guard, own throttle key (notes_last_full_reconcile_at), note lock domain; wired into SyncEngine background + reconcileNow. (3) Note sync/reconcile integration tests through the real DB: - note_deletion_reconcile_test (8): gone-purge, transient-skip, safety valve, dead-session abort, feature-disabled, throttle - note_sync_test (3): nanosecond-watermark pull (+ chat watermark untouched), the DB-level conflict copy (concurrent data edit → two surviving notes, none lost), updateNoteWithOutbox row+op in one tx - runner tests extended for the Option B deferral policy Full suite 1911 green, analyze clean. * feat(persistence): CDT-RFC-001 Option B — headless background completions (live-validated) Live validation against the env.local server proved the load-bearing fact: Open WebUI persists the assistant message SERVER-SIDE during a completion (server's utils/middleware.py upsert_message_to_chat...; the outlet handler 'replaces the POST /api/chat/completed round-trip'). A probe that fired a real completion and DISCARDED every stream chunk still found the full reply persisted on the chat. Also confirmed: note updated_at is 19-digit NANOSECONDS and chat updated_at is 10-digit SECONDS (D-11/R-09 holds on the real server), and the persisted reply lands in history.messages in the shape the pull-merge consumes. So the headless consumer needs NO re-implementation of stream accumulation — the pattern is fire -> drain -> pull: - runHeadlessCompletion (chat_providers): builds the request from the target chat's DB rows (not the globally-active chatMessagesProvider), fires sendMessageSession, drains the byte stream to EOF so the server runs to completion, then pullChatNow merges the server-persisted reply into the local DB (Phase 3 merge) — bounded poll for the socket/task flow. - request_completion_runner now drives LIVE (Option A) only when the target chat is the one the user is viewing, else runs HEADLESS — never switching the user's active conversation. Replaces the prior defer-only policy. Full suite 1911 green, analyze clean. * refactor(persistence): review + simplify pass over the offline-persistence branch Multi-round adversarial review/simplify loop (findings verified before applying; sync invariants preserved). Full suite 1911 green, analyze clean. Correctness: - note_deletion_reconcile: the session-liveness probe now actually detects a dead session — getNoteListRaw swallows 401/403 and signals via featureEnabled=false, so a thrown error OR (_, false) both abort with no purge (the prior try/catch could not catch the swallowed auth failure) - runHeadlessCompletion: both transport flows now poll pullChatNow with backoff until the reply lands (the server persists asynchronously — ENABLE_REALTIME_CHAT_SAVE defaults False; live-validated that attempt 0 still catches the common case, retries cover the async-persist edge) Simplification / dedup / dead-code (behavior-preserving): - chats_dao: _writeServerRows+_writeMergedRows → one _writeChatRows; shared _activeChatsListQuery() backs watchChatList+getChatPage; _maxLastReadAt via math.max - id_remapper: chat+note FTS remap share _remapFtsRowsWhere - chat_providers: runQueuedCompletion/runHeadlessCompletion share request-build helpers; _modelUsesReasoning + _lastUserMessageId extracted - note_sync/note_mapper/folders_dao/notes_dao: shared decodeJsonMap + asNs - outbox_drainer: dropped 3 unused injected deps; collapsed duplicate branches - pull_sync/chat_merger: removed dead params; optimized_storage_service: deleted the dead legacy-cache migration path - chats_drawer/media_upload_controller: minor dedup + firstOrNull Deliberately NOT applied (documented): cross-file timestamp-helper collapse (load-bearing R-09 ns-vs-seconds semantics), conversation_assembler key-coercion tolerance, and an upload-concurrency change (not behavior-preserving). * refactor(sync): remove dead NotePullSync.run() parallel driver; test the live path Final review round (1 confirmed finding). NotePullSync.run() + pullNote() + _parseListItem + NotePullResult + _ChangedNote + kNotePullFetchConcurrency were a ~95-line parallel copy of the generic runPullFor driver, dead in production (the engine drives note pull EXCLUSIVELY via runPullFor(NoteAdapter), which delegates only getListPageRaw/fetchRaw/mergeNoteResponse). Deleted the copy and rewired note_sync_test to drive the real production path (runPullFor over a NoteAdapter) — so the watermark / conflict-copy / outbox assertions now exercise the code that actually runs. Kept the live seam (NotePushSync, mergeNoteResponse, _asNs, fetch/list helpers). Full suite 1911 green, analyze clean. * fix(sync): address greptile review — reconcile safety-valve floor (P1) + archive race (P2) Greptile branch review (PR #508) found a real defect my own loop missed. P1 (deletion_reconcile + note_deletion_reconcile): the safety valve 'candidates > total * 0.5' tripped for small libraries — a user with ONE server chat/note that was genuinely deleted hit '1 > 0.5 = true', so the valve aborted on every run (even manualRefresh) and the phantom row never purged. Added an absolute floor: the fraction valve now trips only when candidates exceed max(kReconcileMinCandidatesForValve=5, total*0.5). Safe because the session-liveness guard + per-candidate confirmed-gone probe are the real token-expiry protection; the fraction valve is only a coarse backstop against an implausibly-LARGE candidate set. Regression tests added (single deletion now purges; valve still trips above the floor). P2 (push_sync archive toggle): gave the archive toggle the same read-before- toggle race protection the pin path already has — re-read the live archived state via getChatRaw and flip only on a real delta, so a concurrent client can't make it blind-toggle archive back to the wrong state. Full suite 1913 green, analyze clean. * fix(sync): address greptile re-review round 2 (note-pin probe, archive null-guard, dead push, notes reconcile gate) Greptile re-review confirmed the prior P1/P2 resolved, then found 4 more: 1. P1 pushNotePin did a blind toggle-FIRST then corrected, leaving a transient wrong-state window (and the doc claimed it probed first). Now probes live is_pinned via getNoteRaw and toggles only on a real delta — symmetric with the chat pin/archive paths. Regression test added. 2. P2 (regression from the prior archive fix): getChatRaw can 404 if the chat was deleted in the window after updateChat succeeded; toggling a gone chat threw a terminal error and PARKED an op whose updateChat already succeeded. Now skips the toggle when the probe returns null (reconcile purges the row). 3. P2 dead code: _ensureDrainer built a discarded PushSync purely for a null-guard already subsumed by _buildAdapters(). Removed. 4. P2 efficiency: pre-advance the NOTE reconcile gate after a watermark-0 pull (symmetric with the chat gate) so a fresh install skips a redundant getNoteListRaw + full-ID diff right after the first full pull. Full suite 1914 green, analyze clean. * polish(sync): address greptile round-3 minor observations (4/5 'safe to merge') All P1/P2 resolved last round; greptile rated this 'safe to merge'. Cleaning up the remaining minor observations: - note_adapter.listPageSize → large sentinel (1<<30): notes fetch in a single unpaged call, so this stops runPullFor from ever requesting an empty page 2 for users with ≥60 notes (was one no-op Dart loop iteration, no network). - note_conflict.resolveNoteMerge: added an assert(serverUpdatedAt >= base) for parity with chat_merger, surfacing a server-clock regression in tests instead of silently no-op'ing. - note_deletion_reconcile: documented the deliberate trade-off that a Notes- feature-disabled-mid-reconcile reads as session-dead (conservative skip, no data loss; chat side can't conflate because getChatListPage throws on auth). - migrator raw==null: left as-is — the existing comment already documents why the flag stays unset (a later SharedPrefs→Hive migration could still land tasks); marking it migrated would risk dropping those. Full suite 1914 green, analyze clean. * polish(sync): address greptile round-4 style observations (still 4/5 'safe to merge') All correctness concerns resolved in prior rounds; these are the 3 remaining style-level observations: - sync_engine: capture + log the note pull's AdapterPullResult (note-cycle-done telemetry) so a stuck note watermark / failed fetch has success-path observability, mirroring PullSync.run's chat cycle-done log. - SyncEntityAdapter: removed the dead 'locks' getter (never read through the interface — each adapter's real lock domain lives in its injected PullSync/ PushSync). Dropped the now-unused _locks field + constructor param from both ChatAdapter/NoteAdapter and their construction sites; future adapters no longer implement an unused member. - dedup the pull concurrency constant: kPullFetchConcurrency now DERIVES from the canonical kAdapterPullFetchConcurrency (single literal), so chat and note pull concurrency can never silently diverge. Full suite 1914 green, analyze clean. * fix(sync): address greptile round-5 — migration duplicate-turn (P1) + reconcile try/catch (P2) P1 (data corruption for upgrading users): OutboxTaskQueueMigrator's existing- chat branch deduped only on a deterministic v5 message id, which never matches the server's own ids. _runOnce pulls BEFORE migration, so a legacy 'running' task whose completion already finished server-side was re-appended as a DUPLICATE turn + an unwanted second requestCompletion (extra AI generation, corrupted conversation). Added _latestTurnAlreadyCompleted: if the chat's most- recent user message matches the task text and already has a non-empty assistant reply, the turn is done → skip. Targets the latest turn (not any historical message) to avoid false-skipping legitimately-repeated text. Regression test added. P2: split the shared chat+note reconcile try/catch in _runOnce and reconcileNow so an unexpected chat-reconcile error can't skip the note reconcile (each entity now has its own try/catch + log scope). Also fixed a latent log-level bug: the benign FTS db-closed teardown race was still logging at ERROR because the substring check was 'closed' but the SQLite message says 'closing'/'re-open'; now matched and downgraded to debug. Full suite 1915 green, analyze clean. * polish(sync): address greptile round-6 edge cases (active-branch migration guard, pin 404) Both non-blocking P2s (greptile: 4/5 'safe to merge'); refining for correctness: - _latestTurnAlreadyCompleted now follows the ACTIVE branch via currentMessageId (tip → parent user message) instead of orderIndex. orderIndex is a DFS over the full message DAG, so a regeneration branch could lead it and an off-branch message with coincidentally-matching text could false-skip a migration task. Walking currentMessageId is the precise active-branch turn. Test updated. - push pin/archive: ONE getChatRaw liveness fetch now guards BOTH toggles. The pin path previously called getChatPinned with no 404 guard (asymmetric with the archive path); a chat deleted in the window would throw and cost an extra drainer retry cycle. Now a 404 on the shared liveness fetch skips both toggles (reconcile purges the row); getChatPinned is still used for pin accuracy but only after the chat is confirmed to exist. Full suite 1915 green, analyze clean. * fix(sync): address greptile round-7 P1 — migrated turn orphaned in pulled chats The migrator's existing-chat branch always set the new user message's parentId to null. For an already-pulled chat (with prior messages), the migrated turn became a DISCONNECTED root in the conversation DAG: pushing updateChat sent a blob where the new user message floats as a new root, and OpenWebUI's active- branch trace (currentId → parent) reached only the orphan, HIDING all prior history — a permanently malformed conversation. rowsToBlob emits each message's payload verbatim (no childrenIds derivation), so the fix threads existing.currentMessageId (the prior active-branch tip) as the new user message's parentId (column + payload). existing == null (fresh stub) correctly stays a null root. Regression test asserts the migrated turn hangs off the prior tip. Full suite 1916 green, analyze clean. * harden(sync): address greptile round-8 defensive P2s (4/5 'safe to merge') Both are defensive guards for conditions that should not arise in normal operation: - chat_merger: added a release-mode runtime guard for serverUpdatedAt < base. The assert is elided in release, so a clock-skew/server-bug could fall through to fast-forward, overwriting local rows with a STALE server snapshot AND regressing the merge base below its prior value. Now mirrors resolveNoteMerge: leaves local rows untouched (noRemoteChange). The assert stays to surface the anomaly in tests. - chat_adapter.pushOp: requestCompletion (which IS in ownsKind but is dispatched by the drainer's RequestCompletionRunner seam BEFORE the adapter loop) now THROWS instead of silently no-op'ing, so a future reorder that lets it fall through surfaces loudly instead of silently dropping the completion. Full suite 1916 green, analyze clean. * fix(sync): preserve folder on envelope stub refresh * fix(sync): harden migration and folder delete dedupe * fix(sync): park malformed folder delete ops * perf(sync): avoid redundant pull work * fix(sync): dedupe note delete and pin-only pulls * fix(sync): retry task queue migration failures * fix(sync): treat missing folder delete as success * fix(sync): guard corrupted chat merge cycles * fix(sync): defer stub chat update pushes * fix(sync): stop retrying missing folder updates * fix(sync): full-fetch notes and close create remap gap * fix(sync): harden note merge and fts cleanup * fix(sync): remove stale note conflict claims * fix(sync): dedupe pending chat deletes * fix(sync): close note create remap window * fix(sync): retry fts build after watermark advances * test(sync): cover fts build retry failure * fix(sync): prevent recursive note conflicts * fix(sync): preserve dirty stubs during pull * fix(sync): drop local note tombstones * fix(search): page combined results and filter fts backfill * fix(sync): drop annihilated chat and folder stubs * fix(sync): close chat create remap window * fix(sync): park malformed completion ops * docs(sync): update create heal lock comment * fix(sync): address CodeRabbit review findings * fix(sync): preserve folder parent edits * fix(sync): use remapped folder outbox id * fix(sync): clear CodeRabbit folder follow-ups * fix(sync): skip tombstoned note creates * fix(sync): merge coalesced folder upserts * fix(sync): resolve CodeRabbit follow-ups * fix(sync): crash-heal note creates * docs(sync): clarify note create lock ordering * fix(sync): bind cycles to dependency epoch * fix(search): make fts backfill idempotent * fix(sync): harden note and fts error handling * perf(sync): bound outbox and search scans * fix(sync): defer chat create for local folders * chore(sync): tidy note search review items * fix(sync): refresh coalesced note create hash * fix: address CodeRabbit review findings * fix(sync): assert coalesced note row invariant * fix(sync): harden note pull and pin coalescing * fix(sync): clamp null current message ids * fix(sync): refire start and abort terminal reconcile * fix(sync): back off chat folder remap deferral * fix(sync): fail malformed note list pages * fix(sync): skip malformed note list slots * fix(sync): atomically reassert chat merge pushes * fix(sync): address greptile review feedback * fix(sync): keep echo turns and fts deletes consistent * fix(sync): abort chat reconcile on auth probe failure * Fix CodeRabbit sync review issues * fix(sync): harden echo replay and note reconcile * Fix CodeRabbit drain migration issues * fix(sync): preserve dirty chat titles on stub refresh * Fix CodeRabbit replay and remap issues * fix(sync): refresh coalesced chat create hashes * address greptile review feedback (greploop iteration 1) * fix(sync): guard deferred pulls and completions * fix(sync): prevent duplicate headless completions * fix(sync): address CodeRabbit review feedback * fix(sync): isolate outbox queue domains * fix(sync): cleanly cancel pending completions * test(database): avoid duplicate FTS migration message id * fix note pull pagination after malformed pages * fix(chat): preserve image attachment types in durable sends * fix(sync): tighten outbox follow-up handling * fix(sync): unblock deletes and bound pull pagination * fix(sync): guard reconcile pagination * fix(sync): stop pull pagination on malformed pages * Sandbox inline web embeds * fix(sync): harden note merge edge cases * fix(sync): drop stale note pin ops * fix(sync): claim create heals before remap * fix(sync): preserve migrated attachment urls * fix(sync): preserve note edits across create remap * fix(sync): resolve stale update and mime edge cases * fix(sync): purge outbox ops after push deletes * fix(sync): preserve note conflict metadata * fix(sync): remap open note routes * fix(sync): harden note remap edge cases * fix(sync): handle archive confirmation races * fix(sync): purge orphaned folder outbox ops * fix(sync): avoid stale streaming and archive stalls * fix(sync): mirror chat pin races * fix(sync): harden upload and legacy migration races * fix(sync): clarify outbox remap coverage * Address CodeRabbit review findings * Implement chat message queuing and handling for offline scenarios, including UI updates for queued completions and localized messages for user feedback. Enhance the `ChatsDao` with methods to manage queued completions and update assistant message links. Refactor `AssistantMessageWidget` to display queued message states and integrate new localization strings for various languages. * Address Greptile chat merge dedupe finding * Refactor authentication and database handling: Removed redundant checks in `AuthStateManager`, updated database schema documentation, added error handling in conversation loading, and enhanced message retrieval methods in `MessagesDao`. Improved attachment upload handling with cancellation support and refined sync triggers for better error management. * refactor: apply reviewed simplifications and edge-case fixes Round 1 of an automated review+simplify pass over the branch diff. Verified each change is behavior-preserving (or a confirmed edge-case fix) and runs clean through `flutter analyze` + the test suite. Bug/edge-case fixes: - database_manager: track and await pending db close before deleteFor, closing an open/close race - pull_sync: stop double-counting archived-with-synced-body chats in changedChats / cycle-done log - media_upload_controller: clean up converted temp dir on the error/abort path instead of leaking it - sync_triggers: seed foreground state on cold launch so the periodic foreground pull starts when launched already foregrounded Simplifications / dead-code removal: - auth_state_manager: drop unused params and duplicate error log - notes_dao: remove dead `meta` param from updateNoteWithOutbox - search_dao: drop unused `kind` projection from the hits CTE - fts_query: make _quoteToken non-nullable, remove unreachable guard - note_mapper / note_sync: collapse duplicate _decodeMap/_asNs helpers onto the shared decodeJsonMap/asNs - outbox_task_queue_migrator: fetch chat row once instead of twice - push_sync / sync_api_client: remove always-true guard and unreachable 404 branch - backoff: drop redundant .toInt() - chat_transport_dispatch / request_completion_runner / local_conversation_loader: simplify return, fix stale doc comments, drop redundant cast/import - chat_page: fire attachment uploads non-blocking with error logging * fix(sync): keep remap route consumer live across session rebinds remapRouteSyncProvider subscribes to SyncEngine.remapEvents exactly once at startup, but remapEvents returned the current per-session IdRemapper's stream. Every dependency rebind (db/client/auth/lock/clock/backoff/ completion-runner swap) disposes that remapper and lazily mints a new one with a fresh stream, leaving the startup subscription bound to the dead stream — so post-rebind remaps were silently dropped and an open chat route would never swap its local id to the server id in place. Back remapEvents with a long-lived broadcast controller owned by the engine and forward each session's remapper into it, re-establishing the forward on every rebind. The consumer's single subscription now survives rebinds. Verified by remap_route_sync_test + sync_engine_test. * refactor: second review+simplify pass (dead code, parity, doc fixes) Round 2 over the branch diff. Verified behavior-preserving (or a confirmed consistency fix) via flutter analyze + full test suite. Behavior / consistency fixes: - app_providers: Conversations.updateConversation now requests a reconcile pull on a cold/missing chat-list projection instead of silently dropping the envelope mutation, mirroring Folders.updateFolder - database_manager: chain pending db closes (don't drop an in-flight close on rapid switches) and await the pending close in closeActive Dead-code removal / simplification: - sync_api_client: drop the no-op _withTerminalAuth wrapper; the *Raw endpoints already translate 401/403 -> SyncTerminalException, so its auth branch was unreachable. createFolder keeps an explicit inline translation (its endpoint does not translate) - chats_dao: merge adjacent identical `if (placeholder != null)` blocks - folders_dao / note_sync: inline single-use private helpers - deletion_reconcile: drop always-true operand from safety-valve guard - request_completion_runner: gate live-only reads behind the active-chat check via short-circuit Doc-comment corrections: - id_remapper: fix trigger #6 -> #7 FTS citations - api_service / request_completion_runner_provider / app_providers: correct stale references and offline-only comment * fix: third review+simplify pass (verified findings) Applies 17 adversarially-verified findings from a multi-agent review of the branch's changed production files. Correctness: - chat_merger: carry server.unmappableMessageOrder through three-way merge so unmappable history keeps its original positions on round-trip. - chat_page: clear context attachments on durable send (were silently re-sent next message) and recover the streaming placeholder + log on durableSend failure instead of swallowing it. - folder_page: fire attachment uploads concurrently (unawaited + catchError) so one upload failure no longer aborts the remaining attachments. - user_message_bubble: skip note attachments in _inlineEditAttachmentIds so a note id is not re-sent as a bogus file attachment. - home_widget_service: fire photo uploads concurrently and focus the composer once up front rather than after each blocking upload. - deletion_reconcile: log the underlying error/stackTrace on preflight abort. Simplification / docs: - auth_state_manager: drop dead canCommit param from _claimAuthCommit. - chat_providers: drop dead parentId param from _localEchoRow. - chat_transport_dispatch: remove unused writeAbortHandleMetadata. - push_sync: drop redundant trailing return. - note_deletion_reconcile: drop dead localServerIds.isNotEmpty guard. - Doc fixes: chats_dao getChatPage, sync_meta_dao watermark, app_database trigger count, id_remapper outbox-status comment. flutter analyze clean; affected sync/chat/dao/widget suites pass. * fix: fourth review+simplify pass (round 2 verified findings) Three adversarially-verified findings from re-reviewing the round-1 changes. Correctness: - folder_page: recover the streaming placeholder + log on durableSend failure in the folder composer send path (parity with chat_page; was a try/finally with no catch, leaving the assistant bubble stuck isStreaming forever). - push_sync: a folder move forces pinned=false server-side, so run the move BEFORE the pin reconcile and treat the post-move pin as false — a pinned chat moved in the same coalesced update no longer silently loses its pin. Adds a regression test (and teaches the fake server to reset pin on move, mirroring the vendored update_chat_folder_id_by_id_and_user_id). Simplification: - auth_state_manager: drop the dead canCommit/claimCommit/trackAuthAttempt plumbing from _loginInternal/_loginWithApiKeyInternal (their only callers are the public wrappers, which never pass these), removing ~6 always-true branches from security-sensitive code. Silent-login path untouched. flutter analyze clean; push_sync (incl. new pin+move test) and folder_page suites pass. * docs: correct stale _claimAuthCommit comment (round 3) Round 2 removed the two login() call sites that passed claimCommit: null, so _claimAuthCommit's only remaining caller is the silent-login commit, where claimCommit is non-null on the background path. Update the comment to describe the actual foreground(null)/background(non-null) split instead of the now-false "null at every call site" claim. Comment-only; no behavior change. * refactor: drop now-dead clearOnFailure param (round 4) Cascade from round 2/3: once the login methods stopped passing clearOnFailure, no caller supplies it, so the guard `clearOnFailure == null || clearOnFailure()` was always true. Remove the parameter and collapse the catch to the unconditional token clear it always performed. Behavior-preserving. * feat: enhance notes feature with user-specific data and markdown previews - Updated NoteListEntry to include userId and a bounded markdown preview. - Modified watchNotes method to filter notes by userId and return a markdown preview. - Implemented searchNotesByQuery to allow searching notes with user context. - Added functionality to getNoteForUser to restrict note retrieval to the current user. - Introduced caching for feature availability based on user and server context. - Enhanced note context actions to handle markdown previews correctly. - Updated note editor and list views to utilize new data structure and features. This commit improves the user experience by ensuring that notes are displayed and managed according to the authenticated user's context, while also optimizing data handling for markdown previews. * address greptile review feedback (greploop iteration 1) * address greptile review feedback (greploop iteration 2) * refactor: enhance chat request completion logic to handle optimistic placeholders - Updated the logic in ChatRequestCompletionRunner to prevent deferring when the assistant's own optimistic placeholder is active. - Added checks for the active streaming assistant ID and its relation to the current assistant message ID. - Enhanced logging to include additional context data for better debugging. - Introduced a test to verify that the runner does not defer its own optimistic streaming placeholder. This change improves the responsiveness of the chat feature by ensuring that the assistant can continue processing without unnecessary delays when it is actively streaming its own message. * address greptile review feedback (greploop iteration 1) * refactor: apply review + simplification findings (ultracode round 1) Per-file code review + simplification pass over the offline-persistence/sync branch. 34 confirmed, behavior-preserving fixes plus targeted bug fixes: - id_remapper: repoint nested child folders' parent_id on folder remap (prevents orphaned subfolders after an id swap) - share_receiver/chat_page: harden attachment upload loops against a throwing file.length() and run uploads non-blocking - notes_providers: cancel Drift watch subscription on every recompute (remove one-shot onDispose guard that leaked the subscription) - many duplicate-logic, dead-code, and idiom cleanups across daos/sync/mappers Reverted two auto-applied changes that regressed: a streaming_helper snapshot-refresh that contradicts existing tests, and a chats_drawer firstWhereOrNull that pulled in a non-production collection dependency. Verified: flutter analyze clean, full test suite (2099) green. * fix: resolve 4 sync/persistence correctness bugs (ultracode round 2) Adversarially confirmed (with code + openwebui-src evidence) and fixed the real bugs round 1 flagged but deemed unsafe to auto-apply: - outbox_task_queue_migrator: new-chat dedup contentHash folded in per-message wall-clock timestamps, so a partial-failure re-run on a later second produced a different hash, missed dedup, and POSTed a DUPLICATE chat. Blob per-message timestamps are now deterministic (decoupled from the clock); row envelope timestamps unchanged. createChatContentHash (binding contract) untouched. - chat_providers: headless stream drain leaked the HTTP byte-stream subscription/socket on timeout (Future.timeout does not cancel the drain). Now aborts the request (CancelToken) before deferring, in both error branches. - remap_route_sync_provider: an open /folder/<local-id> route was not remapped after a folder id swap, leaving a dead 'unable to load folder' page. Now rewrites the open route to the server id, mirroring the note-route branch. - sync_api_client.updateFolder: a 2xx response with a non-map/null body (server returns 200+null on duplicate-name/db-error while the folder still exists) was treated as a 404 and PURGED the local folder. Null is now reserved for genuine 404s; a 2xx-null is treated as success and keeps the folder. Added regression tests for each. Verified: analyze clean, full suite (2104) green. * refactor: convergence cleanups (ultracode round 3) Re-reviewed the round 1+2 diffs for newly-introduced issues. No bugs found; addressed the remaining low-severity duplication/dead-code: - fts_query: drop redundant terms.isEmpty guard (join already yields '') - note_mapper: inline _serverJsonObjectOrEmpty (now a pure passthrough to _asMap) - remap_route_sync_provider: collapse the duplicated note/folder route helpers into one shared _remappedSingleSegmentRoute, and extract the duplicated log/go/try-catch navigation into a single goRemappedRoute closure - notes_providers: remove the now-dead _watchedDb/_watchedUserId dedup branch (the dispose-before-recompute fix makes the watch always re-subscribe; both branches already cancelled + re-subscribed, so the tracking was inert) Verified: analyze clean, full suite (2104) green. * feat: route note mutations through the durable offline outbox Note create/update/pin/delete and the note editor's save paths were API-first: they wrote through api.* and only persisted after the server responded, so offline edits were lost and never queued — unlike chat, which already uses the *WithOutbox DAO path. (greptile review feedback) - NoteUpdater/NotePinToggler/NoteDeleter now write the row + outbox op transactionally under noteLocks.runExclusive, then kick the drainer; the UI updates from the reactive watchNotes stream. - NoteCreator creates a durable local note + noteCreate op when offline, staying API-first when online so the editor opens on the server id with no local->server remap underneath it. - note_editor_page save/audio/file-remove paths route through a shared durable _persistNoteUpdate helper. - Drop debugPrints that logged note content/data (privacy). - Update NoteUpdater/NotePinToggler tests for durable semantics. * fix: read back durable note edits through the local->server remap Greptile P1: durableUpdate/Pin/Create read back via getNote(id), but the *WithOutbox writers resolve a stale local: id to the remapped server id before mutating. After a create remap, an editor autosave holding the local id wrote the server-id row but read back null -> the editor showed the save as failed and kept stale state. - Add NotesDao.getNoteResolvingRemap; durable helpers use it for read-back. - Test: getNoteResolvingRemap follows a local->server remap. - Make NoteUpdater/NotePinToggler durable tests deterministic with a no-drain SyncEngine override so the queued op stays PENDING. (greploop iteration 2) * fix: take the note lock on the resolved (remapped) id Greptile P1: durable note mutations acquired noteLocks on the caller's stale local: id, but the *WithOutbox writers resolve local->server internally before mutating. The UI write could then serialize on a different key than concurrent pull/push for the same row, interleaving an autosave with a pull merge / outbox push and losing one side's update. Resolve the remap target BEFORE acquiring the lock and use the resolved id for the lock, the DAO write, and the read-back. Replaces the narrower getNoteResolvingRemap read-back helper with NotesDao.resolveNoteRemapTarget. (greploop iteration 3) * fix: use the caller-captured session in _persistNoteUpdate Greptile P1: _persistNoteUpdate re-read the live database/API instead of the session the caller captured before its awaits. If a save, audio attach, or file removal started on one account and the user switched mid in-flight await, the helper could write the old editor's note + content into the newly active account's database. Pass the captured api/db into the helper and bail (return null, persist nothing) when the session is no longer current before writing. (greploop iteration 4) * fix: alias the note lock key on create crash-heal remap Greptile P1: the pull-side create crash-heal remapped a local: note to the server id while holding only the local-id lock but never aliased the lock key. An edit/pin/delete already queued on the stale local id could then run under the local-id lock while the DAO resolves internally to the server id and mutates the server row — interleaving with pull/push that lock on the server id. Mirror the create-push path: call _locks.remapKeyInPlace(localId -> serverId) after remapper.remapNote succeeds so queued mutations reroute to the server-id lock. (greploop iteration 5) * fix: don't publish authenticated without a user on cached-token bootstrap Greptile P1: the stored-token fast-path published AuthStatus.authenticated with user=null when a token existed but no user row was cached, so isAuthenticatedProvider2 became true while currentUserProvider2 stayed null. User-scoped local reads (notes) then cancel their Drift watch and render empty; offline, background validation never recovers the user, so the state stays hidden for the whole session. - When no user is cached, hold the normal startup loading/revalidation state (token kept) instead of authenticating; background validation promotes to authenticated WITH the user once reachable. - Add an offline fallback (_proceedAuthenticatedWithoutCachedUser) so a genuinely-offline no-cached-user bootstrap proceeds instead of hanging on loading — no worse than before for that corner, strictly better online. (greploop iteration 5, auth follow-up) * fix: resolve no-cached-user auth bootstrap to re-login, never userless-auth or hang Greptile P1 follow-ups to the cached-token bootstrap fix: - The offline fallback still published AuthStatus.authenticated without a user, so isAuthenticatedProvider2 was true while currentUserProvider2 was null (user-scoped notes pass the gate then return empty). - A transient (non-auth) getCurrentUser failure only logged, leaving the no-cached-user bootstrap stuck on AuthStatus.loading (splash) forever. Replace the userless-authenticated fallback with _failBootstrapWithoutCachedUser, which resolves the bootstrap to AuthStatus.unauthenticated (needsLogin; token kept) whenever a scoped user can't be recovered — offline, unreachable, user==null, or a transient validation error. Never userless-authenticated, never an indefinite loading hang. The cached-user path (offline-first common case) is unchanged. (greploop iteration 6) * fix: re-check note session after durable await before publishing Greptile P1: the durable update/pin/create provider branches captured api/db, awaited the DAO/lock write, then published the returned note (and updated activeNoteProvider for pin) without re-checking the session. A server/account switch during the await could surface a note from the previous database into the new session. Re-check _isCurrentNoteSession after each durable await before setting shared note state / activeNote (mirrors the API-first branches). Delete is unaffected (publishes only a bool, never a note). (greploop iteration 6) * fix: guard stale session on durable note delete success Greptile P1: the durable delete branch returned true after the DB/outbox await without re-checking the captured session. The editor caller treats true as success and navigates away, so a server/account switch during durableDeleteNote could publish a delete success into the new session. Mirror the create/update/pin post-await guard: re-check _isCurrentNoteSession and report false (no success) when the session changed. (greploop iteration 7) * fix: re-resolve note id after drain in durable update/pin read-back Greptile P1: durableUpdateNote/durablePinNote read back with the id resolved BEFORE the drainer kick. The drain can push a brand-new note and remap its local: id to the server id between the write and the read, so the read looked up the deleted local row and returned null — a false 'save failed' that left the editor on stale state. durableCreateNote already re-resolved post-drain; unify all three through a shared _drainAndReadBackNote helper so update/pin do too. (greploop iteration 8) * fix: hold loading during bootstrap silent login to avoid sign-in flash Greptile P1: on cold start with saved credentials but no stored token, the init branch published AuthStatus.unauthenticated before starting the background silent login. authNavigationStateProvider maps that to needsLogin, so the router briefly showed the sign-in page before a valid silent login completed and bounced the user back to chat. - Hold AuthStatus.loading (splash) while the bootstrap silent login is in flight instead of unauthenticated. - Widen the background canCommit() to accept the loading state so a successful login still commits authenticated. - Wrap the call (_bootstrapSilentLogin) with a safety net that resolves loading -> unauthenticated when the login commits nothing, so failures reach the sign-in page instead of hanging on the splash. (greploop iteration 8, auth follow-up) * fix: final freshness gate before committing silent-login session Greptile P1: _commitSilentLoginResult published the in-memory authenticated state (invalidate providers, _update, install token) after the last _canCommitAuth check without a final gate. A logout / newer auth attempt landing during persistence could let a stale background login resurrect the previous session. Add a final _canCommitAuth check immediately before the in-memory commit; when stale, restore the prior persistence (mirroring the existing setActiveServerId/saveAuthToken stale branches) and return false. (greploop iteration 9, auth follow-up) * fix: gate bootstrap silent-login fallback on the attempt revision Greptile P1: _bootstrapSilentLogin's fallback fired whenever the result was uncommitted and state was loading-without-token — but a newer foreground login that just started is also loading-without-token. A stale bootstrap task could then publish unauthenticated and interrupt the newer login, bouncing the user to sign-in. Capture _authAttemptRevision before the background login and only run the fallback when it is unchanged (no newer login/logout/token-invalidation, all of which bump it via _beginAuthAttempt). (greploop iteration 10, auth follow-up) * fix: clear rejected credentials on background silent-login auth failure Greptile P1: on a confirmed auth failure the background silent-login path returned false without deleting the saved credential or publishing credentialError. _bootstrapSilentLogin then turned that into a generic unauthenticated state, so a remembered-but-rejected credential/JWT was retried on every cold start instead of being cleaned up. Gate the cleanup on staleness (_canCommitAuth) instead of bailing for all background calls: a non-stale confirmed auth failure now deletes the bad credential and publishes credentialError (re-checking freshness after the delete await before committing state). (greploop iteration 11, auth follow-up) * fix: clear credentials for a deleted server on background silent login Greptile P1: when cold start has saved credentials whose server config was removed, the background silent-login branch returned without deleting the credentials or clearing the active server. _bootstrapSilentLogin fell back to unauthenticated, leaving stale creds so every later cold start re-entered the same impossible silent-login path. Gate the missing-server cleanup on staleness (_canCommitAuth) instead of bailing for all background calls: a non-stale attempt now deletes the credentials + clears the active server (re-checking freshness after the delete awaits before publishing the error state). (greploop iteration 12, auth follow-up) * fix: harden note readback + silent-login TOCTOU races (greptile round) - notes: read the just-written row back BEFORE kicking the fire-and-forget drainer (_readBackThenDrainNote). The write is already committed under the note lock, so reading first can't race a push+remap that deletes the local row — no UI-blocking await needed. (notes_providers.dart:145) - auth #1091: when a CLAIMED background silent login fails during persistence, resolve the in-memory state to unauthenticated before rethrowing, so the claim's revision bump doesn't leave cold start stuck on loading. - auth #1191: only clear missing-server credentials/active-server when the stored tuple still matches the attempted one (value-match) and the active server still points at the missing id. - auth #1422: snapshot the attempted credentials and delete on auth failure only if the stored tuple still matches, so a concurrent login's freshly saved credentials aren't clobbered. (greploop iteration 13) * fix: gate foreground silent login + tighten active-server clear - auth #1033: foreground _performSilentLogin now runs under a revision freshness gate (canCommit/claimCommit), so a saved-credential silent login in flight can't publish authenticated/credentialError/error over a newer manual login or logout that bumped _authAttemptRevision. - auth #1209: clear the dangling active server only when a final _canCommitAuth check passes AND it still equals the missing serverId, so a concurrent foreground login/server switch isn't clobbered. (greploop iteration 14) * fix: compare-and-clear active server on missing-server cleanup Greptile P1: the missing-server bootstrap cleanup read getActiveServerId() then cleared it, which it flagged as a clear-after-read race. Add an atomic-style OptimizedStorageService.clearActiveServerIdIfMatches(expected) helper (read + conditional write in one continuation) and use it, so a concurrently-selected active server is never cleared by a stale cleanup. (greploop iteration 15) * refactor(auth-storage): serialize auth read-modify-writes under a lock Greptile flagged the compare-and-clear/restore helpers as non-atomic (Hive has no CAS). Introduce a single OptimizedStorageService._authStateLock (package:synchronized) that serializes ALL writes to the auth token, saved credentials, and active server id, and run the compound read-modify-write helpers under one hold via private _*Unlocked bodies (lock is non-reentrant): - saveAuthToken/deleteAuthToken/saveCredentials/deleteSavedCredentials/ setActiveServerId now lock + delegate to _*Unlocked. - clearActiveServerIdIfMatches: locked compare-and-clear. - deleteSavedCredentialsIfMatches(expected): locked compare-and-delete. - restoreActiveServerAndTokenIfStale(...): locked value-matched restore. - auth_state_manager routes missing-server cleanup, auth-failure credential delete, and _restoreStaleSilentLoginPersistence through these atomic helpers, so a stale silent-login task can no longer clobber a newer login / server selection. Adds synchronized as a direct dependency. (greploop iteration 16) * fix: clear raw active-server id + restore partial silent-login writes - #344: clearActiveServerIdIfMatches / restoreActiveServerAndTokenIfStale compared against getActiveServerId(), which validates against saved configs and returns null once the server is deleted — the exact missing-server case — so the dangling raw preference was never cleared. Compare the RAW stored id (_readActiveServerIdState().rawServerId) under the lock instead. - #1395: _commitSilentLoginResult now restores value-matched persistence whenever a partial write happened (wrotePersistence), not only when the attempt went stale, so a storage failure between setActiveServerId and saveAuthToken can't strand the app on the silent-login server/token. (greploop iteration 17) * fix: read raw Hive active-server id (bypass cache) in compare-and-clear #346: clearActiveServerIdIfMatches / restoreActiveServerAndTokenIfStale read the active-server id via _readActiveServerIdState(), which returns the cached value — and getActiveServerId() may have already validated a deleted server and cached null. The compare then saw null and skipped clearing the dangling raw preference. Read the raw Hive value directly (_rawStoredActiveServerId, bypassing cache + validation) under the lock so a removed server's stale active-server id is detected and cleared. (greploop iteration 18) * fix: gate foreground logins on attempt revision + clear invalid saved tokens - #553: _loginWithApiKeyInternal, _loginInternal, and ldapLogin now capture the auth attempt revision and skip persisting a token / publishing authenticated|error state when _authAttemptSuperseded (a newer login / logout / token-invalidation started). The gate only trips under genuine concurrency, so single logins are unaffected; it stops a slow attempt from overwriting a newer one's session. - #1458: local saved-token validation failures (apiKeyNotSupported, invalid token format, empty token) from _authenticateSavedJwt are now treated as terminal credential failures, so the bad saved credential is value-match cleared instead of being retried on every cold start. (greploop iteration 19) * fix: re-check attempt revision before publishing foreground login state Closes the residual window named in greptile's summary: the foreground login gate checked _authAttemptSuperseded before saveAuthToken, but a newer login/logout could still start during the saveAuthToken/saveCredentials awaits, after which the older attempt would publish authenticated state. _loginWithApiKeyInternal, _loginInternal, and ldapLogin now re-check _authAttemptSuperseded immediately before the _update(authenticated) too, so a superseded attempt never publishes over the newer session. (greploop iteration 20) * fix: roll back superseded foreground login's persisted writes Greptile P1 (#781/#636): the post-persistence freshness gate stopped a superseded login from publishing state but left the tokenA/credentials it had already written to secure storage, so the next cold start could restore the rejected session. Add OptimizedStorageService.deleteAuthTokenIfMatches (locked compare-and- delete) and a _rollbackSupersededLoginWrites helper; the pre-publish superseded branches in _loginWithApiKeyInternal, _loginInternal, and ldapLogin now value-match delete the token (and remembered credentials) they wrote before returning false — never clobbering a newer login's writes. (greploop iteration 21) * fix: roll back token/credentials on failed foreground login Greptile summary residual: a foreground login that threw AFTER writing the token (e.g. saveCredentials or a later step fails) left the token in secure storage, so a cold start could restore a session the user saw fail. _loginWithApiKeyInternal, _loginInternal, and ldapLogin now track the persisted token/credentials and, in their catch, value-match roll them back via _rollbackUncommittedLoginWrites before rethrowing — so a failed login never leaves a restorable session. (greploop iteration 22) * fix: restore interceptor token when a foreground login is superseded Greptile P1: _validateIssuedToken installs the candidate token on the shared ApiService before the staleness check, so a superseded login left tokenA on the interceptor — subsequent requests could use it until the next update. Add _restoreApiServiceTokenToCurrent() and call it in every post-validation superseded branch (pre-save gate, pre-publish gate, and the catch) of _loginWithApiKeyInternal, _loginInternal, and ldapLogin, so the interceptor token is reset to the authoritative current auth state instead of keeping the rejected attempt's token. (greploop iteration 23) * fix: claim own attempt revision in foreground silent login Greptile P1: _performSilentLogin captured _authAttemptRevision without starting its own attempt. A silent re-login (retry / token-invalidation) could see the same revision as an in-flight manual login and claimCommit the OLD saved credentials, superseding the manual login. Call _beginAuthAttempt() up front so silent login owns a distinct revision: a manual login that starts afterward bumps again and wins, and a stale silent login can't commit over it. (greploop iteration 24) * fix: use one credential snapshot for silent-login attempt and cleanup Greptile P1: _performSilentLoginInternal snapshotted credentials, but _performSilentLoginAttempt re-read them for the actual login. If they changed between the two reads and the second set failed terminal validation, the failure cleanup value-match-deleted the FIRST snapshot, leaving the truly-bad credentials in storage to be retried each cold start. Thread the single snapshot into _performSilentLoginAttempt so the attempted login and the cleanup target are always the same credentials. (greploop iteration 25) * fix: roll back LDAP token writes on post-save !ref.mounted returns Greptile P1: ldapLogin had early 'if (!ref.mounted) return false' guards after saveAuthToken/saveCredentials that bypass the catch rollback, leaving an uncommitted LDAP token (and possibly credentials) in secure storage — a cold start could then restore a session that never committed in memory. Each post-save !ref.mounted guard now value-match rolls back persistedToken and writtenCredentials before returning. (greploop iteration 26) * fix: no retry/cancel banner on first send; gate terminal tab offline by cache 1. Queued-completion banner on first prompt of a new chat: queuedCompletionInfoForMessageProvider hid a fresh, never-attempted requestCompletion op only when isOnline. At app/first-send time connectivity is often still resolving (reported not-online), so the retry/cancel banner flashed on the first message. Hide any fresh pending op (no lastError, no nextAttemptAt) regardless of isOnline — it's 'sending', not 'queued'. The banner still appears once the op is genuinely offline-deferred (lastError='offline'), backoff-scheduled, or failed. 2. Terminal tab shown in offline mode even when disabled on the server: the sidebar derived showTerminalTab from terminalAvailableServersProvider with error/loading => true, with NO cached flag (unlike notes/channels). Offline, it always showed. Add a cached terminalFeatureEnabledProvider (per server/user, same _FeatureAvailabilityCache as notes/channels) and a terminalTabVisibleProvider that uses the live server list when resolved (writing back the cache) and the cached last-known value when offline. So a terminal-disabled server no longer surfaces the tab offline. Notes/channels offline gating was already correct (cached per server/user, default true); verified, no change. Tests: queued_completion_provider_test (fresh hidden offline; offline-deferred and parked still show) and terminal_tab_visible_test (live empty/non-empty; offline falls back to cached, not default-true). * fix: don't cache a placeholder empty terminal server list Greptile P1: terminalAvailableServersProvider returns an empty list when terminalServiceProvider is transiently null (startup / auth / active-server rebuild before the API is ready). terminalTabVisibleProvider treated that placeholder as a real 'terminal disabled' probe and cached false, poisoning the per-server/user cache so the offline/error fallback kept hiding the tab for a terminal-enabled server. Only persist the enablement after a real getAvailableServers() succeeds (service present); the null-service placeholder writes nothing. The cache write moves into terminalAvailableServersProvider (gated on a real probe); terminalTabVisibleProvider now just shows live data when resolved and the cached last-known value while loading/errored. * fix: keep terminal availability unresolved (not empty) when no service Greptile P1 (x2): the no-service placeholder still resolved terminalAvailableServersProvider as data:[], which terminalTabVisibleProvider treated as a completed 'terminal disabled' probe — briefly hiding the tab (and clamping navigation away) during startup/auth/rebuild even when the cached flag was enabled. When terminalServiceProvider is null, the provider now stays UNRESOLVED (loading) instead of returning an empty list, so terminalTabVisibleProvider's orElse falls back to the cached last-known value. A real probe (service present) still resolves to the live list and persists the enablement. selected-server consumers already tolerate the loading state (.asData?.value). * fix: derive sidebar create-action terminal visibility from the shared provider Greptile: sidebar_create_action computed terminal-tab presence with its own terminalAvailableServersProvider.maybeWhen(orElse: true), which no longer matched the sidebar's cached-aware terminalTabVisibleProvider after the stay-loading change. The two could disagree (e.g. terminal loading or cached-disabled), shifting the create-action's tab-index mapping out of sync with the rendered tabs and hiding the Channels create action. Both _watchTerminalTabVisible/_readTerminalTabVisible now read terminalTabVisibleProvider — one source of truth shared with the sidebar. * fix: only mark login credentials rollback-owned after they're written Greptile P1: writtenCredentials was set BEFORE saveCredentials succeeded, so if saveAuthToken succeeded and saveCredentials then threw, the catch's _rollbackUncommittedLoginWrites value-match-deleted a pre-existing identical remembered credential this attempt never wrote — losing remembered login data after a transient secure-storage failure. Set writtenCredentials only AFTER saveCredentials completes in _loginWithApiKeyInternal, _loginInternal, and ldapLogin. A failed credential write now leaves writtenCredentials null, so the catch rolls back only the token this attempt actually persisted. * fix: coalesce token-invalidation onto an in-flight silent re-login Greptile P1: onTokenInvalidated() bumped _authAttemptRevision before checking for an in-flight silent login. Two near-simultaneous 401s would have the second call bump the revision (marking the first silentLogin stale) and then skip starting a replacement (reloginInProgress) — the only re-login bailed and the app stayed in tokenExpired despite valid saved credentials. Early-return and coalesce onto the running silent login before any revision bump / token clear / state publish, so the in-flight login resolves normally. * style: group test imports (dart, external, project) per CodeRabbit Reorder imports in queued_completion_provider_test.dart (CodeRabbit finding) and terminal_tab_visible_test.dart for consistency: dart core, then external packages, then conduit project imports. * fix: don't flash queued banner for a transient completion retry The first message of a session can race a cold network/socket connection: the completion's HTTP request (sendMessageSession) fails once, the drainer markFailedRetryable's it with a ~1s backoff, and the auto-retry then succeeds. The previous queued_completion_provider fix still surfaced the retry/cancel banner during that ~1s window, so it flashed on the first chat message. Surface a PENDING completion only when it genuinely needs attention: - offline-deferred (lastError == 'offline'), or - stalled across >= _queuedCompletionStallAttempts (2) attempts. A failed (parked) op always surfaces for manual retry. A single transient first-attempt retry now auto-recovers invisibly. Socket lifecycle: the socket already connects proactively on auth-ready (_scheduleConnect), force-reconnects on connectivity regained, and the completion path ensureConnected's it (1200ms); the residual transient is the cold HTTP request, which the auto-retry recovers. Tests cover transient-hidden and stalled-shown. * feat: warm the API connection on auth so the first completion isn't cold The first chat completion of a session posts to /api/chat/completions via ApiService._dio. On a fresh session that connection is cold, so the first request can lose a TLS/HTTP handshake race and transiently fail (then the outbox auto-retries ~1s later). Warm that exact connection pool as soon as we're authenticated and the API service is ready: _runPostAuthenticationStartup now fires a cheap, fire-and- forget GET /health (ApiService.checkHealth, same _dio the completion POST reuses). The keep-alive connection is then warm for the first send. Pairs with the queued-completion banner fix: warming prevents the cold-start transient in the common case; the banner still stays hidden for any residual transient (e.g. connection idled out before the first send). * fix: keep the socket continuously available across manager rebuilds Root cause of the socket not being connected for the whole session: the socket manager is an async (FutureProvider) keepAlive provider, and socketServiceProvider collapsed its `loading` state to null on every rebuild — so the live socket momentarily read as null (dropping sends to HTTP-only). It also rebuilt needlessly because SocketTransportAvailability (derived fresh from backend config) had no value equality, so each recompute (e.g. when backendConfig resolves shortly after boot) emitted a new instance that forced a manager rebuild even when the transport was unchanged — landing right in the first-message window. Two fixes (no behavior change to genuine server/transport-change recreation): - Add == / hashCode to SocketTransportAvailability so a logically-identical resolved value doesn't churn watchers (incl. the socket manager). - socketServiceProvider now falls back to the manager's currentService while the manager is reloading, instead of returning null, so the live socket stays visible across rebuilds. Tests: socket_transport_availability_test (value equality). * fix: clear the rejected token before coalescing token-invalidation Follow-up to the coalescing fix: onTokenInvalidated early-returned when ANY silent re-login was in flight (manual retry / bootstrap, not just a prior invalidation), skipping the rejected-token cleanup. If that unrelated login then failed, the invalid token lingered and could be restored on the next cold start. Now value-match delete the rejected token (deleteAuthTokenIfMatches, so a fresh token the in-flight login may have saved is never clobbered) BEFORE coalescing. Still no revision bump / no duplicate login, so the in-flight login resolves normally. * fix: preserve note versions/files on editor saves (no data truncation) The note editor's save paths (plain save, audio-attach, remove-file) rebuilt the note `data` map from scratch with only content (+files), dropping `versions` — and the plain save also dropped `files`. Since the durable update persists/sends this map as the note's full data, every save truncated server-managed note data (version history, and attachments on a text-only save). Add _composeUpdatedNoteData: start from the existing note's data (_note.data.toJson() — content/versions/files) and override only content (and files when changed). All three save sites use it, so versions are always preserved and files survive a text-only save. * fix: merge note data patch in durableUpdateNote (preserve versions/files) Greptile: NoteUpdater.updateNote builds a content-only data blob, which (as the note's whole data) dropped versions/files locally and on the next server push — a latent data-loss path (not currently wired to UI, but real). durableUpdateNote now merges the partial `data` patch onto the existing row's data (decodeNoteData(existing) + patch), so content/files override while versions (and anything else) are preserved. This is the single durable chokepoint for both NoteUpdater and the editor; the editor's own _composeUpdatedNoteData still covers its reviewer/no-db API path. Test: NoteUpdater content-only update preserves existing versions and files. * fix: read the merge baseline inside the note lock in durableUpdateNote Greptile: the existing-note read for the data merge ran BEFORE acquiring the note lock, so a concurrent pull/merge (which takes the same note lock) could change the row between the read and the locked write, invalidating the merge baseline and clobbering the concurrent change. Move the getNote + merge inside noteLocks.runExclusive so read+merge+write are atomic w.r.t. other note ops on that id. * fix: editor sends a minimal note-data patch (no stale versions) Greptile: _composeUpdatedNoteData spread the stale in-memory _note.data (including versions); durableUpdateNote then merged {...dbRow, ...patch}, so the editor's stale versions overrode the DB row's current ones — reverting any server-added versions a pull made while the note was open, and pushing the reversion on the next drain. The editor now emits only what it changes (content, plus files when provided). durableUpdateNote's DB-row merge preserves server-managed versions (and files when unchanged) from the CURRENT row. * fix: show typing indicator directly so no empty block is reserved first The typing indicator was a child of the content AnimatedSwitcher, so when the gate elapsed the switcher reserved its full height while fading it up from zero opacity (220ms) on top of the dots' own fade-in. With the queued banner no longer flashing into that slot, the result read as an empty "blocked" block for a beat before the dots appeared. Render the indicator directly (instant) and keep the AnimatedSwitcher only for the content path. * feat: typing indicator becomes the streaming footer, swaps to action row The typing indicator now lives in the action-row position below the message and persists for the whole generation while text/tool-calls/status stream in above it, then crossfades to the action row once streaming completes — matching OpenWebUI. Previously it sat in the content slot and vanished as soon as the first tokens arrived. Also removes the action-row flash: the row is gated behind a settle signal (genuine streaming-end transition or a confirmed responseDone) and is reset while streaming, so a message that is or was streaming never paints the row mid-flight. History messages (never streamed in this widget) still show their row immediately. The footer slot is a single AnimatedSwitcher swapping typing <-> actions; the content slot no longer renders the indicator. Haptics tests now disable animations (the persistent indicator's repeating animation otherwise leaves pending timers at teardown); adds footer-slot + history-message tests. * feat: sidebar generating spinner for active chats (cold open + global) Adds OpenWebUI-style sidebar activity indication for any chat with an active server generation, including on cold open and for generations started by other sessions/devices. - api.checkActiveChats: POST /api/tasks/active/chats {chat_ids} -> {active_chat_ids}; empty input short-circuits, 404 degrades to empty and is cached so older servers aren't re-probed. - ActiveChatsSync (keepAlive): bulk-fetches into activeChatIds on first conversation-list load and on socket reconnect (setAll), and registers a GLOBAL wildcard chat:active handler so the set tracks every chat, not just the locally-streaming one. Activated from post-auth startup. - ConversationTile/Content gain an isGenerating spinner (tap-load spinner takes precedence); the drawer drives it from activeChatIds. Tests: checkActiveChats request shape + 404 caching; tile generating spinner and precedence. * feat: resume streaming when reopening a chat that's still generating Reopening a chat that is still generating on the server now shows the typing indicator and streams content in progressively, instead of an empty/partial response. The server never sends isStreaming, so: - _detectActiveOnOpen: on conversation change, probe the task registry (activeChatIds fast-path, else getTaskIdsByChat). If active, mark the last assistant message isStreaming:true (without clobbering content), pre-seed _observedRemoteTask, and start the remote-task monitor. Skips temporary chats, offline (probe failure), and genuine local streams; re-checks the active conversation after the await. - _syncRemoteTaskStatus: while tasks are active and no local transport owns the chat, adopt growing server content (monotonic-length guard via pullChatOrFetch) so the resume streams in. Final adopt on tasksDone is unchanged. - _isResumeStreamingActive guard: during a resume the progressive poll owns content; passive server refreshes and same-id snapshot adoption are skipped so an isStreaming:false snapshot can't end the resume prematurely. Tests: reopen-active re-engages streaming; settled chat does not; progressive content growth; temporary chats are never probed. * fix: value-match the token delete in onTokenInvalidated non-coalesce path The non-coalesce path called deleteAuthToken() unconditionally, which (when a concurrent foreground login had already saved a fresh token through _authStateLock) could delete that fresh token via lock-serialised save-fresh -> delete-fresh, leaving the app authenticated in memory with no stored token (a cold start then has nothing to restore). Capture the rejected token synchronously at the top of onTokenInvalidated() (before the silent-login check and any await/revision bump) and use deleteAuthTokenIfMatches(rejectedToken) in both the coalesce and non-coalesce paths, mirroring the already-fixed coalesce branch. Greptile #14/#15 (the 4/5 blocker). * fix: session-guard the cached-note fallback in noteById The catch path returned `cachedNote` (read before the detail fetch await) without the session check the success path performs. If the account switched during a failed `getNoteById`, a note from the previous session could leak into the new one. Add the same `ref.mounted` + `_isCurrentNoteSession` guard before returning the cached note. Greptile 4/5 blocker. * fix: re-arm active-chats cold-open fetch when a new socket is bound _initialFetchDone is a one-shot guard that was never reset, so after a logout/login cycle (a new SocketService is bound) the conversations listener skipped the initial bulk active-chats fetch and the new session's sidebar spinners were never populated. Reset the flag when binding a new non-null socket so the next conversation-list resolution re-fetches. (CodeRabbit) * fix: hoist noteById provider reads before the first await ref.watch(apiServiceProvider)/ref.watch(isOnlineProvider) ran after the getNoteForUser await, which is a Riverpod anti-pattern (the dependency may not be tracked and can throw on newer SDK versions). Read all provider deps up-front before any await. Tests that exercise noteById now override isOnlineProvider (previously short-circuited before it was watched). (Greptile) * test: assert the bootstrap silent-login revision-sharing contract Greptile's remaining 4/5 note: the _bootstrapSilentLogin / _performSilentLoginInBackground pair is correct but relies on a subtle revision-sharing pattern (both share the pre-existing revision; a successful login bumps it lazily via claimCommit) with no dedicated test, so a future change could silently break the concurrency contract. Extract the fallback decision into a pure bootstrapShouldFallbackToUnauthenticated() and refactor _bootstrapSilentLogin to use it, then assert the contract: the bootstrap fallback to unauthenticated fires only when nothing committed and the revision is unchanged in the loading/no-token state, and is suppressed by a successful commit, a newer attempt bumping the revision, a published session, or a restored token. (The success path itself is network-bound via an internal ApiService and not unit-testable; this locks in the revision logic.) |
||
|
|
1347b68098 | fix(network): enhance image URL origin validation and add effective port handling | ||
|
|
b058393596 | fix(security): restrict auth headers on markdown fetches | ||
|
|
9d28ea7fae | fix(security): use mermaid strict mode and escape chart error output | ||
|
|
b6ee878775 | fix(security): restrict external link launching to safe schemes | ||
|
|
107f6a90ef |
feat(pdf): integrate PDF rendering support in markdown
- Added support for rendering PDF links within markdown content using the new PdfInlineView widget. - Enhanced BlockRenderer to extract and render single PDF links with appropriate styling. - Updated InlineRenderer to conditionally display PDF previews instead of standard links based on configuration. - Introduced new dependencies in pubspec.yaml and updated pubspec.lock to include the pdfrx package for PDF handling. - Improved overall markdown rendering capabilities by accommodating PDF content. |
||
|
|
aa42cc9cc7 |
feat(markdown): enhance BlockRenderer to support standalone images in paragraphs
- Introduced functionality to render standalone images within markdown paragraphs, improving the visual layout of mixed content. - Added methods to handle soft line breaks before and after images, ensuring proper spacing and alignment. - Updated the rendering logic to accommodate inline text and images seamlessly, enhancing overall markdown rendering capabilities. |
||
|
|
67f0519483 |
fix(adaptive_ui): implement opaque glass fallback for adaptive buttons across chat and note editor components
- Introduced `conduitUsesOpaqueGlassFallback` utility to determine button styles based on platform capabilities. - Updated button styles in `ChatPage`, `ExpandedTextEditor`, `ModernChatInput`, and `NoteEditorPage` to utilize opaque glass fallback where necessary. - Enhanced visual consistency by applying the new styling logic to various adaptive buttons throughout the chat and note editing interfaces. |
||
|
|
5b18b3aa56 |
feat(chat): implement composer text insertion functionality
- Added `ComposerTextInsertion` class and provider to manage text insertion requests in the chat interface. - Integrated `composerTextInsertionTargetId` into relevant components, allowing for targeted text insertion. - Enhanced `ModernChatInput` to handle text insertion at the current selection, improving user experience during message composition. - Updated `StreamingMarkdownWidget` and `AssistantMessageWidget` to support the new text insertion feature, ensuring consistent behavior across chat components. |
||
|
|
c541667348 |
refactor(settings): remove large text preference and related functionality
- Eliminated the `largeText` preference from `UserSettings`, `AppSettings`, and related services to streamline accessibility settings. - Updated persistence keys and migrator logic to reflect the removal of large text handling. - Refactored UI components to remove dependencies on large text scaling, ensuring a more consistent text rendering approach. - Adjusted tests to align with the updated settings structure, removing checks for the now-removed large text preference. |
||
|
|
40c2c8034a |
feat(fonts): add Geist Sans and Geist Mono font families to pubspec.yaml
- Introduced new font families, Geist Sans and Geist Mono, with various weights and styles for enhanced typography options across the application. - Updated the main.dart file to streamline MediaQuery usage, improving accessibility handling. - Refactored accessibility service methods to remove unnecessary text scale clamping, ensuring more flexible text scaling. - Adjusted styles in various widgets to utilize the new typography settings, enhancing visual consistency. - Added tests to verify the integration of the new font families and their application in the UI. |
||
|
|
f337d88165 |
feat(tags): add support for model tags in platform APIs and UI components
- Updated the `PlatformNativeSheetModelOption` to include a `tags` field, allowing for better categorization and filtering of models. - Enhanced serialization and deserialization logic to handle tags in both Dart and Swift implementations. - Modified UI components to display tags, improving user experience by providing additional context for each model. - Updated search functionality to include tags, enabling users to find models more efficiently based on associated tags. - Added tests to ensure correct extraction and handling of tags from model data. |
||
|
|
20d7fcf099 |
feat(pinning): implement model pinning functionality in selectors
- Enhanced the NativeModelSelectorConfiguration to support pinned models, allowing users to pin and unpin models within the selection interface. - Updated the NativeSheetBridge to handle pin toggle actions and communicate changes to the UI. - Introduced methods for managing pinned models in the ServerUserSettings and SettingsService, ensuring persistence across sessions. - Modified the chat and folder pages to utilize the new pinning features, including context menus for toggling pin states. - Added tests to validate the behavior of pinned models, ensuring correct functionality and state management. |
||
|
|
134f32f895 |
refactor(responsive_drawer): remove haptic feedback on drawer interactions
- Eliminated haptic feedback calls from the ResponsiveDrawerLayout, ensuring a smoother user experience without tactile interruptions. - Updated related tests to reflect the absence of haptic feedback during drawer open, close, and settle actions, confirming that no haptic events are emitted. |
||
|
|
75039191bc |
feat(webview): migrate from webview_flutter_plus to flutter_inappwebview
- Replaced instances of `webview_flutter_plus` with `flutter_inappwebview` across the codebase for enhanced web view capabilities. - Updated related methods and classes to accommodate the new web view implementation, including cookie management and navigation handling. - Ensured compatibility with existing features while improving performance and functionality of web content rendering. |
||
|
|
eff8826bfc |
feat(refresh): standardize edge offsets for pull-to-refresh indicators across various components
- Added `sidebarRefreshIndicatorEdgeOffset` method to calculate edge offsets for pull-to-refresh indicators, ensuring they appear below sidebar chrome. - Updated `ConduitRefreshIndicator` instances in `folder_page`, `chats_drawer`, `notes_list_page`, and `notes_list_tab` to use the new edge offset for consistent UI behavior. - Enhanced `channel_list_tab` to include the new edge offset for its refresh indicator. |