From 2a895311d93df8576a8f3d03642829674de06bc2 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Thu, 8 Jan 2026 20:19:33 +0200 Subject: [PATCH] feat: enhance autocomplete components with agent and command metadata support --- CHANGELOG.md | 6 +- .../chat/AgentMentionAutocomplete.tsx | 95 +++++++++++++----- .../components/chat/CommandAutocomplete.tsx | 97 ++++++++++++------- .../src/components/chat/SkillAutocomplete.tsx | 60 +++++++----- 4 files changed, 169 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 674be98e..31066d5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -- Added support for model variants (thinking effort). -- Switched agent cycling shortcut from TAB to Shift + TAB. +- Chat: Added support for model variants (thinking effort). +- Shortcuts: Switched agent cycling shortcut from TAB to Shift + TAB. +- Skills: added autocomplete for skills on "/" when it is not the first character in input. +- Autocomplete: added scope badges for commands/agents/skills. ## [1.4.4] - 2026-01-08 diff --git a/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx b/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx index a357eaa4..ec9cc094 100644 --- a/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx +++ b/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx @@ -1,12 +1,15 @@ import React from 'react'; import { cn } from '@/lib/utils'; import { useConfigStore } from '@/stores/useConfigStore'; +import { useAgentsStore } from '@/stores/useAgentsStore'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; interface AgentInfo { name: string; description?: string; mode?: string | null; + scope?: string; + isBuiltIn?: boolean; } export interface AgentMentionAutocompleteHandle { @@ -34,17 +37,30 @@ export const AgentMentionAutocomplete = React.forwardRef(null); const [selectedIndex, setSelectedIndex] = React.useState(0); const [agents, setAgents] = React.useState([]); + const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]); const { getVisibleAgents } = useConfigStore(); + const { agents: agentsWithMetadata, loadAgents } = useAgentsStore(); + + React.useEffect(() => { + if (agentsWithMetadata.length === 0) { + void loadAgents(); + } + }, [loadAgents, agentsWithMetadata.length]); React.useEffect(() => { const visibleAgents = getVisibleAgents(); const filtered = visibleAgents .filter((agent) => isMentionable(agent.mode)) - .map((agent) => ({ - name: agent.name, - description: agent.description, - mode: agent.mode ?? undefined, - })); + .map((agent) => { + const metadata = agentsWithMetadata.find(a => a.name === agent.name); + return { + name: agent.name, + description: agent.description, + mode: agent.mode ?? undefined, + scope: (metadata as any)?.scope, + isBuiltIn: (metadata as any)?.native || (metadata as any)?.builtIn, + }; + }); const normalizedQuery = searchQuery.trim().toLowerCase(); const matches = normalizedQuery.length @@ -57,6 +73,13 @@ export const AgentMentionAutocomplete = React.forwardRef { + itemRefs.current[selectedIndex]?.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }); + }, [selectedIndex]); + React.useEffect(() => { const handlePointerDown = (event: MouseEvent | TouchEvent) => { const target = event.target as Node | null; @@ -104,28 +127,50 @@ export const AgentMentionAutocomplete = React.forwardRef ( -
onAgentSelect(agent.name)} - onMouseEnter={() => setSelectedIndex(index)} -> -
-
- #{agent.name} -
- {agent.description && ( -
- {agent.description} -
+ const renderAgent = (agent: AgentInfo, index: number) => { + const isSystem = agent.isBuiltIn; + const isProject = agent.scope === 'project'; + + return ( +
{ + itemRefs.current[index] = el; + }} + className={cn( + 'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label', + index === selectedIndex && 'bg-muted' )} + onClick={() => onAgentSelect(agent.name)} + onMouseEnter={() => setSelectedIndex(index)} + > +
+
+ #{agent.name} + {isSystem ? ( + + system + + ) : agent.scope ? ( + + {agent.scope} + + ) : null} +
+ {agent.description && ( +
+ {agent.description} +
+ )} +
-
- ); + ); + }; return (
([]); const [loading, setLoading] = React.useState(false); + const { commands: commandsWithMetadata, loadCommands: refreshCommands } = useCommandsStore(); const [selectedIndex, setSelectedIndex] = React.useState(0); const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]); const containerRef = React.useRef(null); @@ -65,19 +68,22 @@ export const CommandAutocomplete = React.forwardRef { + // Force refresh to get latest project context when mounting + void refreshCommands(); + }, [refreshCommands]); + React.useEffect(() => { const loadCommands = async () => { setLoading(true); try { - - const apiCommands = await opencodeClient.listCommands(); - - const customCommands: CommandInfo[] = apiCommands.map(cmd => ({ + const customCommands: CommandInfo[] = commandsWithMetadata.map(cmd => ({ name: cmd.name, description: cmd.description, - agent: cmd.agent, - model: cmd.model, - isBuiltIn: false + agent: cmd.agent ?? undefined, + model: cmd.model ?? undefined, + isBuiltIn: cmd.name === 'init' || cmd.name === 'review', + scope: cmd.scope, })); const builtInCommands: CommandInfo[] = [ @@ -154,7 +160,7 @@ export const CommandAutocomplete = React.forwardRef { setSelectedIndex(0); @@ -236,37 +242,56 @@ export const CommandAutocomplete = React.forwardRef ) : (
- {commands.map((command, index) => ( -
{ itemRefs.current[index] = el; }} - className={cn( - "flex items-start gap-2 px-3 py-2 cursor-pointer rounded-lg", - index === selectedIndex && "bg-muted" - )} - onClick={() => onCommandSelect(command)} - onMouseEnter={() => setSelectedIndex(index)} - > -
- {getCommandIcon(command)} -
-
-
- /{command.name} - {command.agent && ( - - {command.agent} - + {commands.map((command, index) => { + const isSystem = command.isBuiltIn; + const isProject = command.scope === 'project'; + + return ( +
{ itemRefs.current[index] = el; }} + className={cn( + "flex items-start gap-2 px-3 py-2 cursor-pointer rounded-lg", + index === selectedIndex && "bg-muted" + )} + onClick={() => onCommandSelect(command)} + onMouseEnter={() => setSelectedIndex(index)} + > +
+ {getCommandIcon(command)} +
+
+
+ /{command.name} + {isSystem ? ( + + system + + ) : command.scope ? ( + + {command.scope} + + ) : null} + {command.agent && ( + + {command.agent} + + )} +
+ {command.description && ( +
+ {command.description} +
)}
- {command.description && ( -
- {command.description} -
- )}
-
- ))} + ); + })} {commands.length === 0 && (
No commands found diff --git a/packages/ui/src/components/chat/SkillAutocomplete.tsx b/packages/ui/src/components/chat/SkillAutocomplete.tsx index a77e5e8e..8893a21d 100644 --- a/packages/ui/src/components/chat/SkillAutocomplete.tsx +++ b/packages/ui/src/components/chat/SkillAutocomplete.tsx @@ -106,34 +106,42 @@ export const SkillAutocomplete = React.forwardRef ( -
{ - itemRefs.current[index] = el; - }} - className={cn( - 'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label', - index === selectedIndex && 'bg-muted' - )} - onClick={() => onSkillSelect(skill.name)} - onMouseEnter={() => setSelectedIndex(index)} - > -
-
- {skill.name} - - {skill.scope} - -
- {skill.description && ( -
- {skill.description} -
+ const renderSkill = (skill: SkillInfo, index: number) => { + const isProject = skill.scope === 'project'; + return ( +
{ + itemRefs.current[index] = el; + }} + className={cn( + 'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label', + index === selectedIndex && 'bg-muted' )} + onClick={() => onSkillSelect(skill.name)} + onMouseEnter={() => setSelectedIndex(index)} + > +
+
+ {skill.name} + + {skill.scope} + +
+ {skill.description && ( +
+ {skill.description} +
+ )} +
-
- ); + ); + }; return (