focus the Claude composer on a background tap

T-227. A tap on empty conversation area now lands the cursor in the
composer. The conversation area is wrapped in a translucent
GestureDetector whose onTap focuses a pane-owned composer FocusNode, so
message links, copy buttons, and the SelectableRegion's selection drags
keep winning their own gestures — only an unclaimed tap reaches us. It's
a no-op while a prompt holds the interaction zone (D-78), so a tap never
pulls focus over an open prompt.

The composer learned to accept an external focus node (the pane owns it,
so it survives composer remounts) and attaches its key handling to
whichever node it's given.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 12:09:17 +02:00
co-authored by Claude Opus 4.8
parent ab5d63debd
commit 4508be76ac
6 changed files with 103 additions and 10 deletions
@@ -336,6 +336,39 @@ void main() {
});
});
group('ClaudeComposer external focus node (T-227)', () {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());
tearDown(() => f.dispose());
testWidgets('uses a supplied focus node and focuses the input on request', (tester) async {
final node = FocusNode();
addTearDown(node.dispose);
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, focusNode: node)));
expect(tester.widget<EditableText>(find.byType(EditableText)).focusNode, same(node));
node.requestFocus(); // what the pane does on a background tap
await tester.pump();
expect(node.hasFocus, isTrue);
});
testWidgets('key handling still works through the supplied node', (tester) async {
final submitted = <String>[];
final node = FocusNode();
addTearDown(node.dispose);
await tester.pumpWidget(harness(
f,
ClaudeComposer(onSubmit: submitted.add, focusNode: node),
));
await tester.enterText(find.byType(EditableText), 'via external node');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(submitted, ['via external node']);
});
});
group('ClaudeComposer prompt history (T-163)', () {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());