Files Service
8.34 FilesService — Search the local file index, read declared files
Runs in: both worker and view.
Permission required: files:search for search/status;
files:read for read/glob/thumbnail.
The same bounded, cached file index that backs the host's own "Search Files" view — your extension gets the identical search capability, not a separate implementation. Search is a per-keystroke-safe operation (bounded work regardless of index size); it does not read file contents.
For file contents there is read: a bounded text read scoped to the
glob patterns your manifest declares in permissionArgs["files:read"].
Those patterns are the entire readable scope — nothing outside them is
readable, and credential stores (~/.ssh, ~/.aws, …) and OS locations
are denied even when a pattern covers them. The declared patterns are
shown to the user in the install/enable permission prompt. Unlike
fs:watch patterns, they may anchor anywhere — another drive
(C:/Program Files (x86)/Steam/**) or nowhere at all
(**/steamapps/appmanifest_*.acf, which covers the file on every drive).
Two companions share read's permission and scope, because each is
strictly less information than the byte read already granted. glob
enumerates existing in-scope files matching a pattern — for names that
can't be known in advance, like content-addressed cache files
(librarycache/<appid>/<sha1>.jpg). thumbnail returns an
asyar-thumb:// URL for an in-scope image, generated and cached by the
same pipeline the launcher's own file previews use — set it directly as a
dynamic command's icon and it renders like the launcher's native
application icons. Extension thumbnails are images-only by design, so the
result is identical on Windows, macOS, and Linux: any non-image resolves
null rather than falling back to per-OS preview generators.
type FileType = 'document' | 'image' | 'code' | 'audio-video' | 'archive' | 'folder' | 'other';
type HitSource = 'local' | 'deep';
interface FileHit {
fileId: string;
name: string;
path: string;
type: FileType;
isDir: boolean;
modifiedAt: number; // unix seconds
score: number;
pinned: boolean;
source: HitSource;
}
interface FileSearchOptions {
typeFilter?: FileType;
limit?: number;
}
type IndexStateKind = 'disabled' | 'building' | 'ready' | 'rescanning' | 'cap-reached';
interface IndexStatus {
state: IndexStateKind;
entryCount: number;
lastScanMs: number;
snapshotLoaded: boolean;
capReached: boolean;
}
interface FileReadOptions {
/** Read at most this many bytes (lossy UTF-8). Defaults to 50,000; the
* host clamps extension reads to a 1 MiB ceiling regardless. */
maxBytes?: number;
}
interface FileGlobOptions {
/** Return at most this many paths. Defaults to the host cap (256), which
* is also the ceiling. */
maxResults?: number;
}
interface FileThumbnailOptions {
/** Longest edge of the generated thumbnail in pixels. Defaults to 256;
* the host clamps to 16–512. */
maxDim?: number;
}
interface IFilesService {
/** Bounded per-keystroke query against the local file index. */
search(query: string, opts?: FileSearchOptions): Promise<FileHit[]>;
/** Current index lifecycle state — useful for showing a "still indexing" hint. */
status(): Promise<IndexStatus>;
/** Bounded text read of an absolute path covered by the extension's
* declared `permissionArgs["files:read"]` globs. Rejects with the
* denial reason when the path is out of scope or protected. */
read(path: string, opts?: FileReadOptions): Promise<string>;
/** Enumerate existing in-scope files matching a glob. The pattern must
* begin with an absolute literal prefix to walk from
* (`C:/Steam/appcache/**` works; a leading `**` doesn't). Returns
* sorted absolute paths of regular files only, filtered to the declared
* scope minus the deny-list; symlinks are neither followed nor
* reported. A missing walk root (an unplugged library drive) resolves
* to `[]`; a pattern outside the declared scope rejects. */
glob(pattern: string, opts?: FileGlobOptions): Promise<string[]>;
/** Thumbnail of an in-scope file as an `asyar-thumb://` URL, usable
* directly as a dynamic command's `icon`. Images only, identical on
* every platform: non-image types resolve `null`. Rejects on scope
* denials. */
thumbnail(path: string, opts?: FileThumbnailOptions): Promise<string | null>;
}
Usage:
const files = context.getService<IFilesService>('files');
// Check whether the index is ready before relying on complete results
const status = await files.status();
if (status.state === 'building' || status.state === 'rescanning') {
console.log('Index still catching up — results may be incomplete');
}
// Search, optionally filtered to one file type
const hits = await files.search('invoice', { typeFilter: 'document', limit: 20 });
for (const hit of hits) {
console.log(`${hit.name} — ${hit.path}`);
}
Declaring a read scope (manifest):
{
"permissions": ["files:read"],
"permissionArgs": {
"files:read": ["**/steamapps/libraryfolders.vdf", "**/steamapps/appmanifest_*.acf"]
}
}
// Reads succeed only for paths a declared pattern covers:
const vdf = await files.read('C:/Program Files (x86)/Steam/steamapps/libraryfolders.vdf');
// Out-of-scope or protected paths reject with the denial reason:
await files.read('C:/Users/me/.ssh/id_rsa'); // → rejects
Real artwork on dynamic commands — enumerate a content-addressed cache
with glob, thumbnail the hit, use the URL as the command's icon:
{
"permissions": ["files:read"],
"permissionArgs": {
"files:read": ["C:/Program Files (x86)/Steam/appcache/librarycache/**"]
}
}
// Steam's square icon is sha1-named — unknowable in advance, but 40 `?`s
// match the hex name precisely:
const hits = await files.glob(
`C:/Program Files (x86)/Steam/appcache/librarycache/${appid}/${'?'.repeat(40)}.jpg`,
);
if (hits.length > 0) {
const icon = await files.thumbnail(hits[0], { maxDim: 64 });
// → 'asyar-thumb://localhost/<key>.png', ready for dynamic command registration
}
Note the surface is deliberately read-shaped: search/status/read/
glob/thumbnail only. There is no write/delete here — for file
operations (reveal in file manager, trash) see
FileManagerService.