fix(tui): complete absolute paths as paths

This commit is contained in:
helix4u 2026-05-04 16:31:20 -06:00 committed by Teknium
parent b632290166
commit b816fd4e26
2 changed files with 71 additions and 21 deletions

View 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()
})
})

View file

@ -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)