Filehigh importancesource

shellHistoryCompletion.ts

utils/suggestions/shellHistoryCompletion.ts

120
Lines
3456
Bytes
4
Exports
2
Imports
10
Keywords

What this is

This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.

Beginner explanation

This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.

How it is used

Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.

Expert explanation

Architecturally, this file intersects with session-engine, shell-safety. It contains 120 lines, 2 detected imports, and 4 detected exports.

Important relationships

Detected exports

  • ShellHistoryMatch
  • clearShellHistoryCache
  • prependToShellHistoryCache
  • getShellHistoryCompletion

Keywords

commandhistoryinputcommandsshellhistorycacheshellcachedisplayshellhistorycachetimestampentry

Detected imports

  • ../../history.js
  • ../debug.js

Source notes

This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.

Open parent directory

Full source

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
}