Inline Editor

Generated output the user edits in place, section by section or field by field, with targeted rewrites of one part.

Pattern

≈ 1,474 tokens per use (tool 805 · guidance 320 · typical payload 349)How token costs are estimated

Example: Inline Editor

Let me tweak the cover letter

Cover letter for Fieldnote

Dear hiring team,

I'm really excited to apply for the Senior Product Designer role at Fieldnote. I've used Fieldnote to plan research sprints for two years, and I think I could help make it the tool every research team reaches for first.

For the last five years I've led design for scheduling and reporting at Tallyworks, a B2B analytics company. I just try to keep complex workflows simple, and I believe the results speak for themselves:

  • Redesigned onboarding, which cut the time to a first report from 3 days to 40 minutes
  • Built and ran a design system that is used by 4 product teams
  • Ran weekly customer interviews with 30+ research teams

What draws me to Fieldnote is the focus on making research actually usable across a whole company. I'd perhaps bring a strong eye for information-dense screens and a habit of testing early with real customers.

I'd love to talk about how I might help with the next stage of Fieldnote. Thank you for your time.

Best regards,
Priya Raman

When to use

  • The user wants to tweak something the agent generated (a cover letter, an announcement, a product listing) rather than get a whole new draft.
  • The user asks to change only one part: "rewrite only the second paragraph", "fix the tagline".
  • The output is long enough that small edits beat regenerating it.

When not to use

  • The user wants alternatives to choose from. Use Draft Options.
  • The text is a sentence or two. Offer drafts or just rewrite it.
  • The agent is proposing changes to a live record in another system. Use a Change Preview with before/after values.
  • The agent needs new input the user hasn't given yet. Use a Generated Form.

How the agent should use it

  • Split text into sections the user would edit separately: paragraphs, or headed sections. Keep the ids stable across calls.
  • Use only **bold**, *italic* and - lists in section text. Blank lines separate paragraphs.
  • Set focusSectionId when the user named the part to change. That section is highlighted with its rewrite panel already open.
  • On rewrite, change only that section (only the selection, when there is one) and follow the instruction. Keep every other section, including the user's edits, exactly as it is.
  • On save, use the returned text and values verbatim. On discard, keep the previous version.
  • Lifecycle: none needed. The document is the user's own and doesn't expire or go stale, so don't set expiresAt or fetchedAt. Use supersede only when a new edit_output call replaces this one (see Rewrites).

Editing text

Each section reads as normal prose. Hovering or focusing a section shows Edit and Rewrite (on touch screens they're always shown, under the text).

  • Edit swaps the section for a text area that grows with its content. Save or Ctrl/⌘+Enter keeps the change; Cancel or Esc drops it. The text area shows the raw markdown, so bold, italic and lists can be edited too.
  • Edited sections get an "Edited" marker, a revert button and Show changes, which renders a word diff against the agent's version: added words are underlined, removed words struck through, and each carries screen-reader text ("added", "removed"), so the diff never relies on color.
  • The footer has Discard and Done, plus labelled icon buttons for Copy (the document as Markdown), Undo (the last edit; also Ctrl/⌘+Z outside text inputs) and Reset all (undoable). Open editors are saved by Done. The keyboard and markdown hints for the text area are read to screen readers rather than shown.

Rewrites

Selecting text inside a section shows a small Rewrite selection toolbar next to it. The keyboard path is the section's Rewrite button: it targets the current selection if there is one, or the whole section. Either opens a compact panel with quick instruction buttons (instructions, default Shorter, More formal, Fix grammar) and a free-text instruction.

The selection is reported as offsets into the section's current text, widened where needed so a **bold** or *italic* span is either fully inside it or not touched, which keeps the markdown balanced when the agent replaces exactly that range.

A rewrite never locks the editor. The section shows "Rewriting…" and the user can keep editing the others. There are two ways to reply:

Host styleWhat to do
Update in place (recommended)Keep the component mounted and pass new sections in which that section's text changed. The section animates, its local edit is replaced by the agent's version, and every other edit stays. Don't pass the rewrite as result
New tool callComplete the call with the rewrite result. The agent calls edit_output again with the new text and folds the returned edits into the other sections, so nothing the user typed is lost. Mark the old call superseded

Editing fields

In fields mode the component is a labelled form prefilled with the agent's values: text, textarea, number, select and tags (Enter or a comma adds a tag; Backspace in the empty input removes the last). Each changed field gets an "Edited" marker and a reset button. required and maxLength are validated, with a live counter (characters, or tags for a tags field) that appears once a value reaches 80% of its limit; screen readers always get it. A field's help is attached to its control for screen readers, not shown as a caption. Done focuses the first invalid field instead of saving.

Installation

npx shadcn@latest add https://your-site.com/r/inline-editor.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { inlineEditorTool } from "@/components/genui/inline-editor.schema"

const tools = {
  // No execute(): the call is completed by the user in the UI.
  [inlineEditorTool.name]: tool({
    description: inlineEditorTool.description,
    inputSchema: inlineEditorTool.inputSchema,
  }),
}
components/message.tsx
<InlineEditor
  {...part.input}
  status={part.state === "input-streaming" ? "streaming" : undefined}
  result={part.state === "output-available" ? part.output : undefined}
  onResult={(result) =>
    addToolResult({ tool: "edit_output", toolCallId: part.toolCallId, output: result })
  }
/>

The agent receives one of:

{
  action: "save",
  sections: [{ id: "opening", text: "Dear hiring team, …" }, …],
  edited: ["opening", "why"],
}
{ action: "save", fields: { name: "…", price: 34, tags: ["steel", "hiking"] }, edited: ["tags"] }
{
  action: "rewrite",
  sectionId: "experience",
  text: "For the last five years I've led design …",
  selection: { start: 109, end: 152, text: "I just try to keep complex workflows simple" },
  instruction: "More confident",
  edits: [{ id: "opening", text: "Dear hiring team, …" }],
}
{ action: "discard" }

With LangGraph

Push it from a graph node with ui.push({ name: "edit_output", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer. To reply to a rewrite in place, push the same UI message id again with the updated sections.

Restoring from history

Pass the stored result back as result. A save shows the final version read-only with "Saved" (the user's edits and Show changes included), a discard shows the original, and a rewrite shows the document read-only with the request in the footer. None of them call onResult again.

<InlineEditor
  {...part.input}
  result={part.state === "output-available" ? part.output : undefined}
/>

Schema

PropTypeNotes
titlestringWhat is being edited
mode"text" | "fields"
sections{ id, heading?, text }[]Text mode, 1–20. text uses **bold**, *italic*, - lists
fields{ id, label, type, value, options?, maxLength?, required?, help? }[]Fields mode. type: text, textarea, number, select, tags
allowAiRewriteboolean"Rewrite" per section and on a selection. Default true
instructionsstring[]?Up to 6 quick rewrite buttons. Default Shorter, More formal, Fix grammar
focusSectionIdstring?Highlights a section and opens its rewrite panel

Host props: status (streaming, ready, submitting, saved, discarded), onResult, result and className.

Source

On this page