<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zhuo Jinggang</title>
    <description>The latest articles on DEV Community by Zhuo Jinggang (@zhuojg).</description>
    <link>https://dev.to/zhuojg</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1241915%2F8a2d3d33-ed18-4cbd-8fa2-90f8515a5c77.jpeg</url>
      <title>DEV Community: Zhuo Jinggang</title>
      <link>https://dev.to/zhuojg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zhuojg"/>
    <language>en</language>
    <item>
      <title>Generative Harness: When Agents Start Writing Their Own Execution Structures</title>
      <dc:creator>Zhuo Jinggang</dc:creator>
      <pubDate>Sat, 30 May 2026 10:11:48 +0000</pubDate>
      <link>https://dev.to/zhuojg/generative-harness-when-agents-start-writing-their-own-execution-structures-g6</link>
      <guid>https://dev.to/zhuojg/generative-harness-when-agents-start-writing-their-own-execution-structures-g6</guid>
      <description>&lt;p&gt;Agent systems have mostly been built around a simple assumption: the model may decide what to do next, but the surrounding execution structure is designed by the developer. We give the model tools, define a loop, manage context, constrain permissions, record traces, add retries, and sometimes insert human approval. The model acts, but the system decides the shape of action.&lt;/p&gt;

&lt;p&gt;That assumption is starting to weaken.&lt;/p&gt;

&lt;p&gt;A new pattern is emerging across several parts of the agent stack. Models are no longer only choosing the next tool inside a fixed loop. They are beginning to generate parts of the execution structure itself: code that composes tools, scripts that coordinate workers, workflows that decide how a task should be decomposed, parallelized, checked, and summarized. I would call this pattern &lt;strong&gt;generative harness&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The term is less important than the shift it points to. A harness is the execution structure that turns model capability into task completion. In earlier systems, this structure was fixed or developer-authored. In generative harness, the model begins to produce task-specific orchestration, and the runtime executes it.&lt;/p&gt;

&lt;p&gt;This is powerful, but it changes the central problem. The bottleneck is no longer just whether an agent can execute. It is whether we can verify the orchestration it generated.&lt;/p&gt;

&lt;h2&gt;
  
  
  From actions to orchestration
&lt;/h2&gt;

&lt;p&gt;The earliest tool-calling agents were deliberately simple. The model selected an action, the tool returned an observation, and the next model call decided what to do with it. This loop is limited and often inefficient, but it has one useful property: feedback is local. If a tool call uses the wrong argument, tries to read a missing file, or hits a permission error, the mistake usually appears immediately. The next turn can correct it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LangGraph&lt;/strong&gt; represents a more explicit answer to the same problem. Instead of relying on an implicit loop, the developer can define the orchestration as a graph: state, transitions, checkpoints, human-in-the-loop gates, and durable execution. LangGraph is still an important milestone because it makes agent orchestration visible and controllable rather than hiding it inside a generic loop. But the orchestration is still primarily developer-authored. The graph exists before the task runs, and the model acts within it. LangGraph’s own positioning as a low-level orchestration framework for long-running, stateful agents makes this role clear.&lt;/p&gt;

&lt;p&gt;This is a useful contrast for what is changing now. Developer-authored orchestration is valuable because it is inspectable and governable. It is also expensive to design for every task shape. Some tasks require fan-out search, some require map-reduce, some require independent verification, and some require a multi-stage migration followed by tests and repair. A single fixed loop is too weak, while a pre-authored graph can be too static. The natural question is whether the model can generate the right orchestration for the task at hand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj50t3x0omyreyyrcotkw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj50t3x0omyreyyrcotkw.webp" alt="Orchestration spectrum from tool loop to developer graph to CodeAct to Code Mode to dynamic workflow, with a verification rail below" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CodeAct&lt;/strong&gt; is one answer to that question at the action level. Its core idea is to let the model use executable code as the action, rather than being limited to JSON calls or predefined text commands. In the CodeAct paper, executable Python code becomes a unified action space that can compose tools and dynamically revise actions based on execution results. This matters because code can express control flow that is awkward to express through one tool call at a time. A model can loop over files, call functions, parse results, aggregate data, check conditions, and return structured output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TanStack AI Code Mode&lt;/strong&gt; makes this pattern more concrete for application tools. Instead of forcing the model through a sequence of individual tool calls, it lets the model write and execute TypeScript programs in a sandbox. Those programs can compose tools, branch, loop, parallelize, and return structured results. TanStack describes the motivation directly: one tool call at a time is the bottleneck.&lt;/p&gt;

&lt;p&gt;This is already a form of generative harness, but it is still mostly local. The model generates a small executable structure, and the runtime gives relatively fast feedback. The code may fail to parse, throw a runtime error, time out, call a tool incorrectly, or return a value with the wrong shape. The model can then revise the code. The key point is that the generated structure is small enough that execution feedback is still available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code dynamic workflows&lt;/strong&gt; push the idea to a larger scale. Here the model is not just writing a local program that composes a few tools. Claude can write an orchestration script that coordinates many subagents, intermediate results, and verification passes. The official documentation describes dynamic workflows as scripts Claude writes and can rerun to orchestrate many subagents for codebase audits, large migrations, and cross-checked research; Anthropic’s launch post describes Claude dynamically writing orchestration scripts that run tens to hundreds of parallel subagents in a single session.&lt;/p&gt;

&lt;p&gt;That is a qualitatively different boundary. CodeAct makes code the action. Code Mode makes local tool composition programmable. Dynamic workflows make task-level orchestration generative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verification gap
&lt;/h2&gt;

&lt;p&gt;The attraction of generative harness is obvious. Complex tasks do not all have the same shape. Some require parallel exploration, some require staged refinement, some require independent critique, and some require a workflow that can collect evidence before making a final claim. If every task has to pass through the same fixed loop, the agent wastes context and time. If every workflow has to be manually designed by a developer, the system cannot adapt quickly enough. Letting the model generate task-specific orchestration is a natural next step.&lt;/p&gt;

&lt;p&gt;The problem is that orchestration is harder to verify than action.&lt;/p&gt;

&lt;p&gt;A single tool call has local feedback. Did the call succeed? Was the argument valid? Did the file exist? Did the API return data? A code action has execution feedback. Did it run? Did it typecheck? Did the test pass? Did the program return the expected shape? A developer-authored graph can be reviewed before deployment. Its nodes, transitions, and checkpoints can be inspected.&lt;/p&gt;

&lt;p&gt;A generated workflow needs a different kind of feedback. Was the task decomposed along the right dimensions? Did each worker receive the right context? Were the right subtasks parallelized? Did the verifier check the real risks, or merely check formatting? Were the completion criteria strong enough? Did the workflow miss an entire class of evidence?&lt;/p&gt;

&lt;p&gt;This is what makes generative harness risky. A bad tool call is a local error. A bad code action often fails at runtime. A bad workflow can run successfully and still be wrong.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr0maxswc2088g4e21sih.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr0maxswc2088g4e21sih.webp" alt="Verification gap: generated orchestration on top, execution traces in the middle, and a partial inspection grid below that doesn't fully cover all branches" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The failure mode is subtle because the system may not crash. In fact, it may look more convincing precisely because every stage appears to have run. There are workers, intermediate summaries, a verifier, and a final report. The process looks complete, but the structure of the process may never have been validated.&lt;/p&gt;

&lt;p&gt;This is where the analogy to AI slop becomes useful, although it should not be the main topic. AI slop is often discussed as low-quality generated content, but its deeper pattern is unverified completion. The output has sections, fluent language, conclusions, and sometimes citations, yet lacks real judgment or evidence density. Generative harness can produce the same pattern at the process level. A workflow may have stages, workers, verification, and a polished final answer, while the orchestration itself is flawed.&lt;/p&gt;

&lt;p&gt;In that sense, AI slop is content-level unverified completion. Workflow slop is process-level unverified completion. The same pattern moves from outputs to execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Orchestration can fail silently
&lt;/h2&gt;

&lt;p&gt;The reason this matters is that a generated workflow is an amplifier. When the orchestration is right, it can broaden search, parallelize investigation, run independent critiques, coordinate large migrations, and check results before they reach the user. When the orchestration is wrong, it can amplify the wrong assumption across every worker.&lt;/p&gt;

&lt;p&gt;A task may be decomposed along the wrong dimensions. All subagents may inherit the same flawed framing. A verifier may check whether the final report is well-formed rather than whether the evidence supports the claim. A workflow may treat “all workers returned” as equivalent to “the task is complete.” The final synthesis may hide disagreement between workers because the summarizer was never instructed to preserve conflicts.&lt;/p&gt;

&lt;p&gt;These are not ordinary execution errors. They are orchestration errors. They do not necessarily surface as exceptions, failed tests, or invalid tool responses. They can produce polished, structured, and wrong results.&lt;/p&gt;

&lt;p&gt;This is the central tension of generative harness: the model can generate the structure of work before we have a mature way to verify that structure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmcxr745ws46w2d5wn8q6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmcxr745ws46w2d5wn8q6.webp" alt="Silent orchestration failure: a clean branching workflow surface above, and a shadow layer below where branches are misaligned or disconnected from evidence" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Toward verified generative harness
&lt;/h2&gt;

&lt;p&gt;The answer is not to avoid generative harness. Fixed loops are too rigid, and developer-authored graphs cannot cover every task shape. If agents are going to work on open-ended tasks, they will need some ability to synthesize execution structures on demand.&lt;/p&gt;

&lt;p&gt;The next step is to make those structures governable. A runtime for generative harness cannot only execute tools and launch workers. It also has to inspect, constrain, and validate the generated orchestration. That means workflow preflight, scope and budget checks, context coverage checks, checkpoint reviews, independent verifiers, trace-based final validation, human approval at structural boundaries, and regression tests for reusable workflows.&lt;/p&gt;

&lt;p&gt;The important word here is structural. We already have partial ways to validate individual actions and code execution. What we lack is a strong feedback loop for the shape of work itself. A verified generative harness should be able to answer questions such as: What workflow did the model generate? Why was this decomposition chosen? What context did each worker receive? What evidence supports the final answer? What did the verifier actually verify? Where did the workflow exceed its original scope? Should this workflow be saved, reused, or discarded?&lt;/p&gt;

&lt;p&gt;This turns the platform’s responsibility upward. It is no longer enough for the runtime to safely execute a tool call. The runtime must supervise generated orchestration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz8b8hvxt37h81djdqjp2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz8b8hvxt37h81djdqjp2.webp" alt="Verified generative harness: a branching generated structure above, connected by alignment pins to a verification substrate with checkpoints, trace ledger, scope boundary, and approval gate" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The history of agent systems can be read as a gradual movement of orchestration. In early tool-calling agents, orchestration lived in a fixed loop. In LangGraph, it became explicit and developer-authored. In CodeAct and Code Mode, the model began to generate local executable control flow. In dynamic workflows, the model begins to generate task-level orchestration.&lt;/p&gt;

&lt;p&gt;That is the rise of generative harness: agents are starting to write their own execution structures.&lt;/p&gt;

&lt;p&gt;The opportunity is obvious. A model that can generate the right orchestration for the task can do more than call tools one step at a time. It can adapt the structure of work to the problem.&lt;/p&gt;

&lt;p&gt;The risk is just as important. Orchestration can fail silently. A generated workflow can run, produce intermediate artifacts, invoke verifiers, and return a polished final result, while the task was decomposed incorrectly from the start.&lt;/p&gt;

&lt;p&gt;The next bottleneck is not execution. It is verification of generated orchestration.&lt;/p&gt;

&lt;p&gt;Generative harness may be the next step, but verified generative harness is the part that will make it reliable.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Product Messages Are Not LLM Messages</title>
      <dc:creator>Zhuo Jinggang</dc:creator>
      <pubDate>Fri, 29 May 2026 03:25:06 +0000</pubDate>
      <link>https://dev.to/zhuojg/product-messages-are-not-llm-messages-nld</link>
      <guid>https://dev.to/zhuojg/product-messages-are-not-llm-messages-nld</guid>
      <description>&lt;p&gt;Most agent products start from a simple shape:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A user sends something.&lt;br&gt;&lt;br&gt;
The assistant responds.&lt;br&gt;&lt;br&gt;
The app stores the conversation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That works for a prototype, but it starts to break when the product becomes real. Once an agent supports files, images, tools, approvals, citations, artifacts, resumable streams, long-running tasks, or multiple model providers, the word "conversation" starts hiding two different records.&lt;/p&gt;

&lt;p&gt;The first record is what the product needs to store. The second record is what the LLM actually saw. They are related, but they should not be the same stored object.&lt;/p&gt;

&lt;p&gt;The product needs a durable record of the user experience: what the user sent, what the assistant displayed, which files appeared, which tools ran, which artifacts were created, which approvals happened, and how the conversation should be rendered, resumed, searched, audited, and migrated. The LLM needs a different record: the instructions, selected history, resolved attachments, tool observations, retrieved facts, provider-specific input parts, model output, token usage, and errors involved in a particular execution.&lt;/p&gt;

&lt;p&gt;Those two records serve different systems. They change at different speeds, fail in different ways, and become harder to reason about when they are forced into one message object. The better rule is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Store what the product experienced separately from what the LLM saw.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or shorter:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Product messages are not LLM messages.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Hidden Mistake: One Record for Two Worlds
&lt;/h2&gt;

&lt;p&gt;A chat-like interface makes the mistake feel natural. The user sees a sequence of messages, and the model also appears to consume a sequence of messages, so the first implementation often stores one list and uses it for everything.&lt;/p&gt;

&lt;p&gt;That list becomes the product history, the model input, the streaming state, the audit log, and eventually the place where tool results, file references, citations, approvals, provider-specific fields, and partial execution state all accumulate. At that point, the message list is no longer a clean product model. It is an accidental mixture of product state and execution state.&lt;/p&gt;

&lt;p&gt;This creates problems on both sides. The product record becomes polluted by temporary model concerns: provider-specific content parts, prompt formatting, compressed tool observations, retrieved snippets, model-specific attachment formats, and context-management artifacts. The model input becomes polluted by product concerns: display metadata, timestamps, loading states, preview URLs, frontend editor structures, collapsed panels, UI labels, and other details that exist only so the product can render correctly.&lt;/p&gt;

&lt;p&gt;Neither side gets what it wants. The product wants a stable, durable, user-facing record. The LLM wants a focused execution input. Those should not be the same stored object.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fowhy9oztm1zrbtvp5ct6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fowhy9oztm1zrbtvp5ct6.webp" alt="One message object split into product messages and LLM messages" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Product Messages: What the Product Experienced
&lt;/h2&gt;

&lt;p&gt;Product messages are the canonical record of the conversation from the product's point of view. They should answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What did the user see?&lt;/li&gt;
&lt;li&gt;What did the user send?&lt;/li&gt;
&lt;li&gt;What did the assistant display?&lt;/li&gt;
&lt;li&gt;Which files were attached?&lt;/li&gt;
&lt;li&gt;Which tool runs appeared in the conversation?&lt;/li&gt;
&lt;li&gt;Which artifacts were created?&lt;/li&gt;
&lt;li&gt;Which citations were shown?&lt;/li&gt;
&lt;li&gt;Which approvals were requested or granted?&lt;/li&gt;
&lt;li&gt;Which errors should be visible?&lt;/li&gt;
&lt;li&gt;How should the conversation be resumed later?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This record should be durable, renderable, searchable, paginatable, auditable, and migration-friendly. It should preserve stable IDs, timestamps, authorship, message structure, attachment metadata, artifact references, tool progress, approval state, citations, and user-visible errors.&lt;/p&gt;

&lt;p&gt;It may also include rich product-specific structure. A user input might be a structured editor document, not just plain text; it might include images, PDFs, spreadsheets, selected workspace objects, mentions, or pasted screenshots. An assistant response might include text, generated files, citations, tool activity, expandable logs, intermediate status, or custom UI blocks.&lt;/p&gt;

&lt;p&gt;That richness is not a problem. It is exactly what makes the product useful. The problem starts when this product-shaped record is treated as the LLM's own record. The LLM does not need every display field, timestamp, collapsed panel state, frontend-only metadata, object URL, or internal UI structure.&lt;/p&gt;

&lt;p&gt;Product messages should be allowed to stay product-shaped. They are the source of truth for the user experience, not a prompt format.&lt;/p&gt;

&lt;h2&gt;
  
  
  LLM Messages: What the LLM Saw
&lt;/h2&gt;

&lt;p&gt;LLM messages are different. They are not the canonical conversation; they are the execution record of a specific model run. They should answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which model was called?&lt;/li&gt;
&lt;li&gt;Which provider was used?&lt;/li&gt;
&lt;li&gt;What instructions were included?&lt;/li&gt;
&lt;li&gt;Which parts of the conversation were selected?&lt;/li&gt;
&lt;li&gt;Which files or artifacts were resolved?&lt;/li&gt;
&lt;li&gt;Which tool definitions were available?&lt;/li&gt;
&lt;li&gt;Which tool results were included?&lt;/li&gt;
&lt;li&gt;Which retrieved facts or memories were added?&lt;/li&gt;
&lt;li&gt;What exact input did the model receive?&lt;/li&gt;
&lt;li&gt;What did the model output?&lt;/li&gt;
&lt;li&gt;How many tokens were used?&lt;/li&gt;
&lt;li&gt;What error or finish reason occurred?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This record is valuable, but for a different reason. Product messages help the app render and resume the conversation; LLM messages help engineers debug, replay, evaluate, compare, and audit model behavior. They are an execution trace.&lt;/p&gt;

