Surface is a local, editable Raycast-style overlay for macOS.
The first version is model-first and block-first:
Sources/Core block, layout, and persistence models
Sources/App app entry, Surface state, overlay view, and AppKit system UI
plugins/ installed blocks plus each block's source and tests
tools/ block preview renderer
docs/ architecture notes
tests/ model tests
Surface does not have a separate plugin/provider layer. A block type is the unit of extension. The architecture details live in docs/overlay-model.md.
Clone the repo on macOS, build the SwiftPM package, then launch the local app bundle:
swift build
./script/build_and_run.sh./script/build_and_run.sh writes dist/Surface.app and opens it. Use that app bundle for normal local testing because the script also stops stale Surface processes that can hold old global hotkeys.
To test the installed-app path, copy or install dist/Surface.app to /Applications/Surface.app, then run:
./script/verify_alt_e.sh --app /Applications/Surface.app- macOS 14 or newer.
- Swift 6 toolchain / Xcode command line tools.
swift buildRun the menu-bar app and overlay:
./script/build_and_run.shWhile the app is running:
Option-Etoggles the Surface overlay.- The menu-bar icon can show, edit, hide, and quit Surface.
Escapehides the overlay.- In edit mode, the Block Registry is generated from the installed blocks; enable/disable blocks there and drag block cards on the grid.
- The Quicksave block registers
Option-Cwhile it is enabled. - The Copy History block watches the macOS pasteboard while enabled, stores recent text copies locally, and lets you click an item to copy it back.
- The Activity Context block reads Coast screen-activity summaries when live processes are allowed.
- The Follow Ups block reads local Cued follow-up candidates when live processes are allowed.
- The GitHub Queue block reads current-repo pull requests through
gh pr listwhen live processes are allowed. - The Integration Hub block shows local readiness for agent/workflow CLIs and source-backed integration ideas without running automations or storing credentials.
The run script builds the SwiftPM product, stages dist/Surface.app, kills stale project-local raw App processes and legacy Surface.app processes that can hold old hotkeys, and opens the bundled app. Use this path for normal UI testing instead of repeatedly launching the raw SwiftPM executable.
Extra run modes:
./script/build_and_run.sh --verify
./script/build_and_run.sh --logs
./script/build_and_run.sh --telemetry
./script/build_and_run.sh --debugVerify the installed /Applications/Surface.app Option-E path, including an idle window:
./script/verify_alt_e.shRun the full test suite:
swift testUseful focused checks:
swift test --filter CoreTests
swift test --filter BlockPreviewTests
swift test --filter QuicksaveTests
swift test --filter CopyHistoryTests
swift test --filter CodexLogTests
swift test --filter ActivityContextTests
swift test --filter FollowUpQueueTests
swift test --filter GitHubQueueTests
swift test --filter IntegrationHubTestsUse the preview harness for plugin UI iteration before relying on the full overlay. The harness renders real block runtimes through Blocks.registry, Block.makeRuntime(...), and BlockRuntime.makeView().
List available preview cases:
swift run block-preview listRender one block with a deterministic fixture:
swift run block-preview quicksave --fixture notes-and-captures --size 420x520Render every block fixture:
swift run block-preview all --output .build/block-previewsRender the whole default Surface layout:
swift run block-preview surface --output .build/block-previewsThen inspect the PNGs in .build/block-previews/ and run:
swift test --filter BlockPreviewTestsThe expected loop is:
- Render the single block fixture.
- Inspect the PNG.
- Patch the real block view or fixture.
- Rerender the single block.
- Render all fixtures and run the preview smoke test.
Do not create a preview-only SwiftUI surface for a block. If a block needs deterministic data, pass it through Block.Context and the fixture code in tools/block-preview/support/BlockPreviewSupport.swift.
The extension unit is a Block:
Core.Blockstores catalog metadata, default size, and a runtime factory.Core.BlockRuntimeowns live behavior:start(),stop(),refresh(), andmakeView().Core.Block.Contextpasses app services and deterministic test/preview inputs such askeyboardShortcuts,storageDirectory,now,allowsLiveProcesses, andallowsExternalWrites.plugins/Blocks.swiftbuilds the activeBlocks.registry.Sources/App/Surface.swiftcreatesRunningBlocks, starts runtimes for enabled layout instances, stops disabled runtimes, and asks each runtime for its SwiftUI view.Sources/Core/Layout.swiftstores block ids, enabled state, and grid frames. Runtime behavior never lives in persisted layout JSON.
Current plugin targets are Quicksave, CopyHistory, CodexLog, ActivityContext, FollowUpQueue, GitHubQueue, and IntegrationHub. Each exposes Plugin.block from its plugins/<name>/source directory. CopyHistory uses the same runtime contract as the other blocks: previews load copyhistory.txt from Block.Context.storageDirectory, while the live app watches NSPasteboard.general and persists history under Application Support.
Context-aware blocks follow the same contract. Previews read deterministic fixture files from Block.Context.storageDirectory, while live adapters are guarded by Block.Context.allowsLiveProcesses. Live Coast, Cued, and GitHub CLI reads run in plugin-owned background tasks so app launch and global shortcut registration stay responsive. Integration Hub is intentionally lighter: it checks executable availability and environment readiness, then copies commands or opens source docs from explicit row actions.
This table mirrors the block registry in plugins/Blocks.swift. Treat the Swift registry as the source of truth; update this table when adding, removing, or renaming a block.
| Block ID | Title | Target | Owner Path | Block Default | Initial Layout | Live Dependencies | Preview Fixtures | Boundary |
|---|---|---|---|---|---|---|---|---|
quicksave |
Quicksave | Quicksave |
plugins/quicksave |
10x5 |
8x5 |
macOS pasteboard and Option-C hotkey through Block.Context.keyboardShortcuts |
empty, notes-and-captures |
Captures clipboard/notes into local files and optional Obsidian paths; does not own Copy History. |
copyhistory |
Copy History | CopyHistory |
plugins/copyhistory |
8x8 |
7x6 |
macOS pasteboard; Application Support history file | empty, mixed-clipboard |
Stores recent text copies and copy-back actions; no rule engine or cross-plugin clipboard router. |
codexlog |
Codex Log | CodexLog |
plugins/codexlog |
8x10 |
7x8 |
Local Codex state, session files, action log, and optional process scan | empty, active-thread |
Shows Codex threads/actions and approval rows; remains the generic action-log owner. |
activitycontext |
Activity Context | ActivityContext |
plugins/activitycontext |
7x6 |
7x6 |
Optional coast CLI via Core.LocalCommand |
empty, work-session |
Shows bounded current/recent screen context; not a timeline browser or OCR search UI. |
followupqueue |
Follow Ups | FollowUpQueue |
plugins/followupqueue |
8x5 |
8x5 |
Optional cued CLI via Core.LocalCommand |
empty, mixed-followups |
Shows local follow-up candidates; does not send messages or duplicate Cued's contact graph. |
githubqueue |
GitHub Queue | GitHubQueue |
plugins/githubqueue |
8x5 |
8x5 |
Optional gh CLI via Core.LocalCommand |
empty, mixed-prs |
Shows current-repo PR cards and copy/open actions; not a full GitHub client. |
integrationhub |
Integration Hub | IntegrationHub |
plugins/integrationhub |
10x4 |
16x5 |
Optional CLI/env checks for browse, bb, coast, cued, gh, and selected agent tools |
empty, mixed-integrations |
Readiness/source cards only; no credential store, installer, automation runner, network crawler, or second registry. |
External integrations are optional. Missing CLIs should produce visible empty or unavailable states, not startup failure. Blocks that read local command output use Core.LocalCommand so executable lookup and process execution stay centralized.
Use the existing block path instead of adding a second plugin manager.
- Add the plugin target under
plugins/<id>/source. - Expose
public enum Plugin { public static let block = Block(...) }. - Implement a runtime that conforms to
BlockRuntime. - Put side effects in
start(), undo them instop(), reload cheap state fromrefresh(), and return the real SwiftUI UI frommakeView(). - Use
Block.Contextfor app services and preview/test determinism. For example, register hotkeys throughcontext.keyboardShortcuts, read fixture files fromcontext.storageDirectory, and disable process scans or external writes when the context flags say so. - Register the block in
plugins/Blocks.swiftand wire the SwiftPM target/dependencies inPackage.swift. - Add focused plugin tests under
plugins/<id>/tests. - Add preview fixtures in
tools/block-preview/support/BlockPreviewSupport.swift, then updateBlockPreviewTestscoverage. - If the block should appear by default, add a
Block.InstancetoSurfaceLayout.defaultLayout. - Validate with
swift test --filter <PluginTests>, the single-block preview command,swift run block-preview all --output .build/block-previews, andswift test --filter BlockPreviewTests.
The app currently uses Carbon RegisterEventHotKey, not an Accessibility event tap:
- Overlay toggle:
Option-EinSources/App/main.swift. - Quicksave capture:
Option-Cinplugins/quicksave/source/Runtime.swift. - Registration failures are logged through the
com.snbafana.Surfacesubsystem and surfaced in the menu-bar menu as a shortcut issue.
If hotkeys stop working:
- Relaunch with
./script/build_and_run.sh; it kills the bundled app, stale project-local rawAppprocesses, and legacySurface.appprocesses before openingdist/Surface.app. - Check that the Surface menu-bar icon is present; if it is not, the app is not running.
- Open the menu-bar menu and look for a
Shortcut issue:row. - Wake/screen changes should trigger
reconnectRegisteredShortcuts(), but relaunch if a shortcut disappears after display changes. - Run
./script/verify_alt_e.sh --idle-seconds 130to relaunch the installed app, press Option-E before and after the idle window, and write logs plus a screenshot under.build/surface-status. - Check macOS keyboard shortcut conflicts for
Option-EorOption-C.
The current code does not call AXIsProcessTrusted or request Accessibility permission. If macOS permission prompts appear while testing future UI automation, grant permission to the launched app or to Terminal when using raw executable debugging. If a future block uses event taps, UI scripting, or input monitoring APIs, add an explicit permission check and a visible failure state rather than assuming Carbon hotkey behavior covers it.