seed resumed Claude session from the transcript tail

claude --resume keeps the model's prior context but emits no past
turns over stream-json, so the pane was visually empty until the
user sent a new prompt. The orchestrator now reads the last 256 KB
of the on-disk transcript JSONL when SpawnSpec.resume is true and
seeds the ConversationController with the parsed items before the
stream subscription starts. Best-effort: missing or unreadable file
just falls back to the previous empty-pane behaviour.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2026-05-28 10:17:32 +02:00
co-authored by Claude
parent 762d66e819
commit 6e4c3c4bf4
5 changed files with 112 additions and 4 deletions
@@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:clide/builtin/claude/src/session_orchestrator.dart';
import 'package:clide/builtin/claude/src/stream_json_session.dart';
import 'package:clide/builtin/claude/src/transcript_reader.dart';
import 'package:flutter_test/flutter_test.dart';
class _FakeProc implements StreamJsonProcess {
@@ -122,4 +124,55 @@ void main() {
expect(orch.broker.members.map((m) => m.name), ['lead']);
});
});
group('resume hydration', () {
test('seeds the controller with prior items from the transcript', () async {
final tmp = await Directory.systemTemp.createTemp('clide-resume-');
final file = File('${tmp.path}/session.jsonl');
await file.writeAsString(
'{"type":"user","uuid":"u1","timestamp":"2026-05-26T00:00:00Z","isSidechain":false,"message":{"role":"user","content":"hello"}}\n'
'{"type":"assistant","uuid":"a1","timestamp":"2026-05-26T00:00:01Z","isSidechain":false,"message":{"role":"assistant","content":[{"type":"text","text":"hi back"}]}}\n',
);
final managed = await orch.spawn(SpawnSpec(
id: 'primary',
role: 'primary',
sessionId: 'primary-uuid',
cwd: '/repo',
resume: true,
transcriptPath: file.path,
));
final items = managed.conversation.items;
expect(items, hasLength(2));
expect(items.first, isA<UserMessage>());
expect((items.first as UserMessage).text, 'hello');
expect(items.last, isA<AssistantTextMessage>());
await tmp.delete(recursive: true);
});
test('non-resume spawn does not read the transcript', () async {
final managed = await orch.spawn(SpawnSpec(
id: 'primary',
role: 'primary',
sessionId: 'primary-uuid',
cwd: '/repo',
// resume:false → transcriptPath ignored even if set
transcriptPath: '/does/not/exist.jsonl',
));
expect(managed.conversation.items, isEmpty);
});
test('missing transcript file is tolerated (best-effort hydration)', () async {
final managed = await orch.spawn(SpawnSpec(
id: 'primary',
role: 'primary',
sessionId: 'primary-uuid',
cwd: '/repo',
resume: true,
transcriptPath: '/does/not/exist.jsonl',
));
expect(managed.conversation.items, isEmpty);
});
});
}