&lt;p&gt;That trace should usually be immutable. Once a model run has happened, the record of what the LLM saw should not silently change because the product conversation was edited, an attachment preview changed, a context policy was updated, or a provider adapter was refactored.&lt;/p&gt;

&lt;p&gt;This is especially important for debugging. When a user asks, "Why did the agent do that?", the product conversation is not enough. You need to know what the LLM actually saw at that moment: maybe an older summary was included, a tool result was compressed, a file was too large and only an excerpt was sent, one provider received a native image input while another received a text fallback, or an approval state was visible in the product but not included in the model input.&lt;/p&gt;

&lt;p&gt;Without a separate execution record, these differences are hard to inspect. With one, they become visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Conversation Is Not a Prompt Log
&lt;/h2&gt;

&lt;p&gt;A useful mental model is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The conversation is product messages.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;The prompt log is LLM messages.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They overlap, but they are not the same thing. The conversation is organized around the user experience, while the prompt log is organized around model execution. The conversation should be pleasant to render and stable to evolve; the prompt log should be exact enough to replay and inspect.&lt;/p&gt;

&lt;p&gt;The conversation may contain rich product objects, while the prompt log may contain provider-specific input parts. The conversation may be edited, redacted, migrated, or re-rendered, while the prompt log should preserve what happened during a specific run.&lt;/p&gt;

&lt;p&gt;This distinction becomes more important as the product grows. A simple text-only assistant may get away with one message list for a while, but an agent product with files, tools, artifacts, permissions, and multiple models needs a stronger boundary. Do not store the product experience as if it were merely a prompt, and do not store the prompt as if it were the product experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Storage Separation Matters
&lt;/h2&gt;

&lt;p&gt;It is tempting to describe this as a schema preference, but the real reason is operational. The model layer needs stable prepared context. The product layer needs durable user history. Those are different stability contracts.&lt;/p&gt;

&lt;p&gt;A naive chat product treats the stored message list as the model context: take the messages in order, append the new user input, and send the whole thing to the model. That is simple, but it makes product history and prepared context collapse into the same thing. Real agent products cannot live with that assumption for long, because the model input has to be selected, resolved, compacted, cached, adapted, and inspected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fictqobae1mpb57wcug8z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fictqobae1mpb57wcug8z.webp" alt="A product record and message list transformed by context policy into run context" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt Caching Needs Stable Context
&lt;/h3&gt;

&lt;p&gt;Prompt caching makes this separation concrete. Caching works best when the model input has stable, repeatable pieces: system instructions, tool definitions, long-lived summaries, selected history, resolved artifacts, and other context blocks that can be arranged in a predictable order. If every request is assembled directly from a noisy product message list, small product changes can disturb the cacheable prefix for reasons that have nothing to do with model behavior.&lt;/p&gt;

&lt;p&gt;That instability is not a product problem. The product should be free to evolve its conversation model, add display metadata, change attachment previews, update tool event UI, or migrate how artifacts are rendered. It becomes a model problem only when the same stored object is also treated as the context sent to the LLM.&lt;/p&gt;

&lt;p&gt;Context compaction has the same shape. A compacted summary may be exactly what the model should see for a new run, and it may even become part of the stable cacheable prefix. But that summary should not replace the original product conversation, and it should not silently rewrite what an older run saw. It is prepared context, not the conversation itself.&lt;/p&gt;

&lt;p&gt;This does not mean every model call should be frozen forever. Context still changes when the user asks something new, when a relevant file is added, when a tool result matters, or when the compaction policy produces a better summary. The point is that these changes should be intentional model-context changes, not accidental cache misses caused by UI state leaking into the prompt.&lt;/p&gt;

&lt;p&gt;An uploaded image shows the same boundary in a smaller form. The product may keep one stable image card with a file name, thumbnail, permissions, and placement in the conversation. The model context may receive native image input, compressed bytes, a text fallback, or only an artifact pointer depending on the provider and the task. Those are not competing versions of the same field; they are different records with different stability rules.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faus0gqbo6e1188lcanub.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faus0gqbo6e1188lcanub.webp" alt="A product image card branching into run-specific LLM representations" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Old Runs Need Execution Truth
&lt;/h3&gt;

