Add new features and update configurations for improved functionality
This commit is contained in:
@@ -21,10 +21,10 @@ class ConversationHistory {
|
||||
_session = s;
|
||||
}
|
||||
|
||||
void addMessage(String role, String content, {int? tokens}) {
|
||||
void addMessage(String role, String content, {int? tokens, int? contextTokens}) {
|
||||
if (_session == null) return;
|
||||
|
||||
final msg = Message(role: role, content: content, tokens: tokens);
|
||||
final msg = Message(role: role, content: content, tokens: tokens, contextTokens: contextTokens);
|
||||
|
||||
_session!.messages.add(msg);
|
||||
_session!.updated = DateTime.now().toUtc();
|
||||
@@ -41,10 +41,23 @@ class ConversationHistory {
|
||||
content: "${lastMessage.content}$text",
|
||||
timestamp: lastMessage.timestamp,
|
||||
tokens: lastMessage.tokens,
|
||||
contextTokens: lastMessage.contextTokens,
|
||||
);
|
||||
_session!.updated = DateTime.now().toUtc();
|
||||
}
|
||||
|
||||
void setLastMessageContextTokens(int contextTokens) {
|
||||
if (_session == null || _session!.messages.isEmpty) return;
|
||||
final last = _session!.messages.last;
|
||||
_session!.messages[_session!.messages.length - 1] = Message(
|
||||
role: last.role,
|
||||
content: last.content,
|
||||
timestamp: last.timestamp,
|
||||
tokens: last.tokens,
|
||||
contextTokens: contextTokens,
|
||||
);
|
||||
}
|
||||
|
||||
void removeLastMessage() {
|
||||
if (_session == null || _session!.messages.isEmpty) {
|
||||
return;
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
// Parity exception: Claude Code stores sessions in ~/.claude/projects/<sanitized-cwd>/<id>.jsonl
|
||||
// The Agency uses <workingDirectory>/.the_agency/sessions/<id>.json instead, because the UI
|
||||
// is multi-project and it makes more sense for session data to live alongside the project.
|
||||
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
|
||||
import "../local_state.dart";
|
||||
import "package:path/path.dart" as p;
|
||||
|
||||
import "session_types.dart";
|
||||
|
||||
const _encoder = JsonEncoder.withIndent(" ");
|
||||
|
||||
// sessions live in ~/.clawd_code/sessions/{id}.json
|
||||
String getSessionsDir() {
|
||||
return joinPath(getConfigHomeDir(), "sessions");
|
||||
// sessions live in <workingDirectory>/.the_agency/sessions/{id}.json
|
||||
String getProjectAgencyDir(String workingDirectory) {
|
||||
return p.join(workingDirectory, ".the_agency");
|
||||
}
|
||||
|
||||
String _sessionPath(String id) {
|
||||
return joinPath(getSessionsDir(), "$id.json");
|
||||
String getProjectSessionsDir(String workingDirectory) {
|
||||
return p.join(getProjectAgencyDir(workingDirectory), "sessions");
|
||||
}
|
||||
|
||||
String _sessionPath(String workingDirectory, String id) {
|
||||
return p.join(getProjectSessionsDir(workingDirectory), "$id.json");
|
||||
}
|
||||
|
||||
class SessionStore {
|
||||
@@ -20,20 +29,22 @@ class SessionStore {
|
||||
|
||||
static final SessionStore instance = SessionStore._();
|
||||
|
||||
|
||||
Future<void> saveSession(ConversationSession session) async {
|
||||
final dir = Directory(getSessionsDir());
|
||||
final workingDir = session.workingDirectory;
|
||||
if (workingDir == null || workingDir.isEmpty) return;
|
||||
|
||||
final dir = Directory(getProjectSessionsDir(workingDir));
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
|
||||
final file = File(_sessionPath(session.id));
|
||||
final file = File(_sessionPath(workingDir, session.id));
|
||||
final json = _encoder.convert(session.toJson());
|
||||
await file.writeAsString("$json\n");
|
||||
}
|
||||
|
||||
Future<ConversationSession?> loadSession(String id) async {
|
||||
final file = File(_sessionPath(id));
|
||||
Future<ConversationSession?> loadSession(String id, {required String workingDirectory}) async {
|
||||
final file = File(_sessionPath(workingDirectory, id));
|
||||
if (!await file.exists()) return null;
|
||||
|
||||
try {
|
||||
@@ -43,15 +54,15 @@ class SessionStore {
|
||||
return ConversationSession.fromJson(decoded);
|
||||
}
|
||||
} catch (_) {
|
||||
// corrupt file - just return null
|
||||
// corrupt file — return null
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// returns summaries sorted newest first
|
||||
Future<List<SessionSummary>> listSessions() async {
|
||||
final dir = Directory(getSessionsDir());
|
||||
// lists sessions for a single project, sorted newest first
|
||||
Future<List<SessionSummary>> listSessionsForProject(String workingDirectory) async {
|
||||
final dir = Directory(getProjectSessionsDir(workingDirectory));
|
||||
if (!await dir.exists()) return <SessionSummary>[];
|
||||
|
||||
final summaries = <SessionSummary>[];
|
||||
@@ -76,39 +87,23 @@ class SessionStore {
|
||||
return summaries;
|
||||
}
|
||||
|
||||
Future<bool> deleteSession(String id) async {
|
||||
final file = File(_sessionPath(id));
|
||||
// lists all sessions across multiple projects, sorted newest first
|
||||
Future<List<SessionSummary>> listAllSessions(List<String> workingDirectories) async {
|
||||
final all = <SessionSummary>[];
|
||||
|
||||
for (final dir in workingDirectories) {
|
||||
all.addAll(await listSessionsForProject(dir));
|
||||
}
|
||||
|
||||
all.sort((a, b) => b.updated.compareTo(a.updated));
|
||||
return all;
|
||||
}
|
||||
|
||||
Future<bool> deleteSession(String id, {required String workingDirectory}) async {
|
||||
final file = File(_sessionPath(workingDirectory, id));
|
||||
if (!await file.exists()) return false;
|
||||
|
||||
await file.delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
// case insensitive search by name
|
||||
Future<ConversationSession?> findSessionByName(String name) async {
|
||||
final dir = Directory(getSessionsDir());
|
||||
if (!await dir.exists()) return null;
|
||||
|
||||
final lowerName = name.toLowerCase();
|
||||
|
||||
await for (final entity in dir.list()) {
|
||||
if (entity is! File) continue;
|
||||
if (!entity.path.endsWith(".json")) continue;
|
||||
|
||||
try {
|
||||
final raw = await entity.readAsString();
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final sess = ConversationSession.fromJson(decoded);
|
||||
if (sess.name.toLowerCase() == lowerName) {
|
||||
return sess;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ class Message {
|
||||
required this.content,
|
||||
DateTime? timestamp,
|
||||
this.tokens,
|
||||
this.contextTokens,
|
||||
}) : timestamp = timestamp ?? DateTime.now().toUtc();
|
||||
|
||||
factory Message.fromJson(Map<String, dynamic> json) {
|
||||
@@ -17,6 +18,7 @@ class Message {
|
||||
? DateTime.tryParse(json["timestamp"] as String)
|
||||
: null,
|
||||
tokens: json["tokens"] as int?,
|
||||
contextTokens: json["contextTokens"] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,12 +29,17 @@ class Message {
|
||||
// approx token count - may be null if not tracked
|
||||
final int? tokens;
|
||||
|
||||
// full context window size from the last API response usage field
|
||||
// (input + cache_creation + cache_read + output), same as Claude Code
|
||||
final int? contextTokens;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
"role": role,
|
||||
"content": content,
|
||||
"timestamp": timestamp.toIso8601String(),
|
||||
if (tokens != null) "tokens": tokens,
|
||||
if (contextTokens != null) "contextTokens": contextTokens,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user