Settings, storage and host services
After building a standalone page, you may want it to remember its layout, retain a selection, or request remote data. Extend the existing plugin one requirement at a time.
These are additions to your plugin, not another complete project. Merge manifest fragments into plugin.json. Put interface code inside an existing async mount(ctx, root) or button handler. Trigger requests and exports through explicit user actions, and catch errors at the call site to display useful feedback.
1. Let users configure the plugin
Section titled “1. Let users configure the plugin”Put user-adjustable preferences in settings, such as a compact-layout option:
{ "settings": { "compact": { "title": "Compact layout", "type": "boolean", "default": false } }, "storage": { "maxBytes": 1048576 }}This fragment also reserves 1 MiB of private storage for the next section. After enabling the plugin, users can change Compact layout in its host settings. Read the value in a View:
const compact = await ctx.capabilities.settings?.get("compact")root.dataset.compact = String(compact === true)Apply your layout based on this value. Views read settings; ordinary command contexts can also set and reset them. Declare keys first rather than treating settings as arbitrary application storage.
2. Save plugin-owned data
Section titled “2. Save plugin-owned data”Use storage to remember a selection or cache. It is plugin-private, device-local, and survives View and app restarts:
const storage = ctx.capabilities.storageif (!storage) throw new Error("Private storage unavailable")await storage.write("last-selection", new TextEncoder().encode("inbox"))const bytes = await storage.read("last-selection")const selection = bytes ? new TextDecoder().decode(bytes) : nullValues are Uint8Array; encode text or JSON yourself. Reading a missing key returns null. maxBytes is the total quota, and writes can fail when it is exceeded.
Use settings for user preferences, storage for local plugin data, and eidos/config for configuration that should travel with an Eidos file.
3. Show feedback or open a page
Section titled “3. Show feedback or open a page”For a short message, call await ctx.capabilities.ui.notify("Saved."). Show persistent connection or batch-processing state in the plugin interface rather than relying on a notification alone.
To open a declared page, check for ui.navigate and call ui.navigate("main", "/"). Use ui.openFile(relativePath) for a Space file. See Commands and formatters for a complete Cmd+K entry.
Export a generated file
Section titled “Export a generated file”Let users choose where to save a report without requesting Space-wide write access. Put this code in a View’s export-button handler:
const exportFile = ctx.capabilities.ui.exportFileif (!exportFile) throw new Error("Export is unavailable in this host")const result = await exportFile({ name: "report.txt", mimeType: "text/plain", data: new TextEncoder().encode("Report contents"),})if (result.status === "saved") { await ctx.capabilities.ui.notify("Report saved.")}Lite opens a save dialog and returns cancelled if dismissed. CLI Serve returns download-started, which does not confirm a completed save. The name must not contain directories; data is limited to 16 MiB. Mobile and Action contexts do not currently provide this interface.
4. Request network data
Section titled “4. Request network data”Host-mediated Worker-style fetch in Lite requires API 3.5.0. Declare the allowed origin first:
{ "requires": { "pluginApi": "3.5.0" }, "browser": { "networkOrigins": ["https://api.example.com"] }}Replace the example URL with your HTTPS service. In the page:
const response = await fetch("https://api.example.com/status", { signal: ctx.signal,})if (!response.ok) { await response.body?.cancel() throw new Error("HTTP " + response.status)}const data: unknown = await response.json()root.textContent = JSON.stringify(data, null, 2)Pass ctx.signal so requests can be cancelled when the View closes. HTTP error responses do not automatically throw; check response.ok first.
For public HTTPS access without a fixed whitelist, use browser: { network: true }.
Adding networkOrigins restricts access to that list, even with network: true.
A background independently declares background.network or background.networkOrigins.
Page requests do not start a background. Both environments use host authorization,
without ambient browser cookies or browser CORS restrictions. Private/local addresses
remain blocked. See Fetch API for limits.
Read a streaming response
Section titled “Read a streaming response”Get a normal request working before adding a stream-reading loop:
async function readEvents(signal: AbortSignal, onText: (text: string) => void) { const response = await fetch("https://api.example.com/events", { signal }) if (!response.ok) { await response.body?.cancel() throw new Error("HTTP " + response.status) } if (!response.body) throw new Error("Response has no body") const reader = response.body.pipeThrough(new TextDecoderStream()).getReader() try { for (;;) { const { done, value } = await reader.read() if (done) break onText(value) } } finally { await reader.cancel().catch(() => {}) reader.releaseLock() }}A View calls readEvents(ctx.signal, onText). A background owns an AbortController and aborts it on a stop message. Each result is a text chunk, not necessarily a complete SSE message. Accumulate and parse SSE framing separately.
For form uploads, pass FormData as the fetch body without manually setting Content-Type.
5. Use a service that requires credentials
Section titled “5. Use a service that requires credentials”fetch accepts an explicit Authorization header but does not read host-managed
connection credentials. Use connections to submit JSON business data while
the host supplies credentials. It is available in Views and ordinary commands,
not backgrounds.
Let the host manage user credentials rather than putting secrets in plugin source. Declare a connection:
{ "connections": { "summary": { "title": "Summary service", "url": "https://api.example.com/summary" } }}Inside a page, file View, or ordinary command, check configuration first:
const connections = ctx.capabilities.connectionsif (!connections) throw new Error("Connections unavailable")if (!(await connections.isConfigured("summary"))) { await ctx.capabilities.ui.notify( "Configure the Summary service in plugin settings first." ) return}const result = await connections.request({ connection: "summary", body: { text: "A note to summarize" },})This fragment belongs inside an async handler. summary matches the manifest connection ID. Users configure connection details and secrets in Eidos; the plugin sends requests through the host. Unfinished requests are cancelled when a command invocation ends.
See Settings, Storage, UI, and Network and connections for types.