40 files changed
+1274
-273
lib/core/constants/app_constants.dart
+2
@@ -22,4 +22,6 @@ class AppConstants {
22
static const int ollamaDefaultPort = 11434;
23
24
static const String openRouterBaseUrl = 'https://openrouter.ai/api/v1';
25
+ static const String ollamaCloudBaseUrl = 'https://ollama.com';
26
+ static const int ollamaCloudDefaultPort = 443;
27
}
lib/core/models/enums.dart
+8
-1
@@ -200,7 +200,14 @@ Voice? voiceFromSettings(String? voiceId, EngineId engine) {
200
}
201
}
202
203
-enum ServerType { lmStudio, openAICompatible, ollama, openRouter, onDevice }
203
+enum ServerType {
204
+ lmStudio,
205
+ openAICompatible,
206
+ ollama,
207
+ ollamaCloud,
208
+ openRouter,
209
+ onDevice,
210
+}
211
212
enum ConnectionStatus { connected, disconnected, checking, error }
213
lib/features/chat/data/chat_service.dart
+2
@@ -53,6 +53,7 @@ abstract class ChatService {
53
case ServerType.openAICompatible:
54
return OpenAICompatibleChatService(dio);
55
case ServerType.ollama:
56
+ case ServerType.ollamaCloud:
57
return OllamaChatService(
58
dio,
59
imageCompressionEnabled: imageCompressionEnabled,
@@ -1351,4 +1352,5 @@ ToolTransportAdapter createAdapterForServerType(ServerType type) => switch (type
1352
ServerType.lmStudio => OpenAiToolAdapter(),
1353
ServerType.onDevice => OpenAiToolAdapter(),
1354
ServerType.ollama => OllamaToolAdapter(),
1355
+ ServerType.ollamaCloud => OllamaToolAdapter(),
1356
};
lib/features/chat/views/components/chat_input_bar.dart
+33
-119
@@ -22,6 +22,7 @@ import '../../providers/chat_providers.dart';
22
import '../../utils/attachment_helpers.dart';
23
import '../../utils/image_upload_utils.dart';
24
import '../../../models/views/model_picker_sheet.dart';
25
+import '../../../models/components/thinking_mode_chip.dart';
26
import 'attach_sheet.dart';
27
import 'image_preview_dialog.dart';
28
import 'model_chip.dart';
@@ -586,105 +587,6 @@ class ChatInputBarState extends ConsumerState<ChatInputBar>
587
);
588
}
589
589
- String _effortLabel(ReasoningEffort effort) {
590
- final l10n = AppLocalizations.of(context)!;
591
- return switch (effort) {
592
- ReasoningEffort.low => l10n.reasoning_effort_low,
593
- ReasoningEffort.medium => l10n.reasoning_effort_medium,
594
- ReasoningEffort.high => l10n.reasoning_effort_high,
595
- };
596
- }
597
-
598
- Widget _buildThinkButton(ThemeData theme) {
599
- final selectedModel = ref.watch(selectedModelProvider);
600
- if (selectedModel?.supportsReasoning != true) {
601
- return const SizedBox.shrink();
602
- }
603
-
604
- final l10n = AppLocalizations.of(context)!;
605
- final reasoningConfig = ref.watch(chatReasoningConfigProvider);
606
- final enabled = reasoningConfig.enabled;
607
- final activeColor = theme.colorScheme.primary;
608
- final fgColor = enabled
609
- ? activeColor
610
- : theme.colorScheme.onSurface.withValues(alpha: 0.6);
611
-
612
- return Padding(
613
- padding: const EdgeInsets.only(left: 6),
614
- child: Container(
615
- height: 32,
616
- decoration: BoxDecoration(
617
- color: enabled
618
- ? activeColor.withValues(alpha: 0.15)
619
- : theme.colorScheme.onSurface.withValues(alpha: 0.06),
620
- borderRadius: BorderRadius.circular(16),
621
- ),
622
- child: Row(
623
- mainAxisSize: MainAxisSize.min,
624
- children: [
625
- InkWell(
626
- borderRadius: BorderRadius.circular(16),
627
- onTap: () {
628
- Haptics.vibrate(HapticsType.light);
629
- ref
630
- .read(chatReasoningConfigProvider.notifier)
631
- .setEnabled(!enabled);
632
- },
633
- child: Padding(
634
- padding: const EdgeInsetsDirectional.only(
635
- start: 10,
636
- end: 4,
637
- top: 6,
638
- bottom: 6,
639
- ),
640
- child: HugeIcon(
641
- icon: enabled
642
- ? HugeIcons.strokeRoundedTick01
643
- : HugeIcons.strokeRoundedSquare01,
644
- size: 18,
645
- color: fgColor,
646
- ),
647
- ),
648
- ),
649
- PopupMenuButton<ReasoningEffort>(
650
- tooltip: '',
651
- padding: EdgeInsets.zero,
652
- onSelected: (effort) {
653
- Haptics.vibrate(HapticsType.light);
654
- ref
655
- .read(chatReasoningConfigProvider.notifier)
656
- .setEffort(effort);
657
- },
658
- itemBuilder: (context) => ReasoningEffort.values
659
- .map(
660
- (effort) => PopupMenuItem(
661
- value: effort,
662
- child: Text(_effortLabel(effort)),
663
- ),
664
- )
665
- .toList(),
666
- child: Padding(
667
- padding: const EdgeInsetsDirectional.only(
668
- end: 12,
669
- top: 6,
670
- bottom: 6,
671
- ),
672
- child: Text(
673
- '${l10n.think_button_label} (${_effortLabel(reasoningConfig.effort)})',
674
- style: TextStyle(
675
- fontSize: 13,
676
- fontWeight: FontWeight.w600,
677
- color: fgColor,
678
- ),
679
- ),
680
- ),
681
- ),
682
- ],
683
- ),
684
- ),
685
- );
686
- }
687
-
590
static const _holdDuration = Duration(milliseconds: 3000);
591
static const _insertHoldThreshold = 500 / 3000;
592
@@ -955,9 +857,9 @@ class ChatInputBarState extends ConsumerState<ChatInputBar>
857
if (ref.read(isStreamingProvider)) return;
858
859
_preSpeechText = _controller.text;
958
- await ref.read(sttProvider.notifier).startListening(
959
- onResult: _onSpeechResult,
960
- );
860
+ await ref
861
+ .read(sttProvider.notifier)
862
+ .startListening(onResult: _onSpeechResult);
863
}
864
});
865
@@ -1226,9 +1128,13 @@ class ChatInputBarState extends ConsumerState<ChatInputBar>
1128
),
1129
1130
// Bottom action row: attach button, model picker chip, optional
1229
- // reasoning/role controls, mic, send. Mirrors the modern
1230
- // composer layout where composer actions sit in a single
1231
- // horizontal row directly below the text input.
1131
+ // thinking-mode chip, optional role swap, mic, voice, send.
1132
+ //
1133
+ // The model chip is wrapped in `Flexible` so a long model name
1134
+ // can take up more horizontal room without pushing the mic /
1135
+ // voice / send buttons off-screen. The right-side cluster has
1136
+ // fixed widths so the layout is stable regardless of how long
1137
+ // the model name gets.
1138
Row(
1139
crossAxisAlignment: CrossAxisAlignment.center,
1140
children: [
@@ -1236,30 +1142,38 @@ class ChatInputBarState extends ConsumerState<ChatInputBar>
1142
1143
const SizedBox(width: 8),
1144
1239
- ModelChip(
1240
- model: selectedModel,
1241
- enabled: isConnected && widget.enabled,
1242
- onTap: () {
1243
- Haptics.vibrate(HapticsType.light);
1244
- showModalBottomSheet(
1245
- context: context,
1246
- isScrollControlled: true,
1247
- useSafeArea: true,
1248
- builder: (_) => const ModelPickerSheet(),
1249
- );
1250
- },
1145
+ Flexible(
1146
+ child: ModelChip(
1147
+ model: selectedModel,
1148
+ enabled: isConnected && widget.enabled,
1149
+ onTap: () {
1150
+ Haptics.vibrate(HapticsType.light);
1151
+ showModalBottomSheet(
1152
+ context: context,
1153
+ isScrollControlled: true,
1154
+ useSafeArea: true,
1155
+ builder: (_) => const ModelPickerSheet(),
1156
+ );
1157
+ },
1158
+ ),
1159
),
1160
1161
if (selectedModel?.supportsReasoning == true) ...[
1162
const SizedBox(width: 6),
1255
- _buildThinkButton(theme),
1163
+ ThinkingModeChip(
1164
+ model: selectedModel,
1165
+ isDark: isDark,
1166
+ compact: true,
1167
+ ),
1168
],
1169
if (showRoleSwapButton) ...[
1170
const SizedBox(width: 4),
1171
_buildRoleSwapButton(theme),
1172
],
1173
1262
- const Spacer(),
1174
+ // Fixed gap so we never collapse the right-side cluster
1175
+ // against the chips, even when the model name is short.
1176
+ const SizedBox(width: 8),
1177
1178
_buildMicButton(isListening, theme),
1179
lib/features/chat/views/components/model_chip.dart
+9
-6
@@ -36,8 +36,8 @@ class ModelChip extends StatelessWidget {
36
final bg = hasModel
37
? theme.colorScheme.primary.withValues(alpha: 0.12)
38
: (isDark
39
- ? AppColors.darkSurfaceInput
40
- : AppColors.lightSurface.withValues(alpha: 0.0));
39
+ ? AppColors.darkSurfaceInput
40
+ : AppColors.lightSurface.withValues(alpha: 0.0));
41
final borderColor = hasModel
42
? theme.colorScheme.primary.withValues(alpha: 0.4)
43
: (isDark ? AppColors.darkBorder : AppColors.lightBorder);
@@ -50,6 +50,10 @@ class ModelChip extends StatelessWidget {
50
onTap: enabled ? onTap : null,
51
borderRadius: BorderRadius.circular(999),
52
child: Container(
53
+ // Constrain the pill itself instead of the entire action row. This
54
+ // lets longer model names be readable while Flexible in the parent
55
+ // row still protects the fixed-size action buttons on the right.
56
+ constraints: const BoxConstraints(minWidth: 0, maxWidth: 280),
57
padding: const EdgeInsetsDirectional.fromSTEB(12, 6, 10, 6),
58
decoration: BoxDecoration(
59
color: bg,
@@ -59,8 +63,7 @@ class ModelChip extends StatelessWidget {
63
child: Row(
64
mainAxisSize: MainAxisSize.min,
65
children: [
62
- ConstrainedBox(
63
- constraints: const BoxConstraints(maxWidth: 160),
66
+ Flexible(
67
child: Text(
68
label,
69
maxLines: 1,
@@ -72,7 +75,7 @@ class ModelChip extends StatelessWidget {
75
),
76
),
77
),
75
- const SizedBox(width: 4),
78
+ const SizedBox(width: 6),
79
HugeIcon(
80
icon: HugeIcons.strokeRoundedArrowDown01,
81
size: 12,
@@ -84,4 +87,4 @@ class ModelChip extends StatelessWidget {
87
),
88
);
89
}
87
-}
\ No newline at end of file
90
+}
lib/features/models/components/model_list.dart
+1
-1
@@ -401,7 +401,7 @@ class _ModelListState extends ConsumerState<ModelList> {
401
}
402
403
if (context.mounted) {
404
- final message = activeServer.type == ServerType.ollama
404
+ final message = activeServer.isOllamaFamily
405
? l10n.model_unloaded_ollama(model.name)
406
: l10n.model_unloaded_success(model.name);
407
ScaffoldMessenger.of(context).showSnackBar(
lib/features/models/components/thinking_mode_chip.dart
new
+222
@@ -0,0 +1,222 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:hugeicons/hugeicons.dart';
4
+
5
+import 'package:localmind/core/theme/colors.dart';
6
+import 'package:localmind/features/chat/providers/chat_reasoning_providers.dart';
7
+import 'package:localmind/features/models/data/models/model_info.dart';
8
+import 'package:localmind/l10n/app_localizations.dart';
9
+
10
+/// Compact, two-part chip used to toggle and configure the reasoning
11
+/// ("thinking") mode for the actively selected model.
12
+///
13
+/// • Left side: tap toggles thinking on / off.
14
+/// • Right side: opens a popup to choose the effort level (Low / Medium /
15
+/// High). The picker collapses to a single tap-toggle button when
16
+/// thinking is disabled, since effort is irrelevant in that state.
17
+///
18
+/// Rendered as a [ConsumerWidget] so both surfaces (the model picker
19
+/// sheet and the chat input bar) stay perfectly in sync with the
20
+/// underlying [chatReasoningConfigProvider].
21
+class ThinkingModeChip extends ConsumerWidget {
22
+ const ThinkingModeChip({
23
+ super.key,
24
+ required this.model,
25
+ required this.isDark,
26
+ this.compact = false,
27
+ this.fullWidth = false,
28
+ });
29
+
30
+ /// When `null` or a model that doesn't support reasoning is passed the
31
+ /// chip returns [SizedBox.shrink] so callers can drop it into any layout
32
+ /// without conditionals.
33
+ final ModelInfo? model;
34
+
35
+ /// Theme context used for the on- / off-state colors.
36
+ final bool isDark;
37
+
38
+ /// When true, uses a slightly tighter horizontal padding so the chip
39
+ /// fits comfortably inside the chat input bar's action row.
40
+ final bool compact;
41
+
42
+ /// When true, the chip will occupy the full available width instead of
43
+ /// hugging its contents. Used by the model picker header so the chip
44
+ /// sits flush under the title.
45
+ final bool fullWidth;
46
+
47
+ @override
48
+ Widget build(BuildContext context, WidgetRef ref) {
49
+ final supportsReasoning = model?.supportsReasoning ?? false;
50
+ if (!supportsReasoning) return const SizedBox.shrink();
51
+
52
+ final l10n = AppLocalizations.of(context)!;
53
+ final theme = Theme.of(context);
54
+ final config = ref.watch(chatReasoningConfigProvider);
55
+ final notifier = ref.read(chatReasoningConfigProvider.notifier);
56
+
57
+ final accent = theme.colorScheme.primary;
58
+ final enabledFg = accent;
59
+ final disabledFg = isDark
60
+ ? AppColors.darkMutedText
61
+ : AppColors.lightMutedText;
62
+ final fgColor = config.enabled ? enabledFg : disabledFg;
63
+ final bgColor = config.enabled
64
+ ? accent.withValues(alpha: 0.15)
65
+ : theme.colorScheme.onSurface.withValues(alpha: 0.06);
66
+ final borderColor = config.enabled
67
+ ? accent.withValues(alpha: 0.4)
68
+ : (isDark ? AppColors.darkBorder : AppColors.lightBorder);
69
+
70
+ final hPad = compact ? 10.0 : 12.0;
71
+ final vPad = compact ? 6.0 : 8.0;
72
+ final borderRadius = BorderRadius.circular(999);
73
+
74
+ final labelText = fullWidth
75
+ ? l10n.thinking_mode_title
76
+ : l10n.think_button_label;
77
+
78
+ final toggleSide = InkWell(
79
+ onTap: () {
80
+ notifier.setEnabled(!config.enabled);
81
+ },
82
+ borderRadius: borderRadius,
83
+ child: Padding(
84
+ padding: EdgeInsetsDirectional.fromSTEB(hPad, vPad, hPad, vPad),
85
+ child: Row(
86
+ mainAxisSize: MainAxisSize.min,
87
+ children: [
88
+ HugeIcon(
89
+ icon: HugeIcons.strokeRoundedBrain,
90
+ size: compact ? 14 : 16,
91
+ color: fgColor,
92
+ ),
93
+ SizedBox(width: compact ? 4 : 6),
94
+ Flexible(
95
+ child: Text(
96
+ labelText,
97
+ maxLines: 1,
98
+ overflow: TextOverflow.ellipsis,
99
+ style: TextStyle(
100
+ fontSize: compact ? 12 : 13,
101
+ fontWeight: FontWeight.w600,
102
+ color: fgColor,
103
+ ),
104
+ ),
105
+ ),
106
+ if (config.enabled) ...[
107
+ SizedBox(width: compact ? 4 : 6),
108
+ Container(
109
+ padding: EdgeInsets.symmetric(
110
+ horizontal: compact ? 6 : 8,
111
+ vertical: 2,
112
+ ),
113
+ decoration: BoxDecoration(
114
+ color: accent.withValues(alpha: 0.18),
115
+ borderRadius: BorderRadius.circular(999),
116
+ ),
117
+ child: Text(
118
+ _effortShortLabel(l10n, config.effort),
119
+ style: TextStyle(
120
+ fontSize: compact ? 10 : 11,
121
+ fontWeight: FontWeight.w700,
122
+ color: enabledFg,
123
+ ),
124
+ ),
125
+ ),
126
+ ],
127
+ ],
128
+ ),
129
+ ),
130
+ );
131
+
132
+ // Right-side effort picker. Hidden when thinking is off because the
133
+ // choice has no effect — the entire chip is then a single tap toggle.
134
+ Widget pickerSide = const SizedBox.shrink();
135
+ if (config.enabled) {
136
+ pickerSide = PopupMenuButton<ReasoningEffort>(
137
+ tooltip: '',
138
+ padding: EdgeInsets.zero,
139
+ position: PopupMenuPosition.under,
140
+ onSelected: (effort) {
141
+ notifier.setEnabled(true);
142
+ notifier.setEffort(effort);
143
+ },
144
+ itemBuilder: (context) => ReasoningEffort.values
145
+ .map(
146
+ (effort) => PopupMenuItem(
147
+ value: effort,
148
+ child: Row(
149
+ mainAxisSize: MainAxisSize.min,
150
+ children: [
151
+ HugeIcon(
152
+ icon: effort == config.effort
153
+ ? HugeIcons.strokeRoundedTick01
154
+ : HugeIcons.strokeRoundedSquare01,
155
+ size: 16,
156
+ color: effort == config.effort
157
+ ? accent
158
+ : Colors.transparent,
159
+ ),
160
+ const SizedBox(width: 8),
161
+ Text(_effortShortLabel(l10n, effort)),
162
+ ],
163
+ ),
164
+ ),
165
+ )
166
+ .toList(),
167
+ child: Padding(
168
+ padding: EdgeInsetsDirectional.fromSTEB(2, vPad, hPad, vPad),
169
+ child: Row(
170
+ mainAxisSize: MainAxisSize.min,
171
+ children: [
172
+ HugeIcon(
173
+ icon: HugeIcons.strokeRoundedArrowDown01,
174
+ size: compact ? 12 : 14,
175
+ color: fgColor.withValues(alpha: 0.7),
176
+ ),
177
+ ],
178
+ ),
179
+ ),
180
+ );
181
+ }
182
+
183
+ final chip = Material(
184
+ color: Colors.transparent,
185
+ child: DecoratedBox(
186
+ decoration: BoxDecoration(
187
+ color: bgColor,
188
+ border: Border.all(color: borderColor),
189
+ borderRadius: borderRadius,
190
+ ),
191
+ child: Row(
192
+ mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min,
193
+ mainAxisAlignment: fullWidth
194
+ ? MainAxisAlignment.spaceBetween
195
+ : MainAxisAlignment.start,
196
+ children: [
197
+ // Toggle side: flexes to absorb available width when [fullWidth]
198
+ // is true, but stays hugging its content otherwise.
199
+ if (fullWidth) Expanded(child: toggleSide) else toggleSide,
200
+ pickerSide,
201
+ ],
202
+ ),
203
+ ),
204
+ );
205
+
206
+ return fullWidth ? SizedBox(width: double.infinity, child: chip) : chip;
207
+ }
208
+
209
+ static String _effortShortLabel(
210
+ AppLocalizations l10n,
211
+ ReasoningEffort effort,
212
+ ) {
213
+ switch (effort) {
214
+ case ReasoningEffort.low:
215
+ return l10n.reasoning_effort_low;
216
+ case ReasoningEffort.medium:
217
+ return l10n.reasoning_effort_medium;
218
+ case ReasoningEffort.high:
219
+ return l10n.reasoning_effort_high;
220
+ }
221
+ }
222
+}
lib/features/models/views/model_picker_sheet.dart
+141
-106
@@ -13,13 +13,15 @@ import 'package:localmind/features/on_device/providers/on_device_providers.dart'
13
import 'package:localmind/features/servers/providers/server_providers.dart';
14
import 'package:localmind/features/lm_studio_catalog/views/lm_studio_download_widgets.dart';
15
import 'package:localmind/core/providers/app_providers.dart';
16
-import 'package:localmind/features/conversations/providers/conversation_providers.dart' as conv;
16
+import 'package:localmind/features/conversations/providers/conversation_providers.dart'
17
+ as conv;
18
import 'package:localmind/l10n/app_localizations.dart';
19
import '../components/model_list.dart';
20
import '../components/model_search_field.dart';
21
import '../components/model_sort_control.dart';
22
import '../components/no_server_state.dart';
23
import '../components/thinking_indicator.dart';
24
+import '../components/thinking_mode_chip.dart';
25
import '../providers/model_picker_providers.dart';
26
27
class ModelPickerSheet extends ConsumerStatefulWidget {
@@ -63,10 +65,7 @@ class _ModelPickerSheetState extends ConsumerState<ModelPickerSheet> {
65
ScaffoldMessenger.of(context).showSnackBar(
66
SnackBar(
67
content: Text(
66
- l10n.model_loaded(
67
- loadedName,
68
- next.backend?.name ?? 'CPU',
69
- ),
68
+ l10n.model_loaded(loadedName, next.backend?.name ?? 'CPU'),
69
),
70
),
71
);
@@ -75,8 +74,9 @@ class _ModelPickerSheetState extends ConsumerState<ModelPickerSheet> {
74
75
final serversAsync = ref.watch(serversProvider);
76
final servers = serversAsync.value ?? [];
78
- final currentServer =
79
- servers.where((s) => s.id == activeServer?.id).firstOrNull;
77
+ final currentServer = servers
78
+ .where((s) => s.id == activeServer?.id)
79
+ .firstOrNull;
80
final isOnDevice =
81
currentServer != null && currentServer.type == ServerType.onDevice;
82
final isLmStudio =
@@ -99,90 +99,105 @@ class _ModelPickerSheetState extends ConsumerState<ModelPickerSheet> {
99
return Cue.onMount(
100
motion: .smooth(),
101
child: Container(
102
- padding: const EdgeInsets.all(16),
103
- decoration: BoxDecoration(
104
- color: isDark ? AppColors.darkBackground : AppColors.lightSurface,
105
- borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
106
- ),
107
- child: Column(
108
- crossAxisAlignment: CrossAxisAlignment.start,
109
- children: [
110
- Center(
111
- child: Container(
112
- width: 40,
113
- height: 4,
114
- decoration: BoxDecoration(
115
- color: isDark ? Colors.grey[600] : Colors.grey[300],
116
- borderRadius: BorderRadius.circular(2),
117
- ),
118
- ),
102
+ padding: const EdgeInsets.all(16),
103
+ decoration: BoxDecoration(
104
+ color: isDark ? AppColors.darkBackground : AppColors.lightSurface,
105
+ borderRadius: const BorderRadius.vertical(
106
+ top: Radius.circular(20),
107
),
120
- const SizedBox(height: 16),
121
- _ModelPickerHeader(
122
- isDark: isDark,
123
- isThinking: isThinking,
124
- loadedCount: loadedCount,
125
- showBrowseButton: isLmStudio,
126
- onBrowseModels: isLmStudio
127
- ? () {
128
- Navigator.of(context).pop();
129
- context.push(
130
- AppRoutes.lmStudioModelBrowser,
131
- extra: currentServer,
132
- );
133
- }
134
- : null,
135
- onRefresh: activeServer != null
136
- ? () {
137
- ref.invalidate(availableModelsProvider(activeServer.id));
138
- ref.invalidate(loadedModelsProvider(activeServer));
139
- }
140
- : null,
141
- onUnloadAll: activeServer != null && loadedCount > 0
142
- ? () => _unloadAllModels(context, ref, activeServer)
143
- : null,
144
- ),
145
- if (activeServer != null)
146
- Padding(
147
- padding: const EdgeInsets.only(top: 4),
148
- child: Text(
149
- activeServer.name,
150
- style: TextStyle(
151
- fontSize: 13,
152
- color: isDark
153
- ? AppColors.darkMutedText
154
- : AppColors.lightMutedText,
108
+ ),
109
+ child: Column(
110
+ crossAxisAlignment: CrossAxisAlignment.start,
111
+ children: [
112
+ Center(
113
+ child: Container(
114
+ width: 40,
115
+ height: 4,
116
+ decoration: BoxDecoration(
117
+ color: isDark ? Colors.grey[600] : Colors.grey[300],
118
+ borderRadius: BorderRadius.circular(2),
119
),
120
),
121
),
158
- const SizedBox(height: 12),
159
- Row(
160
- children: [
161
- const Expanded(child: ModelSearchField()),
162
- const SizedBox(width: 8),
163
- const ModelSortControl(),
122
+ const SizedBox(height: 16),
123
+ _ModelPickerHeader(
124
+ isDark: isDark,
125
+ isThinking: isThinking,
126
+ loadedCount: loadedCount,
127
+ showBrowseButton: isLmStudio,
128
+ onBrowseModels: isLmStudio
129
+ ? () {
130
+ Navigator.of(context).pop();
131
+ context.push(
132
+ AppRoutes.lmStudioModelBrowser,
133
+ extra: currentServer,
134
+ );
135
+ }
136
+ : null,
137
+ onRefresh: activeServer != null
138
+ ? () {
139
+ ref.invalidate(
140
+ availableModelsProvider(activeServer.id),
141
+ );
142
+ ref.invalidate(loadedModelsProvider(activeServer));
143
+ }
144
+ : null,
145
+ onUnloadAll: activeServer != null && loadedCount > 0
146
+ ? () => _unloadAllModels(context, ref, activeServer)
147
+ : null,
148
+ ),
149
+ if (activeServer != null)
150
+ Padding(
151
+ padding: const EdgeInsets.only(top: 4),
152
+ child: Text(
153
+ activeServer.name,
154
+ style: TextStyle(
155
+ fontSize: 13,
156
+ color: isDark
157
+ ? AppColors.darkMutedText
158
+ : AppColors.lightMutedText,
159
+ ),
160
+ ),
161
+ ),
162
+ // Thinking-mode selector surfaces only when the currently
163
+ // selected model can actually reason. Renders nothing
164
+ // otherwise so it never adds dead space.
165
+ if (selectedModel?.supportsReasoning == true) ...[
166
+ const SizedBox(height: 12),
167
+ ThinkingModeChip(
168
+ model: selectedModel,
169
+ isDark: isDark,
170
+ fullWidth: true,
171
+ ),
172
],
165
- ),
166
- const SizedBox(height: 12),
167
- Expanded(
168
- child: activeServer == null
169
- ? NoServerState(isDark: isDark)
170
- : isOnDevice
171
- ? OnDevicePickerSection(
172
- selectedModelId: selectedModel?.id,
173
- isDark: isDark,
174
- scrollController: scrollController,
175
- )
176
- : ModelList(
177
- serverId: activeServer.id,
178
- selectedModelId: selectedModel?.id,
179
- isDark: isDark,
180
- scrollController: scrollController,
181
- ),
182
- ),
183
- ],
173
+ const SizedBox(height: 12),
174
+ Row(
175
+ children: [
176
+ const Expanded(child: ModelSearchField()),
177
+ const SizedBox(width: 8),
178
+ const ModelSortControl(),
179
+ ],
180
+ ),
181
+ const SizedBox(height: 12),
182
+ Expanded(
183
+ child: activeServer == null
184
+ ? NoServerState(isDark: isDark)
185
+ : isOnDevice
186
+ ? OnDevicePickerSection(
187
+ selectedModelId: selectedModel?.id,
188
+ isDark: isDark,
189
+ scrollController: scrollController,
190
+ )
191
+ : ModelList(
192
+ serverId: activeServer.id,
193
+ selectedModelId: selectedModel?.id,
194
+ isDark: isDark,
195
+ scrollController: scrollController,
196
+ ),
197
+ ),
198
+ ],
199
+ ),
200
),
185
- ),
201
);
202
},
203
);
@@ -199,8 +214,9 @@ class _ModelPickerSheetState extends ConsumerState<ModelPickerSheet> {
214
if (activeServer.type == ServerType.onDevice) {
215
await ref.read(onDeviceEngineProvider.notifier).unloadModel();
216
} else {
202
- final loadedInstances =
203
- await ref.read(loadedModelsProvider(activeServer).future);
217
+ final loadedInstances = await ref.read(
218
+ loadedModelsProvider(activeServer).future,
219
+ );
220
final apiService = ref.read(serverApiServiceProvider);
221
await apiService.unloadAllInstances(activeServer, loadedInstances);
222
}
@@ -209,9 +225,9 @@ class _ModelPickerSheetState extends ConsumerState<ModelPickerSheet> {
225
ref.read(selectedModelProvider.notifier).clear();
226
227
if (context.mounted) {
212
- ScaffoldMessenger.of(context).showSnackBar(
213
- SnackBar(content: Text(l10n.all_models_unloaded)),
214
- );
228
+ ScaffoldMessenger.of(
229
+ context,
230
+ ).showSnackBar(SnackBar(content: Text(l10n.all_models_unloaded)));
231
}
232
} catch (e) {
233
if (context.mounted) {
@@ -277,10 +293,11 @@ class _ModelPickerHeader extends ConsumerWidget {
293
vertical: 2,
294
),
295
decoration: BoxDecoration(
280
- color: (isDark
281
- ? AppColors.darkAccent
282
- : AppColors.lightAccent)
283
- .withValues(alpha: 0.15),
296
+ color:
297
+ (isDark
298
+ ? AppColors.darkAccent
299
+ : AppColors.lightAccent)
300
+ .withValues(alpha: 0.15),
301
borderRadius: BorderRadius.circular(999),
302
),
303
child: Text(
@@ -318,7 +335,10 @@ class _ModelPickerHeader extends ConsumerWidget {
335
),
336
borderRadius: BorderRadius.circular(16),
337
child: Container(
321
- padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
338
+ padding: const EdgeInsets.symmetric(
339
+ horizontal: 10,
340
+ vertical: 6,
341
+ ),
342
decoration: BoxDecoration(
343
border: Border.all(
344
color: isDark ? Colors.grey[800]! : Colors.grey[300]!,
@@ -329,8 +349,8 @@ class _ModelPickerHeader extends ConsumerWidget {
349
child: Row(
350
mainAxisSize: MainAxisSize.min,
351
children: [
332
- HugeIcon(icon:
333
- HugeIcons.strokeRoundedLayers01,
352
+ HugeIcon(
353
+ icon: HugeIcons.strokeRoundedLayers01,
354
size: 14,
355
color: isDark ? Colors.grey[400] : Colors.grey[600],
356
),
@@ -353,8 +373,8 @@ class _ModelPickerHeader extends ConsumerWidget {
373
IconButton(
374
onPressed: onUnloadAll,
375
tooltip: l10n.unload_all_models,
356
- icon: HugeIcon(icon:
357
- HugeIcons.strokeRoundedPower,
376
+ icon: HugeIcon(
377
+ icon: HugeIcons.strokeRoundedPower,
378
size: 20,
379
color: Colors.red[400],
380
),
@@ -363,12 +383,18 @@ class _ModelPickerHeader extends ConsumerWidget {
383
IconButton(
384
onPressed: onBrowseModels,
385
tooltip: l10n.lm_studio_browse_models,
366
- icon: const HugeIcon(icon: HugeIcons.strokeRoundedCompass01, size: 20),
386
+ icon: const HugeIcon(
387
+ icon: HugeIcons.strokeRoundedCompass01,
388
+ size: 20,
389
+ ),
390
),
391
const LmDownloadIndicatorButton(compact: true),
392
if (onRefresh != null)
393
IconButton(
371
- icon: const HugeIcon(icon: HugeIcons.strokeRoundedRefresh, size: 20),
394
+ icon: const HugeIcon(
395
+ icon: HugeIcons.strokeRoundedRefresh,
396
+ size: 20,
397
+ ),
398
onPressed: onRefresh,
399
tooltip: l10n.refresh_models,
400
),
@@ -433,7 +459,9 @@ class __ContextLengthEditDialogState
459
if (widget.activeConversationId == null) {
460
ref.read(settingsProvider.notifier).setContextLength(parsed);
461
} else {
436
- ref.read(conv.conversationsProvider.notifier).updateChatParams(
462
+ ref
463
+ .read(conv.conversationsProvider.notifier)
464
+ .updateChatParams(
465
widget.activeConversationId!,
466
contextLength: parsed,
467
);
@@ -451,7 +479,9 @@ class __ContextLengthEditDialogState
479
final suggestions = [2048, 4096, 8192, 16384, 32768, 65536, 131072];
480
481
return Dialog(
454
- backgroundColor: isDark ? AppColors.darkBackground : AppColors.lightSurface,
482
+ backgroundColor: isDark
483
+ ? AppColors.darkBackground
484
+ : AppColors.lightSurface,
485
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
486
child: SingleChildScrollView(
487
child: Padding(
@@ -481,7 +511,10 @@ class __ContextLengthEditDialogState
511
borderRadius: BorderRadius.circular(10),
512
),
513
suffixIcon: IconButton(
484
- icon: const HugeIcon(icon: HugeIcons.strokeRoundedCancel01, size: 18),
514
+ icon: const HugeIcon(
515
+ icon: HugeIcons.strokeRoundedCancel01,
516
+ size: 18,
517
+ ),
518
onPressed: () => _controller.clear(),
519
),
520
),
@@ -502,7 +535,9 @@ class __ContextLengthEditDialogState
535
fontWeight: FontWeight.w500,
536
color: isDark ? Colors.white70 : Colors.black87,
537
),
505
- backgroundColor: isDark ? Colors.grey[850] : Colors.grey[200],
538
+ backgroundColor: isDark
539
+ ? Colors.grey[850]
540
+ : Colors.grey[200],
541
shape: RoundedRectangleBorder(
542
borderRadius: BorderRadius.circular(8),
543
side: BorderSide.none,
@@ -544,4 +579,4 @@ class __ContextLengthEditDialogState
579
),
580
);
581
}
547
-}
\ No newline at end of file
582
+}
lib/features/onboarding/screens/onboarding_server_setup_screen.dart
+47
-13
@@ -44,19 +44,28 @@ class _OnboardingServerSetupScreenState
44
super.initState();
45
String defaultName = '';
46
String defaultPort = '';
47
+ String defaultHost = '';
48
49
switch (widget.selectedType) {
50
case ServerType.lmStudio:
51
defaultName = 'LM Studio';
52
defaultPort = AppConstants.lmStudioDefaultPort.toString();
53
+ defaultHost = Platform.isAndroid ? '192.168.0.0' : '127.0.0.1';
54
break;
55
case ServerType.openAICompatible:
56
defaultName = 'OpenAI Compatible';
57
defaultPort = AppConstants.openAICompatibleDefaultPort.toString();
58
+ defaultHost = Platform.isAndroid ? '192.168.0.0' : '127.0.0.1';
59
break;
60
case ServerType.ollama:
61
defaultName = 'Ollama';
62
defaultPort = AppConstants.ollamaDefaultPort.toString();
63
+ defaultHost = Platform.isAndroid ? '192.168.0.0' : '127.0.0.1';
64
+ break;
65
+ case ServerType.ollamaCloud:
66
+ defaultName = 'Ollama Cloud';
67
+ defaultHost = AppConstants.ollamaCloudBaseUrl;
68
+ defaultPort = AppConstants.ollamaCloudDefaultPort.toString();
69
break;
70
case ServerType.openRouter:
71
defaultName = 'OpenRouter';
@@ -69,11 +78,7 @@ class _OnboardingServerSetupScreenState
78
}
79
80
_nameController = TextEditingController(text: defaultName);
72
- _hostController = TextEditingController(
73
- text: widget.selectedType == ServerType.openRouter
74
- ? ''
75
- : (Platform.isAndroid ? '192.168.0.0' : '127.0.0.1'),
76
- );
81
+ _hostController = TextEditingController(text: defaultHost);
82
_portController = TextEditingController(text: defaultPort);
83
_apiKeyController = TextEditingController();
84
}
@@ -88,13 +93,22 @@ class _OnboardingServerSetupScreenState
93
}
94
95
Server _buildServer() {
96
+ final requiresCloudConfig =
97
+ widget.selectedType == ServerType.ollamaCloud;
98
+ final host = requiresCloudConfig
99
+ ? AppConstants.ollamaCloudBaseUrl
100
+ : _hostController.text.trim();
101
+ final port = requiresCloudConfig
102
+ ? AppConstants.ollamaCloudDefaultPort
103
+ : int.parse(_portController.text.trim());
104
+
105
return Server(
106
id: DateTime.now().millisecondsSinceEpoch.toString(),
107
name: _nameController.text.trim(),
108
type: widget.selectedType,
95
- host: _hostController.text.trim(),
96
- port: int.parse(_portController.text.trim()),
97
- apiKey: widget.selectedType == ServerType.openRouter
109
+ host: host,
110
+ port: port,
111
+ apiKey: _isCloudProvider
112
? _apiKeyController.text.trim()
113
: (_apiKeyController.text.trim().isNotEmpty
114
? _apiKeyController.text.trim()
@@ -106,8 +120,12 @@ class _OnboardingServerSetupScreenState
120
);
121
}
122
123
+ bool get _isCloudProvider =>
124
+ widget.selectedType == ServerType.openRouter ||
125
+ widget.selectedType == ServerType.ollamaCloud;
126
+
127
String? _validateHost(String? value) {
110
- if (widget.selectedType == ServerType.openRouter) return null;
128
+ if (_isCloudProvider) return null;
129
final l10n = AppLocalizations.of(context)!;
130
if (value == null || value.trim().isEmpty) {
131
return l10n.host_required;
@@ -119,7 +137,7 @@ class _OnboardingServerSetupScreenState
137
}
138
139
String? _validatePort(String? value) {
122
- if (widget.selectedType == ServerType.openRouter) return null;
140
+ if (_isCloudProvider) return null;
141
final l10n = AppLocalizations.of(context)!;
142
if (value == null || value.trim().isEmpty) {
143
return l10n.port_required;
@@ -208,7 +226,8 @@ class _OnboardingServerSetupScreenState
226
Widget build(BuildContext context) {
227
final l10n = AppLocalizations.of(context)!;
228
final theme = Theme.of(context);
211
- final isCloud = widget.selectedType == ServerType.openRouter;
229
+ final isCloud = _isCloudProvider;
230
+ final isOllamaCloud = widget.selectedType == ServerType.ollamaCloud;
231
232
return Scaffold(
233
appBar: AppBar(title: Text(l10n.setup_connection)),
@@ -288,10 +307,23 @@ class _OnboardingServerSetupScreenState
307
: l10n.api_key_optional,
308
filled: true,
309
fillColor: theme.colorScheme.surface,
310
+ hintText: isCloud
311
+ ? (isOllamaCloud
312
+ ? l10n.api_key_hint_ollama_cloud
313
+ : l10n.api_key_hint_openrouter)
314
+ : null,
315
),
316
validator: (val) {
317
if (isCloud && (val == null || val.trim().isEmpty)) {
294
- return l10n.api_key_required_openrouter;
318
+ return isOllamaCloud
319
+ ? l10n.api_key_required_ollama_cloud
320
+ : l10n.api_key_required_openrouter;
321
+ }
322
+ if (isCloud &&
323
+ widget.selectedType == ServerType.openRouter &&
324
+ val != null &&
325
+ !val.trim().startsWith('sk-')) {
326
+ return l10n.api_key_format;
327
}
328
return null;
329
},
@@ -319,7 +351,9 @@ class _OnboardingServerSetupScreenState
351
const SizedBox(width: 8),
352
Expanded(
353
child: Text(
322
- l10n.openrouter_disclosure,
354
+ isOllamaCloud
355
+ ? l10n.ollama_cloud_disclosure
356
+ : l10n.openrouter_disclosure,
357
style: theme.textTheme.bodySmall?.copyWith(
358
color: theme.colorScheme.onSurface.withValues(alpha: 0.8),
359
height: 1.4,
lib/features/onboarding/screens/onboarding_server_type_screen.dart
+13
-1
@@ -189,7 +189,7 @@ class _OnboardingServerTypeScreenState
189
type: ServerType.ollama,
190
title: l10n.server_type_ollama,
191
subtitle: l10n.server_type_ollama_desc,
192
- iconWidget: HugeIcon(icon:
192
+ iconWidget: HugeIcon(icon:
193
HugeIcons.strokeRoundedRobot01,
194
color: _selectedType == ServerType.ollama
195
? theme.colorScheme.primary
@@ -197,6 +197,18 @@ class _OnboardingServerTypeScreenState
197
),
198
theme: theme,
199
),
200
+ _buildServerCard(
201
+ type: ServerType.ollamaCloud,
202
+ title: l10n.server_type_ollama_cloud,
203
+ subtitle: l10n.server_type_ollama_cloud_sub,
204
+ iconWidget: HugeIcon(
205
+ icon: HugeIcons.strokeRoundedAiCloud,
206
+ color: _selectedType == ServerType.ollamaCloud
207
+ ? theme.colorScheme.primary
208
+ : theme.colorScheme.onSurface,
209
+ ),
210
+ theme: theme,
211
+ ),
212
_buildServerCard(
213
type: ServerType.openRouter,
214
title: l10n.server_type_openrouter,
lib/features/servers/data/models/server.dart
+20
-4
@@ -1,3 +1,4 @@
1
+import '../../../../core/constants/app_constants.dart';
2
import '../../../../core/models/enums.dart';
3
4
bool _hasExplicitHttpScheme(String input) {
@@ -34,6 +35,9 @@ String displayServerAddress(String host, int port, ServerType type) {
35
if (type == ServerType.openRouter) {
36
return 'openrouter.ai';
37
}
38
+ if (type == ServerType.ollamaCloud) {
39
+ return 'ollama.com';
40
+ }
41
if (type == ServerType.onDevice) {
42
return 'Local inference';
43
}
@@ -65,6 +69,9 @@ String buildServerBaseUrl(String host, int port, ServerType type) {
69
if (type == ServerType.openRouter) {
70
return 'https://openrouter.ai/api/v1';
71
}
72
+ if (type == ServerType.ollamaCloud) {
73
+ return AppConstants.ollamaCloudBaseUrl;
74
+ }
75
if (type == ServerType.onDevice) {
76
return 'on-device';
77
}
@@ -148,7 +155,10 @@ class Server {
155
String get apiPathPrefix => normalizeServerPathPrefix(pathPrefix);
156
157
String _apiPath(String suffix) {
151
- if (type != ServerType.ollama || apiPathPrefix.isEmpty) {
158
+ if (apiPathPrefix.isEmpty) {
159
+ return suffix;
160
+ }
161
+ if (type != ServerType.ollama && type != ServerType.ollamaCloud) {
162
return suffix;
163
}
164
return '$apiPathPrefix$suffix';
@@ -167,6 +177,7 @@ class Server {
177
case ServerType.openAICompatible:
178
return '$baseUrl/v1/chat/completions';
179
case ServerType.ollama:
180
+ case ServerType.ollamaCloud:
181
return '$baseUrl${_apiPath('/api/chat')}';
182
case ServerType.openRouter:
183
return '$baseUrl/chat/completions';
@@ -182,6 +193,7 @@ class Server {
193
case ServerType.openAICompatible:
194
return '$baseUrl/v1/models';
195
case ServerType.ollama:
196
+ case ServerType.ollamaCloud:
197
return '$baseUrl${_apiPath('/api/tags')}';
198
case ServerType.openRouter:
199
return '$baseUrl/models';
@@ -198,8 +210,8 @@ class Server {
210
return '$baseUrl/v1/models';
211
case ServerType.ollama:
212
return '$baseUrl${_apiPath('/api/ps')}';
213
+ case ServerType.ollamaCloud:
214
case ServerType.openRouter:
202
- return '';
215
case ServerType.onDevice:
216
return '';
217
}
@@ -208,6 +220,7 @@ class Server {
220
String get modelDetailsEndpoint {
221
switch (type) {
222
case ServerType.ollama:
223
+ case ServerType.ollamaCloud:
224
return '$baseUrl${_apiPath('/api/show')}';
225
case ServerType.lmStudio:
226
case ServerType.openAICompatible:
@@ -225,8 +238,8 @@ class Server {
238
return '$baseUrl/v1/models/load';
239
case ServerType.ollama:
240
return '$baseUrl${_apiPath('/api/generate')}';
241
+ case ServerType.ollamaCloud:
242
case ServerType.openRouter:
229
- return '';
243
case ServerType.onDevice:
244
return '';
245
}
@@ -240,14 +253,17 @@ class Server {
253
return '$baseUrl/v1/models/unload';
254
case ServerType.ollama:
255
return '$baseUrl${_apiPath('/api/generate')}';
256
+ case ServerType.ollamaCloud:
257
case ServerType.openRouter:
244
- return '';
258
case ServerType.onDevice:
259
return '';
260
}
261
}
262
263
bool get isOnDevice => type == ServerType.onDevice;
264
+ bool get isOllamaCloud => type == ServerType.ollamaCloud;
265
+ bool get isOllamaFamily =>
266
+ type == ServerType.ollama || type == ServerType.ollamaCloud;
267
268
String get displayAddress => displayServerAddress(host, port, type);
269
lib/features/servers/data/server_api_service.dart
+15
-6
@@ -53,6 +53,7 @@ class ServerApiService {
53
models = _parseOpenAICompatibleModels(response.data, server);
54
break;
55
case ServerType.ollama:
56
+ case ServerType.ollamaCloud:
57
models = await _parseAndEnrichOllamaModels(response.data, server);
58
break;
59
case ServerType.openRouter:
@@ -110,9 +111,11 @@ class ServerApiService {
111
}
112
break;
113
case ServerType.ollama:
113
- // Ollama auto-loads models on /api/chat. Calling /api/generate here
114
- // would trigger an auto-pull (download) when the model is not already
115
- // present on the Ollama server, which is not what the user wants.
114
+ case ServerType.ollamaCloud:
115
+ // Ollama (and Ollama Cloud) auto-loads models on /api/chat. Calling
116
+ // /api/generate here would trigger an auto-pull (download) when the
117
+ // model is not already present on the server, which is not what the
118
+ // user wants.
119
return;
120
case ServerType.openRouter:
121
case ServerType.onDevice:
@@ -152,9 +155,11 @@ class ServerApiService {
155
throw Exception(_extractApiErrorMessage(e.response?.data) ?? e.message);
156
}
157
case ServerType.ollama:
155
- // Ollama auto-loads models on /api/chat. Calling /api/generate here
156
- // would trigger an auto-pull (download) when the model is not already
157
- // present on the Ollama server, which is not what the user wants.
158
+ case ServerType.ollamaCloud:
159
+ // Ollama (and Ollama Cloud) auto-loads models on /api/chat. Calling
160
+ // /api/generate here would trigger an auto-pull (download) when the
161
+ // model is not already present on the server, which is not what the
162
+ // user wants.
163
return null;
164
case ServerType.openRouter:
165
case ServerType.onDevice:
@@ -215,6 +220,10 @@ class ServerApiService {
220
options: Options(headers: buildServerAuthHeaders(server)),
221
);
222
break;
223
+ case ServerType.ollamaCloud:
224
+ // Ollama Cloud manages model lifecycle server-side; no client-side
225
+ // unload action is supported.
226
+ return;
227
case ServerType.openRouter:
228
case ServerType.onDevice:
229
break;
lib/features/servers/views/add_server_screen.dart
+48
-12
@@ -39,8 +39,12 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
39
String? _testResult;
40
41
bool get _isEditing => widget.editServer != null;
42
- bool get _requiresEndpoint => _selectedType != ServerType.openRouter;
43
- bool get _requiresMandatoryApiKey => _selectedType == ServerType.openRouter;
42
+ bool get _requiresEndpoint =>
43
+ _selectedType != ServerType.openRouter &&
44
+ _selectedType != ServerType.ollamaCloud;
45
+ bool get _requiresMandatoryApiKey =>
46
+ _selectedType == ServerType.openRouter ||
47
+ _selectedType == ServerType.ollamaCloud;
48
49
@override
50
void initState() {
@@ -82,6 +86,9 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
86
87
if (type == ServerType.openRouter) {
88
_portController.text = '443';
89
+ } else if (type == ServerType.ollamaCloud) {
90
+ _portController.text = AppConstants.ollamaCloudDefaultPort.toString();
91
+ _hostController.text = AppConstants.ollamaCloudBaseUrl;
92
} else if (type == ServerType.lmStudio) {
93
_portController.text = AppConstants.lmStudioDefaultPort.toString();
94
} else if (type == ServerType.openAICompatible) {
@@ -134,15 +141,24 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
141
}
142
143
Server _buildServer() {
144
+ final rawHost = _hostController.text.trim();
145
+ final rawPort = _portController.text.trim();
146
+ final host = _selectedType == ServerType.ollamaCloud
147
+ ? AppConstants.ollamaCloudBaseUrl
148
+ : rawHost;
149
+ final port = _selectedType == ServerType.ollamaCloud
150
+ ? AppConstants.ollamaCloudDefaultPort
151
+ : int.parse(rawPort);
152
+
153
return Server(
154
id:
155
widget.editServer?.id ??
156
DateTime.now().millisecondsSinceEpoch.toString(),
157
name: _nameController.text.trim(),
158
type: _selectedType,
143
- host: _hostController.text.trim(),
144
- port: int.parse(_portController.text.trim()),
145
- apiKey: _selectedType == ServerType.openRouter
159
+ host: host,
160
+ port: port,
161
+ apiKey: _requiresMandatoryApiKey
162
? _apiKeyController.text.trim()
163
: (_apiKeyController.text.trim().isNotEmpty
164
? _apiKeyController.text.trim()
@@ -252,9 +268,12 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
268
if (_requiresMandatoryApiKey) {
269
final l10n = AppLocalizations.of(context)!;
270
if (value == null || value.trim().isEmpty) {
255
- return l10n.api_key_required_openrouter;
271
+ return _selectedType == ServerType.ollamaCloud
272
+ ? l10n.api_key_required_ollama_cloud
273
+ : l10n.api_key_required_openrouter;
274
}
257
- if (!value.trim().startsWith('sk-')) {
275
+ if (_selectedType == ServerType.openRouter &&
276
+ !value.trim().startsWith('sk-')) {
277
return l10n.api_key_format;
278
}
279
}
@@ -486,7 +505,9 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
505
? l10n.api_key_required
506
: l10n.api_key_optional,
507
hintText: _requiresMandatoryApiKey
489
- ? l10n.api_key_hint_openrouter
508
+ ? (_selectedType == ServerType.ollamaCloud
509
+ ? l10n.api_key_hint_ollama_cloud
510
+ : l10n.api_key_hint_openrouter)
511
: l10n.api_key_hint_generic,
512
),
513
validator: _validateApiKey,
@@ -515,7 +536,9 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
536
const SizedBox(width: 8),
537
Expanded(
538
child: Text(
518
- l10n.openrouter_disclosure,
539
+ _selectedType == ServerType.ollamaCloud
540
+ ? l10n.ollama_cloud_disclosure
541
+ : l10n.openrouter_disclosure,
542
style: theme.textTheme.bodySmall?.copyWith(
543
color: theme.colorScheme.onSurface.withValues(alpha: 0.8),
544
height: 1.4,
@@ -576,9 +599,7 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
599
),
600
const SizedBox(height: 6),
601
Text(
579
- _requiresEndpoint
580
- ? 'Configure a local or self-hosted endpoint, then verify the connection before saving.'
581
- : 'Connect through OpenRouter with a valid API key and keep this profile ready for model routing.',
602
+ _addServerHeaderSubtitle(l10n),
603
style: theme.textTheme.bodyMedium?.copyWith(
604
color: colorScheme.onSurfaceVariant,
605
height: 1.4,
@@ -801,8 +822,23 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
822
ServerType.lmStudio => l10n.server_type_lm_studio,
823
ServerType.openAICompatible => l10n.server_type_openai_display,
824
ServerType.ollama => l10n.server_type_ollama,
825
+ ServerType.ollamaCloud => l10n.server_type_ollama_cloud,
826
ServerType.openRouter => l10n.server_type_openrouter,
827
ServerType.onDevice => l10n.server_type_on_device_display,
828
};
829
}
830
+
831
+ String _addServerHeaderSubtitle(AppLocalizations l10n) {
832
+ switch (_selectedType) {
833
+ case ServerType.lmStudio:
834
+ case ServerType.openAICompatible:
835
+ case ServerType.ollama:
836
+ case ServerType.onDevice:
837
+ return l10n.add_server_endpoint_subtitle;
838
+ case ServerType.ollamaCloud:
839
+ return l10n.add_server_ollama_cloud_subtitle;
840
+ case ServerType.openRouter:
841
+ return l10n.add_server_openrouter_subtitle;
842
+ }
843
+ }
844
}
lib/features/servers/views/components/server_card.dart
+7
@@ -44,6 +44,8 @@ class ServerCard extends StatelessWidget {
44
return l10n.server_type_openai_display;
45
case ServerType.ollama:
46
return l10n.server_type_ollama_display;
47
+ case ServerType.ollamaCloud:
48
+ return l10n.server_type_ollama_cloud_display;
49
case ServerType.openRouter:
50
return l10n.server_type_openrouter_display;
51
case ServerType.onDevice:
@@ -55,6 +57,9 @@ class ServerCard extends StatelessWidget {
57
if (server.type == ServerType.openRouter) {
58
return l10n.server_address_openrouter;
59
}
60
+ if (server.type == ServerType.ollamaCloud) {
61
+ return l10n.server_address_ollama_cloud;
62
+ }
63
if (server.type == ServerType.onDevice) {
64
return l10n.server_address_on_device;
65
}
@@ -95,6 +100,8 @@ class ServerCard extends StatelessWidget {
100
return HugeIcons.strokeRoundedAiChat01;
101
case ServerType.ollama:
102
return HugeIcons.strokeRoundedRobot01;
103
+ case ServerType.ollamaCloud:
104
+ return HugeIcons.strokeRoundedAiCloud;
105
case ServerType.openRouter:
106
return HugeIcons.strokeRoundedCloud;
107
case ServerType.onDevice:
lib/features/servers/views/components/server_icon_picker.dart
+5
@@ -234,6 +234,11 @@ HugeIconData? getDefaultServerIcon(String? serverType) {
234
(icon) => icon.name == 'Bot',
235
orElse: () => serverIcons.first,
236
);
237
+ case 'ollamaCloud':
238
+ return serverIcons.firstWhere(
239
+ (icon) => icon.name == 'AI Cloud',
240
+ orElse: () => serverIcons.first,
241
+ );
242
case 'openRouter':
243
return serverIcons.firstWhere(
244
(icon) => icon.name == 'Cloud',
lib/features/servers/views/components/server_type_selector.dart
+6
@@ -38,6 +38,12 @@ class ServerTypeSelector extends StatelessWidget {
38
HugeIcons.strokeRoundedPencilEdit02,
39
Colors.orange,
40
),
41
+ (
42
+ ServerType.ollamaCloud,
43
+ l10n.server_type_ollama_cloud,
44
+ HugeIcons.strokeRoundedAiCloud,
45
+ Colors.deepOrange,
46
+ ),
47
(
48
ServerType.openRouter,
49
l10n.server_type_openrouter,
lib/features/settings/views/settings_screen.dart
+1
@@ -475,6 +475,7 @@ class SettingsViews extends ConsumerWidget {
475
providers: [
476
l10n.server_type_lm_studio,
477
l10n.server_type_ollama,
478
+ l10n.server_type_ollama_cloud,
479
l10n.server_type_openrouter,
480
],
481
highlights: [
lib/l10n/app_ar.arb
+1
@@ -819,6 +819,7 @@
819
"lm_studio_memory_settings_title": "الذاكرة المخصصة للتوصيات",
820
"lm_studio_memory_settings_desc": "تُستخدم لتقدير ما إذا كانت النماذج تتناسب مع جهازك في متصفح النماذج.",
821
"think_button_label": "تفكير",
822
+ "thinking_mode_title": "وضع التفكير",
823
"reasoning_effort_low": "منخفض",
824
"reasoning_effort_medium": "متوسط",
825
"reasoning_effort_high": "مرتفع",
lib/l10n/app_bn.arb
+1
@@ -819,6 +819,7 @@
819
"lm_studio_memory_settings_title": "সুপারিশের জন্য মেমরি",
820
"lm_studio_memory_settings_desc": "মডেল ব্রাউজারে মডেলগুলি আপনার ডিভাইসে চলবে কিনা তা অনুমান করতে ব্যবহৃত হয়।",
821
"think_button_label": "চিন্তা করুন",
822
+ "thinking_mode_title": "চিন্তার মোড",
823
"reasoning_effort_low": "কম",
824
"reasoning_effort_medium": "মাঝারি",
825
"reasoning_effort_high": "উচ্চ",
lib/l10n/app_en.arb
+45
-1
@@ -1345,7 +1345,7 @@
1345
"@onboarding_connect_server": {
1346
"description": "Onboarding heading for server connection"
1347
},
1348
- "onboarding_connect_desc": "Connect to LM Studio, Ollama, or\nOpenRouter to start your private AI\nexperience.",
1348
+ "onboarding_connect_desc": "Connect to LM Studio, Ollama,\nOllama Cloud, or OpenRouter to\nstart your private AI experience.",
1349
"@onboarding_connect_desc": {
1350
"description": "Onboarding server connection description"
1351
},
@@ -1377,6 +1377,46 @@
1377
"@server_type_ollama": {
1378
"description": "Ollama server type card title"
1379
},
1380
+ "server_type_ollama_cloud": "Ollama Cloud",
1381
+ "@server_type_ollama_cloud": {
1382
+ "description": "Ollama Cloud server type card title"
1383
+ },
1384
+ "server_type_ollama_cloud_sub": "CLOUD MANAGED",
1385
+ "@server_type_ollama_cloud_sub": {
1386
+ "description": "Ollama Cloud server type card subtitle"
1387
+ },
1388
+ "server_type_ollama_cloud_display": "Ollama Cloud",
1389
+ "@server_type_ollama_cloud_display": {
1390
+ "description": "Ollama Cloud server type display"
1391
+ },
1392
+ "server_address_ollama_cloud": "ollama.com",
1393
+ "@server_address_ollama_cloud": {
1394
+ "description": "Ollama Cloud default address"
1395
+ },
1396
+ "ollama_cloud_disclosure": "By connecting Ollama Cloud, your chat messages and inputs are sent to Ollama's managed servers for inference. LocalMind does not track or store your conversations. You can revoke this key any time at ollama.com/settings/keys.",
1397
+ "@ollama_cloud_disclosure": {
1398
+ "description": "Disclosure shown when connecting to Ollama Cloud about data being sent to their servers"
1399
+ },
1400
+ "api_key_required_ollama_cloud": "API Key required for Ollama Cloud",
1401
+ "@api_key_required_ollama_cloud": {
1402
+ "description": "Validation: API key required for Ollama Cloud"
1403
+ },
1404
+ "api_key_hint_ollama_cloud": "Paste your Ollama Cloud API key",
1405
+ "@api_key_hint_ollama_cloud": {
1406
+ "description": "Hint for the Ollama Cloud API key input"
1407
+ },
1408
+ "add_server_ollama_cloud_subtitle": "Connect to Ollama Cloud with an API key from ollama.com/settings/keys to access managed cloud models.",
1409
+ "@add_server_ollama_cloud_subtitle": {
1410
+ "description": "Add server screen subtitle for Ollama Cloud"
1411
+ },
1412
+ "add_server_openrouter_subtitle": "Connect through OpenRouter with a valid API key and keep this profile ready for model routing.",
1413
+ "@add_server_openrouter_subtitle": {
1414
+ "description": "Add server screen subtitle for OpenRouter"
1415
+ },
1416
+ "add_server_endpoint_subtitle": "Configure a local or self-hosted endpoint, then verify the connection before saving.",
1417
+ "@add_server_endpoint_subtitle": {
1418
+ "description": "Add server screen subtitle for endpoint-based servers"
1419
+ },
1420
"server_type_openrouter": "OpenRouter",
1421
"@server_type_openrouter": {
1422
"description": "OpenRouter server type card title"
@@ -2977,6 +3017,10 @@
3017
"@think_button_label": {
3018
"description": "Label for the reasoning-toggle button shown for reasoning-capable models"
3019
},
3020
+ "thinking_mode_title": "Thinking mode",
3021
+ "@thinking_mode_title": {
3022
+ "description": "Title for the reasoning mode selector shown in the model picker"
3023
+ },
3024
"reasoning_effort_low": "Low",
3025
"@reasoning_effort_low": {
3026
"description": "Low reasoning effort level"
lib/l10n/app_es.arb
+1
@@ -819,6 +819,7 @@
819
"lm_studio_memory_settings_title": "Memoria para recomendaciones",
820
"lm_studio_memory_settings_desc": "Se utiliza para estimar si los modelos caben en tu máquina en el explorador de modelos.",
821
"think_button_label": "Pensar",
822
+ "thinking_mode_title": "Modo de pensamiento",
823
"reasoning_effort_low": "Bajo",
824
"reasoning_effort_medium": "Medio",
825
"reasoning_effort_high": "Alto",
lib/l10n/app_hi.arb
+1
@@ -819,6 +819,7 @@
819
"lm_studio_memory_settings_title": "सिफारिशों के लिए मेमोरी",
820
"lm_studio_memory_settings_desc": "मॉडल ब्राउज़र में यह अनुमान लगाने के लिए उपयोग किया जाता है कि क्या मॉडल आपकी मशीन में फिट होंगे।",
821
"think_button_label": "सोचें",
822
+ "thinking_mode_title": "सोच का तरीका",
823
"reasoning_effort_low": "कम",
824
"reasoning_effort_medium": "मध्यम",
825
"reasoning_effort_high": "उच्च",
lib/l10n/app_it.arb
+1
@@ -819,6 +819,7 @@
819
"lm_studio_memory_settings_title": "Memoria per raccomandazioni",
820
"lm_studio_memory_settings_desc": "Usato per stimare se i modelli entrano nella memoria della tua macchina nell'esploratore di modelli.",
821
"think_button_label": "Pensa",
822
+ "thinking_mode_title": "Modalità di pensiero",
823
"reasoning_effort_low": "Basso",
824
"reasoning_effort_medium": "Medio",
825
"reasoning_effort_high": "Alto",
lib/l10n/app_ja.arb
+1
@@ -819,6 +819,7 @@
819
"lm_studio_memory_settings_title": "推奨用メモリ設定",
820
"lm_studio_memory_settings_desc": "モデルブラウザーでモデルがお使いのデバイスに収まるかどうかを判断するために使用されます。",
821
"think_button_label": "思考",
822
+ "thinking_mode_title": "思考モード",
823
"reasoning_effort_low": "低",
824
"reasoning_effort_medium": "中",
825
"reasoning_effort_high": "高",
lib/l10n/app_localizations.dart
+67
-1
@@ -1922,7 +1922,7 @@ abstract class AppLocalizations {
1922
/// Onboarding server connection description
1923
///
1924
/// In en, this message translates to:
1925
- /// **'Connect to LM Studio, Ollama, or\nOpenRouter to start your private AI\nexperience.'**
1925
+ /// **'Connect to LM Studio, Ollama,\nOllama Cloud, or OpenRouter to\nstart your private AI experience.'**
1926
String get onboarding_connect_desc;
1927
1928
/// Subtitle for the OpenAI compatible server card in onboarding
@@ -1967,6 +1967,66 @@ abstract class AppLocalizations {
1967
/// **'Ollama'**
1968
String get server_type_ollama;
1969
1970
+ /// Ollama Cloud server type card title
1971
+ ///
1972
+ /// In en, this message translates to:
1973
+ /// **'Ollama Cloud'**
1974
+ String get server_type_ollama_cloud;
1975
+
1976
+ /// Ollama Cloud server type card subtitle
1977
+ ///
1978
+ /// In en, this message translates to:
1979
+ /// **'CLOUD MANAGED'**
1980
+ String get server_type_ollama_cloud_sub;
1981
+
1982
+ /// Ollama Cloud server type display
1983
+ ///
1984
+ /// In en, this message translates to:
1985
+ /// **'Ollama Cloud'**
1986
+ String get server_type_ollama_cloud_display;
1987
+
1988
+ /// Ollama Cloud default address
1989
+ ///
1990
+ /// In en, this message translates to:
1991
+ /// **'ollama.com'**
1992
+ String get server_address_ollama_cloud;
1993
+
1994
+ /// Disclosure shown when connecting to Ollama Cloud about data being sent to their servers
1995
+ ///
1996
+ /// In en, this message translates to:
1997
+ /// **'By connecting Ollama Cloud, your chat messages and inputs are sent to Ollama\'s managed servers for inference. LocalMind does not track or store your conversations. You can revoke this key any time at ollama.com/settings/keys.'**
1998
+ String get ollama_cloud_disclosure;
1999
+
2000
+ /// Validation: API key required for Ollama Cloud
2001
+ ///
2002
+ /// In en, this message translates to:
2003
+ /// **'API Key required for Ollama Cloud'**
2004
+ String get api_key_required_ollama_cloud;
2005
+
2006
+ /// Hint for the Ollama Cloud API key input
2007
+ ///
2008
+ /// In en, this message translates to:
2009
+ /// **'Paste your Ollama Cloud API key'**
2010
+ String get api_key_hint_ollama_cloud;
2011
+
2012
+ /// Add server screen subtitle for Ollama Cloud
2013
+ ///
2014
+ /// In en, this message translates to:
2015
+ /// **'Connect to Ollama Cloud with an API key from ollama.com/settings/keys to access managed cloud models.'**
2016
+ String get add_server_ollama_cloud_subtitle;
2017
+
2018
+ /// Add server screen subtitle for OpenRouter
2019
+ ///
2020
+ /// In en, this message translates to:
2021
+ /// **'Connect through OpenRouter with a valid API key and keep this profile ready for model routing.'**
2022
+ String get add_server_openrouter_subtitle;
2023
+
2024
+ /// Add server screen subtitle for endpoint-based servers
2025
+ ///
2026
+ /// In en, this message translates to:
2027
+ /// **'Configure a local or self-hosted endpoint, then verify the connection before saving.'**
2028
+ String get add_server_endpoint_subtitle;
2029
+
2030
/// OpenRouter server type card title
2031
///
2032
/// In en, this message translates to:
@@ -5027,6 +5087,12 @@ abstract class AppLocalizations {
5087
/// **'Think'**
5088
String get think_button_label;
5089
5090
+ /// Title for the reasoning mode selector shown in the model picker
5091
+ ///
5092
+ /// In en, this message translates to:
5093
+ /// **'Thinking mode'**
5094
+ String get thinking_mode_title;
5095
+
5096
/// Low reasoning effort level
5097
///
5098
/// In en, this message translates to:
lib/l10n/app_localizations_ar.dart
+38
@@ -1007,6 +1007,41 @@ class AppLocalizationsAr extends AppLocalizations {
1007
@override
1008
String get server_type_ollama => 'Ollama';
1009
1010
+ @override
1011
+ String get server_type_ollama_cloud => 'Ollama Cloud';
1012
+
1013
+ @override
1014
+ String get server_type_ollama_cloud_sub => 'CLOUD MANAGED';
1015
+
1016
+ @override
1017
+ String get server_type_ollama_cloud_display => 'Ollama Cloud';
1018
+
1019
+ @override
1020
+ String get server_address_ollama_cloud => 'ollama.com';
1021
+
1022
+ @override
1023
+ String get ollama_cloud_disclosure =>
1024
+ 'By connecting Ollama Cloud, your chat messages and inputs are sent to Ollama\'s managed servers for inference. LocalMind does not track or store your conversations. You can revoke this key any time at ollama.com/settings/keys.';
1025
+
1026
+ @override
1027
+ String get api_key_required_ollama_cloud =>
1028
+ 'API Key required for Ollama Cloud';
1029
+
1030
+ @override
1031
+ String get api_key_hint_ollama_cloud => 'Paste your Ollama Cloud API key';
1032
+
1033
+ @override
1034
+ String get add_server_ollama_cloud_subtitle =>
1035
+ 'Connect to Ollama Cloud with an API key from ollama.com/settings/keys to access managed cloud models.';
1036
+
1037
+ @override
1038
+ String get add_server_openrouter_subtitle =>
1039
+ 'Connect through OpenRouter with a valid API key and keep this profile ready for model routing.';
1040
+
1041
+ @override
1042
+ String get add_server_endpoint_subtitle =>
1043
+ 'Configure a local or self-hosted endpoint, then verify the connection before saving.';
1044
+
1045
@override
1046
String get server_type_openrouter => 'OpenRouter';
1047
@@ -2672,6 +2707,9 @@ class AppLocalizationsAr extends AppLocalizations {
2707
@override
2708
String get think_button_label => 'تفكير';
2709
2710
+ @override
2711
+ String get thinking_mode_title => 'وضع التفكير';
2712
+
2713
@override
2714
String get reasoning_effort_low => 'منخفض';
2715
lib/l10n/app_localizations_bn.dart
+38
@@ -1013,6 +1013,41 @@ class AppLocalizationsBn extends AppLocalizations {
1013
@override
1014
String get server_type_ollama => 'Ollama';
1015
1016
+ @override
1017
+ String get server_type_ollama_cloud => 'Ollama Cloud';
1018
+
1019
+ @override
1020
+ String get server_type_ollama_cloud_sub => 'CLOUD MANAGED';
1021
+
1022
+ @override
1023
+ String get server_type_ollama_cloud_display => 'Ollama Cloud';
1024
+
1025
+ @override
1026
+ String get server_address_ollama_cloud => 'ollama.com';
1027
+
1028
+ @override
1029
+ String get ollama_cloud_disclosure =>
1030
+ 'By connecting Ollama Cloud, your chat messages and inputs are sent to Ollama\'s managed servers for inference. LocalMind does not track or store your conversations. You can revoke this key any time at ollama.com/settings/keys.';
1031
+
1032
+ @override
1033
+ String get api_key_required_ollama_cloud =>
1034
+ 'API Key required for Ollama Cloud';
1035
+
1036
+ @override
1037
+ String get api_key_hint_ollama_cloud => 'Paste your Ollama Cloud API key';
1038
+
1039
+ @override
1040
+ String get add_server_ollama_cloud_subtitle =>
1041
+ 'Connect to Ollama Cloud with an API key from ollama.com/settings/keys to access managed cloud models.';
1042
+
1043
+ @override
1044
+ String get add_server_openrouter_subtitle =>
1045
+ 'Connect through OpenRouter with a valid API key and keep this profile ready for model routing.';
1046
+
1047
+ @override
1048
+ String get add_server_endpoint_subtitle =>
1049
+ 'Configure a local or self-hosted endpoint, then verify the connection before saving.';
1050
+
1051
@override
1052
String get server_type_openrouter => 'OpenRouter';
1053
@@ -2693,6 +2728,9 @@ class AppLocalizationsBn extends AppLocalizations {
2728
@override
2729
String get think_button_label => 'চিন্তা করুন';
2730
2731
+ @override
2732
+ String get thinking_mode_title => 'চিন্তার মোড';
2733
+
2734
@override
2735
String get reasoning_effort_low => 'কম';
2736
lib/l10n/app_localizations_en.dart
+39
-1
@@ -981,7 +981,7 @@ class AppLocalizationsEn extends AppLocalizations {
981
982
@override
983
String get onboarding_connect_desc =>
984
- 'Connect to LM Studio, Ollama, or\nOpenRouter to start your private AI\nexperience.';
984
+ 'Connect to LM Studio, Ollama,\nOllama Cloud, or OpenRouter to\nstart your private AI experience.';
985
986
@override
987
String get openai_compatible_api => 'OpenAI-compatible API';
@@ -1004,6 +1004,41 @@ class AppLocalizationsEn extends AppLocalizations {
1004
@override
1005
String get server_type_ollama => 'Ollama';
1006
1007
+ @override
1008
+ String get server_type_ollama_cloud => 'Ollama Cloud';
1009
+
1010
+ @override
1011
+ String get server_type_ollama_cloud_sub => 'CLOUD MANAGED';
1012
+
1013
+ @override
1014
+ String get server_type_ollama_cloud_display => 'Ollama Cloud';
1015
+
1016
+ @override
1017
+ String get server_address_ollama_cloud => 'ollama.com';
1018
+
1019
+ @override
1020
+ String get ollama_cloud_disclosure =>
1021
+ 'By connecting Ollama Cloud, your chat messages and inputs are sent to Ollama\'s managed servers for inference. LocalMind does not track or store your conversations. You can revoke this key any time at ollama.com/settings/keys.';
1022
+
1023
+ @override
1024
+ String get api_key_required_ollama_cloud =>
1025
+ 'API Key required for Ollama Cloud';
1026
+
1027
+ @override
1028
+ String get api_key_hint_ollama_cloud => 'Paste your Ollama Cloud API key';
1029
+
1030
+ @override
1031
+ String get add_server_ollama_cloud_subtitle =>
1032
+ 'Connect to Ollama Cloud with an API key from ollama.com/settings/keys to access managed cloud models.';
1033
+
1034
+ @override
1035
+ String get add_server_openrouter_subtitle =>
1036
+ 'Connect through OpenRouter with a valid API key and keep this profile ready for model routing.';
1037
+
1038
+ @override
1039
+ String get add_server_endpoint_subtitle =>
1040
+ 'Configure a local or self-hosted endpoint, then verify the connection before saving.';
1041
+
1042
@override
1043
String get server_type_openrouter => 'OpenRouter';
1044
@@ -2681,6 +2716,9 @@ class AppLocalizationsEn extends AppLocalizations {
2716
@override
2717
String get think_button_label => 'Think';
2718
2719
+ @override
2720
+ String get thinking_mode_title => 'Thinking mode';
2721
+
2722
@override
2723
String get reasoning_effort_low => 'Low';
2724
lib/l10n/app_localizations_es.dart
+38
@@ -1014,6 +1014,41 @@ class AppLocalizationsEs extends AppLocalizations {
1014
@override
1015
String get server_type_ollama => 'Ollama';
1016
1017
+ @override
1018
+ String get server_type_ollama_cloud => 'Ollama Cloud';
1019
+
1020
+ @override
1021
+ String get server_type_ollama_cloud_sub => 'CLOUD MANAGED';
1022
+
1023
+ @override
1024
+ String get server_type_ollama_cloud_display => 'Ollama Cloud';
1025
+
1026
+ @override
1027
+ String get server_address_ollama_cloud => 'ollama.com';
1028
+
1029
+ @override
1030
+ String get ollama_cloud_disclosure =>
1031
+ 'By connecting Ollama Cloud, your chat messages and inputs are sent to Ollama\'s managed servers for inference. LocalMind does not track or store your conversations. You can revoke this key any time at ollama.com/settings/keys.';
1032
+
1033
+ @override
1034
+ String get api_key_required_ollama_cloud =>
1035
+ 'API Key required for Ollama Cloud';
1036
+
1037
+ @override
1038
+ String get api_key_hint_ollama_cloud => 'Paste your Ollama Cloud API key';
1039
+
1040
+ @override
1041
+ String get add_server_ollama_cloud_subtitle =>
1042
+ 'Connect to Ollama Cloud with an API key from ollama.com/settings/keys to access managed cloud models.';
1043
+
1044
+ @override
1045
+ String get add_server_openrouter_subtitle =>
1046
+ 'Connect through OpenRouter with a valid API key and keep this profile ready for model routing.';
1047
+
1048
+ @override
1049
+ String get add_server_endpoint_subtitle =>
1050
+ 'Configure a local or self-hosted endpoint, then verify the connection before saving.';
1051
+
1052
@override
1053
String get server_type_openrouter => 'OpenRouter';
1054
@@ -2697,6 +2732,9 @@ class AppLocalizationsEs extends AppLocalizations {
2732
@override
2733
String get think_button_label => 'Pensar';
2734
2735
+ @override
2736
+ String get thinking_mode_title => 'Modo de pensamiento';
2737
+
2738
@override
2739
String get reasoning_effort_low => 'Bajo';
2740
lib/l10n/app_localizations_hi.dart
+38
@@ -1012,6 +1012,41 @@ class AppLocalizationsHi extends AppLocalizations {
1012
@override
1013
String get server_type_ollama => 'Ollama';
1014
1015
+ @override
1016
+ String get server_type_ollama_cloud => 'Ollama Cloud';
1017
+
1018
+ @override
1019
+ String get server_type_ollama_cloud_sub => 'CLOUD MANAGED';
1020
+
1021
+ @override
1022
+ String get server_type_ollama_cloud_display => 'Ollama Cloud';
1023
+
1024
+ @override
1025
+ String get server_address_ollama_cloud => 'ollama.com';
1026
+
1027
+ @override
1028
+ String get ollama_cloud_disclosure =>
1029
+ 'By connecting Ollama Cloud, your chat messages and inputs are sent to Ollama\'s managed servers for inference. LocalMind does not track or store your conversations. You can revoke this key any time at ollama.com/settings/keys.';
1030
+
1031
+ @override
1032
+ String get api_key_required_ollama_cloud =>
1033
+ 'API Key required for Ollama Cloud';
1034
+
1035
+ @override
1036
+ String get api_key_hint_ollama_cloud => 'Paste your Ollama Cloud API key';
1037
+
1038
+ @override
1039
+ String get add_server_ollama_cloud_subtitle =>
1040
+ 'Connect to Ollama Cloud with an API key from ollama.com/settings/keys to access managed cloud models.';
1041
+
1042
+ @override
1043
+ String get add_server_openrouter_subtitle =>
1044
+ 'Connect through OpenRouter with a valid API key and keep this profile ready for model routing.';
1045
+
1046
+ @override
1047
+ String get add_server_endpoint_subtitle =>
1048
+ 'Configure a local or self-hosted endpoint, then verify the connection before saving.';
1049
+
1050
@override
1051
String get server_type_openrouter => 'OpenRouter';
1052
@@ -2686,6 +2721,9 @@ class AppLocalizationsHi extends AppLocalizations {
2721
@override
2722
String get think_button_label => 'सोचें';
2723
2724
+ @override
2725
+ String get thinking_mode_title => 'सोच का तरीका';
2726
+
2727
@override
2728
String get reasoning_effort_low => 'कम';
2729
lib/l10n/app_localizations_it.dart
+38
@@ -1014,6 +1014,41 @@ class AppLocalizationsIt extends AppLocalizations {
1014
@override
1015
String get server_type_ollama => 'Ollama';
1016
1017
+ @override
1018
+ String get server_type_ollama_cloud => 'Ollama Cloud';
1019
+
1020
+ @override
1021
+ String get server_type_ollama_cloud_sub => 'CLOUD MANAGED';
1022
+
1023
+ @override
1024
+ String get server_type_ollama_cloud_display => 'Ollama Cloud';
1025
+
1026
+ @override
1027
+ String get server_address_ollama_cloud => 'ollama.com';
1028
+
1029
+ @override
1030
+ String get ollama_cloud_disclosure =>
1031
+ 'By connecting Ollama Cloud, your chat messages and inputs are sent to Ollama\'s managed servers for inference. LocalMind does not track or store your conversations. You can revoke this key any time at ollama.com/settings/keys.';
1032
+
1033
+ @override
1034
+ String get api_key_required_ollama_cloud =>
1035
+ 'API Key required for Ollama Cloud';
1036
+
1037
+ @override
1038
+ String get api_key_hint_ollama_cloud => 'Paste your Ollama Cloud API key';
1039
+
1040
+ @override
1041
+ String get add_server_ollama_cloud_subtitle =>
1042
+ 'Connect to Ollama Cloud with an API key from ollama.com/settings/keys to access managed cloud models.';
1043
+
1044
+ @override
1045
+ String get add_server_openrouter_subtitle =>
1046
+ 'Connect through OpenRouter with a valid API key and keep this profile ready for model routing.';
1047
+
1048
+ @override
1049
+ String get add_server_endpoint_subtitle =>
1050
+ 'Configure a local or self-hosted endpoint, then verify the connection before saving.';
1051
+
1052
@override
1053
String get server_type_openrouter => 'OpenRouter';
1054
@@ -2699,6 +2734,9 @@ class AppLocalizationsIt extends AppLocalizations {
2734
@override
2735
String get think_button_label => 'Pensa';
2736
2737
+ @override
2738
+ String get thinking_mode_title => 'Modalità di pensiero';
2739
+
2740
@override
2741
String get reasoning_effort_low => 'Basso';
2742
lib/l10n/app_localizations_ja.dart
+38
@@ -991,6 +991,41 @@ class AppLocalizationsJa extends AppLocalizations {
991
@override
992
String get server_type_ollama => 'Ollama';
993
994
+ @override
995
+ String get server_type_ollama_cloud => 'Ollama Cloud';
996
+
997
+ @override
998
+ String get server_type_ollama_cloud_sub => 'CLOUD MANAGED';
999
+
1000
+ @override
1001
+ String get server_type_ollama_cloud_display => 'Ollama Cloud';
1002
+
1003
+ @override
1004
+ String get server_address_ollama_cloud => 'ollama.com';
1005
+
1006
+ @override
1007
+ String get ollama_cloud_disclosure =>
1008
+ 'By connecting Ollama Cloud, your chat messages and inputs are sent to Ollama\'s managed servers for inference. LocalMind does not track or store your conversations. You can revoke this key any time at ollama.com/settings/keys.';
1009
+
1010
+ @override
1011
+ String get api_key_required_ollama_cloud =>
1012
+ 'API Key required for Ollama Cloud';
1013
+
1014
+ @override
1015
+ String get api_key_hint_ollama_cloud => 'Paste your Ollama Cloud API key';
1016
+
1017
+ @override
1018
+ String get add_server_ollama_cloud_subtitle =>
1019
+ 'Connect to Ollama Cloud with an API key from ollama.com/settings/keys to access managed cloud models.';
1020
+
1021
+ @override
1022
+ String get add_server_openrouter_subtitle =>
1023
+ 'Connect through OpenRouter with a valid API key and keep this profile ready for model routing.';
1024
+
1025
+ @override
1026
+ String get add_server_endpoint_subtitle =>
1027
+ 'Configure a local or self-hosted endpoint, then verify the connection before saving.';
1028
+
1029
@override
1030
String get server_type_openrouter => 'OpenRouter';
1031
@@ -2634,6 +2669,9 @@ class AppLocalizationsJa extends AppLocalizations {
2669
@override
2670
String get think_button_label => '思考';
2671
2672
+ @override
2673
+ String get thinking_mode_title => '思考モード';
2674
+
2675
@override
2676
String get reasoning_effort_low => '低';
2677
lib/l10n/app_localizations_ru.dart
+38
@@ -1013,6 +1013,41 @@ class AppLocalizationsRu extends AppLocalizations {
1013
@override
1014
String get server_type_ollama => 'Оллама';
1015
1016
+ @override
1017
+ String get server_type_ollama_cloud => 'Ollama Cloud';
1018
+
1019
+ @override
1020
+ String get server_type_ollama_cloud_sub => 'CLOUD MANAGED';
1021
+
1022
+ @override
1023
+ String get server_type_ollama_cloud_display => 'Ollama Cloud';
1024
+
1025
+ @override
1026
+ String get server_address_ollama_cloud => 'ollama.com';
1027
+
1028
+ @override
1029
+ String get ollama_cloud_disclosure =>
1030
+ 'By connecting Ollama Cloud, your chat messages and inputs are sent to Ollama\'s managed servers for inference. LocalMind does not track or store your conversations. You can revoke this key any time at ollama.com/settings/keys.';
1031
+
1032
+ @override
1033
+ String get api_key_required_ollama_cloud =>
1034
+ 'API Key required for Ollama Cloud';
1035
+
1036
+ @override
1037
+ String get api_key_hint_ollama_cloud => 'Paste your Ollama Cloud API key';
1038
+
1039
+ @override
1040
+ String get add_server_ollama_cloud_subtitle =>
1041
+ 'Connect to Ollama Cloud with an API key from ollama.com/settings/keys to access managed cloud models.';
1042
+
1043
+ @override
1044
+ String get add_server_openrouter_subtitle =>
1045
+ 'Connect through OpenRouter with a valid API key and keep this profile ready for model routing.';
1046
+
1047
+ @override
1048
+ String get add_server_endpoint_subtitle =>
1049
+ 'Configure a local or self-hosted endpoint, then verify the connection before saving.';
1050
+
1051
@override
1052
String get server_type_openrouter => 'OpenRouter';
1053
@@ -2700,6 +2735,9 @@ class AppLocalizationsRu extends AppLocalizations {
2735
@override
2736
String get think_button_label => 'Мыслить';
2737
2738
+ @override
2739
+ String get thinking_mode_title => 'Режим мышления';
2740
+
2741
@override
2742
String get reasoning_effort_low => 'Низкая';
2743
lib/l10n/app_localizations_zh.dart
+41
@@ -984,6 +984,41 @@ class AppLocalizationsZh extends AppLocalizations {
984
@override
985
String get server_type_ollama => 'Ollama';
986
987
+ @override
988
+ String get server_type_ollama_cloud => 'Ollama Cloud';
989
+
990
+ @override
991
+ String get server_type_ollama_cloud_sub => 'CLOUD MANAGED';
992
+
993
+ @override
994
+ String get server_type_ollama_cloud_display => 'Ollama Cloud';
995
+
996
+ @override
997
+ String get server_address_ollama_cloud => 'ollama.com';
998
+
999
+ @override
1000
+ String get ollama_cloud_disclosure =>
1001
+ 'By connecting Ollama Cloud, your chat messages and inputs are sent to Ollama\'s managed servers for inference. LocalMind does not track or store your conversations. You can revoke this key any time at ollama.com/settings/keys.';
1002
+
1003
+ @override
1004
+ String get api_key_required_ollama_cloud =>
1005
+ 'API Key required for Ollama Cloud';
1006
+
1007
+ @override
1008
+ String get api_key_hint_ollama_cloud => 'Paste your Ollama Cloud API key';
1009
+
1010
+ @override
1011
+ String get add_server_ollama_cloud_subtitle =>
1012
+ 'Connect to Ollama Cloud with an API key from ollama.com/settings/keys to access managed cloud models.';
1013
+
1014
+ @override
1015
+ String get add_server_openrouter_subtitle =>
1016
+ 'Connect through OpenRouter with a valid API key and keep this profile ready for model routing.';
1017
+
1018
+ @override
1019
+ String get add_server_endpoint_subtitle =>
1020
+ 'Configure a local or self-hosted endpoint, then verify the connection before saving.';
1021
+
1022
@override
1023
String get server_type_openrouter => 'OpenRouter';
1024
@@ -2615,6 +2650,9 @@ class AppLocalizationsZh extends AppLocalizations {
2650
@override
2651
String get think_button_label => '思考';
2652
2653
+ @override
2654
+ String get thinking_mode_title => '思考模式';
2655
+
2656
@override
2657
String get reasoning_effort_low => '低';
2658
@@ -5380,6 +5418,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
5418
@override
5419
String get think_button_label => '思考';
5420
5421
+ @override
5422
+ String get thinking_mode_title => '思考模式';
5423
+
5424
@override
5425
String get reasoning_effort_low => '低';
5426
lib/l10n/app_ru.arb
+1
@@ -819,6 +819,7 @@
819
"lm_studio_memory_settings_title": "Память для рекомендаций",
820
"lm_studio_memory_settings_desc": "Используется для оценки того, подходят ли модели для вашего устройства.",
821
"think_button_label": "Мыслить",
822
+ "thinking_mode_title": "Режим мышления",
823
"reasoning_effort_low": "Низкая",
824
"reasoning_effort_medium": "Средняя",
825
"reasoning_effort_high": "Высокая",
lib/l10n/app_zh.arb
+1
@@ -819,6 +819,7 @@
819
"lm_studio_memory_settings_title": "用于推荐的内存大小",
820
"lm_studio_memory_settings_desc": "用于在模型浏览器中预估模型是否可以在您的设备上运行。",
821
"think_button_label": "思考",
822
+ "thinking_mode_title": "思考模式",
823
"reasoning_effort_low": "低",
824
"reasoning_effort_medium": "中",
825
"reasoning_effort_high": "高",
lib/l10n/app_zh_TW.arb
+1
@@ -819,6 +819,7 @@
819
"lm_studio_memory_settings_title": "用於推薦的記憶體大小",
820
"lm_studio_memory_settings_desc": "用於在模型瀏覽器中預估模型是否可以在您的設備上運行。",
821
"think_button_label": "思考",
822
+ "thinking_mode_title": "思考模式",
823
"reasoning_effort_low": "低",
824
"reasoning_effort_medium": "中",
825
"reasoning_effort_high": "高",
lib/l10n/untranslated.txt
+118
-1
@@ -1 +1,118 @@
1
-{}
\ No newline at end of file
1
+{
2
+ "ar": [
3
+ "server_type_ollama_cloud",
4
+ "server_type_ollama_cloud_sub",
5
+ "server_type_ollama_cloud_display",
6
+ "server_address_ollama_cloud",
7
+ "ollama_cloud_disclosure",
8
+ "api_key_required_ollama_cloud",
9
+ "api_key_hint_ollama_cloud",
10
+ "add_server_ollama_cloud_subtitle",
11
+ "add_server_openrouter_subtitle",
12
+ "add_server_endpoint_subtitle"
13
+ ],
14
+
15
+ "bn": [
16
+ "server_type_ollama_cloud",
17
+ "server_type_ollama_cloud_sub",
18
+ "server_type_ollama_cloud_display",
19
+ "server_address_ollama_cloud",
20
+ "ollama_cloud_disclosure",
21
+ "api_key_required_ollama_cloud",
22
+ "api_key_hint_ollama_cloud",
23
+ "add_server_ollama_cloud_subtitle",
24
+ "add_server_openrouter_subtitle",
25
+ "add_server_endpoint_subtitle"
26
+ ],
27
+
28
+ "es": [
29
+ "server_type_ollama_cloud",
30
+ "server_type_ollama_cloud_sub",
31
+ "server_type_ollama_cloud_display",
32
+ "server_address_ollama_cloud",
33
+ "ollama_cloud_disclosure",
34
+ "api_key_required_ollama_cloud",
35
+ "api_key_hint_ollama_cloud",
36
+ "add_server_ollama_cloud_subtitle",
37
+ "add_server_openrouter_subtitle",
38
+ "add_server_endpoint_subtitle"
39
+ ],
40
+
41
+ "hi": [
42
+ "server_type_ollama_cloud",
43
+ "server_type_ollama_cloud_sub",
44
+ "server_type_ollama_cloud_display",
45
+ "server_address_ollama_cloud",
46
+ "ollama_cloud_disclosure",
47
+ "api_key_required_ollama_cloud",
48
+ "api_key_hint_ollama_cloud",
49
+ "add_server_ollama_cloud_subtitle",
50
+ "add_server_openrouter_subtitle",
51
+ "add_server_endpoint_subtitle"
52
+ ],
53
+
54
+ "it": [
55
+ "server_type_ollama_cloud",
56
+ "server_type_ollama_cloud_sub",
57
+ "server_type_ollama_cloud_display",
58
+ "server_address_ollama_cloud",
59
+ "ollama_cloud_disclosure",
60
+ "api_key_required_ollama_cloud",
61
+ "api_key_hint_ollama_cloud",
62
+ "add_server_ollama_cloud_subtitle",
63
+ "add_server_openrouter_subtitle",
64
+ "add_server_endpoint_subtitle"
65
+ ],
66
+
67
+ "ja": [
68
+ "server_type_ollama_cloud",
69
+ "server_type_ollama_cloud_sub",
70
+ "server_type_ollama_cloud_display",
71
+ "server_address_ollama_cloud",
72
+ "ollama_cloud_disclosure",
73
+ "api_key_required_ollama_cloud",
74
+ "api_key_hint_ollama_cloud",
75
+ "add_server_ollama_cloud_subtitle",
76
+ "add_server_openrouter_subtitle",
77
+ "add_server_endpoint_subtitle"
78
+ ],
79
+
80
+ "ru": [
81
+ "server_type_ollama_cloud",
82
+ "server_type_ollama_cloud_sub",
83
+ "server_type_ollama_cloud_display",
84
+ "server_address_ollama_cloud",
85
+ "ollama_cloud_disclosure",
86
+ "api_key_required_ollama_cloud",
87
+ "api_key_hint_ollama_cloud",
88
+ "add_server_ollama_cloud_subtitle",
89
+ "add_server_openrouter_subtitle",
90
+ "add_server_endpoint_subtitle"
91
+ ],
92
+
93
+ "zh": [
94
+ "server_type_ollama_cloud",
95
+ "server_type_ollama_cloud_sub",
96
+ "server_type_ollama_cloud_display",
97
+ "server_address_ollama_cloud",
98
+ "ollama_cloud_disclosure",
99
+ "api_key_required_ollama_cloud",
100
+ "api_key_hint_ollama_cloud",
101
+ "add_server_ollama_cloud_subtitle",
102
+ "add_server_openrouter_subtitle",
103
+ "add_server_endpoint_subtitle"
104
+ ],
105
+
106
+ "zh_TW": [
107
+ "server_type_ollama_cloud",
108
+ "server_type_ollama_cloud_sub",
109
+ "server_type_ollama_cloud_display",
110
+ "server_address_ollama_cloud",
111
+ "ollama_cloud_disclosure",
112
+ "api_key_required_ollama_cloud",
113
+ "api_key_hint_ollama_cloud",
114
+ "add_server_ollama_cloud_subtitle",
115
+ "add_server_openrouter_subtitle",
116
+ "add_server_endpoint_subtitle"
117
+ ]
118
+}
test/features/chat/tools/chat_service_adapter_selection_test.dart
+5
@@ -74,6 +74,11 @@ void main() {
74
final adapter = createAdapterForServerType(ServerType.ollama);
75
expect(adapter, isA<OllamaToolAdapter>());
76
});
77
+
78
+ test('uses Ollama adapter for Ollama Cloud servers', () {
79
+ final adapter = createAdapterForServerType(ServerType.ollamaCloud);
80
+ expect(adapter, isA<OllamaToolAdapter>());
81
+ });
82
});
83
84
group('ChatService implementation tool call adapter integration', () {
test/features/servers/server_ollama_cloud_test.dart
new
+104
@@ -0,0 +1,104 @@
1
+import 'package:flutter_test/flutter_test.dart';
2
+import 'package:localmind/core/constants/app_constants.dart';
3
+import 'package:localmind/core/models/enums.dart';
4
+import 'package:localmind/features/servers/data/models/server.dart';
5
+
6
+void main() {
7
+ group('Server base URL & endpoints for Ollama Cloud', () {
8
+ Server makeCloudServer({String? apiKey}) {
9
+ return Server(
10
+ id: 'cloud-1',
11
+ name: 'Ollama Cloud',
12
+ type: ServerType.ollamaCloud,
13
+ host: AppConstants.ollamaCloudBaseUrl,
14
+ port: AppConstants.ollamaCloudDefaultPort,
15
+ apiKey: apiKey,
16
+ createdAt: DateTime.now(),
17
+ lastConnectedAt: DateTime.now(),
18
+ );
19
+ }
20
+
21
+ test('baseUrl resolves to https://ollama.com', () {
22
+ final server = makeCloudServer();
23
+ expect(server.baseUrl, 'https://ollama.com');
24
+ });
25
+
26
+ test('displayAddress renders as ollama.com', () {
27
+ final server = makeCloudServer();
28
+ expect(server.displayAddress, 'ollama.com');
29
+ });
30
+
31
+ test('chat endpoint uses /api/chat on ollama.com', () {
32
+ final server = makeCloudServer();
33
+ expect(server.chatEndpoint, 'https://ollama.com/api/chat');
34
+ });
35
+
36
+ test('models endpoint uses /api/tags on ollama.com', () {
37
+ final server = makeCloudServer();
38
+ expect(server.modelsEndpoint, 'https://ollama.com/api/tags');
39
+ });
40
+
41
+ test('running models endpoint is empty for cloud (no /api/ps)', () {
42
+ final server = makeCloudServer();
43
+ expect(server.runningModelsEndpoint, isEmpty);
44
+ });
45
+
46
+ test('load/unload endpoints are empty for cloud', () {
47
+ final server = makeCloudServer();
48
+ expect(server.loadModelEndpoint, isEmpty);
49
+ expect(server.unloadModelEndpoint, isEmpty);
50
+ });
51
+
52
+ test('api key is sent as Bearer authorization header', () {
53
+ final server = makeCloudServer(apiKey: 'test-key-123');
54
+ expect(server.authHeaders, {
55
+ 'Authorization': 'Bearer test-key-123',
56
+ });
57
+ });
58
+
59
+ test('no Authorization header when api key is missing', () {
60
+ final server = makeCloudServer();
61
+ expect(server.authHeaders, isEmpty);
62
+ });
63
+
64
+ test('isOllamaCloud and isOllamaFamily getters work', () {
65
+ final cloud = makeCloudServer();
66
+ expect(cloud.isOllamaCloud, isTrue);
67
+ expect(cloud.isOllamaFamily, isTrue);
68
+
69
+ final local = Server(
70
+ id: 'local-1',
71
+ name: 'Local',
72
+ type: ServerType.ollama,
73
+ host: 'localhost',
74
+ port: 11434,
75
+ createdAt: DateTime.now(),
76
+ lastConnectedAt: DateTime.now(),
77
+ );
78
+ expect(local.isOllamaCloud, isFalse);
79
+ expect(local.isOllamaFamily, isTrue);
80
+ });
81
+ });
82
+
83
+ group('buildServerBaseUrl ollamaCloud short-circuits', () {
84
+ test('returns https://ollama.com regardless of host', () {
85
+ final url = buildServerBaseUrl(
86
+ 'some-host.example',
87
+ 12345,
88
+ ServerType.ollamaCloud,
89
+ );
90
+ expect(url, 'https://ollama.com');
91
+ });
92
+ });
93
+
94
+ group('displayServerAddress for ollamaCloud', () {
95
+ test('returns ollama.com', () {
96
+ final display = displayServerAddress(
97
+ 'localhost',
98
+ 11434,
99
+ ServerType.ollamaCloud,
100
+ );
101
+ expect(display, 'ollama.com');
102
+ });
103
+ });
104
+}