From b89417403b36e6bd7ad4f0a639b08b8d036a5eaf Mon Sep 17 00:00:00 2001 From: Adam Leith Date: Tue, 21 Apr 2026 05:22:08 +0100 Subject: [PATCH 1/4] chore: prompt inputs are now quill chips, went for bare DOM instead of react portals for performance --- .../message-editor/tiptap/MentionChipNode.ts | 120 ++++++++++++++++- .../message-editor/tiptap/MentionChipView.tsx | 121 ------------------ 2 files changed, 115 insertions(+), 126 deletions(-) delete mode 100644 apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx diff --git a/apps/code/src/renderer/features/message-editor/tiptap/MentionChipNode.ts b/apps/code/src/renderer/features/message-editor/tiptap/MentionChipNode.ts index dcdee19a4..dd6eaf24a 100644 --- a/apps/code/src/renderer/features/message-editor/tiptap/MentionChipNode.ts +++ b/apps/code/src/renderer/features/message-editor/tiptap/MentionChipNode.ts @@ -1,6 +1,7 @@ +import { useSettingsStore as useFeatureSettingsStore } from "@features/settings/stores/settingsStore"; +import { buttonVariants, cn } from "@posthog/quill"; +import { trpcClient } from "@renderer/trpc/client"; import { mergeAttributes, Node } from "@tiptap/core"; -import { ReactNodeViewRenderer } from "@tiptap/react"; -import { MentionChipView } from "./MentionChipView"; export type ChipType = | "file" @@ -26,6 +27,28 @@ declare module "@tiptap/core" { } } +// Compute Quill Chip classes once at module load (size="xs", variant="outline") +const chipClasses = cn( + buttonVariants({ size: "xs", variant: "outline" }), + "gap-1 rounded-sm has-data-[slot=chip-close]:pe-0 bg-background max-w-full", + "relative -top-[2px] cursor-default active:translate-y-0", +); + +const closeClasses = cn( + buttonVariants({ size: "xs", variant: "outline" }), + "size-5 p-0 opacity-50 hover:opacity-100", +); + +const selectedRingClasses = ["border-ring/50", "ring-[3px]", "ring-ring/50"]; + +// Lucide XIcon SVG (matches Quill ChipClose default) +const xIconSvg = + ''; + +// GitHub logo SVG +const githubIconSvg = + ''; + export const MentionChipNode = Node.create({ name: "mentionChip", group: "inline", @@ -66,9 +89,96 @@ export const MentionChipNode = Node.create({ }, addNodeView() { - return ReactNodeViewRenderer(MentionChipView, { - contentDOMElementTag: "span", - }); + return ({ node, getPos, editor }) => { + const { type, id, label, pastedText } = node.attrs as MentionChipAttrs; + const isCommand = type === "command"; + const prefix = isCommand ? "/" : "@"; + + // Outer wrapper — keeps inline flow + const dom = document.createElement("span"); + dom.className = "inline"; + dom.contentEditable = "false"; + + // Chip button — matches Quill + const chip = document.createElement("button"); + chip.type = "button"; + chip.setAttribute("data-slot", "chip"); + chip.contentEditable = "false"; + chip.className = cn( + chipClasses, + isCommand ? "cli-slash-command" : "cli-file-mention", + ); + + // GitHub issue icon + if (type === "github_issue") { + chip.insertAdjacentHTML("beforeend", githubIconSvg); + } + + // Label text + chip.appendChild( + document.createTextNode( + type === "github_issue" ? label : `${prefix}${label}`, + ), + ); + + // Tooltip via title attr + if (type === "file") { + chip.title = id; + } else if (pastedText) { + chip.title = "Click to paste as text instead"; + } + + // Close button — matches Quill + const closeBtn = document.createElement("button"); + closeBtn.type = "button"; + closeBtn.setAttribute("data-slot", "chip-close"); + closeBtn.className = closeClasses; + closeBtn.innerHTML = xIconSvg; + closeBtn.addEventListener("click", (e) => { + e.stopPropagation(); + const pos = getPos(); + if (pos == null) return; + editor + .chain() + .focus() + .deleteRange({ from: pos, to: pos + node.nodeSize }) + .run(); + }); + chip.appendChild(closeBtn); + + // Click handlers + if (type === "github_issue") { + chip.addEventListener("click", () => window.open(id, "_blank")); + } else if (pastedText) { + chip.addEventListener("click", async () => { + useFeatureSettingsStore.getState().markHintLearned("paste-as-file"); + const content = await trpcClient.fs.readAbsoluteFile.query({ + filePath: id, + }); + if (!content) return; + const pos = getPos(); + if (pos == null) return; + editor + .chain() + .focus() + .deleteRange({ from: pos, to: pos + node.nodeSize }) + .insertContentAt(pos, content) + .run(); + }); + } + + dom.appendChild(chip); + + return { + dom, + selectNode() { + for (const cls of selectedRingClasses) chip.classList.add(cls); + }, + deselectNode() { + for (const cls of selectedRingClasses) chip.classList.remove(cls); + }, + }; + }; }, addCommands() { diff --git a/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx b/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx deleted file mode 100644 index ae1733913..000000000 --- a/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { Tooltip } from "@components/ui/Tooltip"; -import { useSettingsStore as useFeatureSettingsStore } from "@features/settings/stores/settingsStore"; -import { GithubLogo } from "@phosphor-icons/react"; -import { trpcClient } from "@renderer/trpc/client"; -import type { Node as PmNode } from "@tiptap/pm/model"; -import type { Editor } from "@tiptap/react"; -import { type NodeViewProps, NodeViewWrapper } from "@tiptap/react"; -import type { MentionChipAttrs } from "./MentionChipNode"; - -const chipClass = - "inline cursor-default select-all rounded-[var(--radius-1)] bg-[var(--accent-a3)] px-1 py-px font-medium text-[var(--accent-11)] text-xs"; - -function DefaultChip({ - type, - id, - label, -}: { - type: string; - id: string; - label: string; -}) { - if (type === "github_issue") { - return ( - - ); - } - - const isCommand = type === "command"; - const prefix = isCommand ? "/" : "@"; - const isFile = type === "file"; - - const chip = ( - - {prefix} - {label} - - ); - - if (isFile) { - return {chip}; - } - - return chip; -} - -function PastedTextChip({ - label, - filePath, - editor, - node, - getPos, -}: { - label: string; - filePath: string; - editor: Editor; - node: PmNode; - getPos: () => number | undefined; -}) { - const handleClick = async () => { - useFeatureSettingsStore.getState().markHintLearned("paste-as-file"); - - const content = await trpcClient.fs.readAbsoluteFile.query({ - filePath, - }); - if (!content) return; - - const pos = getPos(); - if (pos == null) return; - - editor - .chain() - .focus() - .deleteRange({ from: pos, to: pos + node.nodeSize }) - .insertContentAt(pos, content) - .run(); - }; - - return ( - - - - ); -} - -export function MentionChipView({ node, getPos, editor }: NodeViewProps) { - const { type, id, label, pastedText } = node.attrs as MentionChipAttrs; - - return ( - - {pastedText ? ( - - ) : ( - - )} - - ); -} From 6900f4270bc8a78f9704946e498f452571882c9e Mon Sep 17 00:00:00 2001 From: Adam Leith Date: Tue, 21 Apr 2026 11:37:24 +0100 Subject: [PATCH 2/4] update prompt input chips to use quill, rebuilt prompt input storybook stories --- .../components/PromptInput.stories.tsx | 282 ++++++++++++++++++ .../message-editor/tiptap/MentionChipNode.ts | 120 +------- .../message-editor/tiptap/MentionChipView.tsx | 194 ++++++++++++ .../message-editor/tiptap/useTiptapEditor.ts | 2 +- .../features/message-editor/utils/content.ts | 1 + 5 files changed, 483 insertions(+), 116 deletions(-) create mode 100644 apps/code/src/renderer/features/message-editor/components/PromptInput.stories.tsx create mode 100644 apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx diff --git a/apps/code/src/renderer/features/message-editor/components/PromptInput.stories.tsx b/apps/code/src/renderer/features/message-editor/components/PromptInput.stories.tsx new file mode 100644 index 000000000..a2cb68b85 --- /dev/null +++ b/apps/code/src/renderer/features/message-editor/components/PromptInput.stories.tsx @@ -0,0 +1,282 @@ +import type { SessionConfigOption } from "@agentclientprotocol/sdk"; +import { Providers } from "@components/Providers"; +import { ReasoningLevelSelector } from "@features/sessions/components/ReasoningLevelSelector"; +import { UnifiedModelSelector } from "@features/sessions/components/UnifiedModelSelector"; +import type { AgentAdapter } from "@features/settings/stores/settingsStore"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { useEffect, useRef, useState } from "react"; +import type { EditorHandle } from "../types"; +import type { MentionChip } from "../utils/content"; +import { PromptInput } from "./PromptInput"; + +// --- Mock data matching SessionConfigOption shape --- + +const mockModelOption = { + id: "model", + name: "Model", + type: "select" as const, + currentValue: "gpt-5.4", + options: [ + { + group: "recommended", + name: "Recommended", + options: [ + { value: "gpt-5.4", name: "GPT 5.4" }, + { value: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, + ], + }, + { + group: "other", + name: "Other", + options: [ + { value: "claude-opus-4-6", name: "Claude Opus 4.6" }, + { value: "o3-pro", name: "o3-pro" }, + { value: "claude-haiku-4-5", name: "Claude Haiku 4.5" }, + ], + }, + ], +} satisfies SessionConfigOption; + +const mockReasoningOption = { + id: "thought", + name: "Reasoning", + type: "select" as const, + currentValue: "high", + options: [ + { value: "off", name: "Off" }, + { value: "low", name: "Low" }, + { value: "medium", name: "Medium" }, + { value: "high", name: "High" }, + ], +} satisfies SessionConfigOption; + +// --- Wrapper to inject chips after mount --- + +function PromptInputWithChips({ + chips, + ...props +}: React.ComponentProps & { chips?: MentionChip[] }) { + const ref = useRef(null); + + useEffect(() => { + if (!chips?.length) return; + const timer = setTimeout(() => { + for (const chip of chips) { + ref.current?.insertChip(chip); + } + }, 200); + return () => clearTimeout(timer); + }, [chips]); + + return ; +} + +// --- Wrapper with stateful selectors --- + +function PromptInputWithSelectors({ + chips, + showSelectors = true, + ...props +}: React.ComponentProps & { + chips?: MentionChip[]; + showSelectors?: boolean; +}) { + const [adapter, setAdapter] = useState("claude"); + const [modelOption, setModelOption] = + useState(mockModelOption); + const [reasoningOption, setReasoningOption] = + useState(mockReasoningOption); + + const handleModelChange = (value: string) => { + setModelOption({ ...mockModelOption, currentValue: value }); + }; + + const handleReasoningChange = (value: string) => { + setReasoningOption({ ...mockReasoningOption, currentValue: value }); + }; + + return ( + + ) : ( + false + ) + } + reasoningSelector={ + showSelectors ? ( + + ) : ( + false + ) + } + {...props} + /> + ); +} + +const meta: Meta = { + title: "Features/MessageEditor/PromptInput", + component: PromptInputWithSelectors, + parameters: { + layout: "padded", + }, + decorators: [ + (Story) => ( + +
+ +
+
+ ), + ], + args: { + sessionId: "storybook-session", + placeholder: "Type a message...", + disabled: false, + isLoading: false, + autoFocus: true, + isActiveSession: true, + enableBashMode: true, + enableCommands: true, + showSelectors: true, + onSubmit: () => {}, + onCancel: () => {}, + }, + argTypes: { + disabled: { control: "boolean" }, + isLoading: { control: "boolean" }, + enableBashMode: { control: "boolean" }, + enableCommands: { control: "boolean" }, + showSelectors: { control: "boolean" }, + placeholder: { control: "text" }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithFileChip: Story = { + name: "With File Chip", + args: { + chips: [ + { + type: "file", + id: "/src/settings.json", + label: ".claude/settings.json", + }, + ], + }, +}; + +export const WithCommandChip: Story = { + name: "With Command Chip", + args: { + chips: [{ type: "command", id: "good", label: "good" }], + }, +}; + +export const WithMultipleChips: Story = { + name: "With Multiple Chips", + args: { + chips: [ + { + type: "file", + id: "/src/settings.json", + label: ".claude/settings.json", + }, + { type: "command", id: "good", label: "good" }, + { + type: "file", + id: "/workflows/release.yml", + label: "workflows/agent-release.yml", + }, + ], + }, +}; + +export const AllChipTypes: Story = { + name: "All Chip Types", + args: { + chips: [ + { type: "file", id: "/src/index.ts", label: "src/index.ts" }, + { type: "command", id: "review", label: "review" }, + { + type: "github_issue", + id: "https://github.com/org/repo/issues/123", + label: "#123 Fix the bug", + }, + { type: "error", id: "error-1", label: "TypeError: undefined" }, + { type: "experiment", id: "exp-1", label: "new-checkout-flow" }, + { type: "insight", id: "insight-1", label: "Weekly active users" }, + { type: "feature_flag", id: "flag-1", label: "enable-dark-mode" }, + { + type: "file", + id: "/tmp/pasted-content.txt", + label: "pasted-content.txt", + pastedText: true, + }, + ], + }, +}; + +export const BashMode: Story = { + name: "Bash Mode (type ! to activate)", + args: { + enableBashMode: true, + placeholder: "Type ! to enter bash mode...", + }, +}; + +export const Loading: Story = { + name: "Loading (With Cancel)", + args: { + isLoading: true, + }, +}; + +export const Disabled: Story = { + args: { + disabled: true, + }, +}; + +export const LongChipLabels: Story = { + name: "Long Chip Labels", + args: { + chips: [ + { + type: "file", + id: "/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx", + label: + "apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx", + }, + { + type: "file", + id: "/packages/agent/src/adapters/claude/permissions/permission-options.ts", + label: + "packages/agent/src/adapters/claude/permissions/permission-options.ts", + }, + ], + }, +}; + +export const NoToolbar: Story = { + name: "No Toolbar (Minimal)", + args: { + showSelectors: false, + }, +}; diff --git a/apps/code/src/renderer/features/message-editor/tiptap/MentionChipNode.ts b/apps/code/src/renderer/features/message-editor/tiptap/MentionChipNode.ts index dd6eaf24a..dcdee19a4 100644 --- a/apps/code/src/renderer/features/message-editor/tiptap/MentionChipNode.ts +++ b/apps/code/src/renderer/features/message-editor/tiptap/MentionChipNode.ts @@ -1,7 +1,6 @@ -import { useSettingsStore as useFeatureSettingsStore } from "@features/settings/stores/settingsStore"; -import { buttonVariants, cn } from "@posthog/quill"; -import { trpcClient } from "@renderer/trpc/client"; import { mergeAttributes, Node } from "@tiptap/core"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +import { MentionChipView } from "./MentionChipView"; export type ChipType = | "file" @@ -27,28 +26,6 @@ declare module "@tiptap/core" { } } -// Compute Quill Chip classes once at module load (size="xs", variant="outline") -const chipClasses = cn( - buttonVariants({ size: "xs", variant: "outline" }), - "gap-1 rounded-sm has-data-[slot=chip-close]:pe-0 bg-background max-w-full", - "relative -top-[2px] cursor-default active:translate-y-0", -); - -const closeClasses = cn( - buttonVariants({ size: "xs", variant: "outline" }), - "size-5 p-0 opacity-50 hover:opacity-100", -); - -const selectedRingClasses = ["border-ring/50", "ring-[3px]", "ring-ring/50"]; - -// Lucide XIcon SVG (matches Quill ChipClose default) -const xIconSvg = - ''; - -// GitHub logo SVG -const githubIconSvg = - ''; - export const MentionChipNode = Node.create({ name: "mentionChip", group: "inline", @@ -89,96 +66,9 @@ export const MentionChipNode = Node.create({ }, addNodeView() { - return ({ node, getPos, editor }) => { - const { type, id, label, pastedText } = node.attrs as MentionChipAttrs; - const isCommand = type === "command"; - const prefix = isCommand ? "/" : "@"; - - // Outer wrapper — keeps inline flow - const dom = document.createElement("span"); - dom.className = "inline"; - dom.contentEditable = "false"; - - // Chip button — matches Quill - const chip = document.createElement("button"); - chip.type = "button"; - chip.setAttribute("data-slot", "chip"); - chip.contentEditable = "false"; - chip.className = cn( - chipClasses, - isCommand ? "cli-slash-command" : "cli-file-mention", - ); - - // GitHub issue icon - if (type === "github_issue") { - chip.insertAdjacentHTML("beforeend", githubIconSvg); - } - - // Label text - chip.appendChild( - document.createTextNode( - type === "github_issue" ? label : `${prefix}${label}`, - ), - ); - - // Tooltip via title attr - if (type === "file") { - chip.title = id; - } else if (pastedText) { - chip.title = "Click to paste as text instead"; - } - - // Close button — matches Quill - const closeBtn = document.createElement("button"); - closeBtn.type = "button"; - closeBtn.setAttribute("data-slot", "chip-close"); - closeBtn.className = closeClasses; - closeBtn.innerHTML = xIconSvg; - closeBtn.addEventListener("click", (e) => { - e.stopPropagation(); - const pos = getPos(); - if (pos == null) return; - editor - .chain() - .focus() - .deleteRange({ from: pos, to: pos + node.nodeSize }) - .run(); - }); - chip.appendChild(closeBtn); - - // Click handlers - if (type === "github_issue") { - chip.addEventListener("click", () => window.open(id, "_blank")); - } else if (pastedText) { - chip.addEventListener("click", async () => { - useFeatureSettingsStore.getState().markHintLearned("paste-as-file"); - const content = await trpcClient.fs.readAbsoluteFile.query({ - filePath: id, - }); - if (!content) return; - const pos = getPos(); - if (pos == null) return; - editor - .chain() - .focus() - .deleteRange({ from: pos, to: pos + node.nodeSize }) - .insertContentAt(pos, content) - .run(); - }); - } - - dom.appendChild(chip); - - return { - dom, - selectNode() { - for (const cls of selectedRingClasses) chip.classList.add(cls); - }, - deselectNode() { - for (const cls of selectedRingClasses) chip.classList.remove(cls); - }, - }; - }; + return ReactNodeViewRenderer(MentionChipView, { + contentDOMElementTag: "span", + }); }, addCommands() { diff --git a/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx b/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx new file mode 100644 index 000000000..5a75b8118 --- /dev/null +++ b/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx @@ -0,0 +1,194 @@ +import { Tooltip } from "@components/ui/Tooltip"; +import { useSettingsStore as useFeatureSettingsStore } from "@features/settings/stores/settingsStore"; +import { + ChartLineIcon, + FileTextIcon, + FlagIcon, + FlaskIcon, + GithubLogoIcon, + TerminalIcon, + WarningIcon, + XIcon, +} from "@phosphor-icons/react"; +import { Chip } from "@posthog/quill"; +import { trpcClient } from "@renderer/trpc/client"; +import type { Node as PmNode } from "@tiptap/pm/model"; +import type { Editor } from "@tiptap/react"; +import { type NodeViewProps, NodeViewWrapper } from "@tiptap/react"; +import type { ChipType, MentionChipAttrs } from "./MentionChipNode"; + +const chipBase = "group/chip relative top-px active:translate-y-0 pl-1"; + +const selectedRing = "border-ring/50 ring-[1px] ring-ring/50"; + +const typeIconMap: Record> = { + file: FileTextIcon, + command: TerminalIcon, + github_issue: GithubLogoIcon, + error: WarningIcon, + experiment: FlaskIcon, + insight: ChartLineIcon, + feature_flag: FlagIcon, +}; + +function IconCloseButton({ + type, + onRemove, +}: { + type: ChipType; + onRemove: () => void; +}) { + const Icon = typeIconMap[type] || FileTextIcon; + + return ( + + ); +} + +function DefaultChip({ + type, + id, + label, + selected, + onRemove, +}: { + type: string; + id: string; + label: string; + selected: boolean; + onRemove: () => void; +}) { + const isCommand = type === "command"; + const prefix = isCommand ? "/" : "@"; + const isFile = type === "file"; + + const chipContent = ( + window.open(id, "_blank") : undefined + } + className={`${chipBase} ${type === "github_issue" ? "cursor-pointer!" : "cursor-default! active:translate-y-0!"} ${isCommand ? "cli-slash-command" : "cli-file-mention"} ${selected ? selectedRing : ""}`} + > + + {type === "github_issue" ? label : `${prefix}${label}`} + + ); + + if (isFile) { + return {chipContent}; + } + + return chipContent; +} + +function PastedTextChip({ + label, + filePath, + editor, + node, + getPos, + selected, + onRemove, +}: { + label: string; + filePath: string; + editor: Editor; + node: PmNode; + getPos: () => number | undefined; + selected: boolean; + onRemove: () => void; +}) { + const handleClick = async () => { + useFeatureSettingsStore.getState().markHintLearned("paste-as-file"); + + const content = await trpcClient.fs.readAbsoluteFile.query({ + filePath, + }); + if (!content) return; + + const pos = getPos(); + if (pos == null) return; + + editor + .chain() + .focus() + .deleteRange({ from: pos, to: pos + node.nodeSize }) + .insertContentAt(pos, content) + .run(); + }; + + return ( + + + @{label} + + + ); +} + +export function MentionChipView({ + node, + getPos, + editor, + selected, +}: NodeViewProps) { + const { type, id, label, pastedText } = node.attrs as MentionChipAttrs; + + const handleRemove = () => { + const pos = getPos(); + if (pos == null) return; + editor + .chain() + .focus() + .deleteRange({ from: pos, to: pos + node.nodeSize }) + .run(); + }; + + return ( + + {pastedText ? ( + + ) : ( + + )} + + ); +} diff --git a/apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts b/apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts index 952c84914..e46c61239 100644 --- a/apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts +++ b/apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts @@ -529,7 +529,7 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) { type: chip.type, id: chip.id, label: chip.label, - pastedText: false, + pastedText: chip.pastedText ?? false, }); draft.saveDraft(editor, attachments); }, diff --git a/apps/code/src/renderer/features/message-editor/utils/content.ts b/apps/code/src/renderer/features/message-editor/utils/content.ts index 13d1817cd..e3c116021 100644 --- a/apps/code/src/renderer/features/message-editor/utils/content.ts +++ b/apps/code/src/renderer/features/message-editor/utils/content.ts @@ -11,6 +11,7 @@ export interface MentionChip { | "github_issue"; id: string; label: string; + pastedText?: boolean; } export interface FileAttachment { From 13e417a6cf0f939ffd69c576f09def9a3031cb77 Mon Sep 17 00:00:00 2001 From: Adam Leith Date: Tue, 21 Apr 2026 11:42:23 +0100 Subject: [PATCH 3/4] fix: revert pastedText interface change, keep visual-only in story Co-Authored-By: Claude Opus 4.6 (1M context) --- .../features/message-editor/components/PromptInput.stories.tsx | 1 - .../renderer/features/message-editor/tiptap/useTiptapEditor.ts | 2 +- apps/code/src/renderer/features/message-editor/utils/content.ts | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/code/src/renderer/features/message-editor/components/PromptInput.stories.tsx b/apps/code/src/renderer/features/message-editor/components/PromptInput.stories.tsx index a2cb68b85..b530768a9 100644 --- a/apps/code/src/renderer/features/message-editor/components/PromptInput.stories.tsx +++ b/apps/code/src/renderer/features/message-editor/components/PromptInput.stories.tsx @@ -227,7 +227,6 @@ export const AllChipTypes: Story = { type: "file", id: "/tmp/pasted-content.txt", label: "pasted-content.txt", - pastedText: true, }, ], }, diff --git a/apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts b/apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts index e46c61239..952c84914 100644 --- a/apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts +++ b/apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts @@ -529,7 +529,7 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) { type: chip.type, id: chip.id, label: chip.label, - pastedText: chip.pastedText ?? false, + pastedText: false, }); draft.saveDraft(editor, attachments); }, diff --git a/apps/code/src/renderer/features/message-editor/utils/content.ts b/apps/code/src/renderer/features/message-editor/utils/content.ts index e3c116021..13d1817cd 100644 --- a/apps/code/src/renderer/features/message-editor/utils/content.ts +++ b/apps/code/src/renderer/features/message-editor/utils/content.ts @@ -11,7 +11,6 @@ export interface MentionChip { | "github_issue"; id: string; label: string; - pastedText?: boolean; } export interface FileAttachment { From 585664e71990bd14883bda2dbfd5cea7b9805286 Mon Sep 17 00:00:00 2001 From: Adam Leith Date: Tue, 21 Apr 2026 11:59:53 +0100 Subject: [PATCH 4/4] fix: prevent duplicate chip insertion on story re-render Co-Authored-By: Claude Opus 4.6 (1M context) --- .../message-editor/components/PromptInput.stories.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/code/src/renderer/features/message-editor/components/PromptInput.stories.tsx b/apps/code/src/renderer/features/message-editor/components/PromptInput.stories.tsx index b530768a9..9fd2f9938 100644 --- a/apps/code/src/renderer/features/message-editor/components/PromptInput.stories.tsx +++ b/apps/code/src/renderer/features/message-editor/components/PromptInput.stories.tsx @@ -57,9 +57,11 @@ function PromptInputWithChips({ ...props }: React.ComponentProps & { chips?: MentionChip[] }) { const ref = useRef(null); + const insertedRef = useRef(false); useEffect(() => { - if (!chips?.length) return; + if (!chips?.length || insertedRef.current) return; + insertedRef.current = true; const timer = setTimeout(() => { for (const chip of chips) { ref.current?.insertChip(chip);