&lt;p&gt;The second reason is historical truth. Imagine looking at an old agent conversation six months later. The product UI has changed, the file preview component is different, the model provider has changed, and the way you prepare context has changed. Maybe the agent now uses a retrieval system or compaction policy that did not exist when the old run happened.&lt;/p&gt;

&lt;p&gt;The conversation should still render as the user experienced it, and the model run should still show what the LLM saw at the time. Those are two different promises. If the same stored message object is responsible for both, old conversations become fragile: a harmless UI migration can make model debugging harder, a better context strategy can accidentally rewrite the meaning of an old run, and a provider migration can leave historical data in a shape that is neither good product state nor accurate model state.&lt;/p&gt;

&lt;p&gt;Separate records avoid that confusion. The product message says, "This is what happened in the conversation." The LLM message says, "This is what the model saw during this execution." The bridge between them can evolve, but the records should remain honest.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi7mmmqb4nzgld1sjkx0s.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi7mmmqb4nzgld1sjkx0s.webp" alt="The product timeline evolves while the model run snapshot stays fixed" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the architectural benefit:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Product messages preserve the user experience. LLM messages preserve the execution reality.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both are important. They just should not be the same record.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Feeling of a Better Boundary
&lt;/h2&gt;

&lt;p&gt;A good boundary should make the system feel calmer. The conversation can be redesigned without wondering whether a CSS-era field will leak into the next prompt. The model input strategy can change without rewriting old user-visible history. A provider adapter can evolve without mutating the conversation record. A bad run can be inspected without reconstructing the prompt from today's version of the UI.&lt;/p&gt;

&lt;p&gt;This is the feeling I want from the architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The product keeps the story the user experienced.&lt;/li&gt;
&lt;li&gt;The model run keeps the facts the LLM saw.&lt;/li&gt;
&lt;li&gt;The two are connected by IDs, not collapsed into one object.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is enough. You do not need to turn the product schema into a prompt schema, and you do not need to turn the prompt log into a UI schema. You just need to let the two records stay honest about what they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Product Messages Are Not LLM Messages
&lt;/h2&gt;

&lt;p&gt;The mistake is not using the wrong message format. The mistake is using one stored object for two different realities. The product has to remember what the user experienced, and the model run has to remember what the LLM saw. Those are not the same thing.&lt;/p&gt;

&lt;p&gt;A conversation is not a prompt log, and a prompt log is not a conversation. The conversation is product messages: durable, renderable, resumable, searchable, auditable, and migration-friendly. The model run is LLM messages: exact, execution-specific, replayable, debuggable, evaluable, and provider-aware.&lt;/p&gt;

&lt;p&gt;They should be connected and they should reference each other, but they should not share the same schema. This separation gives the product room to evolve without losing execution truth, gives the model layer room to improve without corrupting the product history, and makes context management and prompt caching much easier to reason about.&lt;/p&gt;

&lt;p&gt;The final rule is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Store the conversation the product experienced.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Store the execution the LLM saw.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Link them, but do not collapse them.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the difference between product messages and LLM messages.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Killing the "Lollipop": Rebuilding Rotation UX in React Konva</title>
      <dc:creator>Zhuo Jinggang</dc:creator>
      <pubDate>Sun, 14 Dec 2025 15:32:19 +0000</pubDate>
      <link>https://dev.to/zhuojg/killing-the-lollipop-rebuilding-rotation-ux-in-react-konva-1lo7</link>
      <guid>https://dev.to/zhuojg/killing-the-lollipop-rebuilding-rotation-ux-in-react-konva-1lo7</guid>
      <description>&lt;p&gt;When I started building a canvas-heavy product with React Konva, I ran into a UX problem almost immediately.&lt;/p&gt;

&lt;p&gt;Konva is powerful, stable, and well thought-out — but its &lt;strong&gt;interaction model is old&lt;/strong&gt;. In particular, rotation is controlled by the classic &lt;em&gt;lollipop handle&lt;/em&gt;: a small stick protruding from the bounding box.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fufgh2ly2v77zc1r0ni18.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fufgh2ly2v77zc1r0ni18.webp" alt="Konva Default Rotation Handle"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That handle no longer exists in modern design tools.&lt;/p&gt;

&lt;p&gt;In tools like Figma or Sketch, users have strong muscle memory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grab a corner to resize&lt;/li&gt;
&lt;li&gt;hover just outside a corner to rotate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s no visible widget, no extra UI, and no explanation required. Rotation is discovered spatially, not visually.&lt;/p&gt;

