The last wire: a DrawingMessage conversation item + the Claude-extension subscriber on the `draw` channel (buildSvgDocument → inject), the conversation renderer (DrawingCard), the fold/summary switches, the i18n `drawing` label, and registering `clide draw` at boot (empty template registry for now — primitive SVG works; d2/icon/compare/image handlers plug in as they land). `clide draw --file doc.json` now renders a card in the conversation. Render widget-tested; analyze + format clean across the repo. Templates + per-object overlay remain; the CHANGELOG entry lands with the user-complete feature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
94 lines
3.4 KiB
Dart
94 lines
3.4 KiB
Dart
/// Registers `draw` — drive a drawing card into the Claude conversation from
|
|
/// the CLI (T-318, D-91 / D-103, D-6 parity).
|
|
///
|
|
/// clide draw --file card.json
|
|
///
|
|
/// The card is clide-owned rendering (the SVG engine + the [DrawingCard]
|
|
/// widget); this is its CLI counterpart. Mirroring `image.show`, the handler is
|
|
/// decoupled from the live UI: it reads + parses the JSON document (via an
|
|
/// injected [DrawingFileReader]), lowers it to an SVG string through the
|
|
/// template [DrawingRegistry], then publishes a `draw` message on the kernel
|
|
/// MessageBus. A consumer in the Claude extension builds the SvgDocument and
|
|
/// injects the card into the primary session. Flutter-free so it runs under
|
|
/// `dart test`.
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
|
|
import '../draw/draw_dispatch.dart';
|
|
import '../draw/draw_doc.dart';
|
|
|
|
export '../draw/draw_dispatch.dart' show DrawingFileReader, DrawingRegistry, DrawingTemplateHandler;
|
|
import '../ipc/command_schema.dart';
|
|
import '../ipc/envelope.dart';
|
|
import '../ipc/schema_v1.dart';
|
|
import 'dispatcher.dart';
|
|
import 'ui_command.dart' show MessagePublisher;
|
|
|
|
/// The MessageBus channel `draw` publishes on; the Claude extension subscribes
|
|
/// to the same literal to inject the card.
|
|
const drawShowChannel = 'draw';
|
|
|
|
void registerDrawCommands(
|
|
DaemonDispatcher d,
|
|
MessagePublisher? Function() publisher, {
|
|
required DrawingRegistry registry,
|
|
required DrawingFileReader readFile,
|
|
}) {
|
|
d.register(
|
|
'draw',
|
|
(req) async => _draw(req, publisher, registry, readFile),
|
|
schema: const CommandSchema(args: {'file': ArgSpec(required: true, rejectLeadingDash: true)}),
|
|
);
|
|
}
|
|
|
|
IpcResponse _userErr(String id, String message, {String? hint}) => IpcResponse.err(
|
|
id: id,
|
|
error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: message, hint: hint),
|
|
);
|
|
|
|
Future<IpcResponse> _draw(IpcRequest req, MessagePublisher? Function() publisherSource, DrawingRegistry registry, DrawingFileReader readFile) async {
|
|
final file = req.args['file'] as String?;
|
|
if (file == null || file.trim().isEmpty) {
|
|
return _userErr(req.id, 'a drawing-card document is required (e.g. `draw --file card.json`)');
|
|
}
|
|
|
|
final raw = await readFile(file);
|
|
if (raw == null) {
|
|
return IpcResponse.err(
|
|
id: req.id,
|
|
error: IpcError(
|
|
code: IpcExitCode.notFound,
|
|
kind: IpcErrorKind.notFound,
|
|
message: 'no such file: $file',
|
|
hint: 'path is resolved relative to the workspace root',
|
|
),
|
|
);
|
|
}
|
|
|
|
Object? decoded;
|
|
try {
|
|
decoded = jsonDecode(raw);
|
|
} on FormatException catch (e) {
|
|
return _userErr(req.id, 'invalid JSON in $file: ${e.message}');
|
|
}
|
|
|
|
final doc = parseDrawingCardDoc(decoded);
|
|
if (doc == null) return _userErr(req.id, 'a drawing-card document must be a JSON object');
|
|
|
|
final result = await resolveDrawingSvg(doc, registry, readFile: readFile);
|
|
if (result is DrawErr) return _userErr(req.id, result.message);
|
|
final svg = (result as DrawOk).svg;
|
|
|
|
final publish = publisherSource();
|
|
if (publish == null) {
|
|
return IpcResponse.err(
|
|
id: req.id,
|
|
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'no live UI to drive (clide is not running a GUI)'),
|
|
);
|
|
}
|
|
|
|
publish('cli', drawShowChannel, {'svg': svg, if (doc.label != null) 'label': doc.label, if (doc.description != null) 'description': doc.description});
|
|
return IpcResponse.ok(id: req.id, data: {'shown': true, if (doc.template != null) 'template': doc.template});
|
|
}
|