Docs menu
DocsAPI reference

Files

The user's real disk: dialogs, picked folders, watching, and native search — every path user-consented.

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.