add file/image paste to the Claude composer via native clipboard

Flutter's clipboard is text-only and tmux/send-keys carry text only, so
a pasted file or image must reach Claude as an @path reference (per the
T-134 spike). A native clide/clipboard MethodChannel reads the non-text
clipboard: GTK (gtk_clipboard_wait_for_image/uris) on Linux, NSPasteboard
on macOS. The composer overrides PasteTextIntent — Ctrl/Cmd+V resolves a
file path or writes a clipboard image to a cache dir, inserts the @path,
and falls back to plain-text paste otherwise. No new package dependency.

macOS handler is written but unverified on this Linux box — needs a build
on a Mac. Linux path builds and is covered by tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 23:36:18 +02:00
co-authored by Claude Opus 4.7
parent b3fa69c982
commit 8fd251ac48
7 changed files with 314 additions and 0 deletions
+62
View File
@@ -245,6 +245,68 @@ static void clide_app_activate(GApplication* application) {
},
window, nullptr);
// T-138: method channel for native clipboard image/file reads. Flutter's
// built-in clipboard is text-only; the composer turns a pasted file or
// image into a Claude `@path` reference, which needs the non-text
// clipboard targets read here.
g_autoptr(FlStandardMethodCodec) clip_codec = fl_standard_method_codec_new();
FlMethodChannel* clip_channel = fl_method_channel_new(
fl_engine_get_binary_messenger(engine), "clide/clipboard",
FL_METHOD_CODEC(clip_codec));
g_object_set_data(G_OBJECT(window), "clide_clipboard_channel", clip_channel);
fl_method_channel_set_method_call_handler(
clip_channel,
[](FlMethodChannel* channel, FlMethodCall* method_call,
gpointer user_data) {
const gchar* method = fl_method_call_get_name(method_call);
g_autoptr(FlMethodResponse) response = nullptr;
GtkClipboard* clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
if (g_strcmp0(method, "readImage") == 0) {
GdkPixbuf* pixbuf = gtk_clipboard_wait_for_image(clipboard);
if (pixbuf != nullptr) {
gchar* buffer = nullptr;
gsize buffer_size = 0;
g_autoptr(GError) error = nullptr;
if (gdk_pixbuf_save_to_buffer(pixbuf, &buffer, &buffer_size, "png",
&error, nullptr)) {
g_autoptr(FlValue) val =
fl_value_new_uint8_list((const uint8_t*)buffer, buffer_size);
response = FL_METHOD_RESPONSE(fl_method_success_response_new(val));
g_free(buffer);
} else {
response = FL_METHOD_RESPONSE(
fl_method_success_response_new(fl_value_new_null()));
}
g_object_unref(pixbuf);
} else {
response = FL_METHOD_RESPONSE(
fl_method_success_response_new(fl_value_new_null()));
}
} else if (g_strcmp0(method, "readFiles") == 0) {
g_autoptr(FlValue) list = fl_value_new_list();
gchar** uris = gtk_clipboard_wait_for_uris(clipboard);
if (uris != nullptr) {
for (int i = 0; uris[i] != nullptr; i++) {
g_autofree gchar* path =
g_filename_from_uri(uris[i], nullptr, nullptr);
if (path != nullptr) {
fl_value_append_take(list, fl_value_new_string(path));
}
}
g_strfreev(uris);
}
response =
FL_METHOD_RESPONSE(fl_method_success_response_new(list));
} else {
response =
FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
}
fl_method_call_respond(method_call, response, nullptr);
},
window, nullptr);
gtk_widget_grab_focus(GTK_WIDGET(view));
}