An LLM-first browser. No URL bar — a chat composer drives everything. Ask, and Clara answers as rich interactive HTML; name a site, and it takes the stage while the conversation floats above it. She reads the pages you open, acts on them with your consent, remembers who you are, keeps a searchable memory of everywhere you've been, and curates tabs into groups with thumbnails and her own summaries.
Runs entirely on your ChatGPT subscription via the Codex SDK — no API key, no per-token bill. macOS, Electron, vanilla JS, MIT.
Prototype status: built agent-first (see CLAUDE.md); UI screenshots coming.
- Answers are interfaces: replies render as sandboxed HTML cards — from a one-line answer to a working calculator or comparison table.
- Sites are conversation: open/read/interact tools let Clara browse with you (first interaction per tab requires your consent).
- Memory with a soul: SOUL/USER/MEMORY files compose her identity; she updates her own notes as she learns you.
- "Onde eu vi aquilo?": every visited page is archived as grep-able markdown; Clara searches her own history to answer.
- Groups with a home: popup links auto-group with their origin; each group gets a home page with thumbnails and an LLM-written summary.
npm install
npm start # Electron app
npm run check # node --check every source file (fast, offline)
npm test # node:test unit tests (offline, no quota)
npm run dist # package dist/mac-arm64/Clara.app (unsigned, no icon yet)
node scripts/smoke-mcp.mjs # end-to-end agent test — SPENDS ChatGPT quotaPackaged builds move Clara's agent home to userData/agent-home (seeded on
first run) since the app bundle is read-only; downloads save to ~/Downloads
with a notice in the feed.
main.js Electron main — window, IPC, tool round-trips (askRenderer)
preload.cjs contextBridge: window.clara { send, onEvent, onToolRequest, reply, … }
agent/agent.js AgentService — one Codex instance+thread per conversation
(startThread / resumeThread for persistence)
agent/mcp-server.js Clara's tools over streamable HTTP: open_url, list_tabs,
read_page, interact (stateless, /mcp/<conversationId>)
agent/store.js session.json read/write (debounced) in userData
renderer/session.js serialize/rehydrate the UI to/from that store
tests/ node:test offline units (store round-trip, resume)
agent-home/ Clara's home. AGENTS.md is GENERATED per new thread
from SOUL.md (voice) + USER.md (user profile) +
MEMORY.md (her lessons) + CONTRACT.md (harness rules).
history/ archives every visited page as grep-able
markdown (visits.log + pages/*.md)
renderer/app.js UI: sidebar (tabs+conversations), stage, chat overlay,
composer, consent bar for agent interactions
renderer/page-scripts.js JS injected into webviews: extract text+refs, act on refs
Layout model: a conversation starts as a normal chat feed. Once a tab exists, the site takes the whole main area (the stage) and the chat becomes an autonomous companion dock above the composer. The latest exchange stays visible for a short handoff, then settles into a small Conversa control so it does not cover the site. Sending or receiving reveals it again; the control opens the latest exchange/history, while clicking the site or pressing Esc dismisses it.
Sidebar: tabs and conversations are deliberately the same thing — one
item per context. An item with one tab wears that page's favicon/title; with
several tabs its pages nest under it. Users can create custom groups
(folder-plus button, rename via double-click) and drag items into them or to
an exact position; conversations and groups can both be reordered, and
dropping on the list background ungroups. The sidebar is translucent over macOS vibrancy
(vibrancy: "sidebar" + transparent body, solid #main).
Auto-groups & group home: target=_blank / window.open popups are denied
in main and reopened as sibling items, auto-grouped with the origin site
(group named after its domain; cmd+click keeps focus via disposition).
Clicking a group header opens the group home on the stage: thumbnail
grid of the group's pages (captured on page settle via capturePage) plus a
2–3 sentence summary written by Clara on a shared utility thread
(agent.summarize), cached by member-URL key and refreshed when membership
changes. Esc or opening any item leaves the home.
- Agent:
@openai/codex-sdk→ spawns bundledcodex exec. Auth comes from~/.codex/auth.json(ChatGPT login). Threads persist in~/.codex/sessions. - Cards: short agent replies can be Markdown (rendered locally) or simple
HTML; substantial replies are HTML pages. Both render in sandboxed
iframe[srcdoc](allow-scripts, noallow-same-origin). A wrapper injects Cuts design tokens + Google Fonts and reports height viapostMessage. - Identity: Cuts (EB Garamond / Inter / JetBrains Mono, warm paper + azure).
Tokens mirrored from
GemmaLite/Packages/GemmaDesign/DesignTokens.swift.
- Global MCP servers hang codex exec. The user's
~/.codex/config.tomlregisters ChatGPT.app MCP servers (node_repl,figma); inheriting them stalls turns indefinitely.AgentServicemust always passmcp_servers.<name>.enabled=falseoverrides (plusnotify=[]). - MCP tool calls need
default_tools_approval_mode = "approve". Without it, non-interactive exec raises an approval elicitation that auto-resolves to Cancel and every Clara tool call fails as "cancelled". - Tool→feed scoping: Clara's MCP server runs over local HTTP and the
conversation id is in the URL path (
/mcp/<conversationId>), one Codex instance per conversation. Tool calls land in the right feed with no session state on the server (stateless transport, fresh per request). - Never reparent or
display:nonea<webview>— both reload/detach the guest. All webviews live stacked in#stage-viewsand switch viavisibility. - Cold start: first turn on a thread ≈ 40s; warm turns ≈ 6s. Conversations
are warmed up on creation (one hidden "ok" turn) so the first real answer is
fast. Disable in
renderer/app.js(window.clara.warmup) if quota matters. srcdociframes inherit the parent CSP — the CSP inrenderer/index.htmldeliberately allows inline scripts/styles, Google Fonts and https images so LLM cards work. The iframe sandbox is the real security boundary.- Turns on one thread must never overlap;
AgentServiceserializes them with a per-conversation promise queue.
- ✅ Shell + rich HTML — sidebar/feed/composer, agent → HTML cards, warmup.
- ✅ Navigation —
open_url/list_tabstools (app MCP server over local HTTP), site cards via<webview>inline in the feed, stage mode, tabs in the sidebar, omnibox fast path for typed URLs. - ✅ Reading —
read_page: injected extractor returns url/title/main text (25k char cap) + up to 150 visible interactive elements tagged withdata-clara-ref. - ✅ Interaction —
interact { ref, action: click|fill|press_enter, text }. First interaction on a tab shows a consent bar on the site card ("Permitir nesta aba" / "Negar"); approval sticks per tab. React-safe fill (native value setter + input/change events), Enter simulation withform.requestSubmit()fallback.
All four verified headless via scripts/smoke-mcp.mjs (stub bridge): the
agent opens → reads → answers from content, and fill→re-read→click→re-read
for interactions.
- Identity composition: every new thread regenerates
AGENTS.mdfrom SOUL + USER + MEMORY + CONTRACT, so Clara always starts with her current self. Edit those four files, never AGENTS.md. - Self-written memory: the sandbox is
workspace-writescoped to agent-home; the contract tells Clara to silently update USER.md (facts about the user) and MEMORY.md (her own lessons). - Browsing history: the renderer snapshots every settled page
(
did-stop-loading+ 1.2s debounce) and main archives it underagent-home/history/—visits.logplus one markdown file per URL. Agentread_pageresults are archived too. "Onde eu vi X?" is answered by Clara grepping her own history (no dedicated search tool needed). History is unbounded for now and gitignored (personal data).
Verified headless via scripts/smoke-memory.mjs: identity recall, history
grep ("abajur" → page title+url+detail), and a USER.md self-update.
The whole UI session (conversations, tabs, groups, chat messages, active
selection) is serialized to session.json in Electron's userData and
rehydrated on boot. Webviews reload from their saved URLs; Codex conversations
resume by thread id (resumeThread), so follow-up messages continue the same
thread. Saves are debounced; the thread id is captured from the
thread.started event and merged in main (authoritative). Covered by
tests/store.test.mjs and tests/agent.test.mjs.
Persistence (conversations survive restart via resumeThread), model/effort
picker in the UI, @mozilla/readability for cleaner extraction, screenshots
of tabs as model input (local_image), webview target=_blank handling,
dark mode.