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
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:
@@ -56,11 +56,15 @@ void main() {
|
||||
expect(c.isEmpty, isTrue);
|
||||
ctrl.add(_user('hi'));
|
||||
ctrl.add(_asst('hello'));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
// Wait past the coalescing timer (zero-duration, fires after the
|
||||
// microtask queue drains).
|
||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||
|
||||
expect(c.items, hasLength(2));
|
||||
expect(c.items.first, isA<UserMessage>());
|
||||
expect(notifications, 2);
|
||||
// Notifications are coalesced: a burst of items collapses to a
|
||||
// single notify so the view rebuilds once, not per item.
|
||||
expect(notifications, 1);
|
||||
await ctrl.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -669,6 +669,43 @@ void main() {
|
||||
expect(collected, hasLength(1));
|
||||
expect((collected.first as UserMessage).text, 'real message');
|
||||
});
|
||||
|
||||
test('initial attach reads only the recent tail (initialTailBytes cap)', () async {
|
||||
final projectDir = mungedDir(tempBase, workspace);
|
||||
await projectDir.create(recursive: true);
|
||||
final sessionFile = File('${projectDir.path}/session-abc.jsonl');
|
||||
|
||||
// A long pre-existing transcript — the kind that froze the UI when
|
||||
// parsed in full on attach.
|
||||
writeLines(sessionFile, [
|
||||
for (var i = 0; i < 200; i++) assistantText('a$i', 'reply number $i'),
|
||||
]);
|
||||
|
||||
// Cap the initial read well below the file size so only the last
|
||||
// records are within the tail window.
|
||||
final reader = TranscriptReader(
|
||||
workspace,
|
||||
projectsBase: tempBase.path,
|
||||
pollInterval: const Duration(milliseconds: 20),
|
||||
initialTailBytes: 256,
|
||||
);
|
||||
final collected = <ConversationItem>[];
|
||||
final sub = reader.stream.listen(collected.add);
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
await sub.cancel();
|
||||
await reader.dispose();
|
||||
|
||||
// Far fewer than the 200 records — only the recent tail was read.
|
||||
expect(collected, isNotEmpty);
|
||||
expect(collected.length, lessThan(200));
|
||||
// The most recent record is always intact at the end of the file.
|
||||
expect(
|
||||
collected.whereType<AssistantTextMessage>().last.text,
|
||||
'reply number 199',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user