mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-05 02:31:47 +00:00
fix(tui): complete absolute paths as paths
This commit is contained in:
parent
b632290166
commit
b816fd4e26
2 changed files with 71 additions and 21 deletions
35
ui-tui/src/__tests__/useCompletion.test.ts
Normal file
35
ui-tui/src/__tests__/useCompletion.test.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { completionRequestForInput } from '../hooks/useCompletion.js'
|
||||
|
||||
describe('completionRequestForInput', () => {
|
||||
it('routes real slash commands to slash completion', () => {
|
||||
expect(completionRequestForInput('/help')).toMatchObject({
|
||||
method: 'complete.slash',
|
||||
params: { text: '/help' },
|
||||
replaceFrom: 1
|
||||
})
|
||||
})
|
||||
|
||||
it('does not route absolute paths through slash completion', () => {
|
||||
expect(
|
||||
completionRequestForInput('/home/d/Desktop/agenda/CrimsonRed/.hermes/plans/2026-05-04-HANDOFF-NEXT.md')
|
||||
).toMatchObject({
|
||||
method: 'complete.path',
|
||||
params: { word: '/home/d/Desktop/agenda/CrimsonRed/.hermes/plans/2026-05-04-HANDOFF-NEXT.md' },
|
||||
replaceFrom: 0
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps path completion for trailing absolute path tokens', () => {
|
||||
expect(completionRequestForInput('read /home/d/Desktop/file.md')).toMatchObject({
|
||||
method: 'complete.path',
|
||||
params: { word: '/home/d/Desktop/file.md' },
|
||||
replaceFrom: 5
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves plain text alone', () => {
|
||||
expect(completionRequestForInput('hello there')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,12 +1,43 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import type { CompletionItem } from '../app/interfaces.js'
|
||||
import { looksLikeSlashCommand } from '../domain/slash.js'
|
||||
import type { GatewayClient } from '../gatewayClient.js'
|
||||
import type { CompletionResponse } from '../gatewayTypes.js'
|
||||
import { asRpcResult } from '../lib/rpc.js'
|
||||
|
||||
const TAB_PATH_RE = /((?:["']?(?:[A-Za-z]:[\\/]|\.{1,2}\/|~\/|\/|@|[^"'`\s]+\/))[^\s]*)$/
|
||||
|
||||
export function completionRequestForInput(
|
||||
input: string
|
||||
):
|
||||
| { method: 'complete.path'; params: { word: string }; replaceFrom: number }
|
||||
| { method: 'complete.slash'; params: { text: string }; replaceFrom: number }
|
||||
| null {
|
||||
const isSlashCommand = looksLikeSlashCommand(input)
|
||||
const pathWord = isSlashCommand ? null : (input.match(TAB_PATH_RE)?.[1] ?? null)
|
||||
|
||||
if (!isSlashCommand && !pathWord) {
|
||||
return null
|
||||
}
|
||||
|
||||
// `/model` / `/provider` use the two-step ModelPicker (real curated IDs).
|
||||
// Slash completion here only showed short aliases + vendor/family meta.
|
||||
if (isSlashCommand && /^\/(?:model|provider)(?:\s|$)/.test(input)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isSlashCommand) {
|
||||
return { method: 'complete.slash', params: { text: input }, replaceFrom: 1 }
|
||||
}
|
||||
|
||||
return {
|
||||
method: 'complete.path',
|
||||
params: { word: pathWord! },
|
||||
replaceFrom: input.length - pathWord!.length
|
||||
}
|
||||
}
|
||||
|
||||
export function useCompletion(input: string, blocked: boolean, gw: GatewayClient) {
|
||||
const [completions, setCompletions] = useState<CompletionItem[]>([])
|
||||
const [compIdx, setCompIdx] = useState(0)
|
||||
|
|
@ -33,35 +64,19 @@ export function useCompletion(input: string, blocked: boolean, gw: GatewayClient
|
|||
|
||||
ref.current = input
|
||||
|
||||
const isSlash = input.startsWith('/')
|
||||
const pathWord = isSlash ? null : (input.match(TAB_PATH_RE)?.[1] ?? null)
|
||||
|
||||
if (!isSlash && !pathWord) {
|
||||
const request = completionRequestForInput(input)
|
||||
if (!request) {
|
||||
clear()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// `/model` / `/provider` use the two-step ModelPicker (real curated IDs).
|
||||
// Slash completion here only showed short aliases + vendor/family meta.
|
||||
if (isSlash && /^\/(?:model|provider)(?:\s|$)/.test(input)) {
|
||||
clear()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const pathReplace = input.length - (pathWord?.length ?? 0)
|
||||
|
||||
const t = setTimeout(() => {
|
||||
if (ref.current !== input) {
|
||||
return
|
||||
}
|
||||
|
||||
const req = isSlash
|
||||
? gw.request<CompletionResponse>('complete.slash', { text: input })
|
||||
: gw.request<CompletionResponse>('complete.path', { word: pathWord })
|
||||
|
||||
req
|
||||
gw.request<CompletionResponse>(request.method, request.params)
|
||||
.then(raw => {
|
||||
if (ref.current !== input) {
|
||||
return
|
||||
|
|
@ -71,7 +86,7 @@ export function useCompletion(input: string, blocked: boolean, gw: GatewayClient
|
|||
|
||||
setCompletions(r?.items ?? [])
|
||||
setCompIdx(0)
|
||||
setCompReplace(isSlash ? (r?.replace_from ?? 1) : pathReplace)
|
||||
setCompReplace(request.method === 'complete.slash' ? (r?.replace_from ?? 1) : request.replaceFrom)
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
if (ref.current !== input) {
|
||||
|
|
@ -86,7 +101,7 @@ export function useCompletion(input: string, blocked: boolean, gw: GatewayClient
|
|||
}
|
||||
])
|
||||
setCompIdx(0)
|
||||
setCompReplace(isSlash ? 1 : pathReplace)
|
||||
setCompReplace(request.replaceFrom)
|
||||
})
|
||||
}, 60)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue