181 lines
6.4 KiB
Dart
181 lines
6.4 KiB
Dart
import "package:flutter/foundation.dart";
|
|
import "dart:typed_data";
|
|
|
|
import "../../src/chat/tool_loop_service.dart";
|
|
import "../../src/compact/compact_service.dart";
|
|
import "../../src/hooks/hook_loader.dart";
|
|
import "../../src/hooks/hook_runner.dart";
|
|
import "../../src/permissions/permission_types.dart";
|
|
import "../../src/session/session_runtime.dart";
|
|
import "../../src/session/session_types.dart";
|
|
import "../models/attachment.dart";
|
|
import "settings_provider.dart";
|
|
|
|
|
|
// 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;
|
|
|
|
final Map<String, SessionRuntime> _runtimes = {};
|
|
String? _activeSessionId;
|
|
|
|
// ─── hooks ──────────────────────────────────────────────────────────────────
|
|
|
|
Future<void> _initHooks() async {
|
|
try {
|
|
final hooks = await HookLoader.loadHooks();
|
|
_hookRunner = HookRunner(hooks: hooks);
|
|
_toolLoopService = ToolLoopService(hookRunner: _hookRunner);
|
|
} catch (e) {
|
|
print("Hook init failed: $e");
|
|
}
|
|
}
|
|
|
|
// ─── active runtime accessors ────────────────────────────────────────────────
|
|
|
|
SessionRuntime? get _active =>
|
|
_activeSessionId != null ? _runtimes[_activeSessionId] : null;
|
|
|
|
List<Message> get messages => _active?.messages ?? const [];
|
|
int get messageCount => messages.length;
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// ─── session lifecycle ───────────────────────────────────────────────────────
|
|
|
|
// 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;
|
|
|
|
if (!_runtimes.containsKey(id)) {
|
|
_runtimes[id] = SessionRuntime(
|
|
session: session,
|
|
toolLoopService: _toolLoopService,
|
|
hookRunner: _hookRunner,
|
|
getSettings: () => _settingsProvider.settings,
|
|
normalizeModelId: (m) => _settingsProvider.normalizeModelId(m),
|
|
onChanged: notifyListeners,
|
|
);
|
|
}
|
|
|
|
_activeSessionId = id;
|
|
_runtimes[id]?.markRead();
|
|
notifyListeners();
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
// 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() {
|
|
for (final r in _runtimes.values) {
|
|
r.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
}
|