@tduncan / Cortana / commits / 8470f8e

add icons for vision/reasoning/tooluse

onlypuppy7 committed Jun 29, 2026 at 20:58 UTC 8470f8e46bfb39446a28221509d59b7e020c2817
6 files changed +170
lib/features/models/components/model_tile.dart
+69
@@ -3,6 +3,7 @@ import 'package:localmind/core/models/enums.dart';
3 import 'package:localmind/core/theme/colors.dart';
4 import 'package:localmind/features/models/data/models/model_info.dart';
5 import 'package:localmind/l10n/app_localizations.dart';
6 +import 'package:shadcn_ui/shadcn_ui.dart';
7 import 'metadata_chip.dart';
8
9 class ModelTile extends StatelessWidget {
@@ -65,6 +66,12 @@ class ModelTile extends StatelessWidget {
66 overflow: TextOverflow.ellipsis,
67 ),
68 ),
69 + if (model.supportsVision ||
70 + model.supportsReasoning ||
71 + model.supportsToolUse) ...[
72 + const SizedBox(width: 8),
73 + _ModelCapabilityIcons(model: model, isDark: isDark),
74 + ],
75 ],
76 ),
77 const SizedBox(height: 4),
@@ -143,3 +150,65 @@ class ModelTile extends StatelessWidget {
150 );
151 }
152 }
153 +
154 +class _ModelCapabilityIcons extends StatelessWidget {
155 + const _ModelCapabilityIcons({
156 + required this.model,
157 + required this.isDark,
158 + });
159 +
160 + final ModelInfo model;
161 + final bool isDark;
162 +
163 + @override
164 + Widget build(BuildContext context) {
165 + final color = isDark ? AppColors.darkMutedText : AppColors.lightMutedText;
166 +
167 + return Row(
168 + mainAxisSize: MainAxisSize.min,
169 + children: [
170 + if (model.supportsVision)
171 + _CapabilityIcon(
172 + icon: LucideIcons.eye,
173 + tooltip: 'Vision',
174 + color: color,
175 + ),
176 + if (model.supportsReasoning)
177 + _CapabilityIcon(
178 + icon: LucideIcons.brain,
179 + tooltip: 'Reasoning',
180 + color: color,
181 + ),
182 + if (model.supportsToolUse)
183 + _CapabilityIcon(
184 + icon: LucideIcons.hammer,
185 + tooltip: 'Tool use',
186 + color: color,
187 + ),
188 + ],
189 + );
190 + }
191 +}
192 +
193 +class _CapabilityIcon extends StatelessWidget {
194 + const _CapabilityIcon({
195 + required this.icon,
196 + required this.tooltip,
197 + required this.color,
198 + });
199 +
200 + final IconData icon;
201 + final String tooltip;
202 + final Color color;
203 +
204 + @override
205 + Widget build(BuildContext context) {
206 + return Padding(
207 + padding: const EdgeInsets.only(left: 4),
208 + child: Tooltip(
209 + message: tooltip,
210 + child: Icon(icon, size: 14, color: color),
211 + ),
212 + );
213 + }
214 +}
lib/features/models/data/models/model_info.dart
+12
@@ -17,6 +17,9 @@ class ModelInfo {
17 final OnDeviceModelRuntime? onDeviceRuntime;
18 final OnDeviceModelFormat? onDeviceFormat;
19 final String? localPath;
20 + final bool supportsVision;
21 + final bool supportsReasoning;
22 + final bool supportsToolUse;
23
24 ModelInfo({
25 required this.id,
@@ -34,6 +37,9 @@ class ModelInfo {
37 this.onDeviceRuntime,
38 this.onDeviceFormat,
39 this.localPath,
40 + this.supportsVision = false,
41 + this.supportsReasoning = false,
42 + this.supportsToolUse = false,
43 });
44
45 String get displayName {
@@ -83,6 +89,9 @@ class ModelInfo {
89 OnDeviceModelRuntime? onDeviceRuntime,
90 OnDeviceModelFormat? onDeviceFormat,
91 String? localPath,
92 + bool? supportsVision,
93 + bool? supportsReasoning,
94 + bool? supportsToolUse,
95 }) {
96 return ModelInfo(
97 id: id ?? this.id,
@@ -100,6 +109,9 @@ class ModelInfo {
109 onDeviceRuntime: onDeviceRuntime ?? this.onDeviceRuntime,
110 onDeviceFormat: onDeviceFormat ?? this.onDeviceFormat,
111 localPath: localPath ?? this.localPath,
112 + supportsVision: supportsVision ?? this.supportsVision,
113 + supportsReasoning: supportsReasoning ?? this.supportsReasoning,
114 + supportsToolUse: supportsToolUse ?? this.supportsToolUse,
115 );
116 }
117 }
lib/features/on_device/components/on_device_picker_section.dart
+6
@@ -513,6 +513,9 @@ class _OnDeviceModelTile extends ConsumerWidget {
513 onDeviceRuntime: model.runtime,
514 onDeviceFormat: model.format,
515 localPath: model.localPath,
516 + supportsVision: model.supportsVision,
517 + supportsReasoning: model.supportsThinking,
518 + supportsToolUse: model.supportsFunctionCalling,
519 );
520
521 await engineNotifier.loadModel(model.id, settings.preferredBackend);
@@ -578,6 +581,9 @@ class _OnDeviceModelTile extends ConsumerWidget {
581 onDeviceRuntime: model.runtime,
582 onDeviceFormat: model.format,
583 localPath: model.localPath,
584 + supportsVision: model.supportsVision,
585 + supportsReasoning: model.supportsThinking,
586 + supportsToolUse: model.supportsFunctionCalling,
587 );
588
589 ref.read(selectedModelProvider.notifier).setModel(modelInfo);
lib/features/servers/data/server_api_service.dart
+26
@@ -282,6 +282,8 @@ class ServerApiService {
282 archName = archValue;
283 }
284
285 + final capabilities = _parseModelCapabilities(item['capabilities']);
286 +
287 models.add(
288 ModelInfo(
289 id: id,
@@ -297,6 +299,9 @@ class ServerApiService {
299 architecture: archName,
300 serverType: server.type,
301 serverId: server.id,
302 + supportsVision: capabilities.supportsVision,
303 + supportsReasoning: capabilities.supportsReasoning,
304 + supportsToolUse: capabilities.supportsToolUse,
305 ),
306 );
307 }
@@ -312,6 +317,27 @@ class ServerApiService {
317 return null;
318 }
319
320 + ({
321 + bool supportsVision,
322 + bool supportsReasoning,
323 + bool supportsToolUse,
324 + }) _parseModelCapabilities(dynamic raw) {
325 + if (raw is! Map) {
326 + return (
327 + supportsVision: false,
328 + supportsReasoning: false,
329 + supportsToolUse: false,
330 + );
331 + }
332 +
333 + final reasoning = raw['reasoning'];
334 + return (
335 + supportsVision: raw['vision'] == true,
336 + supportsReasoning: reasoning is Map && reasoning.isNotEmpty,
337 + supportsToolUse: raw['trained_for_tool_use'] == true,
338 + );
339 + }
340 +
341 double? _paramCountFromMeta(dynamic nParams) {
342 if (nParams == null) return null;
343 if (nParams is int) return nParams / 1000000000;
lib/features/servers/providers/server_providers.dart
+3
@@ -145,6 +145,9 @@ final availableModelsProvider = FutureProvider.family<List<dynamic>, String>((
145 onDeviceRuntime: m.runtime,
146 onDeviceFormat: m.format,
147 localPath: m.localPath,
148 + supportsVision: m.supportsVision,
149 + supportsReasoning: m.supportsThinking,
150 + supportsToolUse: m.supportsFunctionCalling,
151 ),
152 )
153 .toList();
test/features/servers/server_api_service_test.dart
+54
@@ -134,6 +134,60 @@ void main() {
134 expect(model.architecture, "llama");
135 });
136
137 + test('parses model capabilities from LM Studio models array', () async {
138 + final mockData = {
139 + "models": [
140 + {
141 + "key": "google/gemma-4-e2b",
142 + "display_name": "Gemma 4 E2B",
143 + "capabilities": {
144 + "vision": true,
145 + "trained_for_tool_use": true,
146 + },
147 + },
148 + {
149 + "key": "mistralai/ministral-3-14b-reasoning",
150 + "display_name": "Ministral 3 14B Reasoning",
151 + "capabilities": {
152 + "vision": true,
153 + "trained_for_tool_use": true,
154 + "reasoning": {
155 + "allowed_options": ["off", "on"],
156 + "default": "on",
157 + },
158 + },
159 + },
160 + {
161 + "key": "mradermacher/kimi-vl-a3b-thinking-2506-i1",
162 + "display_name": "Kimi VL A3B Thinking 2506 I1",
163 + "capabilities": {
164 + "vision": false,
165 + "trained_for_tool_use": false,
166 + },
167 + },
168 + ],
169 + };
170 +
171 + final dio = Dio()..interceptors.add(TestInterceptor(mockData));
172 + final service = ServerApiService(dio);
173 +
174 + final models = await service.fetchModels(testServer);
175 +
176 + expect(models, hasLength(3));
177 +
178 + expect(models[0].supportsVision, isTrue);
179 + expect(models[0].supportsToolUse, isTrue);
180 + expect(models[0].supportsReasoning, isFalse);
181 +
182 + expect(models[1].supportsVision, isTrue);
183 + expect(models[1].supportsToolUse, isTrue);
184 + expect(models[1].supportsReasoning, isTrue);
185 +
186 + expect(models[2].supportsVision, isFalse);
187 + expect(models[2].supportsToolUse, isFalse);
188 + expect(models[2].supportsReasoning, isFalse);
189 + });
190 +
191 test('handles running models with fallback data array (llama.cpp)', () async {
192 final mockData = {
193 "object": "list",