window.chatoss.files is the user's real disk. Capability: fileAccess (one prompt on first use; denied dialogs return null — ALWAYS check). Every path is confined to a root the user explicitly granted.
Dialogs
const path = await window.chatoss.files.saveDialog({
defaultPath: 'export.txt',
filters: [{ name: 'Text', extensions: ['txt'] }]
}); // null = cancelled/denied
await window.chatoss.files.writeFile(path, contents);
// contents: string OR binary (ArrayBuffer / typed array / Blob — e.g. jspdf output)
const openPath = await window.chatoss.files.openDialog({ filters: [...], multiple: false });
const text = await window.chatoss.files.readFile(openPath); // text
const bytesB64 = await window.chatoss.files.readFile(openPath, { binary: true }); // raw bytes as base64
const folder = await window.chatoss.files.pickFolder(); // null if cancelled
Drag and drop (capability: fileDrop — no prompt)
Dropping is the user's action, so it never prompts:
window.chatoss.files.onDrop(async (files) => {
// each file: { name, type, size, text(): Promise<string>, arrayBuffer(): Promise<ArrayBuffer> }
const content = await files[0].text();
});
List and watch a picked folder
After pickFolder() returns a path, you can list and watch that subtree:
const folder = await window.chatoss.files.pickFolder(); // null if cancelled
if (folder) {
// List one directory level (non-recursive). Entries: { name, isDir, size }.
const entries = await window.chatoss.files.listDir(folder);
for (const e of entries) console.log(e.name, e.isDir ? 'dir' : 'file', e.size);
// Watch a path (recursive) for changes. Returns an unsubscribe function.
// The callback fires with debounced batches (~300ms) of { type, path } where
// type is 'create' | 'modify' | 'delete' — handy to refresh a tree when a
// CLI agent edits files. Only paths inside a picked folder may be watched.
const stop = window.chatoss.files.watch(folder, (events) => {
for (const ev of events) console.log(ev.type, ev.path);
});
// later:
stop(); // tears down the watcher (also auto-torn-down on app close)
}
listDir, watch, and search only accept a path from pickFolder() (or a path inside one) — there is no path access outside user-picked folders.
Search inside picked folders
Native grep — no shell, no approval prompt:
const result = await window.chatoss.files.search('needle', {
path: folder, // optional: narrow to one subtree inside a picked folder
contextLines: 2, // surrounding lines per match (0–5)
caseSensitive: false, // default: case-insensitive substring
maxResults: 100, // cap (1–500); result.truncated reports the cap was hit
});
// → { matches: [{ path, lineNumber, line, contextBefore: [...], contextAfter: [...] }], truncated }
It skips dot-directories (.git, node_modules, …), symlinks, files over 1MB, and binary files — the same discipline a coding agent's grep uses.
Headless runs
files read/write works headless (readFile, writeFile, listDir, search, watch) — provided a root was already picked in an earlier visible run (picked roots are persisted); with no picked roots they reject. The dialogs (openDialog, saveDialog, pickFolder) and files.onDrop refuse headless — there's no window to use. See Background tasks.