feat(draw): wire the drawing card into the conversation — end to end (T-318)
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>
This commit is contained in:
@@ -162,6 +162,7 @@ bool _isFoldable(ConversationItem item, FoldLevel level, Map<String, String> too
|
||||
case UserMessage():
|
||||
case AssistantTextMessage():
|
||||
case ImageMessage():
|
||||
case DrawingMessage():
|
||||
return false;
|
||||
// Thinking folds at L2+, first-class at L1.
|
||||
case AssistantThinkingMessage():
|
||||
|
||||
@@ -22,6 +22,8 @@ import 'package:clide/builtin/claude/src/file_tail_follower.dart';
|
||||
import 'package:clide/builtin/claude/src/image_thumbnail.dart';
|
||||
import 'package:clide/builtin/claude/src/prompt_card.dart';
|
||||
import 'package:clide/builtin/claude/src/transcript_reader.dart';
|
||||
import 'package:clide/src/svg/svg_document.dart' show buildSvgDocument;
|
||||
import 'package:clide/widgets/src/draw/drawing_card.dart';
|
||||
import 'package:clide/builtin/claude/src/workflow_run.dart';
|
||||
import 'package:clide/kernel/src/facade.dart';
|
||||
import 'package:clide/kernel/src/keymap/intents.dart';
|
||||
@@ -701,9 +703,21 @@ class _ConversationTurn extends StatelessWidget {
|
||||
AssistantToolUse() => collapseTools ? _toolUseCollapser(context, i) : _toolContentCard(context, i),
|
||||
ToolResultMessage() => _toolResult(context, i),
|
||||
ImageMessage() => _image(context, i),
|
||||
DrawingMessage() => _drawing(context, i),
|
||||
};
|
||||
}
|
||||
|
||||
/// A driven-in drawing card (T-318): the SVG rendered inline by clide's own
|
||||
/// CustomPaint engine (D-103), display-only per D-78, with an optional
|
||||
/// label/description caption.
|
||||
Widget _drawing(BuildContext context, DrawingMessage m) {
|
||||
return ConversationCard(
|
||||
accent: tokens.globalTextMuted,
|
||||
label: ClideSettings.i18n.string(context, 'conversation.label.drawing', namespace: 'builtin.claude', placeholder: 'drawing'),
|
||||
body: DrawingCard(document: buildSvgDocument(m.svg), label: m.label, description: m.description),
|
||||
);
|
||||
}
|
||||
|
||||
/// A driven-in image card (T-249): the image rendered inline, clide-owned
|
||||
/// (Flutter's [Image.file], no third-party viewer), display-only per D-78.
|
||||
/// Bounded so a large image scales down to the pane width and never pushes
|
||||
@@ -1277,5 +1291,7 @@ String _summarizeActivity(BuildContext context, ConversationItem item) {
|
||||
return text;
|
||||
case ImageMessage(:final path):
|
||||
return '${label('conversation.label.image', 'image')} $path';
|
||||
case DrawingMessage(label: final cardLabel):
|
||||
return '${label('conversation.label.drawing', 'drawing')}${cardLabel != null ? ' $cardLabel' : ''}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,10 @@ import 'package:clide/builtin/claude/src/session_index.dart';
|
||||
import 'package:clide/builtin/claude/src/stream_json_session.dart' show kEffortLevels, kFallbackModels, kPermissionModes;
|
||||
import 'package:clide/builtin/claude/src/session_storage.dart';
|
||||
import 'package:clide/builtin/claude/src/ticket_pick_up.dart';
|
||||
import 'package:clide/builtin/claude/src/transcript_reader.dart' show ImageMessage;
|
||||
import 'package:clide/builtin/claude/src/transcript_reader.dart' show DrawingMessage, ImageMessage;
|
||||
import 'package:clide/src/daemon/claude_account_commands.dart' show accountActionChannel;
|
||||
import 'package:clide/src/daemon/project_commands.dart' show projectCreatedChannel;
|
||||
import 'package:clide/src/daemon/draw_commands.dart' show drawShowChannel;
|
||||
import 'package:clide/src/daemon/image_commands.dart' show imageShowChannel;
|
||||
import 'package:clide/builtin/claude/src/team_chat_sidebar.dart' show TeamChatPane;
|
||||
import 'package:clide/builtin/claude/src/team_panel_host.dart';
|
||||
@@ -524,6 +525,11 @@ class ClaudeExtension extends ClideExtension {
|
||||
// user is looking at (the primary lead, else the first visible session).
|
||||
_subs.add(ctx.messages.subscribe(channel: imageShowChannel).listen(_onImageShow));
|
||||
|
||||
// `clide draw --file <doc>` (T-318): the dispatcher lowers the doc to SVG
|
||||
// and publishes a 'draw' message; we inject the drawing card into the
|
||||
// conversation the user is looking at.
|
||||
_subs.add(ctx.messages.subscribe(channel: drawShowChannel).listen(_onDrawShow));
|
||||
|
||||
// A sidebar "pick up" click (T-327) publishes the full ticket; inject it
|
||||
// into the active conversation as a user turn so Claude starts working it.
|
||||
_subs.add(ctx.messages.subscribe(publisher: 'builtin.tickets', channel: 'pick-up').listen(_onTicketPickUp));
|
||||
@@ -639,6 +645,26 @@ class ClaudeExtension extends ClideExtension {
|
||||
);
|
||||
}
|
||||
|
||||
/// Inject a [DrawingMessage] from a published `draw` bus message (T-318).
|
||||
/// Dropped silently if no live conversation is available — the CLI already
|
||||
/// reported success at publish time, and a missing pane is transient.
|
||||
void _onDrawShow(Message m) {
|
||||
final svg = m.data['svg'] as String?;
|
||||
if (svg == null || svg.isEmpty) return;
|
||||
final target = _orchestrator?.byId('primary') ?? _orchestrator?.visibleSessions.firstOrNull;
|
||||
if (target == null) return;
|
||||
target.conversation.inject(
|
||||
DrawingMessage(
|
||||
uuid: 'draw-${DateTime.now().microsecondsSinceEpoch}',
|
||||
timestamp: DateTime.now(),
|
||||
isSidechain: false,
|
||||
svg: svg,
|
||||
label: m.data['label'] as String?,
|
||||
description: m.data['description'] as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deactivate() async {
|
||||
for (final s in _subs) {
|
||||
|
||||
@@ -190,6 +190,24 @@ final class ImageMessage extends ConversationItem {
|
||||
String toString() => 'ImageMessage($path${caption != null ? ', "$caption"' : ''})';
|
||||
}
|
||||
|
||||
/// A locally-injected drawing card (T-318). Not parsed from the transcript —
|
||||
/// driven into the conversation by `clide draw --file <doc>` (D-6 parity) and
|
||||
/// rendered display-only per D-78. [svg] is the SVG substrate the renderer
|
||||
/// paints (already lowered from the doc's template / primitive source);
|
||||
/// [label] / [description] are the optional card caption.
|
||||
final class DrawingMessage extends ConversationItem {
|
||||
const DrawingMessage({required super.uuid, required super.timestamp, required super.isSidechain, required this.svg, this.label, this.description});
|
||||
|
||||
/// The SVG document source the renderer paints.
|
||||
final String svg;
|
||||
|
||||
/// Optional card-level caption (label + supporting description).
|
||||
final String? label, description;
|
||||
|
||||
@override
|
||||
String toString() => 'DrawingMessage(${label ?? '<svg>'})';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -39,6 +39,7 @@ import 'package:clide/builtin/claude/src/account_registry.dart';
|
||||
import 'package:clide/clide.dart' show clideVersion;
|
||||
import 'package:clide/src/daemon/claude_account_commands.dart';
|
||||
import 'package:clide/src/daemon/dispatcher.dart';
|
||||
import 'package:clide/src/daemon/draw_commands.dart';
|
||||
import 'package:clide/src/daemon/editor_commands.dart';
|
||||
import 'package:clide/src/daemon/files_commands.dart';
|
||||
import 'package:clide/src/daemon/git_commands.dart';
|
||||
@@ -361,6 +362,24 @@ Future<void> main() async {
|
||||
return file.existsSync() ? file.absolute.path : null;
|
||||
},
|
||||
);
|
||||
// `clide draw --file <doc>` — drive a drawing card into the Claude
|
||||
// conversation (T-318, drive-half of D-6). Reads the JSON doc relative to
|
||||
// workRoot, lowers it to SVG via the template registry (primitive svg now;
|
||||
// d2/icon/compare/image handlers register as they land), then publishes a
|
||||
// 'draw' message the Claude extension injects.
|
||||
registerDrawCommands(
|
||||
dispatcher,
|
||||
() => kernelMessages?.publish,
|
||||
registry: DrawingRegistry(),
|
||||
readFile: (path) async {
|
||||
final file = File(path.startsWith('/') ? path : '${workRoot.path}/$path');
|
||||
try {
|
||||
return file.existsSync() ? await file.readAsString() : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
// `clide claude account …` — manage per-repo Claude accounts (T-480, epic
|
||||
// T-476). Registry reads/writes go through the user-scope SettingsStore;
|
||||
// side-effects (respawn, login pane, --purge) are published on
|
||||
|
||||
@@ -17,6 +17,8 @@ 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';
|
||||
|
||||
Reference in New Issue
Block a user