Skills

A controlled skill-picker panel for the Composer. The TipTap editor remains mounted and focused while the panel filters from text typed after /.

Skills
Pre-call briefOne-pager before any call
Meeting recap & follow-upAfter every call
MEDDPICC gap checkScore any deal
Forecast scrubWeekly sweep

Nothing selected yet.

Installation

import {
  Skill,
  SkillChipExtension,
  getSkillChipsFromDoc,
  insertSkillChipAt,
  useComposerSkillSuggestion,
} from "@o/pipeline/experimental/components/composer/skills";

Usage

Skill.Root is controlled. The host supplies the query from the active TipTap suggestion and forwards keyboard events through handleRef.

<Skill.Root
  items={skills}
  query={query}
  onSkillSelect={selectSkill}
  handleRef={skillListHandleRef}
>
  <Skill.Header>
    <Skill.HeaderTitle>Skills</Skill.HeaderTitle>
  </Skill.Header>
  <Skill.List>{(item) => <Skill.Item key={item.id} item={item} />}</Skill.List>
  <Skill.Empty>
    <Skill.EmptyMessage />
    <Skill.Create />
  </Skill.Empty>
</Skill.Root>

Mount the panel inside Composer.Editor, directly above Composer.EditorBody, so it sits on the composer’s own surface with the editor still visible and focused beneath it. Wrap it in a div whose onPointerDown calls preventDefault() so clicking a row never steals the editor’s focus or dissolves the active match.

Opening the picker

useComposerSkillSuggestion uses TipTap’s Suggestion utility. It opens after / at a block start or after whitespace, updates the controlled query, and exits when the match dissolves. matchRange() returns the active /query range; pass it to insertSkillChipAt so removing the typed trigger and inserting the chip land in one transaction. insertTrigger() lets a footer button insert / at the caret, with a leading space when necessary.

const { matchRange, insertTrigger } = useComposerSkillSuggestion({
  onStart: (query) => setSuggestion({ query, hiddenBy: null }),
  onUpdate: (query) => setSuggestion({ query, hiddenBy: null }),
  onExit: () => setSuggestion(null),
  onKeyDown: (event) => listHandleRef.current?.onKeyDown(event) ?? false,
});

An outside click should hide the host panel without disabling the plugin. Its next onUpdate then reopens the panel when the user resumes typing. Escape may remain hidden for the same match.

The picker currently uses a listbox rather than full combobox wiring. Associating the editor with the active item through aria-activedescendant is a known follow-up.

Chips

Register SkillChipExtension on Editor.Root. A selected chip is an inline atom inserted exactly where the trigger was typed: insertSkillChipAt(editor, matchRange() ?? collapsedSelection, attrs) replaces the /query text with the chip and a separator space. Selecting another skill adds another chip wherever its trigger was. getSkillChipsFromDoc returns every chip in document order at send time.

Pre-call briefCreate skillEdit skill: Pre-call brief