feat(image): --file metadata payload for annotated image cards (T-316)
`clide image show --file meta.json` reads a {path,label,description,caption}
payload so an image can carry a title and a longer description, not just a
one-line caption. ImageMessage + the image card render the richer metadata;
the bare `image show <path> [--caption]` form is unchanged. Honest userError
on a malformed/missing payload. Text annotation only (option a) — visual
marker overlays stay a follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,8 @@ AssistantToolUse _tool(String name, Map<String, dynamic> input) =>
|
||||
AssistantToolUse(uuid: 'tu', timestamp: _t, isSidechain: false, toolUseId: 'x1', name: name, input: input);
|
||||
ToolResultMessage _result(String content, {bool isError = false}) =>
|
||||
ToolResultMessage(uuid: 'r', timestamp: _t, isSidechain: false, toolUseId: 'x1', content: content, isError: isError);
|
||||
ImageMessage _image(String path, {String? caption}) => ImageMessage(uuid: 'i', timestamp: _t, isSidechain: false, path: path, caption: caption);
|
||||
ImageMessage _image(String path, {String? caption, String? label, String? description}) =>
|
||||
ImageMessage(uuid: 'i', timestamp: _t, isSidechain: false, path: path, caption: caption, label: label, description: description);
|
||||
|
||||
class _MockClipboard {
|
||||
Map<String, dynamic> _data = {'text': null};
|
||||
@@ -472,6 +473,13 @@ void main() {
|
||||
expect((img.image as FileImage).file.path, '/no/such/file.png');
|
||||
});
|
||||
|
||||
testWidgets('an image card renders its --file label and description (T-316)', (tester) async {
|
||||
await pumpWith(tester, [_image('/no/such/shot.png', label: 'HUD v3', description: 'note the cramped status row', caption: 'before')]);
|
||||
expect(find.text('HUD v3'), findsOneWidget);
|
||||
expect(find.text('note the cramped status row'), findsOneWidget);
|
||||
expect(find.text('before'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('inject() drives a new image card into a live view (T-249)', (tester) async {
|
||||
final c = await pumpWith(tester, [_user('hi')]);
|
||||
expect(find.text('image'), findsNothing);
|
||||
|
||||
@@ -14,13 +14,18 @@ void main() {
|
||||
|
||||
// [found] is the set of paths the fake resolver treats as existing on disk;
|
||||
// it echoes them back prefixed with /abs to stand in for an absolute path.
|
||||
void wire({bool liveUi = true, Set<String> found = const {'docs/diagram.png'}, bool withResolver = true}) {
|
||||
void wire({bool liveUi = true, Set<String> found = const {'docs/diagram.png'}, bool withResolver = true, Map<String, String> files = const {}}) {
|
||||
published = [];
|
||||
d = DaemonDispatcher();
|
||||
registerImageCommands(d, () {
|
||||
if (!liveUi) return null;
|
||||
return (publisher, channel, data) => published.add((publisher: publisher, channel: channel, data: data));
|
||||
}, resolve: withResolver ? (path) => found.contains(path) ? '/abs/$path' : null : null);
|
||||
registerImageCommands(
|
||||
d,
|
||||
() {
|
||||
if (!liveUi) return null;
|
||||
return (publisher, channel, data) => published.add((publisher: publisher, channel: channel, data: data));
|
||||
},
|
||||
resolve: withResolver ? (path) => found.contains(path) ? '/abs/$path' : null : null,
|
||||
readFile: (path) async => files[path],
|
||||
);
|
||||
}
|
||||
|
||||
Future<IpcResponse> show(List<String> positional, {Map<String, Object?>? flags}) =>
|
||||
@@ -44,6 +49,26 @@ void main() {
|
||||
expect(published.single.data, {'path': '/abs/docs/diagram.png', 'caption': 'before the fix'});
|
||||
});
|
||||
|
||||
test('--file metadata payload carries label + description (T-316)', () async {
|
||||
wire(found: {'docs/shot.png'}, files: {'meta.json': '{"path":"docs/shot.png","label":"HUD v3","description":"cramped status row","caption":"before"}'});
|
||||
final r = await show([], flags: {'file': 'meta.json'});
|
||||
expect(r.ok, isTrue, reason: r.error?.message);
|
||||
expect(published.single.data, {'path': '/abs/docs/shot.png', 'caption': 'before', 'label': 'HUD v3', 'description': 'cramped status row'});
|
||||
});
|
||||
|
||||
test('a malformed --file payload is an honest userError, nothing published', () async {
|
||||
wire(files: {'meta.json': 'not json'});
|
||||
final r = await show([], flags: {'file': 'meta.json'});
|
||||
expect(r.error?.kind, IpcErrorKind.userError);
|
||||
expect(published, isEmpty);
|
||||
});
|
||||
|
||||
test('a missing --file is notFound', () async {
|
||||
wire();
|
||||
final r = await show([], flags: {'file': 'gone.json'});
|
||||
expect(r.error?.kind, IpcErrorKind.notFound);
|
||||
});
|
||||
|
||||
test('--fullscreen rides along in the payload (T-252)', () async {
|
||||
wire();
|
||||
final r = await show(['docs/diagram.png'], flags: {'fullscreen': true});
|
||||
@@ -117,7 +142,9 @@ void main() {
|
||||
expect(spec['verb'], 'show');
|
||||
expect(spec['positional'], ['path']);
|
||||
final args = spec['args'] as Map<String, Object?>;
|
||||
expect((args['path'] as Map)['required'], true);
|
||||
// path is no longer required — it may come from a --file payload (T-316).
|
||||
expect((args['path'] as Map)['required'], isNot(true));
|
||||
expect(args.containsKey('caption'), isTrue);
|
||||
expect(args.containsKey('file'), isTrue);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user