Add new features and update configurations for improved functionality
This commit is contained in:
@@ -3,48 +3,130 @@ import "dart:convert";
|
||||
|
||||
import "../../src/chat/tool_loop_service.dart";
|
||||
import "../../src/api/openrouter_client.dart";
|
||||
import "../../src/hooks/hook_loader.dart";
|
||||
import "../../src/hooks/hook_runner.dart";
|
||||
import "../../src/hooks/hook_types.dart";
|
||||
import "../../src/permissions/permission_types.dart";
|
||||
import "../../src/session/conversation_history.dart";
|
||||
import "../../src/session/session_store.dart";
|
||||
import "../../src/session/session_types.dart";
|
||||
import "../../src/services/cost_tracker.dart" as cost_tracker;
|
||||
import "settings_provider.dart";
|
||||
|
||||
enum QueuePriority {
|
||||
now(0),
|
||||
next(1),
|
||||
later(2);
|
||||
|
||||
final int order;
|
||||
const QueuePriority(this.order);
|
||||
}
|
||||
|
||||
class QueuedMessage {
|
||||
final String text;
|
||||
final QueuePriority priority;
|
||||
|
||||
const QueuedMessage({required this.text, required this.priority});
|
||||
}
|
||||
|
||||
|
||||
class ChatProvider extends ChangeNotifier {
|
||||
ChatProvider(this._settingsProvider);
|
||||
ChatProvider(this._settingsProvider) {
|
||||
_initHooks();
|
||||
}
|
||||
|
||||
final SettingsProvider _settingsProvider;
|
||||
final ToolLoopService _toolLoopService = ToolLoopService();
|
||||
ToolLoopService _toolLoopService = ToolLoopService();
|
||||
HookRunner? _hookRunner;
|
||||
ConversationHistory? _conversationHistory;
|
||||
OpenRouterClient? _client;
|
||||
bool _stopRequested = false;
|
||||
PendingPermission? _pendingPermission;
|
||||
|
||||
PendingPermission? get pendingPermission => _pendingPermission;
|
||||
|
||||
Future<void> _initHooks() async {
|
||||
try {
|
||||
final hooks = await HookLoader.loadHooks();
|
||||
_hookRunner = HookRunner(hooks: hooks);
|
||||
_toolLoopService = ToolLoopService(hookRunner: _hookRunner);
|
||||
} catch (e) {
|
||||
// hooks are optional, carry on without them
|
||||
print("Hook init failed: $e");
|
||||
}
|
||||
}
|
||||
|
||||
List<Message> _messages = <Message>[];
|
||||
List<Map<String, dynamic>> _apiMessages = <Map<String, dynamic>>[];
|
||||
bool isLoading = false;
|
||||
final List<QueuedMessage> _messageQueue = [];
|
||||
|
||||
List<Message> get messages => _messages;
|
||||
int get messageCount => _messages.length;
|
||||
List<Message> get messages => _conversationHistory?.getMessages() ?? const [];
|
||||
int get messageCount => messages.length;
|
||||
String? get workingDirectory => _conversationHistory?.session?.workingDirectory;
|
||||
|
||||
/// Context window size from the last API response — derived from persisted
|
||||
/// message data, same as Claude Code (walks backwards to find the last
|
||||
/// assistant message that has contextTokens set).
|
||||
int get contextTokens {
|
||||
final msgs = messages;
|
||||
for (var i = msgs.length - 1; i >= 0; i--) {
|
||||
final ct = msgs[i].contextTokens;
|
||||
if (ct != null && ct > 0) return ct;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
bool get hasConversation => _conversationHistory != null;
|
||||
bool get isStopping => _stopRequested;
|
||||
int get queuedMessageCount => _messageQueue.length;
|
||||
|
||||
// only user-visible messages (priority != now)
|
||||
List<String> get queuedMessages =>
|
||||
List.unmodifiable(_messageQueue.map((m) => m.text));
|
||||
|
||||
void removeQueuedMessage(int index) {
|
||||
if (index < 0 || index >= _messageQueue.length) return;
|
||||
_messageQueue.removeAt(index);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
QueuedMessage? _dequeue() {
|
||||
if (_messageQueue.isEmpty) return null;
|
||||
|
||||
int bestIdx = 0;
|
||||
for (int i = 1; i < _messageQueue.length; i++) {
|
||||
if (_messageQueue[i].priority.order < _messageQueue[bestIdx].priority.order) {
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
final cmd = _messageQueue[bestIdx];
|
||||
_messageQueue.removeAt(bestIdx);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
void setConversation(ConversationHistory history) {
|
||||
_conversationHistory = history;
|
||||
_messages = history.getMessages();
|
||||
_apiMessages = _buildApiMessages(_messages);
|
||||
_apiMessages = _buildApiMessages(history.getMessages());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearConversation() {
|
||||
_conversationHistory = null;
|
||||
_messages = <Message>[];
|
||||
_apiMessages = <Map<String, dynamic>>[];
|
||||
_messageQueue.clear();
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> sendMessage(String text) async {
|
||||
Future<void> sendMessage(String text, {QueuePriority priority = QueuePriority.next}) async {
|
||||
if (text.isEmpty || _conversationHistory == null) return;
|
||||
|
||||
if (isLoading) {
|
||||
_messageQueue.add(QueuedMessage(text: text, priority: priority));
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final apiKey = _settingsProvider.settings.openRouterApiKey;
|
||||
if (apiKey == null || apiKey.isEmpty) {
|
||||
throw Exception(
|
||||
@@ -72,25 +154,35 @@ class ChatProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// fire UserPromptSubmit hook
|
||||
await _hookRunner?.runHooksForKind(
|
||||
HookKind.userPromptSubmit,
|
||||
input: {"message": text},
|
||||
);
|
||||
|
||||
// add user message to conversation
|
||||
_conversationHistory!.addMessage("user", text);
|
||||
_messages = _conversationHistory!.getMessages();
|
||||
|
||||
_apiMessages.add(<String, dynamic>{"role": "user", "content": text});
|
||||
isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
final advisorModel = _settingsProvider.settings.advisorModel;
|
||||
|
||||
final toolLoopResult = await _toolLoopService.runTurn(
|
||||
client: _client!,
|
||||
model: model,
|
||||
apiKey: apiKey,
|
||||
getSettings: () => _settingsProvider.settings,
|
||||
apiMessages: _apiMessages.take(_apiMessages.length - 1).toList(),
|
||||
userText: text,
|
||||
workingDirectory: workingDirectory,
|
||||
advisorModel: advisorModel,
|
||||
onToolCall: (toolName, input) {
|
||||
_conversationHistory!.addMessage(
|
||||
"tool",
|
||||
_formatToolCall(toolName, input),
|
||||
);
|
||||
_messages = _conversationHistory!.getMessages();
|
||||
notifyListeners();
|
||||
},
|
||||
onToolResult: (toolName, result) {
|
||||
@@ -98,7 +190,6 @@ class ChatProvider extends ChangeNotifier {
|
||||
"tool",
|
||||
_formatToolResult(toolName, result),
|
||||
);
|
||||
_messages = _conversationHistory!.getMessages();
|
||||
notifyListeners();
|
||||
},
|
||||
onAssistantTextDelta: (delta) {
|
||||
@@ -107,26 +198,38 @@ class ChatProvider extends ChangeNotifier {
|
||||
hasStreamingAssistantMessage = true;
|
||||
}
|
||||
_conversationHistory!.appendToLastMessage(delta);
|
||||
_messages = _conversationHistory!.getMessages();
|
||||
notifyListeners();
|
||||
},
|
||||
onAssistantMessageComplete: () {
|
||||
hasStreamingAssistantMessage = false;
|
||||
_messages = _conversationHistory!.getMessages();
|
||||
notifyListeners();
|
||||
},
|
||||
onPermissionRequired: (toolName, input) async {
|
||||
final pending = PendingPermission(toolName: toolName, input: input);
|
||||
_pendingPermission = pending;
|
||||
notifyListeners();
|
||||
final decision = await pending.future;
|
||||
_pendingPermission = null;
|
||||
notifyListeners();
|
||||
return decision;
|
||||
},
|
||||
);
|
||||
_apiMessages = toolLoopResult.apiMessages;
|
||||
|
||||
final ct = toolLoopResult.response.contextTokens;
|
||||
|
||||
// add assistant message to visible conversation
|
||||
if (!toolLoopResult.finalResponseWasStreamed) {
|
||||
_conversationHistory!.addMessage(
|
||||
"assistant",
|
||||
toolLoopResult.responseText,
|
||||
tokens: toolLoopResult.response.outputTokens,
|
||||
contextTokens: ct,
|
||||
);
|
||||
} else {
|
||||
// streamed message was built incrementally — patch contextTokens onto it
|
||||
_conversationHistory!.setLastMessageContextTokens(ct);
|
||||
}
|
||||
_messages = _conversationHistory!.getMessages();
|
||||
|
||||
// track cost (set to 0 for now — OpenRouter pricing varies by model)
|
||||
final inputTokens = toolLoopResult.response.inputTokens ?? 0;
|
||||
@@ -138,6 +241,8 @@ class ChatProvider extends ChangeNotifier {
|
||||
outputTokens: outputTokens,
|
||||
cacheReadTokens: 0,
|
||||
cacheCreationTokens: 0,
|
||||
webSearchRequests: toolLoopResult.webSearchRequests,
|
||||
webFetchRequests: toolLoopResult.webFetchRequests,
|
||||
model: toolLoopResult.response.model,
|
||||
);
|
||||
|
||||
@@ -154,7 +259,7 @@ class ChatProvider extends ChangeNotifier {
|
||||
if (error is RequestCancelledException) {
|
||||
_conversationHistory!.addMessage("assistant", "Generation stopped.");
|
||||
final session = _conversationHistory!.session;
|
||||
_messages = _conversationHistory!.getMessages();
|
||||
|
||||
if (session != null) {
|
||||
await SessionStore.instance.saveSession(session);
|
||||
}
|
||||
@@ -171,7 +276,7 @@ class ChatProvider extends ChangeNotifier {
|
||||
);
|
||||
|
||||
final session = _conversationHistory!.session;
|
||||
_messages = _conversationHistory!.getMessages();
|
||||
|
||||
if (session != null) {
|
||||
await SessionStore.instance.saveSession(session);
|
||||
}
|
||||
@@ -183,6 +288,26 @@ class ChatProvider extends ChangeNotifier {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
final next = _dequeue();
|
||||
if (next != null) {
|
||||
notifyListeners();
|
||||
await sendMessage(next.text, priority: next.priority);
|
||||
}
|
||||
}
|
||||
|
||||
void resolvePermission(PermissionDecision decision) async {
|
||||
final pending = _pendingPermission;
|
||||
if (pending == null) return;
|
||||
|
||||
if (decision == PermissionDecision.allowAlways) {
|
||||
// persist to settings so this tool is auto-allowed from now on
|
||||
await _settingsProvider.addAlwaysAllowRule(pending.toolName);
|
||||
}
|
||||
|
||||
pending.resolve(decision);
|
||||
_pendingPermission = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void stopGenerating() {
|
||||
@@ -190,10 +315,15 @@ class ChatProvider extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingPermission?.resolve(PermissionDecision.reject);
|
||||
_pendingPermission = null;
|
||||
_messageQueue.clear();
|
||||
_stopRequested = true;
|
||||
print("Stopping active turn");
|
||||
_client?.cancelActiveRequest();
|
||||
notifyListeners();
|
||||
|
||||
_hookRunner?.runHooksForKind(HookKind.stop);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -232,7 +362,10 @@ class ChatProvider extends ChangeNotifier {
|
||||
|
||||
String _formatToolCall(String toolName, Map<String, dynamic> input) {
|
||||
const encoder = JsonEncoder.withIndent(" ");
|
||||
return "$toolName call\n${encoder.convert(input)}";
|
||||
final visibleInput = Map<String, dynamic>.fromEntries(
|
||||
input.entries.where((entry) => !entry.key.startsWith("_")),
|
||||
);
|
||||
return "$toolName call\n${encoder.convert(visibleInput)}";
|
||||
}
|
||||
|
||||
String _formatToolResult(String toolName, String result) {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import "package:file_picker/file_picker.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
|
||||
import "../../src/project_store.dart";
|
||||
import "../../src/session/session_types.dart";
|
||||
import "chat_provider.dart";
|
||||
import "projects_provider.dart";
|
||||
import "session_provider.dart";
|
||||
import "settings_provider.dart";
|
||||
|
||||
class HomeCoordinator extends ChangeNotifier {
|
||||
|
||||
HomeCoordinator(this._projects, this._session, this._chat, this._settings);
|
||||
|
||||
final ProjectsProvider _projects;
|
||||
final SessionProvider _session;
|
||||
final ChatProvider _chat;
|
||||
final SettingsProvider _settings;
|
||||
|
||||
String? _error;
|
||||
String? get error => _error;
|
||||
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setError(String msg) {
|
||||
_error = msg;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
Future<void> pickProjectDirectory() async {
|
||||
try {
|
||||
final selectedDirectory = await FilePicker.platform.getDirectoryPath(
|
||||
dialogTitle: "Select project directory",
|
||||
);
|
||||
|
||||
if (selectedDirectory == null) return;
|
||||
|
||||
final project = await _projects.addProject(selectedDirectory);
|
||||
if (project == null) {
|
||||
_setError("The selected folder could not be added as a project.");
|
||||
return;
|
||||
}
|
||||
|
||||
_projects.selectProject(project.id);
|
||||
_session.clearCurrentSession(workingDirectory: project.workingDirectory);
|
||||
_chat.clearConversation();
|
||||
await _settings.setActiveProject(project.workingDirectory);
|
||||
} catch (e, st) {
|
||||
print("Project directory picker failed: $e");
|
||||
print(st);
|
||||
_setError(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createNewChat() async {
|
||||
final selectedProject = _projects.selectedProject;
|
||||
if (selectedProject == null) {
|
||||
_setError("Choose a project first so the new chat has a working directory.");
|
||||
return;
|
||||
}
|
||||
|
||||
await _session.createNewSession(
|
||||
workingDirectory: selectedProject.workingDirectory,
|
||||
name: "New Chat",
|
||||
model: _settings.settings.model,
|
||||
);
|
||||
_settings.setThreadModel(_settings.settings.model);
|
||||
_chat.setConversation(_session.getConversationHistory());
|
||||
}
|
||||
|
||||
Future<void> selectProject(ProjectRecord project) async {
|
||||
_projects.selectProject(project.id);
|
||||
await _settings.setActiveProject(project.workingDirectory);
|
||||
|
||||
if (_session.currentSession?.workingDirectory == project.workingDirectory) return;
|
||||
|
||||
_session.clearCurrentSession(workingDirectory: project.workingDirectory);
|
||||
_settings.setThreadModel(null);
|
||||
_chat.clearConversation();
|
||||
}
|
||||
|
||||
Future<void> openSession(SessionSummary session) async {
|
||||
await _session.loadSession(session);
|
||||
_chat.setConversation(_session.getConversationHistory());
|
||||
_projects.selectProjectByWorkingDirectory(_session.activeWorkingDirectory);
|
||||
_settings.setThreadModel(_session.currentSession?.model);
|
||||
}
|
||||
|
||||
Future<void> sendMessage(String text) async {
|
||||
if (text.isEmpty) return;
|
||||
|
||||
if (_session.currentSession == null) {
|
||||
final selectedProject = _projects.selectedProject;
|
||||
if (selectedProject == null) {
|
||||
_setError("Pick a project before starting a chat.");
|
||||
return;
|
||||
}
|
||||
await _session.createNewSession(
|
||||
workingDirectory: selectedProject.workingDirectory,
|
||||
name: "New Chat",
|
||||
model: _settings.settings.model,
|
||||
);
|
||||
_settings.setThreadModel(_settings.settings.model);
|
||||
_chat.setConversation(_session.getConversationHistory());
|
||||
}
|
||||
|
||||
try {
|
||||
await _chat.sendMessage(text);
|
||||
} catch (e, st) {
|
||||
print("Failed to send message: $e");
|
||||
print(st);
|
||||
_setError(e.toString());
|
||||
} finally {
|
||||
await _session.refreshSessions();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteSession(SessionSummary session) async {
|
||||
await _session.deleteSession(session);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:uuid/uuid.dart";
|
||||
|
||||
import "../../src/project_store.dart";
|
||||
import "../../src/session/conversation_history.dart";
|
||||
import "../../src/session/session_store.dart";
|
||||
import "../../src/session/session_types.dart";
|
||||
|
||||
class SessionProvider extends ChangeNotifier {
|
||||
SessionProvider() {
|
||||
SessionProvider(this._projectStore) {
|
||||
_loadSessions();
|
||||
}
|
||||
|
||||
final ProjectStore _projectStore;
|
||||
final SessionStore _sessionStore = SessionStore.instance;
|
||||
final ConversationHistory _conversationHistory = ConversationHistory();
|
||||
|
||||
@@ -59,7 +61,12 @@ class SessionProvider extends ChangeNotifier {
|
||||
|
||||
Future<void> _loadSessions() async {
|
||||
try {
|
||||
_sessions = await _sessionStore.listSessions();
|
||||
final workingDirs = _projectStore.projects
|
||||
.map((p) => p.workingDirectory)
|
||||
.where((d) => d.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
_sessions = await _sessionStore.listAllSessions(workingDirs);
|
||||
notifyListeners();
|
||||
} catch (error, stackTrace) {
|
||||
_logException("Failed to load sessions", error, stackTrace);
|
||||
@@ -70,6 +77,7 @@ class SessionProvider extends ChangeNotifier {
|
||||
Future<void> createNewSession({
|
||||
String? workingDirectory,
|
||||
String? name,
|
||||
String? model,
|
||||
}) async {
|
||||
try {
|
||||
const uuid = Uuid();
|
||||
@@ -86,6 +94,7 @@ class SessionProvider extends ChangeNotifier {
|
||||
normalizedDirectory == null || normalizedDirectory.isEmpty
|
||||
? null
|
||||
: normalizedDirectory,
|
||||
model: model,
|
||||
);
|
||||
|
||||
await _sessionStore.saveSession(newSession);
|
||||
@@ -101,29 +110,38 @@ class SessionProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadSession(String id) async {
|
||||
Future<void> loadSession(SessionSummary summary) async {
|
||||
try {
|
||||
final session = await _sessionStore.loadSession(id);
|
||||
final workingDir = summary.workingDirectory;
|
||||
if (workingDir == null || workingDir.isEmpty) return;
|
||||
|
||||
final session = await _sessionStore.loadSession(
|
||||
summary.id,
|
||||
workingDirectory: workingDir,
|
||||
);
|
||||
if (session != null) {
|
||||
_conversationHistory.setSession(session);
|
||||
_currentSession = session;
|
||||
_currentSessionId = id;
|
||||
_currentSessionId = summary.id;
|
||||
_activeWorkingDirectory = session.workingDirectory;
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
_logException("Failed to load session $id", error, stackTrace);
|
||||
_logException("Failed to load session ${summary.id}", error, stackTrace);
|
||||
_currentSession = null;
|
||||
_currentSessionId = null;
|
||||
_activeWorkingDirectory = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteSession(String id) async {
|
||||
Future<void> deleteSession(SessionSummary summary) async {
|
||||
try {
|
||||
await _sessionStore.deleteSession(id);
|
||||
final workingDir = summary.workingDirectory;
|
||||
if (workingDir == null || workingDir.isEmpty) return;
|
||||
|
||||
if (_currentSessionId == id) {
|
||||
await _sessionStore.deleteSession(summary.id, workingDirectory: workingDir);
|
||||
|
||||
if (_currentSessionId == summary.id) {
|
||||
_conversationHistory.setSession(
|
||||
ConversationSession(
|
||||
id: "",
|
||||
@@ -140,7 +158,7 @@ class SessionProvider extends ChangeNotifier {
|
||||
await _loadSessions();
|
||||
notifyListeners();
|
||||
} catch (error, stackTrace) {
|
||||
_logException("Failed to delete session $id", error, stackTrace);
|
||||
_logException("Failed to delete session ${summary.id}", error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +170,15 @@ class SessionProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the model on the current in-memory session and persists it
|
||||
Future<void> updateSessionModel(String model) async {
|
||||
final session = _currentSession;
|
||||
if (session == null) return;
|
||||
|
||||
session.model = model;
|
||||
await _sessionStore.saveSession(session);
|
||||
}
|
||||
|
||||
ConversationHistory getConversationHistory() => _conversationHistory;
|
||||
|
||||
void _logException(String message, Object error, StackTrace stackTrace) {
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
import "package:flutter/foundation.dart";
|
||||
|
||||
import "../../src/local_state.dart";
|
||||
import "../../src/project_settings_store.dart";
|
||||
|
||||
class SettingsProvider extends ChangeNotifier {
|
||||
SettingsProvider(this._settingsStore) : settings = _settingsStore.settings;
|
||||
SettingsProvider(this._settingsStore) : _globalSettings = _settingsStore.settings;
|
||||
|
||||
static const Map<String, String> _legacyModelAliases = {
|
||||
"google/gemini-2.0-flash": "google/gemini-2.0-flash-001",
|
||||
};
|
||||
|
||||
final SettingsStore _settingsStore;
|
||||
LocalSettings settings;
|
||||
|
||||
LocalSettings _globalSettings;
|
||||
LocalSettings? _projectSettings;
|
||||
String? _threadModel;
|
||||
|
||||
String? _activeProjectDir;
|
||||
|
||||
// Effective settings: global → project override → thread model
|
||||
LocalSettings get settings {
|
||||
var merged = _globalSettings.mergeWith(_projectSettings);
|
||||
if (_threadModel != null && _threadModel!.isNotEmpty) {
|
||||
merged = merged.copyWith(model: _threadModel);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
String normalizeModelId(String? modelId) {
|
||||
if (modelId == null || modelId.isEmpty) {
|
||||
@@ -20,12 +35,36 @@ class SettingsProvider extends ChangeNotifier {
|
||||
return _legacyModelAliases[modelId] ?? modelId;
|
||||
}
|
||||
|
||||
// Called when the active project changes
|
||||
Future<void> setActiveProject(String? workingDirectory) async {
|
||||
_activeProjectDir = workingDirectory;
|
||||
_projectSettings = null;
|
||||
_threadModel = null;
|
||||
|
||||
if (workingDirectory != null && workingDirectory.isNotEmpty) {
|
||||
_projectSettings = await ProjectSettingsStore.instance.load(workingDirectory);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Called when a thread is loaded or cleared
|
||||
void setThreadModel(String? model) {
|
||||
_threadModel = model != null ? normalizeModelId(model) : null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updateModel(String newModel) async {
|
||||
final normalizedModel = normalizeModelId(newModel);
|
||||
final normalized = normalizeModelId(newModel);
|
||||
|
||||
// update thread model in memory
|
||||
_threadModel = normalized;
|
||||
|
||||
// also persist to global settings as the new default
|
||||
await _settingsStore.update(
|
||||
(current) => current.copyWith(model: normalizedModel),
|
||||
(current) => current.copyWith(model: normalized),
|
||||
);
|
||||
settings = _settingsStore.settings;
|
||||
_globalSettings = _settingsStore.settings;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -33,13 +72,13 @@ class SettingsProvider extends ChangeNotifier {
|
||||
await _settingsStore.update(
|
||||
(current) => current.copyWith(openRouterApiKey: newKey),
|
||||
);
|
||||
settings = _settingsStore.settings;
|
||||
_globalSettings = _settingsStore.settings;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updateTheme(String newTheme) async {
|
||||
await _settingsStore.update((current) => current.copyWith(theme: newTheme));
|
||||
settings = _settingsStore.settings;
|
||||
_globalSettings = _settingsStore.settings;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -47,13 +86,35 @@ class SettingsProvider extends ChangeNotifier {
|
||||
await _settingsStore.update(
|
||||
(current) => current.copyWith(effortLevel: newLevel),
|
||||
);
|
||||
settings = _settingsStore.settings;
|
||||
_globalSettings = _settingsStore.settings;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> addAlwaysAllowRule(String toolName) async {
|
||||
final current = _globalSettings.alwaysAllowRules;
|
||||
if (current.contains(toolName)) return;
|
||||
await _settingsStore.update(
|
||||
(s) => s.copyWith(alwaysAllowRules: [...current, toolName]),
|
||||
);
|
||||
_globalSettings = _settingsStore.settings;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> resetToDefaults() async {
|
||||
await _settingsStore.update((_) => const LocalSettings());
|
||||
settings = _settingsStore.settings;
|
||||
_globalSettings = _settingsStore.settings;
|
||||
_projectSettings = null;
|
||||
_threadModel = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Save project-level settings override
|
||||
Future<void> updateProjectSetting(LocalSettings projectOverride) async {
|
||||
final dir = _activeProjectDir;
|
||||
if (dir == null || dir.isEmpty) return;
|
||||
|
||||
await ProjectSettingsStore.instance.save(dir, projectOverride);
|
||||
_projectSettings = projectOverride;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user