mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
LR-BEHAV-TEXTFIELD: ui.clipboard needs a focused text field
With no text field focused, `ui.clipboard` (and MCP `clipboard`) sent the clipboard event to the app's shortcuts: it pasted edit settings on Windows, Linux and in headless runs, and did nothing on macOS, whose menu bar takes ⌘V. It is now refused there; agents run `develop.copy` and `develop.paste` instead. The docs also say that cut and copy write the system clipboard in the desktop app. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
2e23c767b4
commit
7cc9980eca
5 files changed
+25
-3
No files matched your search
@@ -308,7 +308,7 @@ pub fn helper_tools(has_ui: bool) -> Vec<Value> {
|
||||
tool(
|
||||
"clipboard",
|
||||
"Cut, copy or paste",
|
||||
"Cut, copy or paste in the focused text field, as ⌘X / ⌘C / ⌘V do. `paste` pastes `text`, or what the system clipboard holds without it. inspect_ui → `copied` is the text the app last copied.",
|
||||
"Cut, copy or paste in the focused text field (click one first), as ⌘X / ⌘C / ⌘V do; cut and copy write the system clipboard. `paste` pastes `text`, or what the system clipboard holds without it. inspect_ui → `copied` is the text the app last copied.",
|
||||
json!({"action": {"type": "string", "enum": ["cut", "copy", "paste"]}, "text": {"type": "string"}}),
|
||||
&["action"],
|
||||
),
|
||||
|
||||
@@ -278,6 +278,11 @@ pub fn handle(app: &mut LightcraftApp, ctx: &egui::Context, req: &ControlRequest
|
||||
// what the system does for ⌘X / ⌘C / ⌘V in a text field: paste `text`, or (without it)
|
||||
// what the system clipboard holds
|
||||
"ui.clipboard" => {
|
||||
// outside a text field ⌘C / ⌘V are the app's shortcuts (and the macOS menu bar's):
|
||||
// agents run those as commands (`develop.copy`, `develop.paste`)
|
||||
if !ctx.text_edit_focused() {
|
||||
return err("ui.clipboard: no text field has the focus (click one first)");
|
||||
}
|
||||
match s("action") {
|
||||
Some("cut") => app.synthetic.push(egui::Event::Cut),
|
||||
Some("copy") => app.synthetic.push(egui::Event::Copy),
|
||||
|
||||
@@ -662,6 +662,23 @@ mod tests {
|
||||
assert_eq!(r["ok"], false, "an unknown action is refused: {r}");
|
||||
}
|
||||
|
||||
/// `ui.clipboard` acts in a text field: with none focused it is refused, rather than doing
|
||||
/// what ⌘V does there on one platform (paste edit settings) and nothing on another.
|
||||
#[test]
|
||||
fn the_clipboard_method_needs_a_text_field() {
|
||||
let mut h = demo([1200.0, 760.0]);
|
||||
let t = Duration::from_secs(10);
|
||||
h.settle(SETTLE);
|
||||
let undo = h.app.session.undo.len();
|
||||
for action in ["copy", "paste"] {
|
||||
let r = h.request("ui.clipboard", json!({"action": action, "text": "travel"}), t);
|
||||
assert_eq!(r["ok"], false, "{action}: {r}");
|
||||
assert!(r["error"].as_str().is_some_and(|e| e.contains("text field")), "{r}");
|
||||
}
|
||||
h.settle(SETTLE);
|
||||
assert_eq!(h.app.session.undo.len(), undo, "nothing was pasted");
|
||||
}
|
||||
|
||||
/// `ui.clickWidget` right-clicks with `button: "right"`, as `ui.click` does (MCP `click`
|
||||
/// passes it on with a widget): a filmstrip photo's context menu opens.
|
||||
#[test]
|
||||
|
||||
@@ -37,7 +37,7 @@ mode ([mcp.md](mcp.md)) is a thin layer over this channel. Implementation:
|
||||
| `ui.pointer` | `{events: [{kind: down\|drag\|up, x, y}], alt?, shift?, cmd?}` | Gesture in normalized image coordinates (Detail view) |
|
||||
| `ui.key` | `{key, cmd?, shift?, alt?, ctrl?}` | Key press |
|
||||
| `ui.text` | `{text}` | Text input |
|
||||
| `ui.clipboard` | `{action: cut\|copy\|paste, text?}` | Cut, copy or paste in the focused text field, as the system does for ⌘X / ⌘C / ⌘V; `paste` pastes `text`, or the system clipboard without it. `ui.inspect` → `copied` is the text the UI last put on the clipboard |
|
||||
| `ui.clipboard` | `{action: cut\|copy\|paste, text?}` | Cut, copy or paste in the focused text field, as the system does for ⌘X / ⌘C / ⌘V; `paste` pastes `text`, or the system clipboard without it. Refused when no text field has the focus (outside one, ⌘C / ⌘V are the commands `develop.copy` / `develop.paste`). In the desktop app cut and copy write the system clipboard. `ui.inspect` → `copied` is the text the UI last put on the clipboard |
|
||||
| `ui.scroll` | `{dx, dy, cmd?, ctrl?, shift?, alt?}` | Wheel / two-finger scroll at the current pointer; pans over the image, modifier-scroll zooms |
|
||||
| `ui.zoom` | `{factor}` | Pinch zoom at the current pointer (positive scale multiplier; 1 = unchanged). Position it first with `ui.move` or `ui.hoverWidget` |
|
||||
| `ui.set` | partial UI state, e.g. `{"view": "detail"}` | Resulting UI state |
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ Tools taking `id` make that photo active first; without it they act on the activ
|
||||
| `set_ui {state}` | Merge UI state, e.g. `{"view": "detail"}` |
|
||||
| `list_widgets {filter?}` / `click {widget \| x,y, count?}` | Widgets by automation id; real egui clicks |
|
||||
| `press_key {key, cmd?, shift?, alt?}` / `type_text {text}` | Keyboard input (shortcuts) |
|
||||
| `clipboard {action, text?}` | Cut, copy or paste in the focused text field (⌘X / ⌘C / ⌘V); `inspect_ui` → `copied` is what was copied |
|
||||
| `clipboard {action, text?}` | Cut, copy or paste in the focused text field (⌘X / ⌘C / ⌘V; refused when none has the focus; cut and copy write the system clipboard); `inspect_ui` → `copied` is what was copied |
|
||||
| `pointer_gesture {events}` | Gestures in normalized image coordinates (brush strokes, gradients, crop handles) |
|
||||
|
||||
In headless mode these return a tool error explaining how to start the app.
|
||||
|
||||
Reference in new issue
Block a user