Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 81 additions & 3 deletions apps/web/src/components/ComposerPromptEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ import {
KEY_ENTER_COMMAND,
KEY_TAB_COMMAND,
COMMAND_PRIORITY_HIGH,
COMMAND_PRIORITY_LOW,
KEY_BACKSPACE_COMMAND,
BLUR_COMMAND,
FOCUS_COMMAND,
$getRoot,
HISTORY_MERGE_TAG,
DecoratorNode,
Expand Down Expand Up @@ -185,7 +188,7 @@ class ComposerMentionNode extends DecoratorNode<React.ReactElement> {

override createDOM(): HTMLElement {
const dom = document.createElement("span");
dom.className = "inline-flex align-middle leading-none";
dom.className = "composer-inline-chip relative inline-flex align-middle leading-none";
return dom;
}

Expand Down Expand Up @@ -323,7 +326,7 @@ class ComposerSkillNode extends DecoratorNode<React.ReactElement> {

override createDOM(): HTMLElement {
const dom = document.createElement("span");
dom.className = "inline-flex align-middle leading-none";
dom.className = "composer-inline-chip relative inline-flex align-middle leading-none";
return dom;
}

Expand Down Expand Up @@ -394,7 +397,7 @@ class ComposerTerminalContextNode extends DecoratorNode<React.ReactElement> {

override createDOM(): HTMLElement {
const dom = document.createElement("span");
dom.className = "inline-flex align-middle leading-none";
dom.className = "composer-inline-chip relative inline-flex align-middle leading-none";
return dom;
}

Expand Down Expand Up @@ -1167,6 +1170,80 @@ function ComposerInlineTokenBackspacePlugin() {
return null;
}

/**
* Chips render as non-editable decorators, so the browser never paints the
* native text selection over them; without help, a selection spanning chips
* is only visible in the slivers between them. Mirror the selection onto the
* chips with a data attribute the stylesheet turns into a highlight overlay.
*/
function ComposerChipSelectionPlugin() {
const [editor] = useLexicalComposerContext();

useEffect(() => {
let selectedKeys = new Set<string>();
// Lexical keeps the range selection on blur without emitting an update,
// so focus is tracked separately; while blurred the native highlight is
// gone and the mirrored one has to go with it.
let hasFocus = editor.getRootElement() === document.activeElement;

const applyKeys = (nextKeys: Set<string>) => {
for (const key of selectedKeys) {
if (!nextKeys.has(key)) {
editor.getElementByKey(key)?.removeAttribute("data-composer-chip-selected");
}
}
for (const key of nextKeys) {
editor.getElementByKey(key)?.setAttribute("data-composer-chip-selected", "true");
}
selectedKeys = nextKeys;
};

const readSelectedKeys = () => {
const nextKeys = new Set<string>();
editor.getEditorState().read(() => {
const selection = $getSelection();
if ($isRangeSelection(selection) && !selection.isCollapsed()) {
for (const node of selection.getNodes()) {
if (node instanceof DecoratorNode) {
nextKeys.add(node.getKey());
}
}
}
});
return nextKeys;
};

const unregisterUpdate = editor.registerUpdateListener(() => {
applyKeys(hasFocus ? readSelectedKeys() : new Set());
});
const unregisterFocus = editor.registerCommand(
FOCUS_COMMAND,
() => {
hasFocus = true;
applyKeys(readSelectedKeys());
return false;
},
COMMAND_PRIORITY_LOW,
);
const unregisterBlur = editor.registerCommand(
BLUR_COMMAND,
() => {
hasFocus = false;
applyKeys(new Set());
return false;
},
COMMAND_PRIORITY_LOW,
);
return () => {
unregisterUpdate();
unregisterFocus();
unregisterBlur();
};
}, [editor]);
Comment thread
cursor[bot] marked this conversation as resolved.

return null;
}

function ComposerInlineTokenPastePlugin() {
const [editor] = useLexicalComposerContext();

Expand Down Expand Up @@ -1701,6 +1778,7 @@ function ComposerPromptEditorInner({
<ComposerInlineTokenSelectionNormalizePlugin />
<ComposerInlineTokenBackspacePlugin />
<ComposerInlineTokenPastePlugin />
<ComposerChipSelectionPlugin />
<HistoryPlugin />
</div>
</ComposerTerminalContextActionsContext>
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -1031,3 +1031,15 @@ label:has(> select#reasoning-effort) select {
.dark .model-picker-list::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.15);
}

/* Composer chips are non-editable decorators, so the browser skips them when
painting text selection; this overlay stands in for the native highlight. */
.composer-inline-chip[data-composer-chip-selected]::after {
content: "";
position: absolute;
inset: 0;
border-radius: 6px;
background-color: Highlight;
opacity: 0.3;
pointer-events: none;
}
Loading