1+ import { CaretDown } from "@phosphor-icons/react" ;
12import { isUploadableSkillSource } from "@posthog/core/message-editor/skillTags" ;
23import { useHostTRPC } from "@posthog/host-router/react" ;
4+ import {
5+ Combobox ,
6+ ComboboxContent ,
7+ ComboboxEmpty ,
8+ ComboboxInput ,
9+ ComboboxItem ,
10+ ComboboxList ,
11+ ComboboxTrigger ,
12+ Button as QuillButton ,
13+ } from "@posthog/quill" ;
314import { SettingsOptionSelect } from "@posthog/ui/features/settings/SettingsOptionSelect" ;
415import { useHostCapabilities } from "@posthog/ui/shell/useHostCapabilities" ;
516import { TextArea } from "@radix-ui/themes" ;
617import { useQuery } from "@tanstack/react-query" ;
7- import { useRef } from "react" ;
18+ import { useRef , useState } from "react" ;
819import type { LoopFormValues } from "../loopFormTypes" ;
920import type { LoopSkillDraft } from "../loopSkill" ;
1021import { Field } from "./LoopFormPrimitives" ;
@@ -31,6 +42,89 @@ function pickableSkillLabel(
3142 return `${ skill . name } (${ origin } )` ;
3243}
3344
45+ interface SkillOption {
46+ value : string ;
47+ label : string ;
48+ }
49+
50+ /** Searchable skill picker, same quill Combobox shape as the repository picker
51+ * on the new-task page. Items are the (unique) labels so cmdk's filter matches
52+ * what the user sees. */
53+ function SkillCombobox ( {
54+ options,
55+ selectedValue,
56+ disabled,
57+ onValueChange,
58+ } : {
59+ options : SkillOption [ ] ;
60+ selectedValue : string ;
61+ disabled : boolean ;
62+ onValueChange : ( value : string ) => void ;
63+ } ) {
64+ const triggerRef = useRef < HTMLButtonElement > ( null ) ;
65+ const [ open , setOpen ] = useState ( false ) ;
66+ const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
67+ const selectedLabel =
68+ options . find ( ( option ) => option . value === selectedValue ) ?. label ?? null ;
69+
70+ return (
71+ < Combobox
72+ items = { options . map ( ( option ) => option . label ) }
73+ value = { selectedLabel }
74+ onValueChange = { ( label ) => {
75+ const picked = options . find ( ( option ) => option . label === label ) ;
76+ if ( picked ) {
77+ onValueChange ( picked . value ) ;
78+ }
79+ } }
80+ open = { open }
81+ onOpenChange = { ( nextOpen ) => {
82+ setOpen ( nextOpen ) ;
83+ if ( ! nextOpen ) {
84+ setSearchQuery ( "" ) ;
85+ }
86+ } }
87+ inputValue = { searchQuery }
88+ onInputValueChange = { setSearchQuery }
89+ disabled = { disabled }
90+ >
91+ < ComboboxTrigger
92+ render = {
93+ < QuillButton
94+ ref = { triggerRef }
95+ variant = "outline"
96+ size = "sm"
97+ disabled = { disabled }
98+ aria-label = "Skill"
99+ className = "w-full justify-between"
100+ >
101+ < span className = "min-w-0 truncate" >
102+ { selectedLabel ?? "Pick a skill…" }
103+ </ span >
104+ < CaretDown size = { 10 } weight = "bold" className = "shrink-0" />
105+ </ QuillButton >
106+ }
107+ />
108+ < ComboboxContent
109+ anchor = { triggerRef }
110+ side = "bottom"
111+ sideOffset = { 6 }
112+ className = "min-w-[280px]"
113+ >
114+ < ComboboxInput placeholder = "Search skills..." />
115+ < ComboboxEmpty > No skills found.</ ComboboxEmpty >
116+ < ComboboxList >
117+ { ( label : string ) => (
118+ < ComboboxItem key = { label } value = { label } >
119+ { label }
120+ </ ComboboxItem >
121+ ) }
122+ </ ComboboxList >
123+ </ ComboboxContent >
124+ </ Combobox >
125+ ) ;
126+ }
127+
34128export function LoopInstructionsFields ( {
35129 values,
36130 disabled,
@@ -148,12 +242,10 @@ export function LoopInstructionsFields({
148242 required
149243 hint = "The skill is snapshotted when you save; every run installs that snapshot into its sandbox."
150244 >
151- < SettingsOptionSelect
152- ariaLabel = "Skill"
153- value = { selectedValue }
245+ < SkillCombobox
154246 options = { skillOptions }
247+ selectedValue = { selectedValue }
155248 disabled = { disabled }
156- placeholder = "Pick a skill…"
157249 onValueChange = { handleSkillChange }
158250 />
159251 </ Field >
0 commit comments