Architecture
A map of Floe’s processes and code, not an API reference. Development has the build steps and the details of each part, and Coding standards says how code is written here.
Floe is a macOS launcher that runs unmodified Raycast extensions. It is written in Swift with AppKit and SwiftUI, with a TypeScript runtime that Bun runs for extensions and a Rust file index.
Processes
┌────────────────────────────────────────────┐
│ Floe (stays running) │
│ hotkeys, status item, launcher window, │
│ sync, background extensions, clipboard │
│ │ spawns │ spawns │
│ ▼ ▼ │
│ Settings process Bun, one per │
│ (while Settings running command │
│ is open) (runtime/host.ts)│
└────────────────────────────────────────────┘- Floe’s own process stays running and holds everything that has to answer at once: the hotkeys, the status item, the launcher window, the updater, sync, background extensions and the clipboard recorder. It gives memory back when the launcher has been closed a while. See One process, and Settings.
- Settings is a process of its own, started when Settings opens and gone when it closes. The two processes talk
through
ProcessLink, and each store has one process that writes it. - Each running extension command is a Bun process.
runtime/host.tsbundles the command, renders it with a custom React reconciler and speaks NDJSON over stdin and stdout;runtime/api/index.tsstands in for@raycast/api. Extensions run under the user’s account without a sandbox. - The file index is Rust (
Vendor/FloeRust), built byscripts/build-rust.shand called from Swift.
Floe stores its data on the Mac: extensions and their data under ~/Library/Application Support/Floe, the file
index under ~/Library/Caches/Floe, passwords in the Keychain, and hang reports under ~/Library/Logs/Floe. Settings
can sync through iCloud; see Settings sync.
Where the code lives
-
Sources/Floe: the app: panel, hotkey, app index, and a renderer for the JSON tree the host sends. One folder per area, and no Swift file outside a folder:App: the entry point and app delegate (main.swift), the main menu, thefloe://link router and what decides what a link is (IncomingLink.swift, whichFuzzingcompiles on its own), the debug options, the update rules and the generated credits. Also the launcher’s side of Settings: the process handle (SettingsProcess.swift), the messages (ProcessLink.swift) and what the launcher does with them.Launcher: the panel’s model (Model.swift, with oneModel+….swiftper view it drives and one for the keys) and its root views (Views.swift,IconView.swift), its layout and appearance, the Actions menus, the catalog of apps and commands with its scans (Catalog.swift, which also holdsPaths), and Markdown.Actions: the keys and the user’s choices for a result’s Actions menu, by kind of result (app, file, folder or link). Floe’s own keys for its rows (ActionKeys.swift, withSubjectKindandKeyLayout), the choices for one kind and the rules they follow (ActionChoices.swift), what the launcher asks for Return, the menu and the keys (AlternativeActions.swift), and what the Actions tab in Settings lists and writes (ActionOptions.swift).Search: ranking and fuzzy matching, the command lookup the App Intents use, andSearchProviders(below).BuiltIn: what Floe does itself: the calculator, system commands and toggles, System Settings panes, emoji, clipboard history, snippets and their expander, quicklinks, the calendar, the file search and its index of file names (FileIndexService.swift; the index itself is Rust, inVendor/FloeRust/rust/src/index.rs, and is saved to~/Library/Caches/Floeso a later launch reads it back and asks FSEvents only for what changed since; iCloud Drive sits inside~/Library, which the walk of the home folder skips, so it has a second index of its own), browser tabs, menu bar items, script commands and Thaw’s actions, with the AppleScript runner and the selection and pasteboard helpers.AI: Ask AI and the sources that answer a prompt. An answer can be followed up:AskAIModelkeeps the turns of one conversation in memory while the answer view is open and drops them when it closes; none of it is saved or logged. A question travels with its earlier turns as anAIConversation, and each source is given them its own way (AISources.live): an API as user and assistant messages, Apple Intelligence as the transcript of a session made for that one request, and a command line tool, which starts fresh every time, written out in its prompt (AIConversation.replayedPrompt).AIConversation.Limitcaps what is sent again, oldest turns dropped first: 8 turns and 12,000 UTF-8 bytes, or 6,000 for Apple Intelligence’s 4,096 tokens. An extension’sAI.askis a conversation with no earlier turns.Extensions: a running command (Session.swiftand its extensions), what it asks the app for (HostRequest.swift), the manifest, the store, menu bar commands, the background scheduler, OAuth, hot reload, and the views an extension’s forms and errors are drawn with.Settings: the settings model (Settings.swift), its window, pages and sections, settings search entries, import and export, and the Keychain and preference stores.SettingsMode.swiftis the settings process. It lists commands, scripts and apps from aCatalogStore(inLauncher) of its own, as the launcher does.Sync: settings sync through iCloud, by CloudKit or the key-value store, written to be moved to Thaw as it is. It knows records, not Floe’s settings (see Settings sync).Picker:Floe --pick.PreferredApps,ThawandDroppyCodeare described below.Tests/FloeTestshas the same folders.
-
runtime/host.ts: bundles a command, renders it with a custom React reconciler, speaks NDJSON on stdio. -
runtime/api/index.ts: the@raycast/apistand-in. -
extensions/: one folder per extension (hellois a sample,diagnosticsfails on purpose to exercise the error screen). -
Sources/Floe/Search/SearchProviders: what the root search is made of. Each provider answers a query with rows to rank, to pin on top, to list under a section or to append (SearchProvider.swift);RootSearch.swiftlists the providers and merges what they answer.LauncherModel.refresh()only builds the context they read.- Dictionary answers are read by
BuiltIn/DefinitionSearch.swiftoff the main thread, except the spell checker’s word list. It keeps only the current query’s answer, drops late results, and hands the entry toDefinitionSearchProviderthroughSearchContext. - A scope (
SearchScope.swift) is a search of its own inside the root search:files invoice,clipboard meetingandmenu wifishow only that scope’s rows. It triggers on its keyword, a space and some text; the keyword alone is an ordinary search. A scope may deliver rows later through anAsyncStream, each batch replacing the last;SearchUpdatesdrops batches that arrive for a query that has been replaced. - A source (
SearchSource.swift) is a scope that may also add up to three rows to an ordinary search, in a section of its own below the ranked rows and above the appended ones. Each is off until its switch in Settings › Privacy is on (AppSettings.searchSources), and its keyword works only while it is.SourceSearchasks the enabled sources after the ranked rows are shown, for queries of three characters or more, and follows each with its ownSearchUpdates. The files source isFileSearchScope; the tabs source isTabSearchSource. BuiltIn/BrowserTabs.swiftholds the browsers whose tabs can be listed, oneBrowserAppentry each, with the scripts that list and switch tabs. Add a browser only after reading its scripting definition (sdef /Applications/<App>.app). The script runner is passed in, so tests never talk to a browser; do not run these scripts from a test.
- Dictionary answers are read by
-
Sources/Floe/PreferredApps: the apps Floe hands things to instead of doing their work. A role (AppRole.swift) is a kind of app the user has a preferred one of: what it is handed, the known apps offered by name and what stands in when nothing is chosen.PreferredApps.swiftdecides which app a role resolves to and what it is handed, without opening anything; it reads the Mac’s apps throughAppLookup, which tests replace. Add a known app only with a bundle identifier read from an installed copy’sInfo.plist.- The terminal and the editor are handed files and folders, so they share one path:
NSWorkspaceopens the items with the app. Notes is handed text, which each app takes its own way, so it has its own adapter (Notes.swift): a link for Antinote or a custom app, a script for Apple Notes. A new role is a case ofAppRoleplus, when it takes text, an adapter like that one. - The clipboard role is handed nothing: Clipboard History opens the chosen app, or a link, in place of Floe’s own
history (
ClipboardApp.swift). Floe’s own history is parked byClipboardApps.ownHistoryIsParked: nothing is saved, the pasteboard is not watched, and the command asks for an app until one is chosen.ClipboardApps.destinationdecides where the command goes and never falls back to Floe’s history;recordsis the one place that says whether copies are saved (the role is Floe and the switch is on). An app whose history opens from a link goes inClipboardApps.links, with a link confirmed from the installed copy.
- The terminal and the editor are handed files and folders, so they share one path:
-
Sources/Floe/Thaw: code ported from Thaw that stays Floe’s: the Carbon hotkey registry, onboarding’s steps and the permission model, settings search’s index and ranking, the Sparkle updater, and the App Intents that Shortcuts and Spotlight use (FloeIntents.swift). The views they draw through are in the ThawUI package. -
Sources/Floe/DroppyCode: code ported from Droppy Code. Each file keeps Droppy Code’s copyright and credit line and says what Floe changed;LICENSESholds its license and third-party notices.LoginEnvironment.swiftreads the login shell’s environment, which extensions start with, andShell.swiftruns a tool with a timeout.AI.askis answered by a one-shot run of a command line tool (TextGeneration.swift:claude,codex,opencodeorpi, each with no tools of its own) or by a streamed request to an OpenAI-compatible API (ChatCompletionStream.swift). Settings › AI picks between them; the choice and the request an extension makes are inExtensions/HostRequest.swift, and which tool answers is inAI/AITool.swift.ToolTextStream.swiftreads a tool’s JSON lines as they are printed. What a line means is inClaudeTextStream.swift,OpencodeTextStream.swiftandPiTextStream.swift, beside each tool’s arguments.HangWatchdog.swiftsamples the app when its main thread stops answering for four seconds and writes the stacks to~/Library/Logs/Floe.DirectoryWatcher.swiftwatches a folder tree with FSEvents.Extensions/HotReload.swiftuses it to restart an open view command when a file under its extension’ssrc/orassets/, or itspackage.json, is saved. Only local extensions with asrc/folder are watched, never the ones Raycast installed.
-
CREDITS.mdandSources/Floe/App/Credits.swiftare written byscripts/generate-credits.py; run it after changing a dependency. -
ThawUI, a dependency: the controls, styles and shared pages Thaw and Floe both use. ThawUI holds no text of its own;
Thaw/ThawUIWords*.swiftgives it Floe’s. -
Fuzzing: the fuzzing harness and its corpus, a package of its own..github/workflows/fuzz.ymlruns it nightly and on any change to it.
The security reasoning is in the assurance case, and what Floe exposes to or asks of the outside is in External interfaces.
Licenses
Floe is AGPL-3.0. Code from Thaw and the ThawUI package stay GPL-3.0, and code from Droppy Code keeps its
attribution terms. LICENSES/README.md says which license covers which folder, and extensions under extensions/
keep their own.