claude: live-tail terminal sub-card in expanded Bash cards (T-325, UI)

Wire the detection + follower core into the Bash tool card. A Bash card
with a follow intent (`tail -f …`) gains a "live tail" segment below the
result: an embedded read-only TerminalView fed by FileTailFollower on the
file the command follows, resolved against the open workspace.

Lazy lifecycle for free: the collapser builds its children only when
expanded (clide_collapser_card.dart), so _BashLiveTail starts the follower
in didChangeDependencies on expand and stops it in dispose on collapse —
no follower runs until the card is expanded. No resolvable file-backed
source → a muted "no independent source to follow" note, never an empty
terminal. The workspace root comes from kernel.project.current, so no new
plumbing through the conversation widget tree.

Tests: a tail Bash card surfaces the segment (+ the muted note when no
project/source); an ordinary `ls` card gets no segment; the segment only
builds on expand.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 19:09:01 +02:00
co-authored by Claude Opus 4.8
parent 898a0316e5
commit d43377ac89
3 changed files with 105 additions and 0 deletions
@@ -818,6 +818,29 @@ void main() {
expect(copied, contains('question text'));
expect(copied, contains('answer text'));
});
testWidgets('a tail Bash card shows a live-tail segment; no workspace source → muted note (T-325)', (tester) async {
await pumpWith(tester, [
AssistantToolUse(uuid: 'b1', timestamp: _t, isSidechain: false, toolUseId: 'tb', name: 'Bash', input: const {'command': 'tail -f app.log'}),
]);
// Collapsed by default — the segment only builds (and connects) on expand.
expect(find.text('live tail'), findsNothing);
await tester.tap(find.bySemanticsLabel('Bash, 1 step, collapsed'));
await tester.pumpAndSettle();
expect(find.text('live tail'), findsOneWidget); // segment surfaced for a tail command
// No project open in the fixture → no resolvable source → the muted note,
// never a broken/empty terminal.
expect(find.text('no independent source to follow'), findsOneWidget);
});
testWidgets('an ordinary Bash card has no live-tail segment (T-325)', (tester) async {
await pumpWith(tester, [
AssistantToolUse(uuid: 'b2', timestamp: _t, isSidechain: false, toolUseId: 'tb2', name: 'Bash', input: const {'command': 'ls -la'}),
]);
await tester.tap(find.bySemanticsLabel('Bash, 1 step, collapsed'));
await tester.pumpAndSettle();
expect(find.text('live tail'), findsNothing); // no tail intent → no segment
});
});
group('ClaudeBanner', () {