diff --git a/lib/core/screens/chat_screen.dart b/lib/core/screens/chat_screen.dart index 0ae5983e..b492a4b9 100644 --- a/lib/core/screens/chat_screen.dart +++ b/lib/core/screens/chat_screen.dart @@ -1293,24 +1293,30 @@ class _ChatScreenState extends State with WidgetsBindingObserver { } } + /// Dashboard client used to list models when no Desktop Gateway is + /// configured. Listing needs only `api/model/info` + `api/model/options` + /// over REST — the gateway WebSocket is required solely to push a + /// per-session override, which [_setSessionModel] skips when no gateway is + /// present (the chosen model rides on the chat request instead). + DashboardClient _modelListingClient() => DashboardClient( + host: widget.connection.host, + port: widget.connection.dashboardPort, + pathPrefix: widget.connection.dashboardPrefix ?? '', + proxied: widget.connection.dashboardProxied, + useHttps: widget.connection.useHttps, + username: widget.connection.dashboardUsername, + password: widget.connection.dashboardPassword, + ); + Future _showModelSelector() async { final desktopGateway = _desktopGateway; - if (desktopGateway == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Configure Dashboard credentials to choose a chat model.', - ), - ), - ); - return; - } + final restClient = desktopGateway == null ? _modelListingClient() : null; setState(() => _loadingModelOptions = true); try { final results = await Future.wait([ - desktopGateway.getModelInfo(), - desktopGateway.getModelOptions(), + desktopGateway?.getModelInfo() ?? restClient!.getModelInfo(), + desktopGateway?.getModelOptions() ?? restClient!.getModelOptions(), ]); if (!mounted) return; final modelInfo = results[0]; @@ -1325,9 +1331,11 @@ class _ChatScreenState extends State with WidgetsBindingObserver { _sessionReasoningEffort ?? WsClient.normalizeReasoningEffort(modelInfo['reasoning_effort']); try { - currentEffort = await desktopGateway.getSessionReasoning( - widget.session.id, - ); + if (desktopGateway != null) { + currentEffort = await desktopGateway.getSessionReasoning( + widget.session.id, + ); + } } catch (_) { // Older gateways may not expose session-scoped config.get. The model // selector remains usable with the profile/default effort. @@ -1486,19 +1494,25 @@ class _ChatScreenState extends State with WidgetsBindingObserver { Future _setSessionModel(_ModelSelection selection) async { final desktopGateway = _desktopGateway; - if (desktopGateway == null || _changingModel) return; + if (_changingModel) return; final choice = selection.choice; setState(() => _changingModel = true); try { - await desktopGateway.setSessionModel( - sessionId: widget.session.id, - provider: choice.provider, - model: choice.model, - ); - await desktopGateway.setSessionReasoning( - sessionId: widget.session.id, - effort: selection.reasoningEffort, - ); + // Without a gateway there is no session-scoped RPC to push the override + // to, so the selection stays local and is sent as the `model` field on + // each chat request instead. Reasoning effort is gateway-only and is + // simply not applied in that mode. + if (desktopGateway != null) { + await desktopGateway.setSessionModel( + sessionId: widget.session.id, + provider: choice.provider, + model: choice.model, + ); + await desktopGateway.setSessionReasoning( + sessionId: widget.session.id, + effort: selection.reasoningEffort, + ); + } final store = await _chatModelStore; await store.save( connectionIdentity: _chatModelConnectionIdentity, @@ -1621,6 +1635,10 @@ class _ChatScreenState extends State with WidgetsBindingObserver { await _gateway.sendMessageStreaming( message: text, sessionId: widget.session.id, + // Carry the per-chat override on the request. The API server resolves + // `model` per call, so this reproduces session-scoped model selection + // without needing the gateway WebSocket to hold session state. + model: _sessionModelOverride ? _sessionModel : null, history: history, imageDataUrl: imageDataUrl, onToken: (token) {