Add command files and enhance session management features

This commit is contained in:
ImBenji
2026-04-28 19:00:27 +01:00
parent 3588783001
commit 728c0ffe81
146 changed files with 6854 additions and 7783 deletions
+56 -4
View File
@@ -1,4 +1,5 @@
import "package:flutter/foundation.dart";
import "package:flutter/scheduler.dart";
import "dart:typed_data";
import "../../src/chat/tool_loop_service.dart";
@@ -6,9 +7,12 @@ 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/services/cost_tracker.dart" as cost_tracker;
import "../../src/session/session_runtime.dart";
import "../../src/session/session_store.dart";
import "../../src/session/session_types.dart";
import "../models/attachment.dart";
import "cost_provider.dart";
import "settings_provider.dart";
@@ -20,18 +24,35 @@ import "settings_provider.dart";
// running and save themselves to disk; when you switch back you see their
// live state.
class ChatProvider extends ChangeNotifier {
ChatProvider(this._settingsProvider) {
ChatProvider(this._settingsProvider, this._costProvider) {
_initHooks();
}
final SettingsProvider _settingsProvider;
final CostProvider _costProvider;
void Function(String sessionId, String newName)? onSessionNameChanged;
ToolLoopService _toolLoopService = ToolLoopService();
HookRunner? _hookRunner;
final Map<String, SessionRuntime> _runtimes = {};
final Map<String, ConversationSession> _sessions = {};
String? _activeSessionId;
bool _notifyScheduled = false;
void _scheduleNotify() {
if (_notifyScheduled) return;
_notifyScheduled = true;
SchedulerBinding.instance.scheduleFrameCallback((_) {
_notifyScheduled = false;
notifyListeners();
_costProvider.refreshCost();
});
}
// ─── hooks ──────────────────────────────────────────────────────────────────
Future<void> _initHooks() async {
@@ -82,6 +103,10 @@ class ChatProvider extends ChangeNotifier {
return r != null && r.hasUnreadResult;
}
void markSessionRead(String sessionId) {
_runtimes[sessionId]?.setUnreadResult(false);
}
int get contextTokens {
final msgs = messages;
for (var i = msgs.length - 1; i >= 0; i--) {
@@ -99,19 +124,46 @@ class ChatProvider extends ChangeNotifier {
final id = session.id;
if (!_runtimes.containsKey(id)) {
_sessions[id] = session;
_runtimes[id] = SessionRuntime(
session: session,
toolLoopService: _toolLoopService,
hookRunner: _hookRunner,
getSettings: () => _settingsProvider.settings,
normalizeModelId: (m) => _settingsProvider.normalizeModelId(m),
onChanged: notifyListeners,
onChanged: _scheduleNotify,
onCostAdded: (_) {
final s = _sessions[id];
if (s != null) SessionStore.instance.saveSession(s);
},
isActive: () => _activeSessionId == id,
onNameGenerated: (sid, name) {
onSessionNameChanged?.call(sid, name);
notifyListeners();
},
onPersistAllowRule: (rule) => _settingsProvider.addAlwaysAllowRule(rule),
);
}
_activeSessionId = id;
_runtimes[id]?.markRead();
// sync global cost tracker to this thread's persisted cost
cost_tracker.resetCostState();
final sessionCost = (_sessions[id] ?? session).cost;
if (sessionCost > 0) {
cost_tracker.setCostStateForRestore(
totalCostUsd: sessionCost,
totalApiDurationMs: 0,
totalApiDurationWithoutRetriesMs: 0,
totalToolDurationMs: 0,
totalLinesAdded: 0,
totalLinesRemoved: 0,
);
}
notifyListeners();
_costProvider.refreshCost();
}
// Fast-path: switch focus to an already-running runtime without touching disk.
@@ -164,8 +216,8 @@ class ChatProvider extends ChangeNotifier {
_active?.runCompact(customInstructions: customInstructions) ??
Future.value();
Future<void> resolvePermission(PermissionDecision decision) =>
_active?.resolvePermission(decision) ?? Future.value();
Future<void> resolvePermission(PermissionDecision decision, {String? persistRule}) =>
_active?.resolvePermission(decision, persistRule: persistRule) ?? Future.value();
void removeQueuedMessage(int index) => _active?.removeQueuedMessage(index);
-2
View File
@@ -12,8 +12,6 @@ class CostProvider extends ChangeNotifier {
String getFormattedTotalCost() => cost_tracker.formatTotalCost();
Future<void> refreshCost() async {
// read current values from cost tracker
final _ = cost_tracker.getTotalCostUsd();
notifyListeners();
}
}
+6 -1
View File
@@ -11,7 +11,9 @@ import "settings_provider.dart";
class HomeCoordinator extends ChangeNotifier {
HomeCoordinator(this._projects, this._session, this._chat, this._settings);
HomeCoordinator(this._projects, this._session, this._chat, this._settings) {
_chat.onSessionNameChanged = (_, __) => _session.refreshSessions();
}
final ProjectsProvider _projects;
final SessionProvider _session;
@@ -86,6 +88,7 @@ class HomeCoordinator extends ChangeNotifier {
// without reloading from disk — avoids disrupting an in-progress turn.
if (_chat.isSessionRunning(session.id)) {
_chat.activateSessionById(session.id);
_chat.markSessionRead(session.id);
_session.setActiveSessionId(session.id);
_projects.selectProjectByWorkingDirectory(session.workingDirectory);
_settings.setThreadModel(session.model);
@@ -96,6 +99,7 @@ class HomeCoordinator extends ChangeNotifier {
final loaded = _session.currentSession;
if (loaded != null) {
_chat.activateSession(loaded);
_chat.markSessionRead(session.id);
}
_projects.selectProjectByWorkingDirectory(_session.activeWorkingDirectory);
_settings.setThreadModel(_session.currentSession?.model);
@@ -120,6 +124,7 @@ class HomeCoordinator extends ChangeNotifier {
final newSession = _session.currentSession;
if (newSession != null) {
_chat.activateSession(newSession);
_chat.markSessionRead(newSession.id);
}
}
+16
View File
@@ -82,6 +82,22 @@ class SettingsProvider extends ChangeNotifier {
notifyListeners();
}
Future<void> updateAdvisorEffortLevel(String? level) async {
await _settingsStore.update(
(current) => current.copyWith(advisorEffortLevel: level),
);
_globalSettings = _settingsStore.settings;
notifyListeners();
}
Future<void> updateAdvisorModel(String? model) async {
await _settingsStore.update(
(current) => current.copyWith(advisorModel: model),
);
_globalSettings = _settingsStore.settings;
notifyListeners();
}
Future<void> updateEffortLevel(String newLevel) async {
await _settingsStore.update(
(current) => current.copyWith(effortLevel: newLevel),