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:
co-authored by
Claude
parent
762d66e819
commit
6e4c3c4bf4
@@ -184,6 +184,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
sessionId: _sessionId!,
|
||||
cwd: repoRoot,
|
||||
resume: resume,
|
||||
transcriptPath: resume ? transcriptFile : null,
|
||||
));
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _error = 'Could not start claude: $e');
|
||||
|
||||
@@ -15,13 +15,18 @@ import 'package:clide/kernel/src/events/message_bus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class ConversationController extends ChangeNotifier {
|
||||
/// Listens to [stream] and accumulates items. [onDispose] is invoked
|
||||
/// from [dispose] — wire it to the reader's `dispose` so cancelling
|
||||
/// the view tears down the underlying tail.
|
||||
/// Listens to [stream] and accumulates items. [seed] pre-populates the
|
||||
/// item list synchronously before subscribing — used when resuming a
|
||||
/// session (D-77), where `claude --resume` doesn't replay prior turns
|
||||
/// over stream-json so the pane would otherwise start empty. [onDispose]
|
||||
/// is invoked from [dispose] — wire it to the reader's `dispose` so
|
||||
/// cancelling the view tears down the underlying tail.
|
||||
ConversationController({
|
||||
required Stream<ConversationItem> stream,
|
||||
Iterable<ConversationItem>? seed,
|
||||
Future<void> Function()? onDispose,
|
||||
}) : _onDispose = onDispose {
|
||||
if (seed != null) _items.addAll(seed);
|
||||
_sub = stream.listen(_onItem);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,22 @@
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clide/builtin/claude/src/conversation_controller.dart';
|
||||
import 'package:clide/builtin/claude/src/session_naming.dart';
|
||||
import 'package:clide/builtin/claude/src/stream_json_session.dart';
|
||||
import 'package:clide/builtin/claude/src/team_broker.dart';
|
||||
import 'package:clide/builtin/claude/src/transcript_reader.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Max bytes of recent transcript to replay into the [ConversationController]
|
||||
/// when resuming a session — `claude --resume` carries Claude's context but
|
||||
/// emits no past turns over stream-json, so the pane would start empty
|
||||
/// without this hydration. Matches [TranscriptReader]'s initial-tail size.
|
||||
const _resumeTailBytes = 256 * 1024;
|
||||
|
||||
/// Creates the subprocess for a session — production uses
|
||||
/// [ClaudeStreamJsonProcess.start]; tests inject a fake.
|
||||
typedef ProcessFactory = Future<StreamJsonProcess> Function({
|
||||
@@ -36,6 +45,7 @@ class SpawnSpec {
|
||||
required this.sessionId,
|
||||
required this.cwd,
|
||||
this.resume = false,
|
||||
this.transcriptPath,
|
||||
this.env,
|
||||
this.visible = true,
|
||||
this.team = false,
|
||||
@@ -49,6 +59,11 @@ class SpawnSpec {
|
||||
|
||||
/// Resume an existing session (`--resume`) vs create one (`--session-id`).
|
||||
final bool resume;
|
||||
|
||||
/// Transcript JSONL to replay into the conversation when [resume] is true.
|
||||
/// Optional — without it, a resumed session still works but its pane starts
|
||||
/// empty until the user sends a new prompt.
|
||||
final String? transcriptPath;
|
||||
final Map<String, String>? env;
|
||||
final bool visible;
|
||||
|
||||
@@ -142,7 +157,8 @@ class ClaudeSessionOrchestrator extends ChangeNotifier {
|
||||
env: spec.env,
|
||||
);
|
||||
final session = StreamJsonSession(proc, mcpServers: mcpServers)..start();
|
||||
final conversation = ConversationController(stream: session.items, onDispose: session.dispose);
|
||||
final seed = spec.resume && spec.transcriptPath != null ? await _readTranscriptTail(spec.transcriptPath!) : null;
|
||||
final conversation = ConversationController(stream: session.items, seed: seed, onDispose: session.dispose);
|
||||
final managed = ManagedSession(
|
||||
id: spec.id,
|
||||
role: spec.role,
|
||||
@@ -178,6 +194,32 @@ class ClaudeSessionOrchestrator extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Read up to [_resumeTailBytes] from the end of [path] and parse it into
|
||||
/// items to seed the conversation. Best-effort: a missing/unreadable file
|
||||
/// returns null and the pane resumes empty, same as before this fix.
|
||||
Future<List<ConversationItem>?> _readTranscriptTail(String path) async {
|
||||
try {
|
||||
final f = File(path);
|
||||
if (!await f.exists()) return null;
|
||||
final length = await f.length();
|
||||
final start = length > _resumeTailBytes ? length - _resumeTailBytes : 0;
|
||||
final raf = await f.open();
|
||||
try {
|
||||
await raf.setPosition(start);
|
||||
final bytes = await raf.read(length - start);
|
||||
final text = utf8.decode(bytes, allowMalformed: true);
|
||||
// Started mid-file → drop the partial first line so we never feed
|
||||
// half a JSON record to the parser.
|
||||
final chunk = start == 0 ? text : text.substring(text.indexOf('\n') + 1);
|
||||
return parseTranscriptChunk(chunk).items;
|
||||
} finally {
|
||||
await raf.close();
|
||||
}
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// The team-awareness preamble injected via `--append-system-prompt` (T-170).
|
||||
static String _teamSystemPrompt(String name, String role) => 'You are part of a clide-managed agent team. Your name is "$name" and your role is "$role". '
|
||||
'Coordinate with teammates using the clide-team MCP tools: '
|
||||
|
||||
Reference in New Issue
Block a user