Skip to content

Commit fc65862

Browse files
Align command suggestions with file suggestions and support command arguments
1 parent 6828b66 commit fc65862

File tree

4 files changed

+78
-74
lines changed

4 files changed

+78
-74
lines changed

frontend/src/components/command/CommandSuggestions.tsx

Lines changed: 42 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,34 @@ export function CommandSuggestions({
1818
query,
1919
commands,
2020
onSelect,
21+
onClose,
2122
selectedIndex = 0
2223
}: CommandSuggestionsProps) {
23-
const containerRef = useRef<HTMLDivElement>(null)
24+
const listRef = useRef<HTMLDivElement>(null)
2425

2526
const filteredCommands = commands.filter(command =>
26-
command.name.toLowerCase().includes(query.toLowerCase()) ||
27-
command.description?.toLowerCase().includes(query.toLowerCase())
27+
command.name.toLowerCase().includes(query.toLowerCase())
2828
)
2929

30-
30+
useEffect(() => {
31+
if (!isOpen) return
32+
33+
const handleClickOutside = (e: MouseEvent) => {
34+
if (listRef.current && !listRef.current.contains(e.target as Node)) {
35+
onClose()
36+
}
37+
}
3138

32-
39+
document.addEventListener('mousedown', handleClickOutside)
40+
return () => document.removeEventListener('mousedown', handleClickOutside)
41+
}, [isOpen, onClose])
3342

34-
// Scroll selected item into view
3543
useEffect(() => {
36-
if (!isOpen || !containerRef.current) return
44+
if (!isOpen || !listRef.current) return
3745

38-
const selectedItem = containerRef.current.querySelector(`[data-selected="true"]`) as HTMLElement
46+
const selectedItem = listRef.current.children[selectedIndex] as HTMLElement
3947
if (selectedItem) {
40-
selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
48+
selectedItem.scrollIntoView({ block: 'nearest' })
4149
}
4250
}, [selectedIndex, isOpen])
4351

@@ -47,45 +55,33 @@ export function CommandSuggestions({
4755

4856
return (
4957
<div
50-
ref={containerRef}
51-
className="absolute bottom-full left-0 right-0 mb-2 z-50 bg-background border border-border rounded-lg shadow-lg max-h-64 overflow-y-auto"
58+
ref={listRef}
59+
className="absolute bottom-full left-0 right-0 mb-2 z-50 bg-background border border-border rounded-lg shadow-xl max-h-[30vh] md:max-h-[40vh] lg:max-h-[50vh] overflow-y-auto"
5260
>
53-
<div className="p-1">
54-
{filteredCommands.map((command, index) => {
55-
const isSelected = index === selectedIndex
56-
const displayName = `/${command.name}`
57-
58-
return (
59-
<div
60-
key={command.name}
61-
data-selected={isSelected}
62-
className={`px-3 py-2 cursor-pointer rounded-md transition-colors flex items-center gap-2 ${
63-
isSelected
64-
? 'bg-primary/20 text-foreground'
65-
: 'hover:bg-muted text-muted-foreground'
66-
}`}
67-
onClick={() => onSelect(command)}
68-
69-
>
70-
<Command className="h-4 w-4 text-muted-foreground flex-shrink-0" />
71-
<div className="flex-1 min-w-0">
72-
<div className="font-medium text-sm truncate">{displayName}</div>
73-
{command.description && (
74-
<div className="text-xs text-muted-foreground truncate">{command.description}</div>
75-
)}
76-
</div>
61+
{filteredCommands.map((command, index) => {
62+
const isSelected = index === selectedIndex
63+
const displayName = `/${command.name}`
64+
65+
return (
66+
<button
67+
key={command.name}
68+
onClick={() => onSelect(command)}
69+
className={`w-full px-3 py-2 text-left transition-colors flex items-center gap-2 ${
70+
isSelected
71+
? 'bg-primary text-primary-foreground'
72+
: 'hover:bg-muted text-foreground'
73+
}`}
74+
>
75+
<Command className={`h-4 w-4 flex-shrink-0 ${isSelected ? 'opacity-90' : 'opacity-70'}`} />
76+
<div className="flex-1 min-w-0">
77+
<div className="font-mono text-sm font-medium truncate">{displayName}</div>
78+
{command.description && (
79+
<div className="text-xs opacity-70 mt-0.5 truncate">{command.description}</div>
80+
)}
7781
</div>
78-
)
79-
})}
80-
</div>
81-
82-
<div className="px-3 py-2 border-t border-border text-xs text-muted-foreground">
83-
<div className="flex items-center gap-4">
84-
<span>↑↓ Navigate</span>
85-
<span>↵ Select</span>
86-
<span>Esc Close</span>
87-
</div>
88-
</div>
82+
</button>
83+
)
84+
})}
8985
</div>
9086
)
9187
}

frontend/src/components/message/PromptInput.tsx

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,11 @@ export function PromptInput({
109109

110110
const commandMatch = prompt.match(/^\/([a-zA-Z0-9_-]+)(?:\s+(.*))?$/)
111111
if (commandMatch) {
112-
const [, commandName] = commandMatch
112+
const [, commandName, commandArgs] = commandMatch
113113
const command = filterCommands(commandName)[0]
114114

115115
if (command) {
116-
117-
executeCommand(command)
116+
executeCommand(command, commandArgs?.trim() || '')
118117
setPrompt('')
119118
if (textareaRef.current) {
120119
textareaRef.current.style.height = 'auto'
@@ -149,23 +148,41 @@ export function PromptInput({
149148
setShowSuggestions(false)
150149
setSuggestionQuery('')
151150

152-
const cursorPosition = textareaRef.current.selectionStart
153-
const commandMatch = prompt.slice(0, cursorPosition).match(/(^|\s)\/([a-zA-Z0-9_-]*)$/)
154-
155-
if (commandMatch) {
156-
const beforeCommand = prompt.slice(0, commandMatch.index)
157-
const afterCommand = prompt.slice(cursorPosition)
158-
const newPrompt = beforeCommand + '/' + command.name + ' ' + afterCommand
151+
if (command.template) {
152+
const cleanedTemplate = command.template
153+
.replace(/\$ARGUMENTS/g, '')
154+
.replace(/\$\d+/g, '')
155+
.trim()
159156

160-
setPrompt(newPrompt)
157+
setPrompt(cleanedTemplate)
161158

162159
setTimeout(() => {
163160
if (textareaRef.current) {
164-
const newCursorPos = beforeCommand.length + command.name.length + 2
165161
textareaRef.current.focus()
166-
textareaRef.current.setSelectionRange(newCursorPos, newCursorPos)
162+
textareaRef.current.setSelectionRange(cleanedTemplate.length, cleanedTemplate.length)
163+
textareaRef.current.style.height = 'auto'
164+
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`
167165
}
168166
}, 0)
167+
} else {
168+
const cursorPosition = textareaRef.current.selectionStart
169+
const commandMatch = prompt.slice(0, cursorPosition).match(/(^|\s)\/([a-zA-Z0-9_-]*)$/)
170+
171+
if (commandMatch) {
172+
const beforeCommand = prompt.slice(0, commandMatch.index)
173+
const afterCommand = prompt.slice(cursorPosition)
174+
const newPrompt = beforeCommand + '/' + command.name + ' ' + afterCommand
175+
176+
setPrompt(newPrompt)
177+
178+
setTimeout(() => {
179+
if (textareaRef.current) {
180+
const newCursorPos = beforeCommand.length + command.name.length + 2
181+
textareaRef.current.focus()
182+
textareaRef.current.setSelectionRange(newCursorPos, newCursorPos)
183+
}
184+
}, 0)
185+
}
169186
}
170187
}
171188

frontend/src/hooks/useCommandHandler.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,14 @@ export function useCommandHandler({
2727
const createSession = useCreateSession(opcodeUrl, directory)
2828
const [loading, setLoading] = useState(false)
2929

30-
const executeCommand = useCallback(async (command: CommandType) => {
30+
const executeCommand = useCallback(async (command: CommandType, args: string = '') => {
3131
if (!opcodeUrl) return
3232

3333
setLoading(true)
3434

3535
try {
3636
const client = createOpenCodeClient(opcodeUrl, directory)
3737

38-
// Handle special commands that need UI interaction
3938
switch (command.name) {
4039
case 'sessions':
4140
case 'resume':
@@ -48,35 +47,30 @@ export function useCommandHandler({
4847
break
4948

5049
case 'themes':
51-
// Themes command will be sent to server and appear as message
5250
await client.sendCommand(sessionID, {
5351
command: command.name,
54-
arguments: ''
52+
arguments: args
5553
})
5654
break
5755

5856
case 'help':
5957
onShowHelpDialog?.()
6058
break
6159

62-
case 'new':
60+
case 'new':
6361
case 'clear':
64-
// Create a new session and navigate to it
6562
try {
6663
const newSession = await createSession.mutateAsync({
6764
agent: undefined
6865
})
6966
if (newSession?.id) {
70-
// Navigate to the correct repo session URL pattern
71-
// We need to get the current repo ID from the URL
7267
const currentPath = window.location.pathname
7368
const repoMatch = currentPath.match(/\/repos\/(\d+)\/sessions\//)
7469
if (repoMatch) {
7570
const repoId = repoMatch[1]
7671
const newPath = `/repos/${repoId}/sessions/${newSession.id}`
7772
navigate(newPath)
7873
} else {
79-
// Fallback: try to navigate to session directly if route exists
8074
navigate(`/session/${newSession.id}`)
8175
}
8276
}
@@ -95,18 +89,16 @@ case 'new':
9589
case 'details':
9690
case 'editor':
9791
case 'init':
98-
// These commands will be sent to the server and appear as messages
9992
await client.sendCommand(sessionID, {
10093
command: command.name,
101-
arguments: ''
94+
arguments: args
10295
})
10396
break
10497

10598
default:
106-
// Send custom commands to server
10799
await client.sendCommand(sessionID, {
108100
command: command.name,
109-
arguments: ''
101+
arguments: args
110102
})
111103
}
112104
} catch (error) {

frontend/src/hooks/useCommands.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,7 @@ export function useCommands(opcodeUrl: string | null) {
189189

190190
const searchTerm = query.toLowerCase()
191191
return commands.filter(command =>
192-
command.name.toLowerCase().includes(searchTerm) ||
193-
command.description?.toLowerCase().includes(searchTerm)
192+
command.name.toLowerCase().includes(searchTerm)
194193
)
195194
}
196195

0 commit comments

Comments
 (0)