feat(learn): + opens a sheet as Claude's, Camera, Photos and Files, and what goes along in rounded rows

Following the developer's review of 3 Nov 2026 (ADR 0021, D67):

- + below the assistant's field, and below a conversation's follow-up,
  opens "Add to your question" as Claude's "Add to session": three big
  tiles side by side, Camera (where the device takes photos) and Photos
  for an assistant reading images, and Files; below them, in the field, a
  row leading to what goes along, showing how far the assistant looks and
  how many parts are chosen.
- Camera and Photos through image_picker (flutter.dev's own): the camera's
  own app and the system's photo picker, no permission asked on Android, a
  file dialog on the desktop, which has no Camera; photos made at most
  2048 pixels a side, read on the device like any file attached.
- What goes along is chosen in rounded rows, as Claude's Permission, in
  place of radio buttons and chips: how far it looks with a tick, private
  studysets with a switch, the parts each with a tick, a card's image with
  its thumbnail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-03 21:56:18 +01:00
commit 7be31dcbfc
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -33,6 +33,7 @@ import '../../support/fake_asset_server_gateway.dart';
import '../../support/fake_card_gateway.dart';
import '../../support/fake_conversation_gateway.dart';
import '../../support/fake_grammar_gateway.dart';
import '../../support/fake_photo_picker.dart';
import '../../support/fake_settings_gateway.dart';
import '../../support/fake_studyset_gateway.dart';
import '../../support/memory_asset_file_cache.dart';
@ -117,6 +118,7 @@ void main() {
late FakeSettingsGateway settings;
late FakeLlmClient llm;
late FakeFilePicker picker;
late FakePhotoPicker photos;
late MemoryDeviceSettingsStore store;
late List<Uri> searched;
late FakeAssetGateway assetGateway;
@ -143,6 +145,7 @@ void main() {
settings = FakeSettingsGateway();
llm = FakeLlmClient();
picker = FakeFilePicker();
photos = FakePhotoPicker();
searched = [];
assetGateway = FakeAssetGateway();
assetFiles = MemoryAssetFileCache();
@ -204,6 +207,7 @@ void main() {
grammarGateway: grammar,
grammarLanguages: grammarLanguages,
files: picker,
photos: photos,
),
location: location,
);
@ -270,6 +274,22 @@ void main() {
await closeContext(tester);
}
/// Opens + (with [tooltip]) and chooses [tile] in its sheet.
Future<void> add(
WidgetTester tester,
String tile, {
String tooltip = 'Attach a photo or file',
Finder? within,
}) async {
final plus = find.byTooltip(tooltip);
await tester.tap(
within == null ? plus : find.descendant(of: within, matching: plus),
);
await tester.pumpAndSettle();
await tester.tap(find.text(tile));
await tester.pumpAndSettle();
}
/// Opens the menu of who answers, below the field.
Future<void> openPicker(WidgetTester tester) async {
await tester.tap(find.byTooltip('Who answers'));
@ -1207,6 +1227,17 @@ void main() {
await tester.tap(find.byTooltip('Attach a photo or file'));
await tester.pumpAndSettle();
// As Claude's sheet has them (the developer's review of 3 Nov 2026).
final tiles = [
for (final tile in ['Camera', 'Photos', 'Files'])
tester.getRect(find.text(tile)),
];
expect(tiles[0].center.dy, tiles[1].center.dy);
expect(tiles[1].center.dy, tiles[2].center.dy);
expect(tiles[0].left, lessThan(tiles[1].left));
await tester.tap(find.text('Files'));
await tester.pumpAndSettle();
expect(find.text('Camera'), findsNothing, reason: 'the sheet closed');
expect(picker.asked.single, containsAll(['png', 'pdf', 'md']));
expect(find.text('notes.md'), findsOneWidget);
@ -1225,8 +1256,7 @@ void main() {
await tester.pump(const Duration(seconds: 6));
await tester.pumpAndSettle();
picker.nextMany = [(name: 'extra.txt', bytes: bytesOf('Not this.'))];
await tester.tap(find.byTooltip('Attach a photo or file'));
await tester.pumpAndSettle();
await add(tester, 'Files');
await tester.tap(
find.descendant(
of: find.widgetWithText(InputChip, 'extra.txt'),
@ -1259,6 +1289,44 @@ void main() {
);
});
testWidgets("+'s Camera takes a photo and Photos picks some, each "
'shown above the question; a desktop has no Camera; what goes '
"along is chosen from +'s sheet too", (tester) async {
assistants.offer = AssistantsOffer(assistants: [_seeing]);
await showApp(tester);
photos
..taken = (name: 'taken.jpg', bytes: bytesOf('a board'))
..picked = [
(name: 'one.jpg', bytes: bytesOf('one')),
(name: 'two.png', bytes: bytesOf('two')),
];
await add(tester, 'Camera');
await add(tester, 'Photos');
expect(photos.asked, ['camera', 'photos']);
for (final name in ['taken.jpg', 'one.jpg', 'two.png']) {
expect(find.widgetWithText(InputChip, name), findsOneWidget);
}
await tester.tap(find.byTooltip('Attach a photo or file'));
await tester.pumpAndSettle();
await tester.tap(find.text('What goes along').last);
await tester.pumpAndSettle();
expect(find.text('How far it looks'), findsOneWidget);
});
testWidgets('a desktop offers no Camera', (tester) async {
assistants.offer = AssistantsOffer(assistants: [_seeing]);
photos.hasCamera = false;
await showApp(tester);
await tester.tap(find.byTooltip('Attach a photo or file'));
await tester.pumpAndSettle();
expect(find.text('Camera'), findsNothing);
expect(find.text('Photos'), findsOneWidget);
expect(find.text('Files'), findsOneWidget);
});
testWidgets('an assistant reading no images is offered files alone, '
'and says why a picture picked anyway stays behind', (tester) async {
await showApp(tester);
@ -1266,6 +1334,10 @@ void main() {
await tester.tap(find.byTooltip('Attach a file'));
await tester.pumpAndSettle();
expect(find.text('Camera'), findsNothing);
expect(find.text('Photos'), findsNothing);
await tester.tap(find.text('Files'));
await tester.pumpAndSettle();
expect(picker.asked.single, isNot(contains('png')));
expect(
@ -1284,13 +1356,12 @@ void main() {
await ask(tester, 'Why て-form?');
picker.next = (name: 'lines.txt', bytes: bytesOf('One.\nTwo.'));
await tester.tap(
find.descendant(
of: find.byType(BottomSheet),
matching: find.byTooltip('Attach a file'),
),
await add(
tester,
'Files',
tooltip: 'Attach a file',
within: find.byType(BottomSheet),
);
await tester.pumpAndSettle();
expect(find.widgetWithText(InputChip, 'lines.txt'), findsOneWidget);
expect(
find.descendant(