conduit/lib/shared/widgets/server_version_warning_controller.dart
cogwheel 4e73c9a0ea
Some checks are pending
L10n / l10n (push) Waiting to run
Fix ten reported issues and vendor Hermes Agent as an API reference (#725)
* Fix ten reported issues and vendor Hermes Agent as an API reference

Closes #693, #695, #699, #700, #701, #703, #708, #712, #716, #723.

- Hermes (#693, #699): load transcripts over REST even when session.resume
  fails, page at 200 messages and shrink on oversized responses, and show
  distinct errors for too-large, not-found, and RPC failures. Add the
  hermes-src submodule (NousResearch/hermes-agent v2026.9.11) as the RPC and
  REST contract reference.
- Server version banner (#695): dismissible, remembered per server and version.
- Model selector (#700): overflow fade and selection tint use the card surface.
- OpenRouter (#701): skip Batch API-only ":batch" model variants.
- Persisted chat content (#703): store plain output text instead of rendered
  <details> markup when structured output exists, matching the web client.
- Direct connections (#708): probes report their real failure instead of a
  generic message, the Open WebUI logout fence no longer blocks Direct setup,
  and 401/403 under the api-key header mode hints at Bearer auth.
- Reasoning effort (#712): per-server, per-model user picks override the
  model's configured default and are sent explicitly in the request.
- Voice calls (#716): stop the record plugin from clearing the Android
  communication device and verify the loudspeaker route before reporting it.
- Voice mode (#723): finalize speech on the responseDone marker.
- Add the five new strings to every locale.

* Address review: keep incomplete-clear block, pass effort on sends, keep all dismissals

- Direct profiles stay hidden and unusable after an incomplete full app-data
  clear, in memory and across restarts, until the logout fence is cleared by a
  completed cleanup or a new authenticated session. Ordinary failed sign-outs
  still do not block Direct setup.
- The primary Open WebUI send path now passes the resolved per-model reasoning
  effort like regeneration and queued completions already did.
- Server version warning dismissals are stored as a bounded set so dismissing
  one server no longer forgets another.

* Address review: release rejected speaker device, speak settled text on responseDone

- When Android accepts the built-in speaker but the read-back disagrees,
  clear that selection before the legacy setSpeakerphoneOn fallback, which is
  ignored while a communication device stays selected.
- Voice mode takes the final text from the message once responseDone is set;
  the streaming content provider can lag behind the flushed message.

* Address review: await the incomplete-clear restart marker before returning

The marker is written through the app-data-clear barrier with a checked write
and awaited by the clear coordinator, so a process death right after an
incomplete clear cannot reload the surviving Direct profiles on restart.

* Address review: arm the restart fence before the wipe and propagate marker failures

The incomplete-clear marker is now armed before any data is wiped, disarmed
only when the clear completes or yields ownership, re-asserted on an
incomplete outcome, and a failed checked write aborts the clear instead of
being swallowed. The in-memory block is unchanged.

* Do not raise an error banner for response.incomplete

Open WebUI forwards the provider's `response.incomplete` event over the
socket but treats it as a no-op: the output is kept and the turn still ends
with a normal `done`. Raising an error on that event flashed "The response
stopped before it was complete" under the answer until `done` cleared it.
`response.failed` still surfaces the provider error.
2026-09-15 00:48:03 +05:30

74 lines
2.6 KiB
Dart

import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/persistence/persistence_keys.dart';
import '../../core/persistence/preferences_store.dart';
import '../../core/utils/debug_logger.dart';
/// Builds the dismissal token for the unsupported-server warning.
///
/// The token is scoped to the active server *and* its reported version, so a
/// dismissed warning reappears only for a different server or after that
/// server upgrades to another unsupported version.
String serverVersionWarningToken({
required String serverId,
required String? version,
}) => '$serverId|${version?.trim() ?? ''}';
/// Upper bound on remembered dismissals. Oldest entries drop first, so the
/// preference cannot grow without limit across many servers and upgrades.
const int kMaxServerVersionWarningDismissals = 32;
/// The persisted `<serverId>|<version>` tokens the user dismissed.
final serverVersionWarningDismissedProvider =
NotifierProvider<ServerVersionWarningController, Set<String>>(
ServerVersionWarningController.new,
);
class ServerVersionWarningController extends Notifier<Set<String>> {
@override
Set<String> build() => decodeServerVersionWarningDismissals(
PreferencesStore.getString(PreferenceKeys.serverVersionWarningDismissed),
);
/// Remembers [token] alongside every earlier dismissal, so acknowledging
/// the warning on one server never re-surfaces it on another.
Future<void> dismiss(String token) async {
if (state.contains(token)) return;
final next = <String>{...state, token};
while (next.length > kMaxServerVersionWarningDismissals) {
next.remove(next.first);
}
state = next;
try {
await PreferencesStore.put(
PreferenceKeys.serverVersionWarningDismissed,
jsonEncode(next.toList(growable: false)),
);
} catch (error) {
DebugLogger.warning(
'Failed to persist server version warning dismissal',
scope: 'ui/server-version-warning',
data: {'error': error.toString()},
);
}
}
}
/// Parses the stored dismissal list. A bare token (the first shipped format)
/// is accepted as a single entry; anything unparseable yields no dismissals.
Set<String> decodeServerVersionWarningDismissals(String? raw) {
if (raw == null || raw.isEmpty) return const <String>{};
if (!raw.startsWith('[')) return {raw};
try {
final decoded = jsonDecode(raw);
if (decoded is! List) return const <String>{};
return {
for (final entry in decoded)
if (entry is String && entry.isNotEmpty) entry,
};
} on FormatException {
return const <String>{};
}
}