forked from ultraworkers/claw-code
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshellHistoryCompletion.ts
More file actions
119 lines (105 loc) · 3.38 KB
/
Copy pathshellHistoryCompletion.ts
File metadata and controls
119 lines (105 loc) · 3.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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
}