Skip to content

Magic pen: Option-drag over a board quotes that region in the chat - #198

Merged
Jing-yilin merged 6 commits into
mainfrom
magic-pen
Oct 8, 2026
Merged

Jing-yilin merged 6 commits into
mainfrom
magic-pen

Conversation

@Jing-yilin

@Jing-yilin Jing-yilin commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #197 (jump-to).

Tap Option (press and release, nothing else between) and the pointer becomes a pen. Drag across a board to mark a region; that part of the board goes to the chat as a quote, and the pointer is itself again. Escape, a second tap, or a click off a board puts the pen down without one. Option-drag still copies a board and Option with another key is still that shortcut, since only a lone tap counts.

  • Picture: the region cut from the server's drawing of the board (/__sp/shoot). The crop is sent as fractions of the board, because the drawing is at the display's pixel ratio.
  • Caption: the one the agent gets, e.g. paint-tasks-flow/04f-note-templates.html at 927,665 130x43: span "取消", span "保存". It names the board, the region in board pixels, and the outermost elements wholly inside it (or else the element that holds it). These are found by laying the board's HTML out offscreen at its own size, since the canvas's frame is sandboxed. Capped at the server's 200 characters.

While the pen is up, a layer over the canvas takes the drag, so tldraw never sees it. The drag is held to the board it started on.

Only on a local canvas (local()), as the other chat controls are.

Tested on an isolated dev server in a browser: the cursor, the dashed region, the tile in the chat, and the crop content (2× crop of the 取消/保存 buttons). tsc -b, oxlint and bun run test (189 passed) are clean.

Also here, at the user's request: a second double-click on the board a double-click zoomed to now zooms out to the whole page (zoomToFit), instead of putting back the view from before the first.

And a Colour submenu in the canvas's right-click menu, beside Background: tldraw's palette, each with its dot as the current theme draws it, for any selected shape that takes a colour and is not locked (an arrow, a line, a note). It goes through setStyleForSelectedShapes, which leaves a locked board in the same selection alone, and is saved to canvas.json like any other edit.

🤖 Generated with Claude Code

Jing-yilin and others added 2 commits September 27, 2026 06:33
…n the chat

Held over the canvas, Option makes the pointer a pen. A drag across a board
marks a region, and that part of the board goes to the chat as a picture,
captioned with the board, where on it, and the elements there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Deploying super-prototyping with  Cloudflare Pages  Cloudflare Pages

Latest commit: db27fa2
Status: ✅  Deploy successful!
Preview URL: https://57325c3e.super-prototyping.pages.dev
Branch Preview URL: https://magic-pen.super-prototyping.pages.dev

View logs

Jing-yilin and others added 4 commits September 27, 2026 06:41
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… not the last view

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	canvas/src/ChatPanel.tsx
#	canvas/src/canvasAttach.tsx
#	canvas/src/canvasChrome.tsx
#	canvas/src/canvasClicks.ts
@Jing-yilin
Jing-yilin changed the base branch from jump-to to main October 8, 2026 00:11
@Jing-yilin
Jing-yilin marked this pull request as ready for review October 8, 2026 00:11
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 6 potential issues.

Devin Review

Comment thread canvas/src/magicPen.tsx
Comment on lines +138 to +139
const pageAt = (e: React.PointerEvent) =>
editor.screenToPage({ x: e.clientX, y: e.clientY });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Board quotes miss the pointer

When the canvas sits beside the chat panel, pageAt converts window coordinates as editor-local screen coordinates. The resulting hit misses the board or crops a region displaced from the drag.

Learn more

The pen receives browser clientX and clientY, which are relative to the window. Tldraw screen coordinates are relative to the editor viewport; CanvasAttachButtons adds the viewport's screen origin to its local pointer coordinates before calling document.elementFromPoint. Here the reverse conversion is missing, so the hit test and drag points do not follow the pointer when the editor has an offset. The offset changes when the chat panel or canvas strip is present.

Example: If the canvas begins at window x=360 and a board starts at its local x=20, a click at window x=380 is converted as though it occurred at local x=380 rather than x=20. The pen can dismiss itself even though the click is on the board.