&lt;p&gt;I wanted that same interaction model in my canvas app. Instead of tweaking &lt;code&gt;Konva.Transformer&lt;/code&gt;, I decided to &lt;strong&gt;remove it entirely&lt;/strong&gt; and rebuild rotation from first principles.&lt;/p&gt;

&lt;p&gt;This post walks through the architecture and geometry behind that decision — starting from the interaction idea, moving through the type system, and ending with the rotation math itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgsn1gpsjpu8e5plakxzk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgsn1gpsjpu8e5plakxzk.webp" alt="Improved Rotation Handle"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Customize &lt;code&gt;Konva.Transformer&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;At first glance, the transformer seems like the obvious place to start. But the real problem isn’t its visuals — it’s its &lt;strong&gt;encapsulation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Konva.Transformer&lt;/code&gt; bundles together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hit testing&lt;/li&gt;
&lt;li&gt;cursor logic&lt;/li&gt;
&lt;li&gt;rotation math&lt;/li&gt;
&lt;li&gt;interaction state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;inside Konva’s internal implementation. That makes small UX changes — like &lt;em&gt;where&lt;/em&gt; rotation activates or &lt;em&gt;how&lt;/em&gt; the cursor behaves — disproportionately hard.&lt;/p&gt;

&lt;p&gt;What I wanted instead was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hit testing that lives in application code&lt;/li&gt;
&lt;li&gt;rotation math I can reason about&lt;/li&gt;
&lt;li&gt;cursor feedback that actually reflects what’s happening&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So rather than fighting the transformer, I ejected interaction entirely out of it.&lt;/p&gt;

&lt;p&gt;You can find the whole implementation and try it for yourself:&lt;/p&gt;

&lt;p&gt;

&lt;iframe src="https://stackblitz.com/edit/rebuilt-rotation-in-react-konva?embed=1" width="100%" height="500"&gt;
&lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  The Strategy: “Ejecting” Interaction to the Stage
&lt;/h2&gt;

&lt;p&gt;The core idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The stage owns all interaction. Shapes are dumb.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of letting Konva manage rotation internally, we listen to &lt;code&gt;onMouseDown&lt;/code&gt;, &lt;code&gt;onMouseMove&lt;/code&gt;, and &lt;code&gt;onMouseUp&lt;/code&gt; on the &lt;strong&gt;stage&lt;/strong&gt;, track interaction state ourselves, and update shape state explicitly.&lt;/p&gt;

&lt;p&gt;That immediately raises a question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What exactly is the user doing right now?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To answer that cleanly, I model interaction as a discriminated union.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining Interaction as State
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TransformMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rotate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;center&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nl"&gt;corner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nl"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;drag&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This type does a lot of work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;none&lt;/code&gt; means no active interaction&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;drag&lt;/code&gt; means pointer movement translates the shape&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rotate&lt;/code&gt; carries &lt;em&gt;everything rotation needs&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the rectangle’s center&lt;/li&gt;
&lt;li&gt;the corner that initiated rotation&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;base&lt;/code&gt; angle used for cursor orientation&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;useRef&amp;lt;TransformMode&amp;gt;&lt;/code&gt; holds the current mode. It changes &lt;strong&gt;only&lt;/strong&gt; on &lt;code&gt;mouseDown&lt;/code&gt; and &lt;code&gt;mouseUp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mouseMove&lt;/code&gt; never decides &lt;em&gt;what&lt;/em&gt; the interaction is — it only reacts to the current mode.&lt;/p&gt;

&lt;p&gt;That separation is crucial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hit Testing as a Pure Function
&lt;/h2&gt;

&lt;p&gt;With interaction state defined, the next problem is intent detection.&lt;/p&gt;

&lt;p&gt;When the pointer is here — what &lt;em&gt;would&lt;/em&gt; happen if the user clicked?&lt;/p&gt;

&lt;p&gt;I encode that logic in a single function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkHitArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;TransformMode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function does &lt;strong&gt;no side effects&lt;/strong&gt;. Given a pointer position, it returns the interaction mode that &lt;em&gt;should&lt;/em&gt; activate if the user clicks right now.&lt;/p&gt;

&lt;p&gt;It’s used in two places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;onMouseMove&lt;/code&gt;&lt;/strong&gt; → preview intent (cursor changes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;onMouseDown&lt;/code&gt;&lt;/strong&gt; → commit intent (lock interaction state)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That symmetry keeps the mental model clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolving Drag vs Rotate
&lt;/h2&gt;

&lt;p&gt;There’s a subtle UX edge case:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the pointer is &lt;em&gt;inside&lt;/em&gt; the rectangle but near a corner, users expect to &lt;strong&gt;drag&lt;/strong&gt;, not rotate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Modern design tools resolve this spatially. Rotation only activates &lt;em&gt;outside&lt;/em&gt; the shape.&lt;/p&gt;

&lt;p&gt;I replicate that logic by comparing distances to the rectangle’s center:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;corner&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;cornerList&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;distToCorner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pointsDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;corner&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pointerToCenter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pointsDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cornerToCenter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pointsDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;corner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Pointer must be farther from center than the corner itself&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cornerToCenter&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pointerToCenter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distToCorner&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;ROTATE_HOTZONE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rotate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;corner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;corner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rectState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If rotation doesn’t match, I fall back to a point-in-polygon test to detect dragging.&lt;/p&gt;

&lt;p&gt;This mirrors how Figma resolves ambiguous intent — and it feels immediately familiar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotation Is About the Center, Not the Corner
&lt;/h2&gt;

&lt;p&gt;Once rotation starts, the math begins.&lt;/p&gt;

&lt;p&gt;The key insight is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;You cannot rotate a rectangle in place by changing &lt;code&gt;rotation&lt;/code&gt; alone.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Canvas rotation happens around the shape’s local origin (top-left by default). If you don’t adjust position, the rectangle will orbit instead of spinning.&lt;/p&gt;

&lt;p&gt;To get a stable rotation, the rectangle’s &lt;strong&gt;center must remain fixed&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Measure Angular Delta
&lt;/h3&gt;

&lt;p&gt;We measure how much the pointer has moved &lt;em&gt;around the center&lt;/em&gt;, relative to the original corner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;angle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atan2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;angle2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atan2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;corner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;corner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;angle2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;delta&lt;/code&gt; is the rotation change since the interaction began.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Rotate the Top-Left Around the Center
&lt;/h3&gt;

&lt;p&gt;To keep the center fixed, we rotate the rectangle’s &lt;strong&gt;top-left point&lt;/strong&gt; around the center.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Translate the rectangle so the center is at &lt;code&gt;(0, 0)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Rotate the top-left vector&lt;/li&gt;
&lt;li&gt;Translate back
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deltaRad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deltaRad&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deltaRad&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;initialRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;initialRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;cos&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the entire trick.&lt;/p&gt;

&lt;p&gt;Rotation becomes a pure geometric transformation instead of a Konva side effect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Commit the New State
&lt;/h3&gt;

&lt;p&gt;Finally, we apply both position and rotation together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setRectState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;initialRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;initialRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;initialRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rectangle now spins exactly in place — no drifting, no wobble, no surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cursor Feedback Is Part of the Interaction
&lt;/h2&gt;

&lt;p&gt;Most rotation implementations stop once the math works.&lt;/p&gt;

&lt;p&gt;But cursor feedback matters.&lt;/p&gt;

&lt;p&gt;Each corner has an inherent orientation, and the rectangle itself may already be rotated. I combine both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;corner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rectState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That value feeds into a small hook, &lt;code&gt;useRotationCursor&lt;/code&gt;, which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generates a tiny SVG arrow&lt;/li&gt;
&lt;li&gt;rotates it to the correct angle&lt;/li&gt;
&lt;li&gt;encodes it as a data URI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As the user rotates the shape, the cursor rotates &lt;em&gt;with it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is a small detail — but it’s the difference between something that works and something that feels native.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Enables
&lt;/h2&gt;

&lt;p&gt;This demo intentionally leaves out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;resizing&lt;/li&gt;
&lt;li&gt;snapping&lt;/li&gt;
&lt;li&gt;multi-selection&lt;/li&gt;
&lt;li&gt;keyboard modifiers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the architectural shift is already complete.&lt;/p&gt;

&lt;p&gt;Hit testing, interaction state, geometry, and rendering are now &lt;strong&gt;orthogonal concerns&lt;/strong&gt;. Adding resizing doesn’t require rewriting rotation. Snapping doesn’t interfere with dragging.&lt;/p&gt;

&lt;p&gt;And most importantly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The lollipop is gone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What’s left is an interaction model that matches modern design tools — built on simple math, explicit state, and predictable behavior.&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
