From 2058f598424310d597699ecb7a11ce3211eb3027 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Tue, 3 Mar 2026 01:10:28 -0500 Subject: [PATCH 01/12] wip --- .../src/components/canvas/DiscourseNodeUtil.tsx | 4 +++- .../DiscourseRelationTool.tsx | 7 +++++-- .../DiscourseRelationUtil.tsx | 3 ++- .../src/components/canvas/DiscourseToolPanel.tsx | 5 ++++- apps/roam/src/components/canvas/Tldraw.tsx | 1 + apps/roam/src/components/canvas/toolLock.ts | 13 +++++++++++++ 6 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 apps/roam/src/components/canvas/toolLock.ts diff --git a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx index 263973bbe..bc6b807a8 100644 --- a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx +++ b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx @@ -45,6 +45,7 @@ import { getSetting } from "~/utils/extensionSettings"; import DiscourseContextOverlay from "~/components/DiscourseContextOverlay"; import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors"; import { render as renderToast } from "roamjs-components/components/Toast"; +import { setCurrentToolToSelectIfUnlocked } from "./toolLock"; const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -110,6 +111,7 @@ export const createNodeShapeTools = ( return class DiscourseNodeTool extends StateNode { static id = n.type; static initial = "idle"; + static isLockable = true; shapeType = n.type; override onEnter = () => { @@ -130,7 +132,7 @@ export const createNodeShapeTools = ( props: { fontFamily: "sans", size: "s" }, }); this.editor.setEditingShape(shapeId); - this.editor.setCurrentTool("select"); + setCurrentToolToSelectIfUnlocked(this.editor); }; }; }); diff --git a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx index 3e809406b..b3198634b 100644 --- a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx +++ b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx @@ -11,6 +11,7 @@ import { } from "./DiscourseRelationUtil"; import { discourseContext } from "~/components/canvas/Tldraw"; import { dispatchToastEvent } from "~/components/canvas/ToastListener"; +import { setCurrentToolToSelectIfUnlocked } from "~/components/canvas/toolLock"; export type AddReferencedNodeType = Record; type ReferenceFormatType = { @@ -28,6 +29,7 @@ export const createAllReferencedNodeTools = ( class ReferencedNodeTool extends StateNode { static override initial = "idle"; static override id = action; + static override isLockable = true; static override children = (): TLStateNodeConstructor[] => [ this.Idle, this.Pointing, @@ -278,7 +280,7 @@ export const createAllReferencedNodeTools = ( }; override onCancel = () => { - this.editor.setCurrentTool("select"); + setCurrentToolToSelectIfUnlocked(this.editor); }; override onKeyUp: TLEventHandlers["onKeyUp"] = (info) => { @@ -315,6 +317,7 @@ export const createAllRelationShapeTools = ( class RelationShapeTool extends StateNode { static override initial = "idle"; static override id = name; + static override isLockable = true; static override children = (): TLStateNodeConstructor[] => [ this.Idle, this.Pointing, @@ -574,7 +577,7 @@ export const createAllRelationShapeTools = ( }; override onCancel = () => { - this.editor.setCurrentTool("select"); + setCurrentToolToSelectIfUnlocked(this.editor); }; override onKeyUp: TLEventHandlers["onKeyUp"] = (info) => { diff --git a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationUtil.tsx b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationUtil.tsx index 5e40a22ad..c912ed57e 100644 --- a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationUtil.tsx +++ b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationUtil.tsx @@ -110,6 +110,7 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU import { AddReferencedNodeType } from "./DiscourseRelationTool"; import { dispatchToastEvent } from "~/components/canvas/ToastListener"; import internalError from "~/utils/internalError"; +import { setCurrentToolToSelectIfUnlocked } from "~/components/canvas/toolLock"; const COLOR_ARRAY = Array.from(DefaultColorStyle.values) .filter((c) => !["red", "green", "grey"].includes(c)) @@ -1086,7 +1087,7 @@ export class BaseDiscourseRelationUtil extends ShapeUtil static override props = arrowShapeProps; cancelAndWarn = (title: string) => { - this.editor.setCurrentTool("select"); + setCurrentToolToSelectIfUnlocked(this.editor); dispatchToastEvent({ id: `tldraw-cancel-and-warn-${title}`, title, diff --git a/apps/roam/src/components/canvas/DiscourseToolPanel.tsx b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx index 5241973bb..c342a8ca2 100644 --- a/apps/roam/src/components/canvas/DiscourseToolPanel.tsx +++ b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx @@ -16,6 +16,7 @@ import { TOOL_ARROW_ICON_SVG, NODE_COLOR_ICON_SVG } from "~/icons"; import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors"; import { DEFAULT_WIDTH, DEFAULT_HEIGHT } from "./Tldraw"; import { DEFAULT_STYLE_PROPS, FONT_SIZES } from "./DiscourseNodeUtil"; +import { lockToolIfNeeded, setCurrentToolToSelectIfUnlocked } from "./toolLock"; export type DiscourseGraphPanelProps = { nodes: DiscourseNode[]; @@ -167,6 +168,7 @@ const DiscourseGraphPanel = ({ const itemIndex = target.dataset.drag_item_index!; const item = panelItems[+itemIndex]; if (item) { + lockToolIfNeeded(editor); editor.setCurrentTool(item.id); } dragState.set({ @@ -190,9 +192,10 @@ const DiscourseGraphPanel = ({ props: { fontFamily: "sans", size: "s" }, }); editor.setEditingShape(shapeId); - editor.setCurrentTool("select"); + setCurrentToolToSelectIfUnlocked(editor); } else { // For relations, just activate the tool + lockToolIfNeeded(editor); editor.setCurrentTool(current.item.id); } diff --git a/apps/roam/src/components/canvas/Tldraw.tsx b/apps/roam/src/components/canvas/Tldraw.tsx index e77e91dcd..d19bfea5e 100644 --- a/apps/roam/src/components/canvas/Tldraw.tsx +++ b/apps/roam/src/components/canvas/Tldraw.tsx @@ -693,6 +693,7 @@ const TldrawCanvasShared = ({ const discourseGraphTool = class DiscourseGraphTool extends StateNode { static override id = "discourse-tool"; static override initial = "idle"; + static override isLockable = true; }; const discourseNodeTools = createNodeShapeTools(allNodes); const discourseRelationTools = createAllRelationShapeTools(allRelationNames); diff --git a/apps/roam/src/components/canvas/toolLock.ts b/apps/roam/src/components/canvas/toolLock.ts new file mode 100644 index 000000000..b6341c68b --- /dev/null +++ b/apps/roam/src/components/canvas/toolLock.ts @@ -0,0 +1,13 @@ +import { Editor } from "tldraw"; + +export const setCurrentToolToSelectIfUnlocked = (editor: Editor): void => { + if (!editor.getInstanceState().isToolLocked) { + editor.setCurrentTool("select"); + } +}; + +export const lockToolIfNeeded = (editor: Editor): void => { + if (!editor.getInstanceState().isToolLocked) { + editor.updateInstanceState({ isToolLocked: true }); + } +}; \ No newline at end of file From e332ecb2ecf03cbf615ee6a2a1059aec3ba0c23e Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Tue, 3 Mar 2026 01:12:45 -0500 Subject: [PATCH 02/12] lint --- apps/roam/src/components/canvas/toolLock.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/roam/src/components/canvas/toolLock.ts b/apps/roam/src/components/canvas/toolLock.ts index b6341c68b..ce6224a07 100644 --- a/apps/roam/src/components/canvas/toolLock.ts +++ b/apps/roam/src/components/canvas/toolLock.ts @@ -10,4 +10,4 @@ export const lockToolIfNeeded = (editor: Editor): void => { if (!editor.getInstanceState().isToolLocked) { editor.updateInstanceState({ isToolLocked: true }); } -}; \ No newline at end of file +}; From d9255cd487a95bb650b621dae0f3aeca22217584 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Thu, 5 Mar 2026 13:28:37 -0500 Subject: [PATCH 03/12] update --- apps/roam/src/components/canvas/Clipboard.tsx | 3 ++- .../components/canvas/DiscourseNodeUtil.tsx | 14 ++++++++++++-- .../components/canvas/DiscourseToolPanel.tsx | 4 +--- apps/roam/src/components/canvas/Tldraw.tsx | 19 +++++++++++++++++++ apps/roam/src/components/canvas/toolLock.ts | 15 ++++++++++++++- .../src/components/canvas/uiOverrides.tsx | 6 ++++++ 6 files changed, 54 insertions(+), 7 deletions(-) diff --git a/apps/roam/src/components/canvas/Clipboard.tsx b/apps/roam/src/components/canvas/Clipboard.tsx index d5254313f..bf3f239d5 100644 --- a/apps/roam/src/components/canvas/Clipboard.tsx +++ b/apps/roam/src/components/canvas/Clipboard.tsx @@ -56,6 +56,7 @@ import findDiscourseNode from "~/utils/findDiscourseNode"; import calcCanvasNodeSizeAndImg from "~/utils/calcCanvasNodeSizeAndImg"; import { useExtensionAPI } from "roamjs-components/components/ExtensionApiContext"; import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors"; +import { setCurrentToolToSelectIfUnlocked } from "./toolLock"; import { MAX_WIDTH } from "./Tldraw"; import getBlockProps from "~/utils/getBlockProps"; import setBlockProps from "~/utils/setBlockProps"; @@ -672,7 +673,7 @@ const ClipboardPageSection = ({ }, }; editor.createShape(shape); - editor.setCurrentTool("select"); + setCurrentToolToSelectIfUnlocked(editor); }, [editor, extensionAPI, showNodesOnCanvas], ); diff --git a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx index bc6b807a8..76dfc52a4 100644 --- a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx +++ b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx @@ -45,7 +45,7 @@ import { getSetting } from "~/utils/extensionSettings"; import DiscourseContextOverlay from "~/components/DiscourseContextOverlay"; import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors"; import { render as renderToast } from "roamjs-components/components/Toast"; -import { setCurrentToolToSelectIfUnlocked } from "./toolLock"; +import { lockTool, unlockTool } from "./toolLock"; const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -132,7 +132,7 @@ export const createNodeShapeTools = ( props: { fontFamily: "sans", size: "s" }, }); this.editor.setEditingShape(shapeId); - setCurrentToolToSelectIfUnlocked(this.editor); + // setCurrentToolToSelectIfUnlocked(this.editor); }; }; }); @@ -521,6 +521,9 @@ export class BaseDiscourseNodeUtil extends BaseBoxShapeUtil this.updateProps(shape.id, shape.type, { h, w, imageUrl }); }; + // Clear tool lock when opening the dialog so we don't end up with Select + locked + unlockTool(this.editor); + renderModifyNodeDialog({ mode: isCreating ? "create" : "edit", nodeType: shape.type, @@ -573,10 +576,17 @@ export class BaseDiscourseNodeUtil extends BaseBoxShapeUtil } } + // Stay on the discourse node tool after the modal so the user can place another node + this.editor.setCurrentTool(shape.type); + lockTool(this.editor); + editor.setEditingShape(null); dialogRenderedRef.current = false; }, onClose: () => { + // Stay on the discourse node tool after closing so the user can place another node + this.editor.setCurrentTool(shape.type); + lockTool(this.editor); editor.setEditingShape(null); dialogRenderedRef.current = false; }, diff --git a/apps/roam/src/components/canvas/DiscourseToolPanel.tsx b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx index c342a8ca2..ac93b213a 100644 --- a/apps/roam/src/components/canvas/DiscourseToolPanel.tsx +++ b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx @@ -16,7 +16,7 @@ import { TOOL_ARROW_ICON_SVG, NODE_COLOR_ICON_SVG } from "~/icons"; import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors"; import { DEFAULT_WIDTH, DEFAULT_HEIGHT } from "./Tldraw"; import { DEFAULT_STYLE_PROPS, FONT_SIZES } from "./DiscourseNodeUtil"; -import { lockToolIfNeeded, setCurrentToolToSelectIfUnlocked } from "./toolLock"; +import { setCurrentToolToSelectIfUnlocked } from "./toolLock"; export type DiscourseGraphPanelProps = { nodes: DiscourseNode[]; @@ -168,7 +168,6 @@ const DiscourseGraphPanel = ({ const itemIndex = target.dataset.drag_item_index!; const item = panelItems[+itemIndex]; if (item) { - lockToolIfNeeded(editor); editor.setCurrentTool(item.id); } dragState.set({ @@ -195,7 +194,6 @@ const DiscourseGraphPanel = ({ setCurrentToolToSelectIfUnlocked(editor); } else { // For relations, just activate the tool - lockToolIfNeeded(editor); editor.setCurrentTool(current.item.id); } diff --git a/apps/roam/src/components/canvas/Tldraw.tsx b/apps/roam/src/components/canvas/Tldraw.tsx index d19bfea5e..82a7b7779 100644 --- a/apps/roam/src/components/canvas/Tldraw.tsx +++ b/apps/roam/src/components/canvas/Tldraw.tsx @@ -28,6 +28,7 @@ import { defaultShapeUtils, defaultTools, useEditor, + useValue, VecModel, createShapeId, TLPointerEventInfo, @@ -90,6 +91,10 @@ import { } from "./DiscourseRelationShape/DiscourseRelationTool"; import ConvertToDialog from "./ConvertToDialog"; import ToastListener, { dispatchToastEvent } from "./ToastListener"; +import { + setCurrentToolToSelectIfUnlocked, + unlockToolWhenSelect, +} from "./toolLock"; import { CanvasDrawerPanel } from "./CanvasDrawer"; import { ClipboardPanel, ClipboardProvider } from "./Clipboard"; import internalError from "~/utils/internalError"; @@ -819,6 +824,7 @@ const TldrawCanvasShared = ({ ...position, }, ]); + setCurrentToolToSelectIfUnlocked(app); lastInsertRef.current = position; e.detail.onRefresh(); } @@ -1121,6 +1127,18 @@ const InsideEditorAndUiContext = ({ const toasts = useToasts(); const msg = useTranslation(); + // When the user selects the select tool, clear tool lock so we only lock while on discourse tools + const currentToolId = useValue( + "currentToolId", + () => editor.getCurrentToolId(), + [editor], + ); + useEffect(() => { + if (currentToolId === "select") { + unlockToolWhenSelect(editor); + } + }, [currentToolId, editor]); + // const isCustomArrowShape = (shape: TLShape) => { // // TODO: find a better way to identify custom arrow shapes // // possibly migrate to shape.type or shape.name @@ -1176,6 +1194,7 @@ const InsideEditorAndUiContext = ({ }, }, ]); + setCurrentToolToSelectIfUnlocked(editor); }, [editor, extensionAPI], ); diff --git a/apps/roam/src/components/canvas/toolLock.ts b/apps/roam/src/components/canvas/toolLock.ts index ce6224a07..dd63ddc60 100644 --- a/apps/roam/src/components/canvas/toolLock.ts +++ b/apps/roam/src/components/canvas/toolLock.ts @@ -6,8 +6,21 @@ export const setCurrentToolToSelectIfUnlocked = (editor: Editor): void => { } }; -export const lockToolIfNeeded = (editor: Editor): void => { +/** Lock the tool when switching to a discourse graph tool so the user stays on that tool until they choose select. */ +export const lockTool = (editor: Editor): void => { if (!editor.getInstanceState().isToolLocked) { editor.updateInstanceState({ isToolLocked: true }); } }; + +/** When the user selects the select tool, clear tool lock so we only lock while on discourse tools. */ +export const unlockToolWhenSelect = (editor: Editor): void => { + if (editor.getCurrentToolId() === "select") { + editor.updateInstanceState({ isToolLocked: false }); + } +}; + +/** Unlock the tool (e.g. when opening a dialog so the user is not stuck with Select + locked). */ +export const unlockTool = (editor: Editor): void => { + editor.updateInstanceState({ isToolLocked: false }); +}; diff --git a/apps/roam/src/components/canvas/uiOverrides.tsx b/apps/roam/src/components/canvas/uiOverrides.tsx index 049c84edf..1ce592e42 100644 --- a/apps/roam/src/components/canvas/uiOverrides.tsx +++ b/apps/roam/src/components/canvas/uiOverrides.tsx @@ -53,6 +53,10 @@ import { getRelationColor } from "./DiscourseRelationShape/DiscourseRelationUtil import DiscourseGraphPanel from "./DiscourseToolPanel"; import { DISCOURSE_TOOL_SHORTCUT_KEY } from "~/data/userSettings"; import { getSetting } from "~/utils/extensionSettings"; +import { + setCurrentToolToSelectIfUnlocked, + unlockTool, +} from "./toolLock"; import { CustomDefaultToolbar } from "./CustomDefaultToolbar"; import { renderModifyNodeDialog } from "~/components/ModifyNodeDialog"; import { CanvasSyncMode } from "./canvasSyncMode"; @@ -156,6 +160,7 @@ export const getOnSelectForShape = ({ initialText: string; imageUrl?: string; }) => { + unlockTool(editor); renderModifyNodeDialog({ mode: "create", nodeType, @@ -193,6 +198,7 @@ export const getOnSelectForShape = ({ y, }, ]); + setCurrentToolToSelectIfUnlocked(editor); }, onClose: () => {}, }); From 0758a414573cd726c7ec9e42e0da070bcd333ea0 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Thu, 5 Mar 2026 14:39:21 -0500 Subject: [PATCH 04/12] address PR comments --- .../src/components/canvas/DiscourseNodeUtil.tsx | 14 +++++++++----- apps/roam/src/components/canvas/uiOverrides.tsx | 5 +---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx index 76dfc52a4..a0f718e2e 100644 --- a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx +++ b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx @@ -576,17 +576,21 @@ export class BaseDiscourseNodeUtil extends BaseBoxShapeUtil } } - // Stay on the discourse node tool after the modal so the user can place another node - this.editor.setCurrentTool(shape.type); - lockTool(this.editor); + // Stay on the discourse node tool after creation so the user can place another node + if (action === "create") { + this.editor.setCurrentTool(shape.type); + lockTool(this.editor); + } editor.setEditingShape(null); dialogRenderedRef.current = false; }, onClose: () => { // Stay on the discourse node tool after closing so the user can place another node - this.editor.setCurrentTool(shape.type); - lockTool(this.editor); + if (isCreating) { + this.editor.setCurrentTool(shape.type); + lockTool(this.editor); + } editor.setEditingShape(null); dialogRenderedRef.current = false; }, diff --git a/apps/roam/src/components/canvas/uiOverrides.tsx b/apps/roam/src/components/canvas/uiOverrides.tsx index 1ce592e42..849e81f13 100644 --- a/apps/roam/src/components/canvas/uiOverrides.tsx +++ b/apps/roam/src/components/canvas/uiOverrides.tsx @@ -53,10 +53,7 @@ import { getRelationColor } from "./DiscourseRelationShape/DiscourseRelationUtil import DiscourseGraphPanel from "./DiscourseToolPanel"; import { DISCOURSE_TOOL_SHORTCUT_KEY } from "~/data/userSettings"; import { getSetting } from "~/utils/extensionSettings"; -import { - setCurrentToolToSelectIfUnlocked, - unlockTool, -} from "./toolLock"; +import { setCurrentToolToSelectIfUnlocked, unlockTool } from "./toolLock"; import { CustomDefaultToolbar } from "./CustomDefaultToolbar"; import { renderModifyNodeDialog } from "~/components/ModifyNodeDialog"; import { CanvasSyncMode } from "./canvasSyncMode"; From d749a27da9e7e5ab38097640acb341a730786f8e Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 9 Mar 2026 18:23:31 -0400 Subject: [PATCH 05/12] new behavior --- .../components/canvas/DiscourseNodeUtil.tsx | 36 ++++++++++++------- apps/roam/src/components/canvas/toolLock.ts | 9 +---- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx index a0f718e2e..8ef7204bf 100644 --- a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx +++ b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx @@ -45,7 +45,7 @@ import { getSetting } from "~/utils/extensionSettings"; import DiscourseContextOverlay from "~/components/DiscourseContextOverlay"; import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors"; import { render as renderToast } from "roamjs-components/components/Toast"; -import { lockTool, unlockTool } from "./toolLock"; +import { unlockTool } from "./toolLock"; const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -132,7 +132,6 @@ export const createNodeShapeTools = ( props: { fontFamily: "sans", size: "s" }, }); this.editor.setEditingShape(shapeId); - // setCurrentToolToSelectIfUnlocked(this.editor); }; }; }); @@ -521,9 +520,23 @@ export class BaseDiscourseNodeUtil extends BaseBoxShapeUtil this.updateProps(shape.id, shape.type, { h, w, imageUrl }); }; + // Capture lock state before unlocking + const wasToolLocked = this.editor.getInstanceState().isToolLocked; + // Clear tool lock when opening the dialog so we don't end up with Select + locked unlockTool(this.editor); + const restoreToolState = () => { + if (wasToolLocked) { + this.editor.updateInstanceState({ isToolLocked: true }); + this.editor.setCurrentTool(shape.type); + } else { + this.editor.setCurrentTool("select"); + } + editor.setEditingShape(null); + dialogRenderedRef.current = false; + }; + renderModifyNodeDialog({ mode: isCreating ? "create" : "edit", nodeType: shape.type, @@ -576,23 +589,20 @@ export class BaseDiscourseNodeUtil extends BaseBoxShapeUtil } } - // Stay on the discourse node tool after creation so the user can place another node if (action === "create") { - this.editor.setCurrentTool(shape.type); - lockTool(this.editor); + restoreToolState(); + } else { + editor.setEditingShape(null); + dialogRenderedRef.current = false; } - - editor.setEditingShape(null); - dialogRenderedRef.current = false; }, onClose: () => { - // Stay on the discourse node tool after closing so the user can place another node if (isCreating) { - this.editor.setCurrentTool(shape.type); - lockTool(this.editor); + restoreToolState(); + } else { + editor.setEditingShape(null); + dialogRenderedRef.current = false; } - editor.setEditingShape(null); - dialogRenderedRef.current = false; }, }); diff --git a/apps/roam/src/components/canvas/toolLock.ts b/apps/roam/src/components/canvas/toolLock.ts index dd63ddc60..b59ec9a85 100644 --- a/apps/roam/src/components/canvas/toolLock.ts +++ b/apps/roam/src/components/canvas/toolLock.ts @@ -1,4 +1,4 @@ -import { Editor } from "tldraw"; +import { Editor, TLShape } from "tldraw"; export const setCurrentToolToSelectIfUnlocked = (editor: Editor): void => { if (!editor.getInstanceState().isToolLocked) { @@ -6,13 +6,6 @@ export const setCurrentToolToSelectIfUnlocked = (editor: Editor): void => { } }; -/** Lock the tool when switching to a discourse graph tool so the user stays on that tool until they choose select. */ -export const lockTool = (editor: Editor): void => { - if (!editor.getInstanceState().isToolLocked) { - editor.updateInstanceState({ isToolLocked: true }); - } -}; - /** When the user selects the select tool, clear tool lock so we only lock while on discourse tools. */ export const unlockToolWhenSelect = (editor: Editor): void => { if (editor.getCurrentToolId() === "select") { From ab9b9ab8fb7bd2d229cd271d1c2156d73847fcca Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Tue, 10 Mar 2026 11:25:20 -0400 Subject: [PATCH 06/12] address PR comment --- apps/roam/src/components/canvas/DiscourseNodeUtil.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx index 8ef7204bf..a241df942 100644 --- a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx +++ b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx @@ -520,11 +520,11 @@ export class BaseDiscourseNodeUtil extends BaseBoxShapeUtil this.updateProps(shape.id, shape.type, { h, w, imageUrl }); }; - // Capture lock state before unlocking + // Only unlock when creating — editing is already on the select tool const wasToolLocked = this.editor.getInstanceState().isToolLocked; - - // Clear tool lock when opening the dialog so we don't end up with Select + locked - unlockTool(this.editor); + if (isCreating) { + unlockTool(this.editor); + } const restoreToolState = () => { if (wasToolLocked) { From 1dee89a0de25e56d98a594a8025d260d394ae7b2 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Tue, 10 Mar 2026 13:58:20 -0400 Subject: [PATCH 07/12] lint --- apps/roam/src/components/canvas/toolLock.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/roam/src/components/canvas/toolLock.ts b/apps/roam/src/components/canvas/toolLock.ts index b59ec9a85..9ac4b1c69 100644 --- a/apps/roam/src/components/canvas/toolLock.ts +++ b/apps/roam/src/components/canvas/toolLock.ts @@ -1,4 +1,4 @@ -import { Editor, TLShape } from "tldraw"; +import { Editor } from "tldraw"; export const setCurrentToolToSelectIfUnlocked = (editor: Editor): void => { if (!editor.getInstanceState().isToolLocked) { From ab71d87bf0ae7a758c000e31be6878933ac84560 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Thu, 19 Mar 2026 13:19:59 -0400 Subject: [PATCH 08/12] address PR comments --- .../src/components/canvas/DiscourseNodeUtil.tsx | 5 ----- apps/roam/src/components/canvas/Tldraw.tsx | 17 +---------------- apps/roam/src/components/canvas/toolLock.ts | 12 ------------ apps/roam/src/components/canvas/uiOverrides.tsx | 4 ++-- 4 files changed, 3 insertions(+), 35 deletions(-) diff --git a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx index a241df942..aa9494adb 100644 --- a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx +++ b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx @@ -45,7 +45,6 @@ import { getSetting } from "~/utils/extensionSettings"; import DiscourseContextOverlay from "~/components/DiscourseContextOverlay"; import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors"; import { render as renderToast } from "roamjs-components/components/Toast"; -import { unlockTool } from "./toolLock"; const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -520,11 +519,7 @@ export class BaseDiscourseNodeUtil extends BaseBoxShapeUtil this.updateProps(shape.id, shape.type, { h, w, imageUrl }); }; - // Only unlock when creating — editing is already on the select tool const wasToolLocked = this.editor.getInstanceState().isToolLocked; - if (isCreating) { - unlockTool(this.editor); - } const restoreToolState = () => { if (wasToolLocked) { diff --git a/apps/roam/src/components/canvas/Tldraw.tsx b/apps/roam/src/components/canvas/Tldraw.tsx index 82a7b7779..be60e4ac0 100644 --- a/apps/roam/src/components/canvas/Tldraw.tsx +++ b/apps/roam/src/components/canvas/Tldraw.tsx @@ -91,10 +91,7 @@ import { } from "./DiscourseRelationShape/DiscourseRelationTool"; import ConvertToDialog from "./ConvertToDialog"; import ToastListener, { dispatchToastEvent } from "./ToastListener"; -import { - setCurrentToolToSelectIfUnlocked, - unlockToolWhenSelect, -} from "./toolLock"; +import { setCurrentToolToSelectIfUnlocked } from "./toolLock"; import { CanvasDrawerPanel } from "./CanvasDrawer"; import { ClipboardPanel, ClipboardProvider } from "./Clipboard"; import internalError from "~/utils/internalError"; @@ -1127,18 +1124,6 @@ const InsideEditorAndUiContext = ({ const toasts = useToasts(); const msg = useTranslation(); - // When the user selects the select tool, clear tool lock so we only lock while on discourse tools - const currentToolId = useValue( - "currentToolId", - () => editor.getCurrentToolId(), - [editor], - ); - useEffect(() => { - if (currentToolId === "select") { - unlockToolWhenSelect(editor); - } - }, [currentToolId, editor]); - // const isCustomArrowShape = (shape: TLShape) => { // // TODO: find a better way to identify custom arrow shapes // // possibly migrate to shape.type or shape.name diff --git a/apps/roam/src/components/canvas/toolLock.ts b/apps/roam/src/components/canvas/toolLock.ts index 9ac4b1c69..80aead9c8 100644 --- a/apps/roam/src/components/canvas/toolLock.ts +++ b/apps/roam/src/components/canvas/toolLock.ts @@ -5,15 +5,3 @@ export const setCurrentToolToSelectIfUnlocked = (editor: Editor): void => { editor.setCurrentTool("select"); } }; - -/** When the user selects the select tool, clear tool lock so we only lock while on discourse tools. */ -export const unlockToolWhenSelect = (editor: Editor): void => { - if (editor.getCurrentToolId() === "select") { - editor.updateInstanceState({ isToolLocked: false }); - } -}; - -/** Unlock the tool (e.g. when opening a dialog so the user is not stuck with Select + locked). */ -export const unlockTool = (editor: Editor): void => { - editor.updateInstanceState({ isToolLocked: false }); -}; diff --git a/apps/roam/src/components/canvas/uiOverrides.tsx b/apps/roam/src/components/canvas/uiOverrides.tsx index 849e81f13..1c7f7609c 100644 --- a/apps/roam/src/components/canvas/uiOverrides.tsx +++ b/apps/roam/src/components/canvas/uiOverrides.tsx @@ -53,7 +53,7 @@ import { getRelationColor } from "./DiscourseRelationShape/DiscourseRelationUtil import DiscourseGraphPanel from "./DiscourseToolPanel"; import { DISCOURSE_TOOL_SHORTCUT_KEY } from "~/data/userSettings"; import { getSetting } from "~/utils/extensionSettings"; -import { setCurrentToolToSelectIfUnlocked, unlockTool } from "./toolLock"; +import { setCurrentToolToSelectIfUnlocked } from "./toolLock"; import { CustomDefaultToolbar } from "./CustomDefaultToolbar"; import { renderModifyNodeDialog } from "~/components/ModifyNodeDialog"; import { CanvasSyncMode } from "./canvasSyncMode"; @@ -157,7 +157,7 @@ export const getOnSelectForShape = ({ initialText: string; imageUrl?: string; }) => { - unlockTool(editor); + editor.updateInstanceState({ isToolLocked: false }); renderModifyNodeDialog({ mode: "create", nodeType, From 66707488108c6dced79750fcd28c049936b6605a Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Thu, 19 Mar 2026 13:25:43 -0400 Subject: [PATCH 09/12] lint --- apps/roam/src/components/canvas/Tldraw.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/roam/src/components/canvas/Tldraw.tsx b/apps/roam/src/components/canvas/Tldraw.tsx index be60e4ac0..1a2744873 100644 --- a/apps/roam/src/components/canvas/Tldraw.tsx +++ b/apps/roam/src/components/canvas/Tldraw.tsx @@ -28,7 +28,6 @@ import { defaultShapeUtils, defaultTools, useEditor, - useValue, VecModel, createShapeId, TLPointerEventInfo, From a2afcd45df8d2f86ed3fdc876657477900b72d8b Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 23 Mar 2026 18:01:37 -0400 Subject: [PATCH 10/12] address PR comments --- apps/roam/src/components/canvas/Clipboard.tsx | 1 - .../DiscourseRelationShape/DiscourseRelationTool.tsx | 8 -------- apps/roam/src/components/canvas/Tldraw.tsx | 2 -- apps/roam/src/components/canvas/uiOverrides.tsx | 2 -- 4 files changed, 13 deletions(-) diff --git a/apps/roam/src/components/canvas/Clipboard.tsx b/apps/roam/src/components/canvas/Clipboard.tsx index bf3f239d5..b8c96e9ba 100644 --- a/apps/roam/src/components/canvas/Clipboard.tsx +++ b/apps/roam/src/components/canvas/Clipboard.tsx @@ -673,7 +673,6 @@ const ClipboardPageSection = ({ }, }; editor.createShape(shape); - setCurrentToolToSelectIfUnlocked(editor); }, [editor, extensionAPI, showNodesOnCanvas], ); diff --git a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx index b3198634b..1b9b3555d 100644 --- a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx +++ b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx @@ -279,10 +279,6 @@ export const createAllReferencedNodeTools = ( this.editor.setCursor({ type: "cross" }); }; - override onCancel = () => { - setCurrentToolToSelectIfUnlocked(this.editor); - }; - override onKeyUp: TLEventHandlers["onKeyUp"] = (info) => { if (info.key === "Enter") { if (this.editor.getInstanceState().isReadonly) return null; @@ -576,10 +572,6 @@ export const createAllRelationShapeTools = ( this.editor.setCursor({ type: "cross", rotation: 0 }); }; - override onCancel = () => { - setCurrentToolToSelectIfUnlocked(this.editor); - }; - override onKeyUp: TLEventHandlers["onKeyUp"] = (info) => { if (info.key === "Enter") { if (this.editor.getInstanceState().isReadonly) return null; diff --git a/apps/roam/src/components/canvas/Tldraw.tsx b/apps/roam/src/components/canvas/Tldraw.tsx index 1a2744873..30fac0bf9 100644 --- a/apps/roam/src/components/canvas/Tldraw.tsx +++ b/apps/roam/src/components/canvas/Tldraw.tsx @@ -820,7 +820,6 @@ const TldrawCanvasShared = ({ ...position, }, ]); - setCurrentToolToSelectIfUnlocked(app); lastInsertRef.current = position; e.detail.onRefresh(); } @@ -1178,7 +1177,6 @@ const InsideEditorAndUiContext = ({ }, }, ]); - setCurrentToolToSelectIfUnlocked(editor); }, [editor, extensionAPI], ); diff --git a/apps/roam/src/components/canvas/uiOverrides.tsx b/apps/roam/src/components/canvas/uiOverrides.tsx index 1c7f7609c..ef2da9e49 100644 --- a/apps/roam/src/components/canvas/uiOverrides.tsx +++ b/apps/roam/src/components/canvas/uiOverrides.tsx @@ -157,7 +157,6 @@ export const getOnSelectForShape = ({ initialText: string; imageUrl?: string; }) => { - editor.updateInstanceState({ isToolLocked: false }); renderModifyNodeDialog({ mode: "create", nodeType, @@ -195,7 +194,6 @@ export const getOnSelectForShape = ({ y, }, ]); - setCurrentToolToSelectIfUnlocked(editor); }, onClose: () => {}, }); From 3309c11f8297cf0bf9f33d204c4bcd59c6f332f6 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 23 Mar 2026 18:02:26 -0400 Subject: [PATCH 11/12] further cleanup --- apps/roam/src/components/canvas/Clipboard.tsx | 1 - .../DiscourseRelationShape/DiscourseRelationTool.tsx | 1 - .../DiscourseRelationShape/DiscourseRelationUtil.tsx | 2 -- apps/roam/src/components/canvas/DiscourseToolPanel.tsx | 2 -- apps/roam/src/components/canvas/Tldraw.tsx | 1 - apps/roam/src/components/canvas/toolLock.ts | 7 ------- apps/roam/src/components/canvas/uiOverrides.tsx | 1 - 7 files changed, 15 deletions(-) delete mode 100644 apps/roam/src/components/canvas/toolLock.ts diff --git a/apps/roam/src/components/canvas/Clipboard.tsx b/apps/roam/src/components/canvas/Clipboard.tsx index b8c96e9ba..f8a226e2d 100644 --- a/apps/roam/src/components/canvas/Clipboard.tsx +++ b/apps/roam/src/components/canvas/Clipboard.tsx @@ -56,7 +56,6 @@ import findDiscourseNode from "~/utils/findDiscourseNode"; import calcCanvasNodeSizeAndImg from "~/utils/calcCanvasNodeSizeAndImg"; import { useExtensionAPI } from "roamjs-components/components/ExtensionApiContext"; import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors"; -import { setCurrentToolToSelectIfUnlocked } from "./toolLock"; import { MAX_WIDTH } from "./Tldraw"; import getBlockProps from "~/utils/getBlockProps"; import setBlockProps from "~/utils/setBlockProps"; diff --git a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx index 1b9b3555d..3581ccccb 100644 --- a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx +++ b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx @@ -11,7 +11,6 @@ import { } from "./DiscourseRelationUtil"; import { discourseContext } from "~/components/canvas/Tldraw"; import { dispatchToastEvent } from "~/components/canvas/ToastListener"; -import { setCurrentToolToSelectIfUnlocked } from "~/components/canvas/toolLock"; export type AddReferencedNodeType = Record; type ReferenceFormatType = { diff --git a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationUtil.tsx b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationUtil.tsx index c912ed57e..e2cf67677 100644 --- a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationUtil.tsx +++ b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationUtil.tsx @@ -110,7 +110,6 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU import { AddReferencedNodeType } from "./DiscourseRelationTool"; import { dispatchToastEvent } from "~/components/canvas/ToastListener"; import internalError from "~/utils/internalError"; -import { setCurrentToolToSelectIfUnlocked } from "~/components/canvas/toolLock"; const COLOR_ARRAY = Array.from(DefaultColorStyle.values) .filter((c) => !["red", "green", "grey"].includes(c)) @@ -1087,7 +1086,6 @@ export class BaseDiscourseRelationUtil extends ShapeUtil static override props = arrowShapeProps; cancelAndWarn = (title: string) => { - setCurrentToolToSelectIfUnlocked(this.editor); dispatchToastEvent({ id: `tldraw-cancel-and-warn-${title}`, title, diff --git a/apps/roam/src/components/canvas/DiscourseToolPanel.tsx b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx index ac93b213a..c30dbc714 100644 --- a/apps/roam/src/components/canvas/DiscourseToolPanel.tsx +++ b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx @@ -16,7 +16,6 @@ import { TOOL_ARROW_ICON_SVG, NODE_COLOR_ICON_SVG } from "~/icons"; import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors"; import { DEFAULT_WIDTH, DEFAULT_HEIGHT } from "./Tldraw"; import { DEFAULT_STYLE_PROPS, FONT_SIZES } from "./DiscourseNodeUtil"; -import { setCurrentToolToSelectIfUnlocked } from "./toolLock"; export type DiscourseGraphPanelProps = { nodes: DiscourseNode[]; @@ -191,7 +190,6 @@ const DiscourseGraphPanel = ({ props: { fontFamily: "sans", size: "s" }, }); editor.setEditingShape(shapeId); - setCurrentToolToSelectIfUnlocked(editor); } else { // For relations, just activate the tool editor.setCurrentTool(current.item.id); diff --git a/apps/roam/src/components/canvas/Tldraw.tsx b/apps/roam/src/components/canvas/Tldraw.tsx index 30fac0bf9..d19bfea5e 100644 --- a/apps/roam/src/components/canvas/Tldraw.tsx +++ b/apps/roam/src/components/canvas/Tldraw.tsx @@ -90,7 +90,6 @@ import { } from "./DiscourseRelationShape/DiscourseRelationTool"; import ConvertToDialog from "./ConvertToDialog"; import ToastListener, { dispatchToastEvent } from "./ToastListener"; -import { setCurrentToolToSelectIfUnlocked } from "./toolLock"; import { CanvasDrawerPanel } from "./CanvasDrawer"; import { ClipboardPanel, ClipboardProvider } from "./Clipboard"; import internalError from "~/utils/internalError"; diff --git a/apps/roam/src/components/canvas/toolLock.ts b/apps/roam/src/components/canvas/toolLock.ts deleted file mode 100644 index 80aead9c8..000000000 --- a/apps/roam/src/components/canvas/toolLock.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Editor } from "tldraw"; - -export const setCurrentToolToSelectIfUnlocked = (editor: Editor): void => { - if (!editor.getInstanceState().isToolLocked) { - editor.setCurrentTool("select"); - } -}; diff --git a/apps/roam/src/components/canvas/uiOverrides.tsx b/apps/roam/src/components/canvas/uiOverrides.tsx index ef2da9e49..049c84edf 100644 --- a/apps/roam/src/components/canvas/uiOverrides.tsx +++ b/apps/roam/src/components/canvas/uiOverrides.tsx @@ -53,7 +53,6 @@ import { getRelationColor } from "./DiscourseRelationShape/DiscourseRelationUtil import DiscourseGraphPanel from "./DiscourseToolPanel"; import { DISCOURSE_TOOL_SHORTCUT_KEY } from "~/data/userSettings"; import { getSetting } from "~/utils/extensionSettings"; -import { setCurrentToolToSelectIfUnlocked } from "./toolLock"; import { CustomDefaultToolbar } from "./CustomDefaultToolbar"; import { renderModifyNodeDialog } from "~/components/ModifyNodeDialog"; import { CanvasSyncMode } from "./canvasSyncMode"; From 4a8c0c907c786799f00bec71b0dbaa15d01a71d8 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 23 Mar 2026 18:09:15 -0400 Subject: [PATCH 12/12] another fix --- apps/roam/src/components/canvas/DiscourseNodeUtil.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx index aa9494adb..f293e8c1c 100644 --- a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx +++ b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx @@ -583,13 +583,6 @@ export class BaseDiscourseNodeUtil extends BaseBoxShapeUtil }); } } - - if (action === "create") { - restoreToolState(); - } else { - editor.setEditingShape(null); - dialogRenderedRef.current = false; - } }, onClose: () => { if (isCreating) {