mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 16:46:58 +10:00
120 lines
3.4 KiB
TypeScript
120 lines
3.4 KiB
TypeScript
|
|
import { getHistory } from '../../history.js'
|
|||
|
|
import { logForDebugging } from '../debug.js'
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Result of shell history completion lookup
|
|||
|
|
*/
|
|||
|
|
export type ShellHistoryMatch = {
|
|||
|
|
/** The full command from history */
|
|||
|
|
fullCommand: string
|
|||
|
|
/** The suffix to display as ghost text (the part after user's input) */
|
|||
|
|
suffix: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Cache for shell history commands to avoid repeated async reads
|
|||
|
|
// History only changes when user submits a command, so a long TTL is fine
|
|||
|
|
let shellHistoryCache: string[] | null = null
|
|||
|
|
let shellHistoryCacheTimestamp = 0
|
|||
|
|
const CACHE_TTL_MS = 60000 // 60 seconds - history won't change while typing
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Get shell commands from history, with caching
|
|||
|
|
*/
|
|||
|
|
async function getShellHistoryCommands(): Promise<string[]> {
|
|||
|
|
const now = Date.now()
|
|||
|
|
|
|||
|
|
// Return cached result if still fresh
|
|||
|
|
if (shellHistoryCache && now - shellHistoryCacheTimestamp < CACHE_TTL_MS) {
|
|||
|
|
return shellHistoryCache
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const commands: string[] = []
|
|||
|
|
const seen = new Set<string>()
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// Read history entries and filter for bash commands
|
|||
|
|
for await (const entry of getHistory()) {
|
|||
|
|
if (entry.display && entry.display.startsWith('!')) {
|
|||
|
|
// Remove the '!' prefix to get the actual command
|
|||
|
|
const command = entry.display.slice(1).trim()
|
|||
|
|
if (command && !seen.has(command)) {
|
|||
|
|
seen.add(command)
|
|||
|
|
commands.push(command)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Limit to 50 most recent unique commands
|
|||
|
|
if (commands.length >= 50) {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
logForDebugging(`Failed to read shell history: ${error}`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
shellHistoryCache = commands
|
|||
|
|
shellHistoryCacheTimestamp = now
|
|||
|
|
return commands
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Clear the shell history cache (useful when history is updated)
|
|||
|
|
*/
|
|||
|
|
export function clearShellHistoryCache(): void {
|
|||
|
|
shellHistoryCache = null
|
|||
|
|
shellHistoryCacheTimestamp = 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Add a command to the front of the shell history cache without
|
|||
|
|
* flushing the entire cache. If the command already exists in the
|
|||
|
|
* cache it is moved to the front (deduped). When the cache hasn't
|
|||
|
|
* been populated yet this is a no-op – the next lookup will read
|
|||
|
|
* the full history which already includes the new command.
|
|||
|
|
*/
|
|||
|
|
export function prependToShellHistoryCache(command: string): void {
|
|||
|
|
if (!shellHistoryCache) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const idx = shellHistoryCache.indexOf(command)
|
|||
|
|
if (idx !== -1) {
|
|||
|
|
shellHistoryCache.splice(idx, 1)
|
|||
|
|
}
|
|||
|
|
shellHistoryCache.unshift(command)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Find the best matching shell command from history for the given input
|
|||
|
|
*
|
|||
|
|
* @param input The current user input (without '!' prefix)
|
|||
|
|
* @returns The best match, or null if no match found
|
|||
|
|
*/
|
|||
|
|
export async function getShellHistoryCompletion(
|
|||
|
|
input: string,
|
|||
|
|
): Promise<ShellHistoryMatch | null> {
|
|||
|
|
// Don't suggest for empty or very short input
|
|||
|
|
if (!input || input.length < 2) {
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check the trimmed input to make sure there's actual content
|
|||
|
|
const trimmedInput = input.trim()
|
|||
|
|
if (!trimmedInput) {
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const commands = await getShellHistoryCommands()
|
|||
|
|
|
|||
|
|
// Find the first command that starts with the EXACT input (including spaces)
|
|||
|
|
// This ensures "ls " matches "ls -lah" but "ls " (2 spaces) does not
|
|||
|
|
for (const command of commands) {
|
|||
|
|
if (command.startsWith(input) && command !== input) {
|
|||
|
|
return {
|
|||
|
|
fullCommand: command,
|
|||
|
|
suffix: command.slice(input.length),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null
|
|||
|
|
}
|