add Claude session storage view with user-driven cleanup
The claude.session-storage command opens a modal listing the workspace's session transcripts with their on-disk sizes (the <id>.jsonl plus the <id>/ subagents dir) and a total. Each row deletes with a deliberate two-click confirm; deletion is guarded against unsafe ids and clide never removes transcripts on its own. SessionSummary gains a sizeBytes field and session_index gains formatBytes + deleteSession. T-148. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ class SessionSummary {
|
||||
required this.modified,
|
||||
this.firstUser,
|
||||
this.lastUser,
|
||||
this.sizeBytes = 0,
|
||||
});
|
||||
|
||||
/// The session id (the `<uuid>` of `<uuid>.jsonl`).
|
||||
@@ -29,6 +30,10 @@ class SessionSummary {
|
||||
final String? firstUser;
|
||||
final String? lastUser;
|
||||
|
||||
/// On-disk size of this session: the transcript `<id>.jsonl` plus its
|
||||
/// `<id>/` subagents directory, in bytes (T-148).
|
||||
final int sizeBytes;
|
||||
|
||||
/// "first … last" — the picker's primary label. Falls back to the id when
|
||||
/// the session carries no user prompt.
|
||||
String get label {
|
||||
@@ -103,17 +108,52 @@ Future<List<SessionSummary>> listSessions(
|
||||
for (final f in files) {
|
||||
final stat = await f.stat();
|
||||
final bookends = await _bookends(f, window);
|
||||
final id = _sessionId(f.path);
|
||||
summaries.add(SessionSummary(
|
||||
id: _sessionId(f.path),
|
||||
id: id,
|
||||
modified: stat.modified,
|
||||
firstUser: bookends.first,
|
||||
lastUser: bookends.last,
|
||||
sizeBytes: stat.size + await _dirSize(Directory('${dir.path}/$id')),
|
||||
));
|
||||
}
|
||||
summaries.sort((a, b) => b.modified.compareTo(a.modified));
|
||||
return summaries.length > max ? summaries.sublist(0, max) : summaries;
|
||||
}
|
||||
|
||||
/// Total bytes of [dir]'s files (recursive), or 0 if it doesn't exist. Used to
|
||||
/// fold a session's `<id>/subagents/` transcripts into its reported size.
|
||||
Future<int> _dirSize(Directory dir) async {
|
||||
if (!await dir.exists()) return 0;
|
||||
var total = 0;
|
||||
await for (final e in dir.list(recursive: true, followLinks: false)) {
|
||||
if (e is File) total += await e.length();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// Delete a session's transcript (`<id>.jsonl`) and its `<id>/` subagents
|
||||
/// directory from [dir] (T-148). User-driven only — clide never calls this
|
||||
/// on its own. [id] must be a bare filename component; a value with a path
|
||||
/// separator or `..` is rejected to keep the delete inside [dir].
|
||||
Future<void> deleteSession(Directory dir, String id) async {
|
||||
if (id.isEmpty || id.contains('/') || id.contains(r'\') || id.contains('..')) {
|
||||
throw ArgumentError('refusing to delete unsafe session id: $id');
|
||||
}
|
||||
final file = File('${dir.path}/$id.jsonl');
|
||||
if (await file.exists()) await file.delete();
|
||||
final sub = Directory('${dir.path}/$id');
|
||||
if (await sub.exists()) await sub.delete(recursive: true);
|
||||
}
|
||||
|
||||
/// Human-readable byte size: `0 B`, `12 KB`, `3.4 MB`, `1.2 GB`.
|
||||
String formatBytes(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).round()} KB';
|
||||
if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||
}
|
||||
|
||||
String _sessionId(String path) {
|
||||
final base = path.split(Platform.pathSeparator).last;
|
||||
return base.endsWith('.jsonl') ? base.substring(0, base.length - 6) : base;
|
||||
|
||||
Reference in New Issue
Block a user