Update project structure and enhance functionality with new features and dependencies
This commit is contained in:
+122
-319
@@ -1,49 +1,38 @@
|
||||
import "package:flutter/foundation.dart";
|
||||
import "dart:convert";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "../../src/chat/tool_loop_service.dart";
|
||||
import "../../src/api/openrouter_client.dart";
|
||||
import "../../src/compact/compact_service.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_runtime.dart";
|
||||
import "../../src/session/session_types.dart";
|
||||
import "../../src/services/cost_tracker.dart" as cost_tracker;
|
||||
import "../models/attachment.dart";
|
||||
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});
|
||||
}
|
||||
|
||||
|
||||
// ChatProvider is now a thin registry over SessionRuntime instances.
|
||||
//
|
||||
// Each thread gets its own SessionRuntime which holds all the mutable state
|
||||
// that used to live here — api messages, the http client, loading flags, etc.
|
||||
// Switching threads just changes _activeSessionId. Background threads keep
|
||||
// running and save themselves to disk; when you switch back you see their
|
||||
// live state.
|
||||
class ChatProvider extends ChangeNotifier {
|
||||
ChatProvider(this._settingsProvider) {
|
||||
_initHooks();
|
||||
}
|
||||
|
||||
final SettingsProvider _settingsProvider;
|
||||
|
||||
ToolLoopService _toolLoopService = ToolLoopService();
|
||||
HookRunner? _hookRunner;
|
||||
ConversationHistory? _conversationHistory;
|
||||
OpenRouterClient? _client;
|
||||
bool _stopRequested = false;
|
||||
PendingPermission? _pendingPermission;
|
||||
|
||||
PendingPermission? get pendingPermission => _pendingPermission;
|
||||
final Map<String, SessionRuntime> _runtimes = {};
|
||||
String? _activeSessionId;
|
||||
|
||||
// ─── hooks ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> _initHooks() async {
|
||||
try {
|
||||
@@ -51,22 +40,48 @@ class ChatProvider extends ChangeNotifier {
|
||||
_hookRunner = HookRunner(hooks: hooks);
|
||||
_toolLoopService = ToolLoopService(hookRunner: _hookRunner);
|
||||
} catch (e) {
|
||||
// hooks are optional, carry on without them
|
||||
print("Hook init failed: $e");
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _apiMessages = <Map<String, dynamic>>[];
|
||||
bool isLoading = false;
|
||||
final List<QueuedMessage> _messageQueue = [];
|
||||
// ─── active runtime accessors ────────────────────────────────────────────────
|
||||
|
||||
List<Message> get messages => _conversationHistory?.getMessages() ?? const [];
|
||||
SessionRuntime? get _active =>
|
||||
_activeSessionId != null ? _runtimes[_activeSessionId] : null;
|
||||
|
||||
List<Message> get messages => _active?.messages ?? const [];
|
||||
int get messageCount => messages.length;
|
||||
String? get workingDirectory => _conversationHistory?.session?.workingDirectory;
|
||||
String? get workingDirectory => _active?.workingDirectory;
|
||||
bool get hasConversation => _active != null;
|
||||
bool get isLoading => _active?.isLoading ?? false;
|
||||
bool get isCompacting => _active?.isCompacting ?? false;
|
||||
bool get isStopping => _active?.isStopping ?? false;
|
||||
int get queuedMessageCount => _active?.queuedMessageCount ?? 0;
|
||||
List<String> get queuedMessages => _active?.queuedMessages ?? const [];
|
||||
PendingPermission? get pendingPermission => _active?.pendingPermission;
|
||||
String? get lastCompactSummary => _active?.lastCompactSummary;
|
||||
TokenWarningState? get tokenWarningState => _active?.tokenWarningState;
|
||||
|
||||
String get threadPermissionMode => _active?.permissionModeOverride ?? "default";
|
||||
|
||||
Future<void> setThreadPermissionMode(String mode) =>
|
||||
_active?.setPermissionModeOverride(mode) ?? Future.value();
|
||||
|
||||
bool isSessionRunning(String sessionId) {
|
||||
final r = _runtimes[sessionId];
|
||||
return r != null && (r.isLoading || r.isCompacting);
|
||||
}
|
||||
|
||||
bool sessionNeedsAttention(String sessionId) {
|
||||
final r = _runtimes[sessionId];
|
||||
return r != null && r.pendingPermission != null;
|
||||
}
|
||||
|
||||
bool sessionHasUnreadResult(String sessionId) {
|
||||
final r = _runtimes[sessionId];
|
||||
return r != null && r.hasUnreadResult;
|
||||
}
|
||||
|
||||
/// 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--) {
|
||||
@@ -75,304 +90,92 @@ class ChatProvider extends ChangeNotifier {
|
||||
}
|
||||
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));
|
||||
// ─── session lifecycle ───────────────────────────────────────────────────────
|
||||
|
||||
void removeQueuedMessage(int index) {
|
||||
if (index < 0 || index >= _messageQueue.length) return;
|
||||
_messageQueue.removeAt(index);
|
||||
notifyListeners();
|
||||
}
|
||||
// Called when the user switches to (or creates) a session.
|
||||
// Creates a new runtime if one doesn't already exist for this session.
|
||||
void activateSession(ConversationSession session) {
|
||||
final id = session.id;
|
||||
|
||||
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;
|
||||
_apiMessages = _buildApiMessages(history.getMessages());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearConversation() {
|
||||
_conversationHistory = null;
|
||||
_apiMessages = <Map<String, dynamic>>[];
|
||||
_messageQueue.clear();
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
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(
|
||||
"OpenRouter API key not set. Please configure it in settings.",
|
||||
);
|
||||
}
|
||||
|
||||
final savedModel = _settingsProvider.settings.model;
|
||||
final model = _settingsProvider.normalizeModelId(savedModel);
|
||||
if (savedModel != model) {
|
||||
print("Normalizing legacy model ID from $savedModel to $model");
|
||||
await _settingsProvider.updateModel(model);
|
||||
}
|
||||
|
||||
try {
|
||||
_stopRequested = false;
|
||||
bool hasStreamingAssistantMessage = false;
|
||||
_client = await OpenRouterClientFactory.create(apiKey: apiKey);
|
||||
final session = _conversationHistory!.session;
|
||||
final workingDirectory = session?.workingDirectory;
|
||||
if (session != null) {
|
||||
session.model = model;
|
||||
if (session.name == "New Chat") {
|
||||
session.name = _buildSessionName(text);
|
||||
}
|
||||
}
|
||||
|
||||
// fire UserPromptSubmit hook
|
||||
await _hookRunner?.runHooksForKind(
|
||||
HookKind.userPromptSubmit,
|
||||
input: {"message": text},
|
||||
);
|
||||
|
||||
// add user message to conversation
|
||||
_conversationHistory!.addMessage("user", text);
|
||||
|
||||
_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,
|
||||
if (!_runtimes.containsKey(id)) {
|
||||
_runtimes[id] = SessionRuntime(
|
||||
session: session,
|
||||
toolLoopService: _toolLoopService,
|
||||
hookRunner: _hookRunner,
|
||||
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),
|
||||
);
|
||||
notifyListeners();
|
||||
},
|
||||
onToolResult: (toolName, result) {
|
||||
_conversationHistory!.addMessage(
|
||||
"tool",
|
||||
_formatToolResult(toolName, result),
|
||||
);
|
||||
notifyListeners();
|
||||
},
|
||||
onAssistantTextDelta: (delta) {
|
||||
if (!hasStreamingAssistantMessage) {
|
||||
_conversationHistory!.addMessage("assistant", "");
|
||||
hasStreamingAssistantMessage = true;
|
||||
}
|
||||
_conversationHistory!.appendToLastMessage(delta);
|
||||
notifyListeners();
|
||||
},
|
||||
onAssistantMessageComplete: () {
|
||||
hasStreamingAssistantMessage = false;
|
||||
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;
|
||||
},
|
||||
normalizeModelId: (m) => _settingsProvider.normalizeModelId(m),
|
||||
onChanged: notifyListeners,
|
||||
);
|
||||
_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);
|
||||
}
|
||||
|
||||
// track cost (set to 0 for now — OpenRouter pricing varies by model)
|
||||
final inputTokens = toolLoopResult.response.inputTokens ?? 0;
|
||||
final outputTokens = toolLoopResult.response.outputTokens ?? 0;
|
||||
|
||||
cost_tracker.addToTotalSessionCost(
|
||||
cost: 0.0,
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheReadTokens: 0,
|
||||
cacheCreationTokens: 0,
|
||||
webSearchRequests: toolLoopResult.webSearchRequests,
|
||||
webFetchRequests: toolLoopResult.webFetchRequests,
|
||||
model: toolLoopResult.response.model,
|
||||
);
|
||||
|
||||
// save session
|
||||
if (session != null) {
|
||||
await SessionStore.instance.saveSession(session);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
} catch (error, stackTrace) {
|
||||
print("Failed to send message: $error");
|
||||
print(stackTrace);
|
||||
|
||||
if (error is RequestCancelledException) {
|
||||
_conversationHistory!.addMessage("assistant", "Generation stopped.");
|
||||
final session = _conversationHistory!.session;
|
||||
|
||||
if (session != null) {
|
||||
await SessionStore.instance.saveSession(session);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (error is ToolLoopException) {
|
||||
_apiMessages = List<Map<String, dynamic>>.from(error.apiMessages);
|
||||
}
|
||||
|
||||
_conversationHistory!.addMessage(
|
||||
"assistant",
|
||||
_buildTurnFailureMessage(error),
|
||||
);
|
||||
|
||||
final session = _conversationHistory!.session;
|
||||
|
||||
if (session != null) {
|
||||
await SessionStore.instance.saveSession(session);
|
||||
}
|
||||
rethrow;
|
||||
} finally {
|
||||
_client?.close();
|
||||
_client = null;
|
||||
_stopRequested = false;
|
||||
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;
|
||||
_activeSessionId = id;
|
||||
_runtimes[id]?.markRead();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void stopGenerating() {
|
||||
if (!isLoading) {
|
||||
return;
|
||||
// Fast-path: switch focus to an already-running runtime without touching disk.
|
||||
void activateSessionById(String sessionId) {
|
||||
if (_runtimes.containsKey(sessionId)) {
|
||||
_activeSessionId = sessionId;
|
||||
_runtimes[sessionId]?.markRead();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
_pendingPermission?.resolve(PermissionDecision.reject);
|
||||
_pendingPermission = null;
|
||||
_messageQueue.clear();
|
||||
_stopRequested = true;
|
||||
print("Stopping active turn");
|
||||
_client?.cancelActiveRequest();
|
||||
notifyListeners();
|
||||
|
||||
_hookRunner?.runHooksForKind(HookKind.stop);
|
||||
}
|
||||
|
||||
// Called when the user starts a new blank chat — no session exists yet.
|
||||
void clearConversation() {
|
||||
_activeSessionId = null;
|
||||
|
||||
// prune dead runtimes that are done
|
||||
_runtimes.removeWhere((_, r) => !r.isLoading && !r.isCompacting);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Legacy compat — kept so HomeCoordinator doesn't need parallel changes
|
||||
// for paths that still call this. Routes to activateSession.
|
||||
void setConversation(ConversationSession session) => activateSession(session);
|
||||
|
||||
// ─── actions — delegate to active runtime ───────────────────────────────────
|
||||
|
||||
Future<void> sendMessage(
|
||||
String text, {
|
||||
QueuePriority priority = QueuePriority.next,
|
||||
List<Attachment>? attachments,
|
||||
}) async {
|
||||
final runtime = _active;
|
||||
if (runtime == null) return;
|
||||
|
||||
final adapted = attachments
|
||||
?.map((a) => AttachmentData(
|
||||
name: a.name,
|
||||
mimeType: a.mimeType,
|
||||
data: a.data,
|
||||
))
|
||||
.toList();
|
||||
|
||||
await runtime.sendMessage(text, priority: priority, attachments: adapted);
|
||||
}
|
||||
|
||||
void stopGenerating() => _active?.stopGenerating();
|
||||
|
||||
Future<void> runCompact({String? customInstructions}) =>
|
||||
_active?.runCompact(customInstructions: customInstructions) ??
|
||||
Future.value();
|
||||
|
||||
Future<void> resolvePermission(PermissionDecision decision) =>
|
||||
_active?.resolvePermission(decision) ?? Future.value();
|
||||
|
||||
void removeQueuedMessage(int index) => _active?.removeQueuedMessage(index);
|
||||
|
||||
// ─── dispose ────────────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_client?.close();
|
||||
for (final r in _runtimes.values) {
|
||||
r.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _buildSessionName(String text) {
|
||||
final sanitized = text.replaceAll(RegExp(r"\s+"), " ").trim();
|
||||
if (sanitized.isEmpty) {
|
||||
return "New Chat";
|
||||
}
|
||||
|
||||
const maxLength = 48;
|
||||
if (sanitized.length <= maxLength) {
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
return "${sanitized.substring(0, maxLength - 1).trimRight()}…";
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _buildApiMessages(List<Message> messages) {
|
||||
return messages
|
||||
.where(
|
||||
(message) => message.role == "user" || message.role == "assistant",
|
||||
)
|
||||
.map(
|
||||
(message) => <String, dynamic>{
|
||||
"role": message.role,
|
||||
"content": message.content,
|
||||
},
|
||||
)
|
||||
.toList(growable: true);
|
||||
}
|
||||
|
||||
String _formatToolCall(String toolName, Map<String, dynamic> input) {
|
||||
const encoder = JsonEncoder.withIndent(" ");
|
||||
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) {
|
||||
return "$toolName result\n$result";
|
||||
}
|
||||
|
||||
String _buildTurnFailureMessage(Object error) {
|
||||
return "This turn failed before the assistant could finish: $error";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import "package:flutter/foundation.dart";
|
||||
import "../../src/project_store.dart";
|
||||
import "../../src/session/session_types.dart";
|
||||
import "chat_provider.dart";
|
||||
import "../models/attachment.dart";
|
||||
import "projects_provider.dart";
|
||||
import "session_provider.dart";
|
||||
import "settings_provider.dart";
|
||||
@@ -56,20 +57,17 @@ class HomeCoordinator extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createNewChat() async {
|
||||
void createNewChat() {
|
||||
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());
|
||||
// Don't create the session yet — that happens on first message send.
|
||||
// Just clear the current state so the UI shows a blank chat.
|
||||
_session.clearCurrentSession(workingDirectory: selectedProject.workingDirectory);
|
||||
_chat.clearConversation();
|
||||
}
|
||||
|
||||
Future<void> selectProject(ProjectRecord project) async {
|
||||
@@ -84,14 +82,28 @@ class HomeCoordinator extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> openSession(SessionSummary session) async {
|
||||
// If a live runtime exists for this session, just switch focus to it
|
||||
// without reloading from disk — avoids disrupting an in-progress turn.
|
||||
if (_chat.isSessionRunning(session.id)) {
|
||||
_chat.activateSessionById(session.id);
|
||||
_session.setActiveSessionId(session.id);
|
||||
_projects.selectProjectByWorkingDirectory(session.workingDirectory);
|
||||
_settings.setThreadModel(session.model);
|
||||
return;
|
||||
}
|
||||
|
||||
await _session.loadSession(session);
|
||||
_chat.setConversation(_session.getConversationHistory());
|
||||
final loaded = _session.currentSession;
|
||||
if (loaded != null) {
|
||||
_chat.activateSession(loaded);
|
||||
}
|
||||
_projects.selectProjectByWorkingDirectory(_session.activeWorkingDirectory);
|
||||
_settings.setThreadModel(_session.currentSession?.model);
|
||||
}
|
||||
|
||||
Future<void> sendMessage(String text) async {
|
||||
if (text.isEmpty) return;
|
||||
Future<void> sendMessage(String text, {List<Attachment>? attachments}) async {
|
||||
final hasAttachments = attachments != null && attachments.isNotEmpty;
|
||||
if (text.isEmpty && !hasAttachments) return;
|
||||
|
||||
if (_session.currentSession == null) {
|
||||
final selectedProject = _projects.selectedProject;
|
||||
@@ -105,11 +117,14 @@ class HomeCoordinator extends ChangeNotifier {
|
||||
model: _settings.settings.model,
|
||||
);
|
||||
_settings.setThreadModel(_settings.settings.model);
|
||||
_chat.setConversation(_session.getConversationHistory());
|
||||
final newSession = _session.currentSession;
|
||||
if (newSession != null) {
|
||||
_chat.activateSession(newSession);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await _chat.sendMessage(text);
|
||||
await _chat.sendMessage(text, attachments: attachments);
|
||||
} catch (e, st) {
|
||||
print("Failed to send message: $e");
|
||||
print(st);
|
||||
|
||||
@@ -25,6 +25,11 @@ class SessionProvider extends ChangeNotifier {
|
||||
ConversationSession? get currentSession => _currentSession;
|
||||
String? get activeWorkingDirectory => _activeWorkingDirectory;
|
||||
|
||||
void setActiveSessionId(String id) {
|
||||
_currentSessionId = id;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<SessionSummary> sessionsForWorkingDirectory(String? workingDirectory) {
|
||||
final normalizedDirectory = workingDirectory?.trim();
|
||||
if (normalizedDirectory == null || normalizedDirectory.isEmpty) {
|
||||
|
||||
@@ -90,6 +90,14 @@ class SettingsProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updatePermissionMode(String mode) async {
|
||||
await _settingsStore.update(
|
||||
(current) => current.copyWith(permissionMode: mode),
|
||||
);
|
||||
_globalSettings = _settingsStore.settings;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> addAlwaysAllowRule(String toolName) async {
|
||||
final current = _globalSettings.alwaysAllowRules;
|
||||
if (current.contains(toolName)) return;
|
||||
|
||||
Reference in New Issue
Block a user