Architecture & Internals

Architecture Overview & Boundaries

Target split between pure Core domain framework and OpenClip macOS platform integration.

On this page (10 sections)

OpenClip is engineered around a clean target split and single-responsibility architectural boundaries ("Architectural Subsystems"). This design guarantees strict separation between pure domain logic, platform-specific side-effects, dynamic action runtimes, and user interface components.


Target Split#

The codebase is split into two primary targets:

OpenClip Workspace
├── Core (Framework / Swift Package)
│ ├── Pure domain models (Action, ActionChrome, ActionResult, SelectionContext)
│ ├── Selection detection contracts (TextRetrieving, SelectionMonitoring)
│ ├── Central action catalog & ordering (ActionRegistry, ActionCoordinator)
│ ├── Action-search matcher & popup mode (ActionSearch)
│ ├── Strongly-typed settings engine (SettingsStore, SettingKey)
│ ├── Application policy rules (AppRule, RuleEngine)
│ └── Pure snippet & manifest parsing (OpenClipSnippetParser, ExtensionManifest)
│
└── OpenClip (macOS Application Target)
 ├── AppKit floating panels & SwiftUI UI (PopupPanel, PopupView, PreferencesView)
 ├── Action execution runtimes (JavaScriptAction, AppleScriptAction)
 ├── Platform side-effect handler (ActionResultHandler)
 ├── Action factory implementation (DefaultActionFactory)
 ├── macOS selection monitoring & retrieval (MacSelectionMonitor, SelectionRetrievalCoordinator + strategies)
 └── App Composition Root (AppDelegate)

Core Target Constraints#

  • Zero AppKit / SwiftUI UI dependencies in Sources/Core/Actions/ or Sources/Core/Settings/.
  • Pure Swift Types: Value types, protocols, and decoupled services.
  • Dependency Injection: Core components requiring settings accept a SettingsStore instance during initialization (defaulting to DefaultSettingsStore.shared). Builtin actions (SearchAction, CalendarAction, CalculateAction) take the store via BuiltinRegistry.makeCoreBuiltins(settingsStore:); ActionCoordinator takes registry/ruleEngine/extensionManager/settingsStore in init (default .shared). Display title/icon resolution goes through an injected ActionPresenting, never a hidden singleton inside the Action protocol extension.

Core Architectural Subsystems#

OpenClip enforces a single entry point for each cross-cutting concern. Bypassing these single-responsibility interfaces is strictly prohibited.

Rendering diagram…

1. Settings Subsystem — SettingsStore#

  • Responsibility: Centralized persistence and retrieval of application settings.
  • Mechanism: Operates via strongly-typed SettingKey<T> instances.
  • Strict Rule: Zero direct UserDefaults.standard calls anywhere in Sources/. All access goes through SettingsStore in Core via dependency injection or DefaultSettingsStore.shared in the App target. (The only remaining raw access is the one-time aiCloudAPIKey migration in AIServiceManager — read-then-delete; the AI-config/theme @AppStorage surface remains too. Migrating is ongoing — do not add new direct call sites.) Secrets (e.g. the cloud AI API key) live in SecretStore (~/.openclip/secrets.json with POSIX 0600 permissions), never UserDefaults.

2. Action Presentation — ActionCustomizationManager#

  • Responsibility: Resolves display titles and icons for specific UI surfaces (.popup or .table).
  • Mechanism: ActionCustomizationManager.presented(action, surface:) queries user customizations and falls back to action defaults; ActionPresentationModel carries the resolved title/icon.
  • Strict Rule: UI code never computes display icons using type checks or string matches. Preferences table icons delegate to ConfigurableAction.preferenceIconName.

3. Action Chrome Policy — ActionChrome#

  • Responsibility: Exposes UI policy metadata for actions without logic coupling.
  • Metadata:
    • badge: .none, .script, .url, .custom, .extensionPkg(String)
    • rowStyle: .standard, .actionGroup
    • popupBehavior: .perform, .showSubActions, .provideCompletions
    • source: .builtin, .custom, .extensionPkg(packageID: String)
  • Strict Rule: Views must inspect action.chrome.badge or action.chrome.source instead of checking if action is ScriptAction or matching action.id.

4. Action Factory — DefaultActionFactory#

  • Responsibility: Instantiating executable Action objects from extension manifest JSON metadata or parsed script snippet headers.
  • Mechanism: Implements the ActionFactory protocol in the App target where JavaScript, AppleScript, and process runtimes are available.

5. Action Result Handler — ActionResultHandler#

  • Responsibility: Executing platform side-effects returned by actions (such as copying text, pasting into active apps, opening URLs, or showing system sharing services).
  • Strict Rule: PopupWindowController manages window presentation and event filtering, delegating all result execution side-effects to ActionResultHandler.

6. Action Coordinator & Composition — ActionCoordinator#

  • Responsibility: Orchestrates initial state loading, registers builtins, and connects disk extensions (including GUI-authored manifest packages) to the central ActionRegistry.
  • Strict Rule: ExtensionManager does not couple directly to the registry; it reports changes through onRegister/onUnregister callbacks wired by ActionCoordinator.loadInitialState().
  • Search catalog: ActionCoordinator.searchCatalog (→ ActionRegistry.searchCatalog) exposes what the popup's action-search palette may offer: the registered catalog minus anything switched off in settings (per action, whole package, a disabled group's members, an AI preset toggled off) and minus anything that cannot run against the current context. It is the bar's visibility rules on a flat, unpaginated surface; ActionSearch.search ranks it.

Key Design Guidelines#

  1. Accept dependencies, don't create them: Core types accept SettingsStore in init(settingsStore:) with default fallback (see §1 above); ActionCoordinator and the builtin builder take their collaborators in init.
  2. No ActionRegistry.shared inside domain managers: Domain managers report registration changes through onRegister/onUnregister callbacks wired by ActionCoordinator. Only ActionCoordinator touches the registry directly.
  3. No switch action.id string matching in UI: Display formatting relies on ConfigurableAction.preferenceIconName and ActionChrome.
  4. Pure Snippet Parsing: OpenClipSnippetParser is a pure string parser with no UI or @MainActor ties (nonisolated).
  5. Pure Layout Math: PopupPositioner is a pure static struct for computing panel coordinates and edge clamping.
  6. Static previews don't share live state: PopupPreview hardcodes a canonical action set and passes its own PopupHoverState() + isStatic: true to PopupView, so it never reflects — or leaks hover into — the real popup.
  7. Standard windows unless a chrome-less card is the goal: Preferences uses standard window chrome (hiddenTitleBar/fullSizeContentView) with .glassSurface content; onboarding is a .borderless transparent window that re-draws the card, border, and shadow manually.