input: Add atomic inline tokens to Input and Textarea - #3113
Merged
huacnlee merged 11 commits intoSep 18, 2026
Merged
Conversation
6 tasks
A token's ID now identifies the referenced resource, so the same person mentioned twice carries the same ID. Tokens are addressed by their start offset and widths are cached by token value, which removes `DuplicateId`. `refresh_token` is gone: visible tokens are measured on every render and a changed width already reflows the input. `InlineTokenPresentation` becomes crate-private; styled controls install their renderer and click listener through a hidden entry point, and the Shell adapts script callbacks through the elements' own `render_token` / `on_token_click` builders. The default skin is `InlineTokenTag`, and the "Activate token" context menu item is dropped in favor of an application-provided entry; `ActivateToken` remains for key bindings and accessibility. Disabled inputs keep mouse caret placement, and undo only clears the IME marked range on the token-aware path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`set_value` now takes plain text or `InputContent`, so one setter resets the input the way Monaco's `setValue` and ProseMirror's `EditorState.create` do; `set_content` is gone. `InputContent::with_token` validates each token against the text as it is attached and returns a `Result`, so content is consistent before it reaches the input. The JavaScript `set_value` accepts either shape as well. A click selects the whole token before the click listener runs, and the Base token element uses the arrow cursor. The story is a chat composer: commands, images, skills and people are inserted through an input-group toolbar with a Send action, the sample inserter separates a reference from its neighbors, the tag has a border, and the readout keeps its labels on one lane. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The text selection highlight now stops at a token's edges, so a selected token shows only the state its tag paints instead of a rectangle behind the pill. `InlineTokenTag` drops its tooltip: the label already names the reference, and a hover hint on every token is noise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The default tag draws a hairline border at rest and switches both fill and border to the selection color when selected, so the Story no longer adds a border of its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fluent builders in this library name the slot they fill (`trigger`, `content`, `empty`, `footer`) rather than describing the callback, so `render_token` becomes `token` on the Base and Component Input and Textarea, in the JavaScript elements and in the docs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Component parts are named after the control they belong to (InputGroupAddon, DialogFooter, ListItem), so the element a token renders as is `InputToken` rather than a web noun. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`InputToken::icon` follows every other element's icon builder instead of `with_icon`, `InlineTokenClickEvent::click` names the click it carries, and the JavaScript token states drop the undocumented `refresh()` now that tokens re-measure on their own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Coding Guides now say the state is `readonly` everywhere — identifiers, labels and prose — matching the builder and reader, not `read-only` or `read only`. The token Story and docs, the Editor story's menu item and a Shell test name follow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
huacnlee
previously approved these changes
Sep 18, 2026
A pull request that adds, changes or removes anything public now lists it under a `## Public API` section, grouped by crate, with signatures and a line on each item's purpose; changes to existing items still go under `## Breaking Changes` with a diff. The PR template and CLAUDE.md say the same. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
|
I can't find the example in story , could you please link it |
Member
At bottom of Input story. |
Contributor
Contributor
Author
Contributor
|
It seems the website is still not updated for me, it's still showing old components only for me |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes #3110
Description
Add optional atomic inline tokens to Input and Textarea for mentions, commands, file and image references. A token can display “Alice” while the input value and the clipboard contain
@alice. Users select, delete and undo the whole token; the application maps its ID back to the resource it names.Model and API
The input stays a plain-text control: the value is the text, and each token is an annotation over a byte range that follows edits — the same model as Draft.js entity ranges and the VS Code chat input's parts over Monaco decorations, rather than a document tree with atom nodes. Base owns the behavior (atomic caret movement, selection and deletion, history with token deltas, mixed text/element layout and wrapping, activation); Component owns the default appearance.
InlineTokencarries a resource ID, its text and a display label. The ID names the resource and may occur more than once in one input.InputContentis whatcontent()returns and whatset_valueaccepts; plain text converts into content without tokens.InputContent::with_tokenreturnsResult, so a content value is always consistent before it reaches the input.Input/Textarea(Base and Component) take atokenslot andon_token_click. A click selects the whole token and then runs the listener; dragging or Shift-selecting does not open it.ActivateTokenopens an exactly selected token from a key binding or assistive technology.InputTokenby default, withiconandStyled; a selected token paints its own selected state, and the text selection highlight stops at its edges. The Base token element uses the arrow cursor, and tokens are measured whenever they render, so an element that grows reflows on the next frame.InputState/TextareaStateand on the registered components:content(),tokens(),set_value(string | InputContent),replace_with_token,replace_range_with_token,token,on_token_click. Ranges are UTF-16 string offsets, and validation errors carryerror.code.Tokens remain opt-in. Editor, NumberInput, formatted masks and password fields do not accept token insertion. Copy/paste uses ordinary text; resource lookup, delimiters around an inserted reference and submission remain application decisions.
Story and docs
The Story is a chat composer shared by Input and Textarea: an
InputGroupwith a toolbar that inserts a/commit-prcommand, an[Image 1]attachment, a$gpui-kitskill and an@alicemention as tokens, a Send action, draft save/restore, read-only and disabled toggles and a readout of the text, references and last action. The JavaScript Story has the same example. English and Chinese documentation cover insertion, appearance, drafts, validation and the JavaScript API for Input, Textarea and InputGroup.Public API
gpui-base(gpui_base::input)Data:
InlineToken— a reference rendered as one editing unit.new(id, text),with_label(label); readersid(),text(),label(). DerivesClone, Debug, PartialEq, Eq, Hash.InlineTokenSpan— a token at its current UTF-8 byte range:range() -> Range<usize>,token() -> &InlineToken.InputContent— text with its tokens, whatcontent()returns andset_valueaccepts.new(text),with_token(range, token) -> Result<Self, InlineTokenError>(validates boundary, overlap and text match),text(),tokens().Fromfor every text typeset_valueaccepted before (&str,String,SharedString,Cow<str>,Arc<str>,Box<str>,char, and their references).InlineTokenError—#[non_exhaustive]:InvalidRange,InvalidBoundary,InvalidToken,OverlappingTokens,TextMismatch,UnsupportedMode,ValidationRejected,CompositionActive. ImplementsErrorandDisplay.InlineTokenContext— what a token renderer sees:token(),range(),is_selected(),is_disabled(),is_readonly(),line_height(),available_width().InlineTokenClickEvent— what a click listener receives:token(),range(),bounds() -> Bounds<Pixels>,click() -> &ClickEvent.ActivateToken— action that opens an exactly selected token (key bindings, assistive technology).InputState/TextareaState(InputBaseState<InputMode | TextareaMode>):replace_with_token(token, window, cx) -> Result<(), InlineTokenError>— replace the selection, or insert at the caret, with a token.replace_range_with_token(range, token, window, cx) -> Result<(), InlineTokenError>— replace a byte range, expanding overlaps to whole tokens.tokens() -> &[InlineTokenSpan]— the current tokens in document order.content() -> InputContent— the text with its tokens.set_value(value: impl Into<InputContent>, window, cx)— wasimpl Into<SharedString>; plain text still converts, content also restores tokens.Input/Textareaelements:token(render: impl Fn(&InlineTokenContext, &mut Window, &mut App) -> impl IntoElement)— the element each token renders as.on_token_click(listener: impl Fn(&InlineTokenClickEvent, &mut Window, &mut App))— runs after a completed, unconsumed click on a token.Hidden (
#[doc(hidden)], for styled controls only):InlineTokenRenderer,InlineTokenClickListener,InputBaseState::install_token_presentation.gpui-component(gpui_component::input)InputToken— the element a token renders as by default:new(&InlineTokenContext),icon(impl Into<Icon>),Styled. Selected, disabled and readonly states come from the context.Input/Textarea:token(..)andon_token_click(..)as in Base;InputGroupInput/InputGroupTextareainherit them.ActivateToken.gpui-shell(host integration)StateMethodDescriptor— an opt-in operation on retained state exposed to scripts:new::<T>(name, signature, call),with_readonly(bool),name(),signature(),is_readonly().StateDescriptor::with_methods(Vec<StateMethodDescriptor>),methods().ComponentElementCallback::build_interactive_data_with(args, window, cx) -> Result<Option<AnyElement>>— build a frame-owned inline subtree whose child callbacks retire with that frame.ComponentCallback::invoke_data_with(args, window, cx) -> Result<()>.InlineTokenCallbacks— script callbacks adapted to an element'stoken/on_token_clickbuilders:new(&Entity<InputBaseState<M>>, renderer, listener),apply(element, token, on_token_click).inline_token_context_data,inline_token_click_data,input_token_state_methods(),textarea_token_state_methods().JavaScript (
gpui-base/gpui-component)InputState/TextareaState:content(): InputContent,tokens(): InlineTokenSpan[],replace_with_token(token),replace_range_with_token(range, token),set_selected_range(range),replace(text); changed:set_value(next: string | InputContent). Ranges are UTF-16 offsets; validation errors carryerror.code.Input/Textarea/InputGroupInput/InputGroupTextarea:token(render),on_token_click(listener).InputRange,InlineToken,InlineTokenSpan,InputContent,InlineTokenContext,InlineTokenClickEvent.Breaking Changes
None. Existing input APIs and
InputEventvariants are unchanged; the token APIs are additive.set_valuenow takesimpl Into<InputContent>, and every text type it accepted before converts into content, so existing calls compile and behave as they did.How to Test
Validated on macOS on top of the latest
main:The Base tests cover token insertion and rejection, history with token deltas, boundary and word movement, IME composition, mode gates, wrapping and re-measurement, click selection and re-entrant activation. The Story test drives the composer through insertion, partial-selection deletion, undo, save/restore, sending and the read-only/disabled gates for both controls. Host tests cover UTF-16 emoji boundaries, atomic rejection, re-render retention, token activation and custom child callbacks.
In the Story gallery, open Input or Textarea and scroll to “Atomic inline tokens”: click a token to select it and read the status line, insert the same reference twice, select part of a token and delete it, undo, and save and restore the draft.
Real IME candidate/cancellation behavior on Windows and Linux remains unverified.
AI Assistance
The implementation, tests and documentation were written with Codex assistance; the API review and rework (resource IDs,
set_value(content), the presentation seam, click selection, naming and the composer Story) were done with Claude Code and reviewed by hand.🤖 Generated with Claude Code