Recommended fix: Subtract editor.getViewportScreenBounds().x and .y from the event's client coordinates before calling editor.screenToPage, or use the editor's appropriate window-to-page conversion. Verify both board hit testing and crop positions with the chat panel open and closed.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread canvas/src/ChatPanel.tsx
Comment on lines +885 to +890
const part = await createImageBitmap(
whole,
Math.round(crop.x * w),
Math.round(crop.y * h),
Math.round(crop.w * w),
Math.round(crop.h * h),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Small quotes fail on downscaled boards

On a board larger than the 4000-pixel shoot limit, a four-pixel region can round to zero image pixels. createImageBitmap rejects that crop, leaving the quote failed in chat.

Learn more

The pen accepts a region of at least four board pixels at finish. boardShot reduces boards larger than 4000 pixels per side before asking the server to render them. Multiplying the fractional crop width by this smaller PNG width can yield less than half a pixel, which rounds to zero here; image bitmap crop dimensions must be positive.

Example: An 8000-pixel-wide board is rendered at 4000 pixels. A selected four-pixel-wide region becomes two image pixels, but on a 40000-pixel-wide board it becomes 0.4 and rounds to zero, so the quote fails.

Recommended fix: Clamp crop width and height to at least one source pixel, and clamp crop origin and extents to the image bounds. Apply the minimum to the computed image-pixel dimensions, not the board-pixel threshold.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread canvas/src/magicPen.tsx
Comment on lines +227 to +232
onPointerUp={() => {
if (!drag) return;
setDrag(undefined);
setArmed(false);
void finish(drag);
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Cancelled drag leaves pen active

If the browser cancels a pen drag, drag stays set because only onPointerUp clears it. The overlay keeps intercepting canvas gestures until another interaction resets the pen.

Learn more

A browser emits pointercancel without a subsequent pointerup when a gesture is interrupted. The pen has captured that pointer, and the state holding its board and endpoints stays set after cancellation. Since the overlay is rendered while either armed or drag is set, it keeps covering the canvas.

Example: A touch drag begins over a board, then the browser takes over the gesture and cancels the pointer. The region remains drawn and canvas gestures still land on the pen layer.

Recommended fix: Add onPointerCancel that clears drag and disarms the pen without calling finish. Test interrupted gestures as well as normal pointer release.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread canvas/src/magicPen.tsx
Comment on lines +89 to +98
/**
* The magic pen. A tap of Option makes the pointer over the canvas a pen, and a drag across a
* board marks a region of it, which goes to the chat as a quote: that part of the board as a
* picture, captioned with the board, where on it, and the elements there. One region, and the
* pointer is itself again; Escape or another tap puts the pen down without one.
*
* A tap is Option pressed and let go with nothing between, so Option-drag still copies a board
* and Option with a key is still that key. The key is heard in both windows, since the focus may
* be in the chat. A layer over the whole canvas while the pen is up, so tldraw never sees the drag.
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Add release notes for new controls

The pen and Colour menu are user-visible additions, but ## Unreleased has no entry for them. Contributing rules require one in this PR.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread canvas/src/magicPen.tsx
* own at the board's size, since the board's frame on the canvas is sandboxed and cannot be read.
* Scripts stay off there as they do on the canvas, so the layout is the one the person drew over.
*/
async function describeRegion(html: string, w: number, h: number, r: Region) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 One-use region helper adds indirection

describeRegion has one caller, finish. Review rules ask that one-use helpers be inlined; assess whether this needs a separate function.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread canvas/src/magicPen.tsx
* and Option with a key is still that key. The key is heard in both windows, since the focus may
* be in the chat. A layer over the whole canvas while the pen is up, so tldraw never sees the drag.
*/
export function MagicPen() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 New pen paths lack module tests

The pen and crop paths have no adjacent tests. Contributing rules request a test next to each touched viewer module.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@Jing-yilin
Jing-yilin merged commit 3a28cdf into main Oct 8, 2026
7 checks passed
Jing-yilin added a commit that referenced this pull request Oct 8, 2026
Only the attach change had left its line under Unreleased. The rest is
reconstructed from the pull requests merged since v1.7.0: #195, #196, #197
and #198.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant