Skip to content

Providers

lukataylor-pixel edited this page May 3, 2026 · 2 revisions

Providers

Each tab in Soffit is rendered by a provider registered for a URI scheme. The tab's source field is a string like file:///path/to/file.md or folder:///Users/me/notes. Soffit looks up the provider for that scheme and asks it for a SwiftUI view.

This page is a tour of every shipping provider.

Folder — folder://

Opens a directory. Two modes per folder:

Grid — sortable card grid. Cards show live excerpts (first 600 chars of markdown, first 400 of mermaid, first 300 of plain text). Sort by Name, Recent (modification time), or Kind. Each card is single-click to open the file, double-click to open in Split, right-click for Open / Add to Canvas / Reveal in Finder.

Canvas — see Canvas mode.

The mode toggle is the picker in the folder header. Choice persists per folder.

A breadcrumb at the top gets you back up the directory tree. Clicking a folder card navigates in place (the panel's source is rewritten — the panel ID stays the same, so canvas state, recent files, etc. don't get reset).

Default panel created on workspace open: folder://<workspace-root>.

File — file://

Markdown editor (and a fallback monospace viewer for non-markdown text). Three modes — Preview, Source, Split. See Markdown editing.

Recognised extensions for markdown rendering: .md, .markdown, .mdx. Other text files (.txt, .swift, .py, .json, .yml, etc.) open in the fallback monospace Text view, no editor.

Mermaid — mermaid://

Renders a workspace-relative .mmd file as an SVG diagram. The path is the URL path:

mermaid:///diagrams/user-flow.mmd

Soffit reads the .mmd from disk, then loads a local HTML shim (Resources/mermaid-shim.html) into a WKWebView. On the shim's didFinish, it postMessages the diagram source into the page. The shim runs vendored mermaid.min.js (no network required) and renders the SVG.

Why postMessage instead of letting the shim fetch() the file? Two reasons:

  • file:// fetches from a file:// page are blocked by WebKit's cross-origin rules.
  • Going through postMessage keeps the shim purely presentational — no disk access, no workspace-root knowledge.

If the file is missing or unreadable, the shim shows a small placeholder diagram so the panel never goes blank.

Web — https:// and http://

Anything WKWebView can load: dashboards, GitHub, your local dev server, embedded Figma frames.

Figma URLs auto-rewrite to embed form. Paste https://www.figma.com/file/abc123/Design and Soffit transforms it to https://www.figma.com/embed?embed_host=soffit&url=… so the frame renders without the Figma chrome.

http://localhost:* passes through unmodified.

Terminal — terminal://

Embedded shell via SwiftTerm's LocalProcessTerminalView. Starts with cd <workspace> then execs your login shell ($SHELL, defaulting to /bin/zsh).

Each terminal pane is its own process. They don't survive a relaunch — re-opening the app spawns fresh shells. State you care about belongs in scrollback or files, not in the terminal panel itself.

Run claude here for Claude Code sessions, gh for GitHub CLI, vim if that's your jam, npm run dev for a foreground dev server. Anything that runs in iTerm runs here.

Chat — chat:// (legacy)

chat://claude panels stream against https://api.anthropic.com/v1/messages using the API key in your Keychain.

The chat creation UI was removed in v0.3 — there's no menu item, sidebar entry, or [+] option to make a new chat panel. The provider remains registered so any chat panels persisted before v0.3 still load and work.

The terminal + claude Claude Code covers this gap with workspace awareness, tool use, and file editing.

Adding a new provider

Conform to PanelProvider:

protocol PanelProvider {
    static var scheme: String { get }
    static var displayName: String { get }
    func makeView(for source: PanelSource, context: PanelContext) -> AnyView
}

Register in AppServices.init:

registry.register(MyProvider())

Your provider gets a PanelContext with the workspace root, the keychain, and a savePanelState callback for persisting to the panel's state: Data? blob (which round-trips through layout.json for free).

Clone this wiki locally