Skip to content

Commit 1264304

Browse files
Merge pull request #3465 from Kilo-Org/replace-input-on-autocomplete
Replace input on autocomplete
2 parents 25343fc + bd0d51e commit 1264304

File tree

3 files changed

+149
-40
lines changed

3 files changed

+149
-40
lines changed

.changeset/olive-ads-cover.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@kilocode/cli": patch
3+
---
4+
5+
improves autocomplete behavior

cli/src/state/atoms/__tests__/keyboard.test.ts

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,12 +354,64 @@ describe("keypress atoms", () => {
354354
}
355355
store.set(keyboardHandlerAtom, tabKey)
356356

357-
// Should append only 'de' to complete '/mode'
357+
// Should complete to '/mode'
358358
const text = store.get(textBufferStringAtom)
359359
expect(text).toBe("/mode")
360360
})
361361

362-
it("should complete argument by appending only missing part", () => {
362+
it("should complete command even when user types wrong letters", () => {
363+
// Type '/modl' - typo, but 'model' should still be suggested
364+
const chars = ["/", "m", "o", "d", "l"]
365+
for (const char of chars) {
366+
const key: Key = {
367+
name: char,
368+
sequence: char,
369+
ctrl: false,
370+
meta: false,
371+
shift: false,
372+
paste: false,
373+
}
374+
store.set(keyboardHandlerAtom, key)
375+
}
376+
377+
// Autocomplete should now be visible
378+
expect(store.get(showAutocompleteAtom)).toBe(true)
379+
380+
// Set up autocomplete suggestions
381+
const mockCommand: Command = {
382+
name: "model",
383+
description: "Manage models",
384+
aliases: [],
385+
usage: "/model <subcommand>",
386+
examples: ["/model info"],
387+
category: "settings",
388+
handler: vi.fn(),
389+
}
390+
const mockSuggestion: CommandSuggestion = {
391+
command: mockCommand,
392+
matchScore: 70,
393+
highlightedName: "model",
394+
}
395+
store.set(suggestionsAtom, [mockSuggestion])
396+
store.set(selectedIndexAtom, 0)
397+
398+
// Press Tab
399+
const tabKey: Key = {
400+
name: "tab",
401+
sequence: "\t",
402+
ctrl: false,
403+
meta: false,
404+
shift: false,
405+
paste: false,
406+
}
407+
store.set(keyboardHandlerAtom, tabKey)
408+
409+
// Should replace '/modl' with '/model' (not '/modlmodel')
410+
const text = store.get(textBufferStringAtom)
411+
expect(text).toBe("/model")
412+
})
413+
414+
it("should complete argument by replacing partial text", () => {
363415
// Type '/mode tes' - this will automatically trigger autocomplete
364416
const input = "/mode tes"
365417
for (const char of input) {
@@ -398,11 +450,96 @@ describe("keypress atoms", () => {
398450
}
399451
store.set(keyboardHandlerAtom, tabKey)
400452

401-
// Should append only 't' to complete '/mode test'
453+
// Should replace 'tes' with 'test' to complete '/mode test'
402454
const text = store.get(textBufferStringAtom)
403455
expect(text).toBe("/mode test")
404456
})
405457

458+
it("should replace partial argument with full suggestion", () => {
459+
// Bug fix: Type '/model info gpt' with suggestion 'openai/gpt-5'
460+
// This test verifies the fix where Tab was incorrectly appending instead of replacing
461+
const input = "/model info gpt"
462+
for (const char of input) {
463+
const key: Key = {
464+
name: char,
465+
sequence: char,
466+
ctrl: false,
467+
meta: false,
468+
shift: false,
469+
paste: false,
470+
}
471+
store.set(keyboardHandlerAtom, key)
472+
}
473+
474+
// Set up argument suggestions
475+
const mockArgumentSuggestion: ArgumentSuggestion = {
476+
value: "openai/gpt-5",
477+
description: "OpenAI GPT-5 model",
478+
matchScore: 90,
479+
highlightedValue: "openai/gpt-5",
480+
}
481+
store.set(argumentSuggestionsAtom, [mockArgumentSuggestion])
482+
store.set(suggestionsAtom, []) // No command suggestions
483+
store.set(selectedIndexAtom, 0)
484+
485+
// Press Tab
486+
const tabKey: Key = {
487+
name: "tab",
488+
sequence: "\t",
489+
ctrl: false,
490+
meta: false,
491+
shift: false,
492+
paste: false,
493+
}
494+
store.set(keyboardHandlerAtom, tabKey)
495+
496+
// Should replace 'gpt' with 'openai/gpt-5' (not append to get 'gptopenai/gpt-5')
497+
const text = store.get(textBufferStringAtom)
498+
expect(text).toBe("/model info openai/gpt-5")
499+
})
500+
501+
it("should complete argument from empty with trailing space", () => {
502+
// Type '/model info ' (with trailing space)
503+
const input = "/model info "
504+
for (const char of input) {
505+
const key: Key = {
506+
name: char,
507+
sequence: char,
508+
ctrl: false,
509+
meta: false,
510+
shift: false,
511+
paste: false,
512+
}
513+
store.set(keyboardHandlerAtom, key)
514+
}
515+
516+
// Set up argument suggestions
517+
const mockArgumentSuggestion: ArgumentSuggestion = {
518+
value: "openai/gpt-4",
519+
description: "OpenAI GPT-4 model",
520+
matchScore: 100,
521+
highlightedValue: "openai/gpt-4",
522+
}
523+
store.set(argumentSuggestionsAtom, [mockArgumentSuggestion])
524+
store.set(suggestionsAtom, [])
525+
store.set(selectedIndexAtom, 0)
526+
527+
// Press Tab
528+
const tabKey: Key = {
529+
name: "tab",
530+
sequence: "\t",
531+
ctrl: false,
532+
meta: false,
533+
shift: false,
534+
paste: false,
535+
}
536+
store.set(keyboardHandlerAtom, tabKey)
537+
538+
// Should add the full suggestion value
539+
const text = store.get(textBufferStringAtom)
540+
expect(text).toBe("/model info openai/gpt-4")
541+
})
542+
406543
it("should handle exact match completion", () => {
407544
// Type '/help' - this will automatically trigger autocomplete
408545
const input = "/help"

cli/src/state/atoms/keyboard.ts

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -307,38 +307,6 @@ export const submitInputAtom = atom(null, (get, set, text: string | Buffer) => {
307307
// Keyboard Handler System
308308
// ============================================================================
309309

310-
/**
311-
* Helper function to get the completion text (only the missing part to append)
312-
*/
313-
function getCompletionText(currentInput: string, suggestion: CommandSuggestion | ArgumentSuggestion): string {
314-
if ("command" in suggestion) {
315-
// CommandSuggestion - complete the command name
316-
const commandName = suggestion.command.name
317-
const currentText = currentInput.startsWith("/") ? currentInput.slice(1) : currentInput
318-
319-
// If the command name starts with what user typed, return only the missing part
320-
if (commandName.toLowerCase().startsWith(currentText.toLowerCase())) {
321-
return commandName.slice(currentText.length)
322-
}
323-
324-
// Otherwise return the full command (shouldn't happen in normal flow)
325-
return commandName
326-
} else {
327-
// ArgumentSuggestion - complete the last argument
328-
const parts = currentInput.split(" ")
329-
const lastPart = parts[parts.length - 1] || ""
330-
const suggestionValue = suggestion.value
331-
332-
// If suggestion starts with what user typed, return only the missing part
333-
if (suggestionValue.toLowerCase().startsWith(lastPart.toLowerCase())) {
334-
return suggestionValue.slice(lastPart.length)
335-
}
336-
337-
// Otherwise return the full value
338-
return suggestionValue
339-
}
340-
}
341-
342310
/**
343311
* Helper function to format autocomplete suggestions for display/submission
344312
*/
@@ -487,11 +455,10 @@ function handleAutocompleteKeys(get: any, set: any, key: Key): void {
487455
const suggestion = allSuggestions[selectedIndex]
488456
const currentText = get(textBufferStringAtom)
489457

490-
// Get only the missing part to append
491-
const completionText = getCompletionText(currentText, suggestion)
492-
493-
// Insert the completion text
494-
set(insertTextAtom, completionText)
458+
// Always replace the entire input with formatted suggestion
459+
// This handles both commands and arguments correctly
460+
const newText = formatSuggestion(suggestion, currentText)
461+
set(setTextAtom, newText)
495462
}
496463
return
497464

0 commit comments

Comments
 (0)