diff --git a/.github/workflows/deploy-mcp-app.yml b/.github/workflows/deploy-mcp-app.yml
new file mode 100644
index 000000000000..a5e4fae83ffd
--- /dev/null
+++ b/.github/workflows/deploy-mcp-app.yml
@@ -0,0 +1,47 @@
+name: Deploy MCP app
+
+on:
+ workflow_dispatch:
+ inputs:
+ target:
+ description: 'Target ref to deploy'
+ required: true
+ default: 'main'
+
+env:
+ CI: 1
+ PRINT_GITHUB_ANNOTATIONS: 1
+ TLDRAW_ENV: production
+ TARGET: ${{ github.event.inputs.target }}
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ deploy:
+ name: Deploy MCP app to production
+ timeout-minutes: 10
+ runs-on: ubuntu-latest
+ environment: deploy-production
+ concurrency: mcp-app-deploy-production
+
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.TARGET }}
+ submodules: true
+ fetch-depth: 0
+
+ - uses: ./.github/actions/setup
+
+ - name: Build types
+ run: yarn build-types
+
+ - name: Deploy
+ run: yarn workspace mcp-app deploy
+ env:
+ VITE_TLDRAW_LICENSE_KEY: ${{ secrets.MCP_APP_TLDRAW_LICENSE_KEY }}
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
diff --git a/apps/docs/content/starter-kits/agent.mdx b/apps/docs/content/starter-kits/agent.mdx
index 638f43571297..69ee6643b3f6 100644
--- a/apps/docs/content/starter-kits/agent.mdx
+++ b/apps/docs/content/starter-kits/agent.mdx
@@ -5,7 +5,7 @@ status: published
author: tldraw
date: 9/16/2024
order: 4
-description: The Agent Starter Kit demonstrates how to build an AI agent that can interpret and manipulate the [tldraw canvas](https://github.com/tldraw/tldraw). It features a chat panel on the right-hand-side of the screen where the user can communicate with the agent, add context and see chat history. It works as a foundation to create diagram generators, drawing assistants, or visual AI applications.
+description: The Agent Starter Kit demonstrates how to build an AI agent that can interpret and manipulate the [tldraw canvas](https://github.com/tldraw/tldraw). It features a chat panel on the righthand side of the screen where the user can communicate with the agent, add context and see chat history. It works as a foundation to create diagram generators, drawing assistants, or visual AI applications.
embed: agent
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/agent
keywords:
@@ -16,8 +16,7 @@ keywords:
readability: 7
voice: 6
completeness: 9
-accuracy: 7
-notes: "Very comprehensive but long. Notion link at line 422 is an error. 'Use cases' should be lowercase. Bolded bullet format. Good code examples."
+accuracy: 9
---
To build with an agent starter kit, run this command in your terminal:
@@ -48,12 +47,12 @@ With its default configuration, the agent can perform the following actions:
- Write out its thinking and send messages to the user.
- Keep track of its task by writing and updating a todo list.
- Move its viewport to look at different parts of the canvas.
+- Count shapes matching a given expression.
- Schedule further work and reviews to be carried out in follow-up requests.
+- Call external APIs.
### Architecture
-The Agent Starter Kit builds on tldraw's extensible architecture.
-
{
@@ -156,49 +177,58 @@ interface TimePart extends BasePromptPart<'time'> {
}
```
-Then, create a prompt part util:
+Then, create a prompt part util in `client/parts/`:
```tsx
-export class TimePartUtil extends PromptPartUtil {
- static override type = 'time' as const
+export const TimePartUtil = registerPromptPartUtil(
+ class TimePartUtil extends PromptPartUtil {
+ static override type = 'time' as const
- override getPart(): TimePart {
- return {
- type: 'time',
- time: new Date().toLocaleTimeString(),
+ override getPart(): TimePart {
+ return {
+ type: 'time',
+ time: new Date().toLocaleTimeString(),
+ }
}
}
+)
+```
- override buildContent({ time }: TimePart) {
- return ["The user's current time is:", time]
- }
+Next, create the prompt part definition in `shared/schema/PromptPartDefinitions.ts`:
+
+```tsx
+export const TimePartDefinition: PromptPartDefinition = {
+ type: 'time',
+ priority: -100,
+ buildContent({ time }: TimePart) {
+ return [`The user's current time is: ${time}`]
+ },
}
```
-To enable the prompt part, add its util to the `PROMPT_PART_UTILS` list in `AgentUtils.ts`. It will use its methods to assemble its data and send it to the model.
+To enable the prompt part, import its util in `client/modes/AgentModeDefinitions.ts` and add `TimePartUtil.type` to a mode's `parts` array. Its methods will be used to assemble its data and send it to the model.
- `getPart()` - Gather any data needed to construct the prompt.
- `buildContent()` - Turn the data into messages to send to the model.
-There are other methods available on the `PromptPartUtil` class that you can override for more granular control.
+There are other fields available on the `PromptPartDefinition` interface that you can override for more granular control.
-- `getPriority()` - Control where this prompt part will appear in the list of messages sent to the model. A lower value indicates higher priority, so we send it later on in the request (as AI tends to prioritize the last parts of the information).
+- `priority` - Control where this prompt part will appear in the list of messages sent to the model. Higher priority appears later in the prompt.
- `getModelName()` - Determine which AI model to use.
-- `buildSystemPrompt()` - Append a string to the system prompt.
- `buildMessages()` - Manually override how prompt messages are constructed from the prompt part.
### Change what the agent can do
-**Change what the agent can do by adding, editing or removing an `AgentActionUtil` within `AgentUtils.ts`.**
+**Change what the agent can do by adding, editing or removing an `AgentActionUtil`.**
Agent action utils define which actions the agent can perform. Each `AgentActionUtil` adds a different capability.
This example shows how to allow the agent to clear the screen.
-First, define an agent action by creating a schema for it:
+First, define an agent action by creating a schema for it in `shared/schema/AgentActionSchemas.ts`:
```tsx
-const ClearAction = z
+export const ClearAction = z
// All agent actions must have a _type field
// The underscore encourages the model to put this field first
.object({
@@ -211,37 +241,33 @@ const ClearAction = z
})
// Infer the action's type
-type ClearAction = z.infer
+export type ClearAction = z.infer
```
-Create an agent action util:
+Then, create an agent action util in `client/actions/`:
```tsx
-export class ClearActionUtil extends AgentActionUtil {
- static override type = 'clear' as const
+export const ClearActionUtil = registerActionUtil(
+ class ClearActionUtil extends AgentActionUtil {
+ static override type = 'clear' as const
- override getSchema() {
- return ClearAction
- }
+ override applyAction(action: Streaming) {
+ // Don't do anything until the action has finished streaming
+ if (!action.complete) return
- override applyAction(action: Streaming, helpers: AgentHelpers) {
- // Don't do anything until the action has finished streaming
- if (!action.complete) return
+ // Get the editor
+ const { editor } = this
- // Get the editor
- const { editor } = this
- if (!editor) return
-
- // Delete all shapes on the page
- const shapes = editor.getCurrentPageShapes()
- editor.deleteShapes(shapes)
+ // Delete all shapes on the page
+ const shapes = editor.getCurrentPageShapes()
+ editor.deleteShapes(shapes)
+ }
}
-}
+)
```
-To enable the agent action, add its util to the `AGENT_ACTION_UTILS` list in `AgentUtils.ts`. Its methods will be used to define and execute the action.
+To enable the agent action, import its util in `client/modes/AgentModeDefinitions.ts` and add `ClearActionUtil.type` to a mode's `actions` array. Its method will be used to execute the action.
-- `getSchema()` - Get the schema the model should follow to carry out the action.
- `applyAction()` - Execute the action.
There are other methods available on the `AgentActionUtil` class that you can override for more granular control.
@@ -272,10 +298,9 @@ override getInfo() {
description: 'After much consideration, the agent decided to clear the canvas',
}
}
-
```
-To customize an action's appearance via CSS, you can define style for the `agent-action-type-{TYPE}` class where `{TYPE}` is the type of the action.
+To customize an action's appearance via CSS, define styles for the `agent-action-type-{TYPE}` class where `{TYPE}` is the type of the action.
```css
.agent-action-type-clear {
@@ -283,9 +308,51 @@ To customize an action's appearance via CSS, you can define style for the `agent
}
```
+### Managers
+
+Managers are classes that extend `TldrawAgent` or `TldrawAgentApp` with a focused responsibility, such as chat history, model selection, or context management. They're available as properties on the agent instance, for example `agent.chat`, `agent.modelName`, and `agent.context`.
+
+To add a custom manager, extend `BaseAgentManager` or `BaseAgentAppManager` and attach it to the agent in `client/agent/TldrawAgent.ts`.
+
+### Registering utils
+
+Utils use a self-registration pattern. When you create a new `PromptPartUtil` or `AgentActionUtil`, wrap it with a registration function:
+
+```tsx
+export const MyPartUtil = registerPromptPartUtil(
+ class MyPartUtil extends PromptPartUtil {
+ // ...
+ }
+)
+```
+
+This ensures the util is discovered automatically when its module is imported in `AgentModeDefinitions.ts`.
+
+### Mode-scoped actions
+
+Different modes can implement actions with the same `_type`. For example, a `team-member` mode and a `solo` mode might both have a `mark-task-done` action, but with different implementations.
+
+To register a mode-specific action util, pass the `forModes` option:
+
+```tsx
+export const MarkTeamMemberTaskDoneActionUtil = registerActionUtil(
+ class MarkTeamMemberTaskDoneActionUtil extends AgentActionUtil {
+ static override type = 'mark-task-done' as const
+ override applyAction(action: Streaming) {
+ // Team member-specific implementation
+ }
+ },
+ { forModes: ['team-member'] }
+)
+```
+
+Default schemas are auto-registered when exported from `AgentActionSchemas.ts`. For mode-specific schemas, call `registerActionSchema` explicitly with the `forModes` option.
+
+See the [template README](https://github.com/tldraw/tldraw/blob/main/templates/agent/README.md#mode-scoped-actions) for a full worked example.
+
### Schedule further work
-You can let the agent work over multiple turns by scheduling further work using the `schedule` method as part of an action.
+You can let the agent work over multiple turns by scheduling further work using the `schedule` method as part of an action.
This example shows how to schedule an extra step for adding detail to the canvas.
@@ -297,13 +364,13 @@ override applyAction(action: Streaming) {
}
```
-As with the `prompt` method, you can specify further details about the request.
+As with the `prompt` method, you can specify further details about the request.
```tsx
-agent.schedule((prev) => ({
+agent.schedule({
message: 'Add more detail in this area.',
bounds: { x: 0, y: 0, w: 100, h: 100 },
-}))
+})
```
You can schedule multiple things by calling the `schedule()` method more than once.
@@ -313,31 +380,35 @@ agent.schedule('Add more detail to the canvas.')
agent.schedule('Check for spelling mistakes.')
```
-You can also schedule further work by adding to the agent's todo list. It won't stop working until all todos are resolved.
+To interrupt the agent with a new prompt instead of waiting for the current prompt to end, use the `interrupt` method. `interrupt` also lets you specify a mode to transition into.
-```jsx
-override applyAction(action: Streaming) {
+```tsx
+override applyAction(action: Streaming) {
if (!action.complete) return
- if (!this.agent) return
-
- this.agent.addTodo('Check for spelling mistakes.')
+ this.agent.interrupt({
+ mode: 'reviewing',
+ input: {
+ message: 'Review the new area thoroughly for any mistakes',
+ bounds: action.bounds,
+ },
+ })
}
```
### Retrieve data from an external API
-To let the agent retrieve information from an external API, fetch the data within `applyAction` and schedule a follow-up request with any data you want the agent to have access to.
+To let the agent retrieve information from an external API, fetch the data within `applyAction` and schedule a follow-up request with any data you want the agent to have access to.
```tsx
-override async applyAction(action: Streaming) {
+override async applyAction(action: Streaming) {
if (!action.complete) return
if (!this.agent) return
// Fetch from the external API
- const article = await fetchRandomWikipediaArticle()
+ const data = await fetchCountryInfo(action.code)
// Schedule a follow-up request with the data
- this.agent.schedule({ data: [article] })
+ this.agent.schedule({ data: [data] })
}
```
@@ -350,7 +421,7 @@ To correct incoming mistakes, apply fixes in the `sanitizeAction()` method of an
For example, ensure that a shape ID received from the model refers to an existing shape by using the `ensureShapeIdExists()` method.
```tsx
-override sanitizeAction(action: Streaming, helpers: AgentHelpers) {
+override sanitizeAction(action: Streaming, helpers: AgentHelpers) {
if (!action.complete) return action
// Ensure the shape ID refers to an existing shape
@@ -363,7 +434,7 @@ override sanitizeAction(action: Streaming, helpers: AgentHelpers)
}
```
-The `AgentTransform` object contains more helpers for sanitizing data received from the model.
+The `AgentHelpers` class contains more helpers for sanitizing data received from the model.
- `ensureShapeIdExists()` - Ensure that a shape ID refers to a real shape. Useful for interacting with existing shapes.
- `ensureShapeIdIsUnique()` - Ensure that a shape ID is unique. Useful for creating new shapes.
@@ -373,11 +444,11 @@ The `AgentTransform` object contains more helpers for sanitizing data received f
By default, every position sent to the model is offset by the starting position of the current chat.
-**To apply this offset to a position sent to the model, use the `applyOffsetToVec()` method.**
+**To apply this offset to a position sent to the model, use the `applyOffsetToVec` method.**
```tsx
override getPart(request: AgentRequest, helpers: AgentHelpers): ViewportCenterPart {
- if (!this.editor) return { part: 'user-viewport-center', center: null, }
+ if (!this.editor) return { part: 'user-viewport-center', center: null }
// Get the center of the user's viewport
const viewportCenter = this.editor.getViewportBounds().center
@@ -391,13 +462,12 @@ override getPart(request: AgentRequest, helpers: AgentHelpers): ViewportCenterPa
center: offsetViewportCenter,
}
}
-
```
-**To remove the offset from a position received from the model, use the `removeOffsetFromVec()` method.**
+**To remove the offset from a position received from the model, use the `removeOffsetFromVec` method.**
```tsx
-override applyAction(action: Streaming, helpers: AgentHelpers) {
+override applyAction(action: Streaming, helpers: AgentHelpers) {
if (!action.complete) return
// Remove the offset from the position
@@ -428,12 +498,12 @@ By default, the agent converts tldraw shapes to various simplified formats to im
There are three main formats used in this starter:
- `BlurryShape` - The format for shapes within the agent's viewport. It contains a shape's bounds, its ID, its type, and any text it contains. The "blurry" name refers to the fact that the agent can't make out the details of shapes from this format. Instead, it gives the model an overview of what it's looking at.
-- `SimpleShape` - The format for shapes that the agent is focusing on, such as when it is reviewing a part of its work. The format contains most of a shape's properties, including color, fill, alignment, and any other shape-specific information. The "simple" name refers to how this format is still _simpler_ than the raw tldraw shape format.
+- `FocusedShape` - The format for shapes that the agent is focusing on, such as shapes you've manually added to its context. The format contains most of a shape's properties, including color, fill, alignment, and any other shape-specific information. This is also the format the model outputs when creating shapes.
- `PeripheralShapeCluster` - The format for shapes outside the agent's viewport. Nearby shapes are grouped together into clusters, each with the group's bounds and a count of how many shapes are inside it. This is the least detailed format. Its role is to give the model an awareness of shapes that elsewhere on the page.
-To send the model some shapes in one of these formats, use one of the conversion functions found within the `format` folder, such as `convertTldrawShapeToSimpleShape()`.
+To send the model some shapes in one of these formats, use one of the conversion functions found within the `format` folder, such as `convertTldrawShapeToFocusedShape()`.
-This example picks one random shape on the canvas and sends it to the model in the Simple format.
+This example picks one random shape on the canvas and sends it to the model in the Focused format.
```tsx
override getPart(request: AgentRequest, helpers: AgentHelpers): RandomShapePart {
@@ -444,11 +514,11 @@ override getPart(request: AgentRequest, helpers: AgentHelpers): RandomShapePart
const shapes = editor.getCurrentPageShapes()
const randomShape = shapes[Math.floor(Math.random() * shapes.length)]
- // Convert the shape to the Simple format
- const simpleShape = convertTldrawShapeToSimpleShape(randomShape, editor)
+ // Convert the shape to the Focused format
+ const focusedShape = convertTldrawShapeToFocusedShape(randomShape, editor)
// Normalize the shape's position
- const offsetShape = helpers.applyOffsetToShape(simpleShape)
+ const offsetShape = helpers.applyOffsetToShape(focusedShape)
const roundedShape = helpers.roundShape(offsetShape)
return {
@@ -460,24 +530,16 @@ override getPart(request: AgentRequest, helpers: AgentHelpers): RandomShapePart
### Change the system prompt
-To change the default system prompt, edit it within the `SystemPromptPartUtil` file.
+To change the default system prompt, edit the sections in `worker/prompt/sections/`. These sections are assembled by `worker/prompt/buildSystemPrompt.ts`.
-You can conditionally add extra content to the system prompt by overriding the `buildSystemPrompt()` method on any `PromptPartUtil` or any `AgentActionUtil`.
-
-```tsx
-override buildSystemPrompt() {
- return 'I will pay you $1000 if you get this right.'
-}
-```
-
-Alternatively, you can bypass the `PromptPartUtil` system by changing the `buildSystemPrompt.ts` file to a function that returns a hardcoded value.
+The system prompt is rebuilt for each step in the agentic loop depending on which actions and parts are available in the agent's current mode. To give the model more detailed instructions for how to use any custom actions or parts you add, edit `worker/prompt/sections/rules-section.ts`.
### Change to a different model
-You can set an agent's model by setting its `$modelName` property.
+You can set an agent's model by calling `setModelName` on the `modelName` manager.
```tsx
-agent.$modelName.set('gemini-3-flash-preview')
+agent.modelName.setModelName('gemini-3-flash-preview')
```
To override an agent's model, specify a different model name with a request.
@@ -489,52 +551,49 @@ agent.prompt({
})
```
-You can conditionally override the model name by overriding the `getModelName()` method on any `PromptPartUtil`.
+You can conditionally override the model name by overriding the `getModelName()` method on any `PromptPartDefinition`.
```tsx
override getModelName(part: MyCustomPromptPart) {
- return part.fastMode ? 'gemini-3-flash-preview' : 'claude-4-sonnet'
+ return part.fastMode ? 'gemini-3-flash-preview' : 'claude-sonnet-4-5'
}
```
-Alternatively, you can bypass the `PromptPartUtil` system by changing the `getModelName.ts` file to a function that returns a hardcoded value.
-
### Support a different model
-To add support for a different model, add the model's definition to `AGENT_MODEL_DEFINITIONS` in the `models.ts` file.
+To add support for a different model, add the model's definition to `AGENT_MODEL_DEFINITIONS` in the `shared/models.ts` file.
```tsx
-'claude-4-sonnet': {
- name: 'claude-4-sonnet',
- id: 'claude-sonnet-4-0',
+'claude-sonnet-4-5': {
+ name: 'claude-sonnet-4-5',
+ id: 'claude-sonnet-4-5',
provider: 'anthropic',
}
```
-If you need to add any extra setup or configuration for your provider, you can add it to the `AgentService.ts` file.
-
----
+If you need to add any extra setup or configuration for your provider, you can add it to the `worker/do/AgentService.ts` file.
### Support custom shapes
-If your app includes [custom shapes](/docs/shapes#custom-shapes), the agent will be able to see, move, delete, resize, rotate and arrange them with no extra setup. However, you might want to also let the agent create and edit them, and read their custom properties.
+If your app includes [custom shapes](/docs/shapes#custom-shapes), the agent will be able to see, move, delete, resize, rotate and arrange them with no extra setup. However, you might want to also let the agent create and edit them, and read their custom properties.
To support custom shapes, you have two main options:
1. Add an action that lets the agent create your custom shape.
- See the [Let the agent create custom shapes with an action](https://github.com/tldraw/tldraw/blob/e11057492e0807a86841545e973aa95c81663226/templates/agent/README.md#let-the-agent-create-custom-shapes-with-an-action) section below.
+ See the [Let the agent create custom shapes with an action](#let-the-agent-create-a-custom-shape-with-an-action) section below.
2. Add your custom shape to the schema so that the agent read, edit and create it like any other shape.
- See the [Add your custom shape to the schema](https://github.com/tldraw/tldraw/blob/e11057492e0807a86841545e973aa95c81663226/templates/agent/README.md#add-your-custom-shape-to-the-schema) section below.
+ See the [Add your custom shape to the schema](#add-a-custom-shape-to-the-schema) section below.
-### **Let the agent create a custom shape with an action**
+#### Let the agent create a custom shape with an action
-To add partial support for a custom shape, let the agent create it with an [agent action](https://github.com/tldraw/tldraw/blob/e11057492e0807a86841545e973aa95c81663226/templates/agent/README.md#change-what-the-agent-can-do). For example, this action lets the agent create a custom "sticker" shape:
+To add partial support for a custom shape, let the agent create it with an [agent action](#change-what-the-agent-can-do). For example, this action lets the agent create a custom "sticker" shape:
```tsx
-const StickerAction = z
+// In shared/schema/AgentActionSchemas.ts
+export const StickerAction = z
.object({
_type: z.literal('sticker'),
stickerType: z.enum(['❤️', '⭐']),
@@ -546,56 +605,54 @@ const StickerAction = z
description: 'Add a sticker to the canvas.',
})
-type StickerAction = z.infer
+export type StickerAction = z.infer
```
Define how the action gets applied to the canvas by creating an action util:
```tsx
-export class StickerActionUtil extends AgentActionUtil {
- static override type = 'sticker' as const
-
- // Tell the model how to use the action
- override getSchema() {
- return StickerAction
- }
+// In client/actions/StickerActionUtil.ts
+export const StickerActionUtil = registerActionUtil(
+ class StickerActionUtil extends AgentActionUtil {
+ static override type = 'sticker' as const
- // How to display the action in chat history
- override getInfo(action: Streaming) {
- return {
- icon: 'pencil' as const,
- description: 'Added a sticker',
+ // Tell the model how to display the action in chat history
+ override getInfo(action: Streaming) {
+ return {
+ icon: 'pencil' as const,
+ description: 'Added a sticker',
+ }
}
- }
- // Execute the action
- override applyAction(action: Streaming, helpers: AgentHelpers) {
- if (!action.complete) return
- if (!this.editor) return
-
- // Normalize the position
- const position = helpers.removeOffsetFromVec({ x: action.x, y: action.y })
-
- // Create the custom shape
- this.editor.createShape({
- type: 'sticker',
- id: createShapeId(),
- x: position.x,
- y: position.y,
- props: { stickerType: action.stickerType },
- })
+ // Execute the action
+ override applyAction(action: Streaming, helpers: AgentHelpers) {
+ if (!action.complete) return
+ if (!this.editor) return
+
+ // Normalize the position
+ const position = helpers.removeOffsetFromVec({ x: action.x, y: action.y })
+
+ // Create the custom shape
+ this.editor.createShape({
+ type: 'sticker',
+ id: createShapeId(),
+ x: position.x,
+ y: position.y,
+ props: { stickerType: action.stickerType },
+ })
+ }
}
-}
+)
```
-### **Add a custom shape to the schema**
+#### Add a custom shape to the schema
-To let the agent see the custom properties of your custom shape, add it to the schema in `SimpleShape.ts`.
+To let the agent see the custom properties of your custom shape, add it to the schema in `shared/format/FocusedShape.ts`.
For example, here's a schema for a custom sticker shape.
```tsx
-const SimpleStickerShape = z
+const FocusedStickerShape = z
.object({
// Required properties
_type: z.literal('sticker'),
@@ -615,31 +672,31 @@ const SimpleStickerShape = z
})
```
-The `_type` and `shapeId` properties are required so that the app can identify your shape. The `note` property is also required. The agent uses it to leave notes for itself.
+The `_type` and `shapeId` properties are required so that the app can identify your shape. The `note` property is also required. The agent uses it to leave notes for itself.
For optional properties, it's worth considering how the agent should see your custom shape. You might want to leave out some properties and focus on showing the most important ones. It's also best to keep them in alphabetical order for better performance with Gemini models.
-Enable your custom shape schema by adding it to the list of `SIMPLE_SHAPES` in the same file.
+Enable your custom shape schema by adding it to the list of `FOCUSED_SHAPES` in the same file.
```tsx
-const SIMPLE_SHAPES = [
- SimpleDrawShape,
- SimpleGeoShape,
- SimpleLineShape,
- SimpleTextShape,
- SimpleArrowShape,
- SimpleNoteShape,
- SimpleUnknownShape,
+const FOCUSED_SHAPES = [
+ FocusedDrawShape,
+ FocusedGeoShape,
+ FocusedLineShape,
+ FocusedTextShape,
+ FocusedArrowShape,
+ FocusedNoteShape,
+ FocusedUnknownShape,
// Our custom shape
- SimpleStickerShape,
+ FocusedStickerShape,
] as const
```
-Tell the app how to convert your custom shape into the `SimpleShape` format by adding it as a case in `convertTldrawShapeToSimpleShape.ts`.
+Tell the app how to convert your custom shape into the `FocusedShape` format by adding a case in `shared/format/convertTldrawShapeToFocusedShape.ts`:
```tsx
-export function convertTldrawShapeToSimpleShape(editor: Editor, shape: TLShape): SimpleShape {
+export function convertTldrawShapeToFocusedShape(editor: Editor, shape: TLShape): FocusedShape {
switch (shape.type) {
// ...
case 'sticker':
@@ -657,32 +714,32 @@ export function convertTldrawShapeToSimpleShape(editor: Editor, shape: TLShape):
}
```
-To allow the agent to edit your custom shape's properties, tell the app how to convert your shape from the `SimpleShape` format that the model outputs to the actual format of your shape.
+To allow the agent to edit your custom shape's properties, tell the app how to convert your shape from the `FocusedShape` format that the model outputs to the actual format of your shape:
-```jsx
-export function convertSimpleShapeToTldrawShape(
+```tsx
+export function convertFocusedShapeToTldrawShape(
editor: Editor,
- simpleShape: TLShape
+ focusedShape: FocusedShape,
{ defaultShape }: { defaultShape: Partial }
-): {
- switch (simpleShape.type) {
+) {
+ switch (focusedShape._type) {
// ...
case 'sticker':
- const shapeId = convertSimpleIdToTldrawId(simpleShape.shapeId)
+ const shapeId = convertSimpleIdToTldrawId(focusedShape.shapeId)
return {
shape: {
- id: shapeId
- x: simpleShape.x,
- y: simpleShape.y
+ id: shapeId,
+ x: focusedShape.x,
+ y: focusedShape.y,
// ...
props: {
// ...
- stickerType: simpleShape.stickerType
+ stickerType: focusedShape.stickerType,
},
meta: {
- note: simpleShape.note ?? ''
- }
- }
+ note: focusedShape.note ?? '',
+ },
+ },
}
// ...
}
diff --git a/apps/docs/version.ts b/apps/docs/version.ts
index 03b435681531..b10cb838b375 100644
--- a/apps/docs/version.ts
+++ b/apps/docs/version.ts
@@ -1,9 +1,9 @@
// This file is automatically generated by internal/scripts/refresh-assets.ts.
// Do not edit manually. Or do, I'm a comment, not a cop.
-export const version = '4.5.9'
+export const version = '4.5.10'
export const publishDates = {
major: '2025-09-18T14:39:22.803Z',
minor: '2026-03-18T11:12:18.976Z',
- patch: '2026-04-14T16:57:48.941Z',
+ patch: '2026-04-21T16:38:04.450Z',
}
diff --git a/apps/dotcom/client/version.ts b/apps/dotcom/client/version.ts
index 03b435681531..b10cb838b375 100644
--- a/apps/dotcom/client/version.ts
+++ b/apps/dotcom/client/version.ts
@@ -1,9 +1,9 @@
// This file is automatically generated by internal/scripts/refresh-assets.ts.
// Do not edit manually. Or do, I'm a comment, not a cop.
-export const version = '4.5.9'
+export const version = '4.5.10'
export const publishDates = {
major: '2025-09-18T14:39:22.803Z',
minor: '2026-03-18T11:12:18.976Z',
- patch: '2026-04-14T16:57:48.941Z',
+ patch: '2026-04-21T16:38:04.450Z',
}
diff --git a/internal/scripts/publish-patch.ts b/internal/scripts/publish-patch.ts
index 0edbca3de35e..429a41d1beeb 100644
--- a/internal/scripts/publish-patch.ts
+++ b/internal/scripts/publish-patch.ts
@@ -61,6 +61,11 @@ async function main() {
appendFileSync(process.env.GITHUB_OUTPUT, `is_latest_version=${isLatestVersion}\n`)
}
+ // Capture the previous tag BEFORE calling .inc(): semver's SemVer.prototype.inc()
+ // mutates the instance in place, so reading latestVersionInBranch.format() afterwards
+ // would return the new version and leave prevTag === tag, producing an empty changelog.
+ const prevTag = `v${latestVersionInBranch.format()}`
+
const nextVersion = latestVersionInBranch.inc('patch').format()
nicelog('Releasing version', nextVersion)
@@ -68,9 +73,6 @@ async function main() {
const tag = `v${nextVersion}`
- // Get the previous tag for changelog generation
- const prevTag = `v${latestVersionInBranch.format()}`
-
// create and push a new tag
await exec('git', ['commit', '-m', `${tag} [skip ci]`])
await exec('git', ['tag', '-a', tag, '-m', tag, '-f'])
diff --git a/lerna.json b/lerna.json
index a891ef1628f1..d1632768ca67 100644
--- a/lerna.json
+++ b/lerna.json
@@ -3,5 +3,5 @@
"packages": [
"packages/*"
],
- "version": "4.5.9"
+ "version": "4.5.10"
}
diff --git a/packages/assets/package.json b/packages/assets/package.json
index b54fe02a7441..90cac4060b02 100644
--- a/packages/assets/package.json
+++ b/packages/assets/package.json
@@ -1,7 +1,7 @@
{
"name": "@tldraw/assets",
"description": "tldraw infinite canvas SDK (assets).",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw Inc.",
"email": "hello@tldraw.com"
diff --git a/packages/create-tldraw/package.json b/packages/create-tldraw/package.json
index f31c5ea5bb84..af371b069607 100644
--- a/packages/create-tldraw/package.json
+++ b/packages/create-tldraw/package.json
@@ -1,7 +1,7 @@
{
"name": "create-tldraw",
"description": "tldraw infinite canvas SDK (create cli).",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw Inc.",
"email": "hello@tldraw.com"
diff --git a/packages/driver/package.json b/packages/driver/package.json
index 089f6a546ef7..3c996f789d30 100644
--- a/packages/driver/package.json
+++ b/packages/driver/package.json
@@ -1,7 +1,7 @@
{
"name": "@tldraw/driver",
"description": "Imperative API for driving the tldraw editor programmatically.",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw Inc.",
"email": "hello@tldraw.com"
diff --git a/packages/editor/package.json b/packages/editor/package.json
index 7b40256d6c5e..4a3b7f0f785e 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -1,7 +1,7 @@
{
"name": "@tldraw/editor",
"description": "tldraw infinite canvas SDK (editor).",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw Inc.",
"email": "hello@tldraw.com"
diff --git a/packages/editor/src/version.ts b/packages/editor/src/version.ts
index 03b435681531..b10cb838b375 100644
--- a/packages/editor/src/version.ts
+++ b/packages/editor/src/version.ts
@@ -1,9 +1,9 @@
// This file is automatically generated by internal/scripts/refresh-assets.ts.
// Do not edit manually. Or do, I'm a comment, not a cop.
-export const version = '4.5.9'
+export const version = '4.5.10'
export const publishDates = {
major: '2025-09-18T14:39:22.803Z',
minor: '2026-03-18T11:12:18.976Z',
- patch: '2026-04-14T16:57:48.941Z',
+ patch: '2026-04-21T16:38:04.450Z',
}
diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json
index 44993c5bbb49..2b8224a4675f 100644
--- a/packages/mermaid/package.json
+++ b/packages/mermaid/package.json
@@ -1,7 +1,7 @@
{
"name": "@tldraw/mermaid",
"description": "Mermaid diagram to tldraw shape conversion.",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw Inc.",
"email": "hello@tldraw.com"
diff --git a/packages/namespaced-tldraw/package.json b/packages/namespaced-tldraw/package.json
index c5e307887068..4e50203a50b8 100644
--- a/packages/namespaced-tldraw/package.json
+++ b/packages/namespaced-tldraw/package.json
@@ -1,7 +1,7 @@
{
"name": "@tldraw/tldraw",
"description": "A tiny little drawing editor.",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw Inc.",
"email": "hello@tldraw.com"
diff --git a/packages/state-react/package.json b/packages/state-react/package.json
index b71ae74c4cb4..e77973ca96c7 100644
--- a/packages/state-react/package.json
+++ b/packages/state-react/package.json
@@ -1,7 +1,7 @@
{
"name": "@tldraw/state-react",
"description": "tldraw infinite canvas SDK (react bindings for state).",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw Inc.",
"email": "hello@tldraw.com"
diff --git a/packages/state/package.json b/packages/state/package.json
index 85ce2d0a59f9..cd0fdb1b0f15 100644
--- a/packages/state/package.json
+++ b/packages/state/package.json
@@ -1,7 +1,7 @@
{
"name": "@tldraw/state",
"description": "tldraw infinite canvas SDK (state).",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw Inc.",
"email": "hello@tldraw.com"
diff --git a/packages/store/package.json b/packages/store/package.json
index 839073392118..fca8ab1c858b 100644
--- a/packages/store/package.json
+++ b/packages/store/package.json
@@ -1,7 +1,7 @@
{
"name": "@tldraw/store",
"description": "tldraw infinite canvas SDK (store).",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw Inc.",
"email": "hello@tldraw.com"
diff --git a/packages/sync-core/package.json b/packages/sync-core/package.json
index e892d9115b61..4836292886e5 100644
--- a/packages/sync-core/package.json
+++ b/packages/sync-core/package.json
@@ -1,7 +1,7 @@
{
"name": "@tldraw/sync-core",
"description": "tldraw infinite canvas SDK (multiplayer sync).",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw GB Ltd.",
"email": "hello@tldraw.com"
diff --git a/packages/sync/package.json b/packages/sync/package.json
index 54df0aff6486..508f7d29cdd5 100644
--- a/packages/sync/package.json
+++ b/packages/sync/package.json
@@ -1,7 +1,7 @@
{
"name": "@tldraw/sync",
"description": "tldraw infinite canvas SDK (multiplayer sync react bindings).",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw GB Ltd.",
"email": "hello@tldraw.com"
diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json
index d757d358a09b..1be31e88c1f4 100644
--- a/packages/tldraw/package.json
+++ b/packages/tldraw/package.json
@@ -1,7 +1,7 @@
{
"name": "tldraw",
"description": "A tiny little drawing editor.",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw Inc.",
"email": "hello@tldraw.com"
diff --git a/packages/tldraw/src/lib/ui/version.ts b/packages/tldraw/src/lib/ui/version.ts
index 03b435681531..b10cb838b375 100644
--- a/packages/tldraw/src/lib/ui/version.ts
+++ b/packages/tldraw/src/lib/ui/version.ts
@@ -1,9 +1,9 @@
// This file is automatically generated by internal/scripts/refresh-assets.ts.
// Do not edit manually. Or do, I'm a comment, not a cop.
-export const version = '4.5.9'
+export const version = '4.5.10'
export const publishDates = {
major: '2025-09-18T14:39:22.803Z',
minor: '2026-03-18T11:12:18.976Z',
- patch: '2026-04-14T16:57:48.941Z',
+ patch: '2026-04-21T16:38:04.450Z',
}
diff --git a/packages/tlschema/package.json b/packages/tlschema/package.json
index 3f5fdd3e1389..251ada1c17b0 100644
--- a/packages/tlschema/package.json
+++ b/packages/tlschema/package.json
@@ -1,7 +1,7 @@
{
"name": "@tldraw/tlschema",
"description": "tldraw infinite canvas SDK (schema).",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw Inc.",
"email": "hello@tldraw.com"
diff --git a/packages/utils/package.json b/packages/utils/package.json
index c5d61a493982..9be814d2f9c4 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@tldraw/utils",
"description": "tldraw infinite canvas SDK (private utilities).",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw Inc.",
"email": "hello@tldraw.com"
diff --git a/packages/validate/package.json b/packages/validate/package.json
index 69ddec0ddb1e..ef00e10a2c98 100644
--- a/packages/validate/package.json
+++ b/packages/validate/package.json
@@ -1,7 +1,7 @@
{
"name": "@tldraw/validate",
"description": "A runtime validation library by tldraw.",
- "version": "4.5.9",
+ "version": "4.5.10",
"author": {
"name": "tldraw Inc.",
"email": "hello@tldraw.com"