Light Apps
When you ask the agent to generate an HTML page — a data reconciliation tool, format converter, daily report template — and the task is repeatable, the agent will proactively suggest saving it as a Light App. Once saved, you can open it anytime from the Web UI without consuming LLM tokens.
What it is
Section titled “What it is”A Light App is an HTML page written by the agent using pure frontend technologies
(HTML + CSS + JS). It lives under ~/.octo/light-apps/:
~/.octo/light-apps/<slug>/├── manifest.json # metadata: name, description, icon, created_at├── index.html # the page└── … # optional files it references by relative path: scripts, styles, images, fonts, models, mediaEach Light App is an ordinary web page served by octo at /_apps/<slug>/: localStorage
persists between visits (each app’s data is kept apart from the others and from octo’s own
settings), downloads and fullscreen work, and files beside index.html load by relative path.
It needs no backend.
Light Apps work however you reach octo — on the same machine, in the desktop app, or from another device through a tunnel (ngrok, cloudflared) or your own domain. From another device you sign in with the access key as usual, and apps open with it. The phone UI does not show Light Apps.
Sharing an app publicly
Section titled “Sharing an app publicly”On the Light Apps page, each card has a Public link switch. Turn it on and the app’s page is
served to anyone who has the link, without the access key — including every file in the app’s
folder, so only do this for apps meant to be seen. The card then shows the link to copy. On
localhost that link points at your own machine; it becomes shareable once octo is reachable
through a tunnel or a domain. Turn the switch off and the link stops working immediately.
A Light App runs with the same access as the octo UI it is shown in. Only keep apps you asked your agent to make.
Showing data a cron task collects
Section titled “Showing data a cron task collects”For data that piles up over time — prices checked every hour, new posts, daily metrics — the agent
keeps it in a named database: a SQLite file at ~/.octo/databases/<name>.db, written with the
sqlite tool. A cron task appends to it on its schedule; a Light App
reads it and draws the table or chart. The two only share the database name, so the task doesn’t
care which app shows its data, and renaming or rebuilding the app never touches the data.
Ask for it in one go — “every hour, record the price of X into a database, and make me a Light App that charts it” — and the agent creates the database, the task and the app. The page you preview in the conversation already shows the real data, and keeps showing it after you save it.
The app can also change rows (mark one read, delete a bad one). A public app can only read, and
only the databases its manifest.json lists in databases.
The same goes for records you enter in an app yourself — expenses, to-dos, notes. The agent keeps
them in a named database rather than the browser’s localStorage, because localStorage is kept
per address: the desktop app, a browser on localhost and your phone through a tunnel would each
see a different copy, and the agent could not read any of them. localStorage stays for what only
matters to one screen, like the tab you had open or a half-filled form.
When to use
Section titled “When to use”✅ Good candidates for Light Apps:
- Repeatable tools: data reconciliation, format conversion, CSV merging
- Template generators: daily report templates, checklist generators
- Tasks with fixed input → output rules: validators, calculators, formatters
- Tools you want available offline
- Views over data a cron task collects into a named database (see above)
❌ Not suitable for Light Apps:
- One-off analysis or research
- Tasks that genuinely need LLM reasoning each time
- Workflows that need server-side logic on every use (use Skills or Workflows)
How to create one
Section titled “How to create one”The agent automatically evaluates whether an HTML page is worth saving as a Light App. The flow:
- You ask the agent to generate an HTML tool page in conversation
- The agent generates and previews it, then proactively asks: “Save as Light App? Open anytime from the Light Apps panel with zero tokens.”
- On your confirmation, the agent writes both files to
~/.octo/light-apps/<slug>/usingwrite_file - The slug is derived from the app name — lowercase letters, digits, and hyphens only
You don’t need to touch the filesystem — the agent handles everything.
Managing Light Apps
Section titled “Managing Light Apps”Click “Light Apps” in the Web UI navigation bar to see a card grid of all saved apps:
- Open: renders the app in the Artifacts sidebar panel in preview mode — fully interactive
- Edit: opens an agent conversation with the current content inline, so the agent can update it
- Delete: removes the app after confirmation
Agent-generated HTML artifacts can also be saved to Light Apps with one click from the Artifacts panel.
Mount it into the UI
Section titled “Mount it into the UI”A Light App normally lives on the Light Apps page and opens in the Artifacts panel. If you built
one you reach for constantly — a sketchpad, a calculator, a board — add a mount field to its
manifest.json and give it a permanent place in the interface.
{ "slug": "sketch", "name": "Sketchpad", "description": "A canvas for quick diagrams", "icon": "🎨", "mount": "view"}mount |
Where it shows up |
|---|---|
| omitted | The Light Apps page only — the default, and what every existing app does. |
"view" |
Its own entry in the left navigation, rendered full-page. |
"view" is the only value. The right-hand panel belongs to the session — its artifacts and its
diff — and an app is not part of a session; an app that still asks for a slot there lands on the
Light Apps page like any other.
The easiest way to set it is the Show in sidebar switch on the app’s card on the Light Apps page; the entry appears or goes away right away. You can also write the field yourself, or ask the agent to. The agent does not add it on its own, because whether an app deserves a permanent spot in your UI is your call, not its.
A complete example
Section titled “A complete example”A sketchpad that claims a page of its own. Two files, no build step:
{ "slug": "sketch", "name": "Sketchpad", "description": "A canvas for quick diagrams", "icon": "🎨", "created_at": "2026-09-20T00:00:00Z", "mount": "view"}<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><style> :root { color-scheme: light; --bg: #fff; --fg: #1d1d1f; --line: #0001; } :root[data-theme="dark"] { color-scheme: dark; --bg: #16181c; --fg: #e8e8ea; --line: #fff2; } body { margin: 0; height: 100vh; display: flex; flex-direction: column; background: var(--bg); color: var(--fg); font: 13px system-ui, sans-serif; } .bar { display: flex; gap: 10px; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--line); } canvas { flex: 1; touch-action: none; cursor: crosshair; } button { font: inherit; color: inherit; background: none; cursor: pointer; border: 1px solid var(--line); border-radius: 6px; padding: 3px 10px; }</style>
<div class="bar"> <input id="colour" type="color" value="#0e7490"> <input id="size" type="range" min="1" max="24" value="3"> <button id="clear">Clear</button> <button id="save">Save PNG</button></div><canvas id="c"></canvas>
<script> // The host passes the resolved theme on the URL, so the app can match the UI. const theme = new URLSearchParams(location.search).get('theme') if (theme === 'dark') document.documentElement.dataset.theme = 'dark'
const c = document.getElementById('c') const ctx = c.getContext('2d') let drawing = false
// Resizing clears the bitmap, so the strokes are re-drawn from the copy the // canvas held before the resize. function fit() { const snap = document.createElement('canvas') snap.width = c.width; snap.height = c.height if (c.width) snap.getContext('2d').drawImage(c, 0, 0) c.width = c.clientWidth * devicePixelRatio c.height = c.clientHeight * devicePixelRatio ctx.scale(devicePixelRatio, devicePixelRatio) ctx.lineCap = ctx.lineJoin = 'round' if (snap.width) ctx.drawImage(snap, 0, 0, snap.width / devicePixelRatio, snap.height / devicePixelRatio) } addEventListener('resize', fit) fit()
c.addEventListener('pointerdown', e => { drawing = true c.setPointerCapture(e.pointerId) ctx.beginPath() ctx.moveTo(e.offsetX, e.offsetY) }) c.addEventListener('pointermove', e => { if (!drawing) return ctx.strokeStyle = document.getElementById('colour').value ctx.lineWidth = document.getElementById('size').value ctx.lineTo(e.offsetX, e.offsetY) ctx.stroke() }) c.addEventListener('pointerup', () => { drawing = false })
document.getElementById('clear').onclick = () => { ctx.clearRect(0, 0, c.width, c.height) }
// The plain <a download> idiom: the host turns it into a browser download or // the OS save dialog in the desktop app. document.getElementById('save').onclick = () => { const a = document.createElement('a') a.download = 'sketch.png' a.href = c.toDataURL('image/png') a.click() }</script>Reload the Web UI and Sketchpad is in the left navigation, with its emoji for an icon.
Technical constraints
Section titled “Technical constraints”The agent follows these rules when generating a Light App:
- Files the page needs live in the app’s directory and are referenced by relative path, never starting with
/ - External libraries come from a CDN with a pinned version, only when a real library is needed — the app shows nothing if that host is unreachable
- No calls to octo’s own API from inside the page
- File processing via
FileReader+<input type="file"> - Saving results (a generated image, a converted file) via the standard
<a download>idiom — the host writes the file: a regular download in the browser, the OS save dialog in the desktop app - The page sizes itself to the frame, not to a pixel height: the same app appears in a ~420px-wide sidebar, as a full-height mounted page, and — as a landing
hero— in a frame as short as ~300px. Let a flex column fill the height with the main region growing (keep amin-heighton it), and drop the least important blocks under@media (max-height: …). A main element pinned to a pixel height is clipped in every frame shorter than you assumed - octo loads the page with
?theme=darkor?theme=light— the Web UI theme, which may differ from the OS’s. Read it and key the palette off it, before the body paints; the frame reloads on a theme change, so reading it once at load is enough - Icons via emoji or inline SVG
- Layout and color conventions from the
artifact-designskill
The agent already knows these constraints — just describe what you want, and it handles the rest.