Linen is a macOS SwiftUI app around WebKit. Swift 6 strict concurrency and Main Actor default isolation are enabled for the app target.
| Folder | Contents |
|---|---|
Linen/App |
Launch, the application, menus, install and updates. App/Coordinator holds AppCoordinator and its extensions. |
Linen/Core |
Code with no browser or feature knowledge: persistence, runtime helpers, profiles and stage mode. |
Linen/Browser |
The browser engine: models, tabs, splits, search, history, downloads, pages and privacy. It contains no SwiftUI views. |
Linen/Interface |
The window shell and the shared visual system: shell, toolbar, sidebar, start page, command palette and content area. |
Linen/Features |
Self-contained features. Each feature folder holds its model, its engine and its views. |
Linen/Settings |
The settings model, the settings components, navigation and one folder per settings group. |
Linen/Resources |
Assets, the app icon, string catalogs, Info.plist, entitlements and generated data. |
LinenTests uses the same folders. A test goes in the folder of the code it
tests. LinenTests/Support holds shared helpers and fixtures.
LinenTests/Evals holds the evals that Tools/eval runs. Kit holds the
runner and the checks, and Suites holds the tasks. See EVALS.md.
Put a new file in the folder of the feature or layer that owns it. If a feature
has views, put them in a UI subfolder of that feature. If a folder holds more
than about 15 files, divide it by responsibility.
flowchart LR
App["AppDelegate"] --> Application["BrowserApplication"]
Application --> Coordinator["AppCoordinator per window"]
Coordinator --> Profile["Shared BrowserProfileContext"]
Coordinator --> Browser["BrowserModel"]
Coordinator --> Turns["AgentTurnModel"]
Turns --> Agent["AgentRunner"]
Coordinator --> Input["Voice and keyboard input"]
Browser --> Tabs["BrowserTab"]
Tabs --> Process["TabProcessState"]
Tabs --> WebKit["WKWebView"]
Agent --> Toolkit["AgentToolkit"]
Tabs --> Access["TabAssistantAccessCenter"]
Access --> Permissions["SitePermissions"]
Toolkit --> Access
Access --> Driver["PageDriver"]
Driver --> WebKit
Browser --> Stores["Profile stores"]
Turns --> Log["ConversationLog"]
Stores --> Database["AppDatabase / GRDB"]
Log --> Database
Coordinator --> Views["SwiftUI views"]
BrowserApplication owns the native window registry, active-window routing, updates,
and one MCP server. Each window has an AppCoordinator with its own tabs, sidebar,
selection, voice input, and assistant task. Views observe
their coordinator and its focused models. They should not contain persistence, networking or
WebKit policy. New work should prefer a narrow model or protocol over another
coordinator responsibility.
VoiceInputModel owns microphone and transcription state. AgentTurnModel
owns one turn’s task, reply, tab activity and durable completion state. Both
models expose small observable values while their service dependencies stay
outside observation.
BrowserModel owns tab and folder order, activation, session restoration and
history integration. BrowserTab owns one page’s observable state and WebKit
lifecycle. TabProcessState owns process-protection signals, unload status and
unexpected-termination throttling. WebViewPool prepares reusable views
without owning tab state.
A restored tab has no WKWebView until you open it. BrowserTab.webView
builds one on first use and isMaterialised reports whether it exists, so code
that iterates over tabs must check it before accessing the view. A tab unloaded
under memory pressure retains its view but replaces the loaded page.
In both cases the title, address, favicon and WebKit interaction state remain, so
activation loads the page again. Code that adds a new kind of in-progress page work must decide whether
that work prevents discarding.
BrowserProfileContext owns a profile's database, website data store, settings,
permissions, history, downloads, conversation log, web view pool, and extension
manager. Persistent windows using the same profile share this context. Tabs keep
an explicit context even before their web view is materialized. Changing focus
or another window's profile cannot retarget their data stores.
Each private window gets a distinct context with an in-memory database and a nonpersistent website data store. Closing it cancels its tasks, unloads its extensions, clears its conversations, and removes its website data and temporary compiled content rules. Shutdown awaits pending private cleanup, including cleanup started by an already closed window. Private windows never enter persistent session restoration or window-frame autosave.
Session rows include a stable window ID. Saves, close records, folders, and split layouts are scoped to that ID. The migration assigns the old session to one window, and the application gives migrated windows unique IDs across profiles. Revisions reject stale queued saves after a close or synchronous final save. Remapped IDs keep a hidden revision cutoff so delayed saves cannot recreate the old window record. A later profile switch may reuse the ID with a newer revision. A tab transfer moves the existing tab and web view, rebinds callbacks, cancels source-window assistant work, and commits both windows together. A live tab may move only between windows sharing the same context.
A space groups tabs and folders inside one profile. SpaceStore owns the list
of spaces, and each Space names its profile. Tabs and folders store a
spaceID. A switch between spaces of the same profile changes only the visible
space. A space with its own data uses its own profile.
Each window keeps one BrowserModel for each profile it has shown.
AppCoordinator.parkedModels holds the models that are not visible. A parked
model restores its session lazily and builds no web views. A profile switch
closes the outgoing saved session, pauses its media and parks the model. It then
registers the incoming model with the next context's extension manager and
reopens that model's session. A private model is closed, not parked. Code that
keeps a BrowserModel reference must follow AppCoordinator.browser, because a
switch replaces it. Other windows keep their existing contexts and tasks.
Visible and dismissing Peek pages detach immediately during a profile switch or
window teardown, so their callbacks cannot reach the next profile.
Extension window and tab adapters expire when their window unregisters. Toolbar
actions and delayed popup work must check the current registration before reading
the browser, since the same browser model can now belong to another profile.
The model is not a security boundary. Page text is untrusted, even when the model describes it as an instruction.
AgentToolkitexposes the supported browser actions.TabAssistantAccessCenterauthorizes visible-page reads and controls by origin beforeAgentToolkitreaches the page driver.PageDriverresolves actual page elements and enforces sensitive-field and consequential-action rules.AgentActionConsentasks from trusted app UI. The model cannot suppress it.AgentActionPolicystores narrow, revocable category-and-host grants.ConversationLogpersists the activity trail inside the active profile.
Background research uses a non-persistent WebKit data store for each task. It does not inherit cookies, logins or website storage from the active profile. Loading the chosen result into a tab does not grant the assistant access to it.
Add enforcement below the model prompt. Prompts can improve behavior but cannot authorize access, protect credentials or confirm an irreversible action.
External MCP clients run the assistant's own tools. MCPBrowserSession gives
each connection an AgentToolkit with an external AgentDriver and
connection-local consequential-action approvals, so site access, tab scope and
page safety are the assistant's. The only added gate is a one-time connection
approval that names the client's verified developer. External work is logged as
prompt-less tasks in the tab's conversation; it does not enter AgentTurnModel
or the model's context. Each connection binds to one regular window when it
connects. Focus changes do not retarget it.
Closing or switching that window revokes its connections before replacing
stores. Other windows keep their connections. The
bundled --mcp process relays stdio to a user-only Unix socket without opening a
second browser session. See MCP.md for the tool contract and boundaries.
MCP enablement is an app-level preference. Runtime shutdown clears connections without changing that preference. The listener belongs to the application. It refuses new connections when the focused window is private. Existing connections to regular windows stay bound to their original window. The approval sheet attaches to that window.
MCPClientInstaller handles optional client setup on its own actor. It merges
standard JSON configs and uses the installed Codex CLI on a staged TOML copy.
MCPConfigurationFile owns private backups and checked atomic replacement.
These services have no browser, profile, assistant, or sharing-grant dependency.
Providers propose actions. Linen checks permissions, runs each action once, and saves its result in a checkpoint. Resuming an interrupted task preserves user answers and requires verification before retrying an action with an unknown outcome. Repeated failures or unchanged results eventually pause the task.
Conversation messages, attachments, and checkpoints belong to the active profile.
They are private conversation data, not diagnostic exports. Deleting a turn also
invalidates checkpoints that may contain it. AgentRunDiagnostics exports only
approved event names, counts, timings, and status values. It excludes prompts,
answers, page content, URLs, tool arguments, credentials, and raw provider errors.
Automatic and manual compaction use the selected provider. The portable compactor summarizes chronological evidence in bounded slices. It preserves the latest user request, exact answers to questions, and complete recent tool exchanges where they fit. Page content and tool results remain untrusted input. A failed or cancelled compaction leaves the original checkpoint intact. On-device compaction stays on device. The OpenAI adapter also preserves native Responses state.
The context indicator estimates occupancy, including tool definitions. It does not report measured provider token usage. Progress updates remain in private chat history. Diagnostics record only their event type and status.
See OpenAI integration for provider configuration and validation, and Browser autofill for form and credential handling.
Shared mutable models use Observation. View-local state is private. A distinct
screen or independently changing section should be a real View type with
narrow inputs. A computed some View property does not create an observation
boundary.
Use the components and metrics in Linen/Interface/DesignSystem and
Linen/Settings/Components. Theme owns shared visual tokens.
User-facing strings stay localizable. Protocol values, URLs, model IDs and
third-party error text stay verbatim.
GRDB stores structured browser and agent data. Small preferences use
UserDefaults. Provider secrets use Keychain. File-backed models use atomic
writes through the support layer. These are profiles, website permissions, page
zoom and the download list. A write needed for quit or profile teardown
must be awaited or flushed synchronously before its owner is released.
Tests use Swift Testing, with XCTest for the two things it cannot express:
performance baselines (XCTMetric) and a test that drives the main run loop.
Prefer pure parsing and policy functions, injected stores, temporary databases
and local WebKit fixtures. HTTPFixtureServer serves deterministic loopback
pages for navigation and origin-boundary tests. A test should assert a
user-observable result or an enforced invariant. Live services and fixed sleeps
do not belong in the default suite.
Tests use a per-process temporary directory from AppDatabase.supportDirectory.
Profiles, permissions, zoom state and the download list do not access an
installed copy’s support directory.
WebViewGate limits how many cases have a live WKWebView at once, to half the
machine’s cores. The .boundedWebViews trait takes a slot. Apply it to the
tests that build a view, not to a whole suite, so the other tests do not wait
for a resource they never use.
Linen.xctestplan turns on per-test timeouts: 120 seconds by default, 300 at
most. A stalled test times out and reports its name without blocking the full run.
CI runs the full suite without code coverage. See CONTRIBUTING.md for the change checklist.