Fix commands ui

This commit is contained in:
Siddharth Ganesan
2026-01-13 10:09:58 -08:00
parent 4ee863a9ce
commit acb696207d
9 changed files with 126 additions and 23 deletions

View File

@@ -279,6 +279,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
onModeChange={setMode}
panelWidth={panelWidth}
clearOnSubmit={false}
initialContexts={message.contexts}
/>
{/* Inline Checkpoint Discard Confirmation - shown below input in edit mode */}

View File

@@ -497,6 +497,11 @@ const ACTION_VERBS = [
'Accessed',
'Managing',
'Managed',
'Scraping',
'Scraped',
'Crawling',
'Crawled',
'Getting',
] as const
/**
@@ -1160,7 +1165,7 @@ function SubAgentThinkingContent({
* Default behavior is to NOT collapse (stay expanded like edit).
* Only these specific subagents collapse into "Planned for Xs >" style headers.
*/
const COLLAPSIBLE_SUBAGENTS = new Set(['plan', 'debug', 'research', 'info'])
const COLLAPSIBLE_SUBAGENTS = new Set(['plan', 'debug', 'research', 'info', 'superagent'])
/**
* SubagentContentRenderer handles the rendering of subagent content.
@@ -1968,6 +1973,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
'tour',
'info',
'workflow',
'superagent'
]
const isSubagentTool = SUBAGENT_TOOLS.includes(toolCall.name)

View File

@@ -21,6 +21,7 @@ const TOP_LEVEL_COMMANDS = [
{ id: 'fast', label: 'fast' },
{ id: 'superagent', label: 'superagent' },
{ id: 'deploy', label: 'deploy' },
{ id: 'research', label: 'research' },
] as const
/**
@@ -28,10 +29,9 @@ const TOP_LEVEL_COMMANDS = [
*/
const WEB_COMMANDS = [
{ id: 'search', label: 'search' },
{ id: 'research', label: 'research' },
{ id: 'crawl', label: 'crawl' },
{ id: 'read', label: 'read' },
{ id: 'scrape', label: 'scrape' },
{ id: 'crawl', label: 'crawl' },
] as const
/**

View File

@@ -1,9 +1,11 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import type { ChatContext } from '@/stores/panel'
interface UseContextManagementProps {
/** Current message text */
message: string
/** Initial contexts to populate when editing a message */
initialContexts?: ChatContext[]
}
/**
@@ -13,8 +15,17 @@ interface UseContextManagementProps {
* @param props - Configuration object
* @returns Context state and management functions
*/
export function useContextManagement({ message }: UseContextManagementProps) {
const [selectedContexts, setSelectedContexts] = useState<ChatContext[]>([])
export function useContextManagement({ message, initialContexts }: UseContextManagementProps) {
const [selectedContexts, setSelectedContexts] = useState<ChatContext[]>(initialContexts ?? [])
const initializedRef = useRef(false)
// Initialize with initial contexts when they're first provided (for edit mode)
useEffect(() => {
if (initialContexts && initialContexts.length > 0 && !initializedRef.current) {
setSelectedContexts(initialContexts)
initializedRef.current = true
}
}, [initialContexts])
/**
* Adds a context to the selected contexts list, avoiding duplicates
@@ -140,8 +151,10 @@ export function useContextManagement({ message }: UseContextManagementProps) {
// Check for slash command tokens or mention tokens based on kind
const isSlashCommand = c.kind === 'slash_command'
const prefix = isSlashCommand ? '/' : '@'
const token = ` ${prefix}${c.label} `
return message.includes(token)
const tokenWithSpaces = ` ${prefix}${c.label} `
const tokenAtStart = `${prefix}${c.label} `
// Token can appear with leading space OR at the start of the message
return message.includes(tokenWithSpaces) || message.startsWith(tokenAtStart)
})
return filtered.length === prev.length ? prev : filtered
})

View File

@@ -130,11 +130,25 @@ export function useMentionMenu({
// Ensure '/' starts a token (start or whitespace before)
if (slashIndex > 0 && !/\s/.test(before.charAt(slashIndex - 1))) return null
// Check if this '/' is part of a completed slash token ( /command )
// Check if this '/' is part of a completed slash token
if (selectedContexts.length > 0) {
const labels = selectedContexts.map((c) => c.label).filter(Boolean) as string[]
for (const label of labels) {
// Space-wrapped token: " /label "
// Only check slash_command contexts
const slashLabels = selectedContexts
.filter((c) => c.kind === 'slash_command')
.map((c) => c.label)
.filter(Boolean) as string[]
for (const label of slashLabels) {
// Check for token at start of text: "/label "
if (slashIndex === 0) {
const startToken = `/${label} `
if (text.startsWith(startToken)) {
// This slash is part of a completed token
return null
}
}
// Check for space-wrapped token: " /label "
const token = ` /${label} `
let fromIndex = 0
while (fromIndex <= text.length) {
@@ -256,9 +270,10 @@ export function useMentionMenu({
const before = message.slice(0, active.start)
const after = message.slice(active.end)
// Always include leading space, avoid duplicate if one exists
const needsLeadingSpace = !before.endsWith(' ')
const insertion = `${needsLeadingSpace ? ' ' : ''}@${label} `
// Add leading space only if not at start and previous char isn't whitespace
const needsLeadingSpace = before.length > 0 && !before.endsWith(' ')
// Always add trailing space for easy continued typing
const insertion = `${needsLeadingSpace ? ' ' : ''}@${label} `
const next = `${before}${insertion}${after}`
onMessageChange(next)
@@ -290,9 +305,10 @@ export function useMentionMenu({
const before = message.slice(0, active.start)
const after = message.slice(active.end)
// Always include leading space, avoid duplicate if one exists
const needsLeadingSpace = !before.endsWith(' ')
const insertion = `${needsLeadingSpace ? ' ' : ''}/${label} `
// Add leading space only if not at start and previous char isn't whitespace
const needsLeadingSpace = before.length > 0 && !before.endsWith(' ')
// Always add trailing space for easy continued typing
const insertion = `${needsLeadingSpace ? ' ' : ''}/${label} `
const next = `${before}${insertion}${after}`
onMessageChange(next)

View File

@@ -60,6 +60,12 @@ export function useMentionTokens({
const isSlashCommand = matchingContext?.kind === 'slash_command'
const prefix = isSlashCommand ? '/' : '@'
// Check for token at the very start of the message (no leading space)
const tokenAtStart = `${prefix}${label} `
if (message.startsWith(tokenAtStart)) {
ranges.push({ start: 0, end: tokenAtStart.length, label })
}
// Space-wrapped token: " @label " or " /label " (search from start)
const token = ` ${prefix}${label} `
let fromIndex = 0

View File

@@ -68,6 +68,8 @@ interface UserInputProps {
hideModeSelector?: boolean
/** Disable @mention functionality */
disableMentions?: boolean
/** Initial contexts for editing a message with existing context mentions */
initialContexts?: ChatContext[]
}
interface UserInputRef {
@@ -104,6 +106,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
onModelChangeOverride,
hideModeSelector = false,
disableMentions = false,
initialContexts,
},
ref
) => {
@@ -142,7 +145,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
// Custom hooks - order matters for ref sharing
// Context management (manages selectedContexts state)
const contextManagement = useContextManagement({ message })
const contextManagement = useContextManagement({ message, initialContexts })
// Mention menu
const mentionMenu = useMentionMenu({
@@ -410,8 +413,8 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
// Arrow navigation in slash menu
if (showSlashMenu) {
const TOP_LEVEL_COMMANDS = ['plan', 'debug', 'fast', 'superagent', 'deploy']
const WEB_COMMANDS = ['search', 'research', 'crawl', 'read', 'scrape']
const TOP_LEVEL_COMMANDS = ['plan', 'debug', 'fast', 'superagent', 'deploy', 'research']
const WEB_COMMANDS = ['search', 'read', 'scrape', 'crawl']
const ALL_COMMANDS = [...TOP_LEVEL_COMMANDS, ...WEB_COMMANDS]
const caretPos = mentionMenu.getCaretPos()
@@ -488,8 +491,8 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
e.preventDefault()
if (showSlashMenu) {
const TOP_LEVEL_COMMANDS = ['plan', 'debug', 'fast', 'superagent', 'deploy']
const WEB_COMMANDS = ['search', 'research', 'crawl', 'read', 'scrape']
const TOP_LEVEL_COMMANDS = ['plan', 'debug', 'fast', 'superagent', 'deploy', 'research']
const WEB_COMMANDS = ['search', 'read', 'scrape', 'crawl']
const ALL_COMMANDS = [...TOP_LEVEL_COMMANDS, ...WEB_COMMANDS]
const caretPos = mentionMenu.getCaretPos()

View File

@@ -18,6 +18,7 @@ import './other/make-api-request'
import './other/plan'
import './other/research'
import './other/sleep'
import './other/superagent'
import './other/test'
import './other/tour'
import './other/workflow'

View File

@@ -0,0 +1,57 @@
import { Loader2, Sparkles, XCircle } from 'lucide-react'
import {
BaseClientTool,
type BaseClientToolMetadata,
ClientToolCallState,
} from '@/lib/copilot/tools/client/base-tool'
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
interface SuperagentArgs {
instruction: string
}
/**
* Superagent tool that spawns a powerful subagent for complex tasks.
* This tool auto-executes and the actual work is done by the superagent.
* The subagent's output is streamed as nested content under this tool call.
*/
export class SuperagentClientTool extends BaseClientTool {
static readonly id = 'superagent'
constructor(toolCallId: string) {
super(toolCallId, SuperagentClientTool.id, SuperagentClientTool.metadata)
}
static readonly metadata: BaseClientToolMetadata = {
displayNames: {
[ClientToolCallState.generating]: { text: 'Superagent working', icon: Loader2 },
[ClientToolCallState.pending]: { text: 'Superagent working', icon: Loader2 },
[ClientToolCallState.executing]: { text: 'Superagent working', icon: Loader2 },
[ClientToolCallState.success]: { text: 'Superagent completed', icon: Sparkles },
[ClientToolCallState.error]: { text: 'Superagent failed', icon: XCircle },
[ClientToolCallState.rejected]: { text: 'Superagent skipped', icon: XCircle },
[ClientToolCallState.aborted]: { text: 'Superagent aborted', icon: XCircle },
},
uiConfig: {
subagent: {
streamingLabel: 'Superagent working',
completedLabel: 'Superagent completed',
shouldCollapse: true,
outputArtifacts: [],
},
},
}
/**
* Execute the superagent tool.
* This just marks the tool as executing - the actual work is done server-side
* by the superagent, and its output is streamed as subagent events.
*/
async execute(_args?: SuperagentArgs): Promise<void> {
this.setState(ClientToolCallState.executing)
}
}
// Register UI config at module load
registerToolUIConfig(SuperagentClientTool.id, SuperagentClientTool.metadata.uiConfig!)