fix Claude pane hang: cap tail, parse off-isolate, coalesce notifies
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 27s

The native Claude pane (T-137) picks the newest transcript JSONL by
mtime — which on a live session is this multi-MB active file. On attach
it read and parsed the whole file synchronously on the UI isolate and
fired notifyListeners per item, freezing the app.

Three fixes: cap the initial read to a recent tail (256KB, injectable
for tests); run JSON parsing in a background isolate via Isolate.run;
coalesce the controller's notifications with a zero-duration Timer so a
burst collapses into one rebuild (a microtask-scheduled notify wouldn't
— stream events deliver one per microtask and the notify interleaves).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 20:30:30 +02:00
co-authored by Claude Opus 4.7
parent 76c1d24904
commit a6ed22405f
7 changed files with 237 additions and 173 deletions
@@ -33,6 +33,8 @@ class ConversationController extends ChangeNotifier {
final Future<void> Function()? _onDispose;
late final StreamSubscription<ConversationItem> _sub;
final List<ConversationItem> _items = [];
Timer? _notifyTimer;
bool _disposed = false;
/// Items in arrival (transcript) order.
List<ConversationItem> get items => List.unmodifiable(_items);
@@ -41,11 +43,22 @@ class ConversationController extends ChangeNotifier {
void _onItem(ConversationItem item) {
_items.add(item);
notifyListeners();
// Coalesce notifications: the reader emits a burst (the initial tail
// read), and a notify-per-item would thrash the view's rebuild +
// auto-scroll. A zero-duration Timer fires only after the microtask
// queue drains — the stream delivers one event per microtask, so a
// microtask-scheduled notify would interleave between deliveries and
// fire per item. The timer collapses a whole burst into one rebuild.
_notifyTimer ??= Timer(Duration.zero, () {
_notifyTimer = null;
if (!_disposed) notifyListeners();
});
}
@override
void dispose() {
_disposed = true;
_notifyTimer?.cancel();
unawaited(_sub.cancel());
unawaited(_onDispose?.call());
super.dispose();