Ali Rathore.

January 2026

In an LLM app, the only manipulation feature worth building is pointing

I built drag-and-drop authoring, watched it break under JSX iteration, and deleted all 520 lines once I noticed the chat panel could already do every move in one sentence.

The drag-and-drop authoring shipped with six Playwright tests, all green, and broke the first time someone who was not me used it. You would ⌘-drag a column to a new spot, the blue halo would light up, and the move would land. On the first column. Every other column on the page reported the same identity to the selection layer, so the halo snapped to the top one and stayed there no matter what you grabbed. The test suite had only ever rendered one column. Six for six against a fixture that hid the bug.

Here is the mechanism, because it is the whole story. Each authored element carries a source uid. Under JSX iteration, one source uid renders into many DOM nodes: a .map over four columns emits four nodes that all claim to be element col. Selection asked the DOM “which uid did I just click,” got col four times, and resolved it to the first match. The fixture had a single column, so the collision never fired, and I shipped 520 lines and five dependencies of drag machinery on the strength of a green checkmark.

So I started rebuilding selection properly. ⌘-hover would paint a halo over the topmost [data-uid-instance] element under the cursor, click would write that instance into state.ui.selection, and render-time uid disambiguation would give every iterated node its own instance id so the four columns stopped lying about who they were. Maybe a day of work. While I was wiring the selected element into the thing that would actually move it, I noticed the chat panel three inches to the left already moved it. “Put the pricing column first.” Done. No halo, no drag, no instance ids.

That is when I deleted the drag-and-drop. Not just the broken halo: the whole feature, 520 LOC and five deps and fifteen commits of it, abandoned. The realization I had not had walking in is that in an app with a chat panel wired to the document, pointing is the only manipulation primitive worth building, and every gesture you stack on top of pointing is a worse retread of a sentence. Drag could relocate a block inside a layout; the sentence could restyle it, reparent it, duplicate it, delete it, or rewrite its copy. I had spent fifteen commits hand-coding the scope “moves inside a layout.” Anything sayable was already covered, free, the moment the chat panel could see the document.

Pointing earns its keep because it is the one thing the sentence is bad at. “Make this wider” needs a referent for “this,” and typing “the third pricing column, the one with the gradient” is worse than clicking it. So selection stays: ⌘-click sets state.ui.selection, the chat prompt reads it, and “make this wider” resolves against the instance under the halo. Everything past establishing the referent went to the model, which had read more component trees than I will write in my life and did not need me to implement “duplicate” by hand.

I will tell you what this did not fix. Selection still needs stable element identity under iteration, which is the exact bug that killed the drag, hiding now in a smaller blast radius. Render-time disambiguation holds only as long as the iteration order is stable between the render you clicked and the render the model edits; reorder the list and the instance id you selected points at a different column. The collision did not go away. I shrank the surface where it can bite from “every gesture” down to “the moment between click and command,” and I have not proven that surface is zero.

And the drag was better at one thing. For a move that is obvious by sight, dragging the pricing column two slots left, you saw where it should go and your hand put it there in one motion. Now you click it, move to the panel, and type a sentence describing a spatial fact your eyes already knew. For that specific class of move, the thing I deleted was faster than the thing I kept.