How to Implement Search Sessions Using the SearchManager Class in Desktop Commander
Use the SearchManager class in src/search-manager.ts to spawn persistent ripgrep processes that stream results incrementally, allowing you to paginate through matches, monitor active sessions, and terminate searches on demand.
Desktop Commander (wonderwhy-er/DesktopCommanderMCP) provides a robust search session API that wraps ripgrep execution into manageable, stateful objects. When you implement search sessions using the SearchManager class, you gain fine-grained control over long-running file searches, including the ability to fetch partial results while the process continues running in the background.
Understanding the SearchManager Architecture
The SearchManager class maintains an internal map of SearchSession objects, each representing a running ripgrep child process. According to the source code in src/search-manager.ts, a session tracks the process handle, accumulated results, stdout buffers, timestamps, and search metadata.
Key components include:
SearchSessioninterface (lines 17-30): Defines the state structure for active searches, includingtotalMatches,totalContextLines, andbufferfor partial JSON lines.startSearchmethod: Validates paths, constructsripgreparguments, spawns the process, and returns a session ID with initial results captured within the first ~40ms.readSearchResultsmethod: Implements offset-based pagination supporting both range queries (positive offset) and tail behavior (negative offset).terminateSearchmethod: SendsSIGTERMto the child process while preserving results for subsequent reads.
Starting a Search Session
To implement a search session, invoke searchManager.startSearch() with a configuration object specifying the search parameters. The method is located at lines 55-20 in src/search-manager.ts and handles argument construction for literal searches, case sensitivity, hidden files, and context lines.
import { searchManager } from './src/search-manager.js';
async function startNewSearch() {
const { sessionId, results, isComplete, runtime } = await searchManager.startSearch({
rootPath: '/path/to/project',
pattern: 'TODO',
searchType: 'content', // Use 'files' for filename-only search
ignoreCase: true,
maxResults: 200,
contextLines: 2,
earlyTermination: true, // Stops after finding exact filename matches
literalSearch: false,
});
console.log(`Session ${sessionId} started (${runtime}ms)`);
console.log(`Received ${results.length} initial results (complete: ${isComplete})`);
}
The method returns a unique sessionId (formatted as search_{type}_{timestamp}), an array of initial results, and an isComplete boolean indicating whether the ripgrep process has already finished.
Retrieving Paginated Results
Once a session is active, use readSearchResults to fetch slices of the accumulated results without blocking on the process completion. This method supports two pagination modes as implemented in lines 22-40 of src/search-manager.ts:
- Positive offset: Range-based pagination (e.g.,
offset=0, length=100returns results 0-99). - Negative offset: Tail behavior (e.g.,
offset=-10returns the last 10 results).
// Fetch first page of 100 results
const page1 = searchManager.readSearchResults('search_1_1698345678901', 0, 100);
console.log(page1.results);
console.log(`Has more: ${page1.hasMoreResults}`);
// Fetch last 20 results (tail behavior)
const tail = searchManager.readSearchResults('search_1_1698345678901', -20);
console.log('Recent matches:', tail.results);
The return object includes hasMoreResults (indicating if the buffer contains additional unpaginated results), wasIncomplete (true when ripgrep exits with code 2 due to permission-restricted files), and runtime (milliseconds since session start).
Managing Session Lifecycle
Terminating Active Searches
To stop a running search prematurely, call terminateSearch with the session ID. Located at lines 90-07 in src/search-manager.ts, this method sends SIGTERM to the underlying process:
const wasKilled = searchManager.terminateSearch('search_1_1698345678901');
console.log(wasKilled ? 'Process terminated' : 'Session not found');
The session remains in the manager's internal map after termination, allowing you to continue reading accumulated results until the global cleanup interval removes it (default idle timeout is 5 minutes).
Listing Active Sessions
Monitor all search operations using listSearchSessions, which returns a concise summary of every session in the manager's map:
const sessions = searchManager.listSearchSessions();
sessions.forEach(session => {
console.log(
`[${session.id}] ${session.searchType} "${session.pattern}" – ` +
`${session.isComplete ? 'DONE' : 'RUNNING'} (${session.totalResults} matches)`
);
});
Working with High-Level Handlers
For RPC-style integration, Desktop Commander exposes wrapper functions in src/handlers/search-handlers.ts. These handlers validate inputs using Zod schemas (defined in src/tools/schemas.ts) and delegate to the SearchManager:
import {
handleStartSearch,
handleGetMoreSearchResults,
handleStopSearch,
handleListSearches
} from './src/handlers/search-handlers.js';
// Initiate search
const start = await handleStartSearch({
path: '.',
pattern: 'error',
searchType: 'content'
});
// Paginate results
const more = await handleGetMoreSearchResults({
sessionId: 'search_1_1698345678901',
offset: 0,
length: 50
});
// Terminate
const stop = await handleStopSearch({ sessionId: 'search_1_1698345678901' });
// Monitor
const list = await handleListSearches();
These handlers provide the same functionality as direct SearchManager calls but include argument validation and standardized response formatting for the Model Context Protocol (MCP) server interface.
Summary
- Import
searchManagerfromsrc/search-manager.tsto access the singleton instance managing all search sessions. - Start sessions with
startSearch(), which spawnsripgrepand returns a session ID plus initial results captured during the first 40ms. - Paginate results using
readSearchResults()with positive offsets for ranges or negative offsets for tail behavior. - Terminate processes gracefully with
terminateSearch(), which sendsSIGTERMbut preserves results for subsequent reads. - Monitor activity via
listSearchSessions()or use the high-level handlers insrc/handlers/search-handlers.tsfor validated RPC interfaces.
Frequently Asked Questions
What is the difference between searchType: 'content' and 'files'?
Content search scans file contents for pattern matches using ripgrep's standard regex engine, while files search uses the -g glob pattern to match filenames directly. When using searchType: 'files', enable earlyTermination: true to stop the search immediately after finding matching filenames, improving performance for large repositories.
How does the negative offset pagination work in readSearchResults?
Passing a negative offset value (e.g., -20) to readSearchResults activates tail behavior, returning the last N results from the accumulated buffer. This is useful for real-time monitoring scenarios where you need the most recent matches without fetching the entire result set. Positive offsets behave like standard array slicing (offset to offset + length).
What happens when ripgrep encounters permission errors during a search?
If ripgrep exits with code 2 (indicating permission-restricted files were encountered), the SearchManager marks the session with wasIncomplete: true but preserves all successfully read results. You can still paginate through the partial results, and the totalMatches counter reflects only the accessible files. The session is considered complete despite the error condition.
Can I implement custom cleanup intervals for terminated sessions?
The SearchManager runs a global cleanup interval that removes completed sessions after a configurable idle period (default 5 minutes). While the current implementation in src/search-manager.ts uses a fixed timer, you can force immediate cleanup by calling terminateSearch() followed by manual deletion of the session from the internal map, though this is not recommended as it prevents post-termination result retrieval.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →