Skip to content

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.

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.

Use storage to remember a selection or cache. It is plugin-private, device-local, and survives View and app restarts:

const storage = ctx.capabilities.storage
if (!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) : null

Values 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.

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.

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.exportFile
if (!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.

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.

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.connections
if (!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.