Loft is built as a multi-module developer toolbox. This doc explains the module model, the current roadmap, and how to add a new one.
Each module is:
- Focused. One workflow, end to end. Storage Explainer doesn't try to be a tree visualizer and a duplicate finder and a tag manager — it does one thing (explain disk usage) and does it well.
- Native. SwiftUI surfaces, macOS-native interactions (Reveal in Finder, NSWorkspace, TCC permissions). No Electron, no embedded browser.
- Independently testable. A module's models / services / scanners are unit-testable without the UI.
- Failure-isolated. A module's errors don't bring the shell down. Internal subsystems (scanners) catch their own errors and report them as data.
- Safe by default. Anything destructive goes through the macOS Trash + a confirmation + a log entry. Read-only is the default for anything ambiguous.
- Polished. Loft's design system is part of the value proposition. Modules use
Theme.swifttokens — gradients, shadows, typography — not ad-hoc styling.
A typical module under Loft/Modules/<YourModule>/ looks like:
Modules/
└── ProcessManager/ # example module
├── Models/
│ ├── ProcessInfo.swift
│ └── ProcessGroup.swift
├── Services/
│ ├── ProcessSampler.swift # actor that reads /proc-like info
│ └── ProcessKiller.swift # actor wrapping kill(2)
├── ViewModels/
│ └── ProcessViewModel.swift # @MainActor
└── Views/
├── ProcessDashboardView.swift
└── ProcessRowView.swift
Today the Storage module lives at the top of Loft/ (not nested under Modules/) because it's the only one. The second module should land under Modules/ and we'll likely migrate Storage there too once the seam is clearer.
A module needs to expose, at minimum:
- A root view — the SwiftUI view shown when the user selects the module from the sidebar.
- A view model —
@MainActor ObservableObjectholding the module's state. - An enum case in
AppShell.Moduleplus a sidebar icon row and a content branch.
Optional but encouraged:
- Per-module preferences (
UserDefaultskeys prefixed withmodule.<name>.). - An "Ask AI" entry point if the module surfaces ambiguous data the user might want explained.
- Reveal-in-Finder / Reveal-in-app actions on any concrete item.
Concrete steps. Replace MyModule with your name.
Use the new module proposal template. Pin the scope before writing code — a 30-line issue is much cheaper than a 3,000-line PR that needs reshaping.
In Loft/Views/AppShell.swift:
enum Module: String, CaseIterable, Hashable {
case storage
case myModule // <-- add
}Then add an icon row in Sidebar.body:
ModuleIcon(
systemName: "cpu",
isActive: selection == .myModule,
gradient: Theme.primaryGradient,
label: "My Module",
onTap: { selection = .myModule }
)And a content branch in AppShell.content:
case .myModule:
NavigationStack {
MyModuleDashboardView(vm: myModuleVM)
}
.tint(Theme.verdant)You'll need to add the matching @StateObject private var myModuleVM = MyModuleViewModel() (or pass it in from ContentView).
Under Loft/Modules/MyModule/ create the model / service / view model / views. Follow the same patterns as Storage:
- Use design tokens from
Theme.swift. - Wrap I/O in an
actor. - Make services Sendable.
- Catch errors at the service boundary; surface them as data.
- Write tests under
LoftTests/MyModule/.
xcodegen generatexcodegen reads project.yml and picks up new files under recursive source paths automatically.
Update the roadmap in README.md, README.ko.md, and the table below.
Open a PR using the template. Include a short screen recording — modules are visual, and motion conveys what a static screenshot misses.
| Module | Status | Description |
|---|---|---|
| Storage Explainer | shipping | Disk usage breakdown, drill-down, safe delete, Ask AI. |
| Process Manager | proposed | Visualize + kill heavy processes. Activity Monitor reimagined for developers (group by project / dev tool, sort by CPU/memory, kill with confirmation). |
| Load Average | proposed | Historical CPU / memory / load with sensible defaults. A persistent strip in the corner you can glance at. |
| Network Inspector | proposed | Active connections, listening ports, per-process bandwidth. Quick "what's hogging my network" view. |
| Git Lens | proposed | Multi-repo status across your code directories — uncommitted, behind/ahead, stale branches. |
| Dev Server Manager | proposed | Start / stop / log local services (Postgres, Redis, custom processes from a config file). |
| Environment Manager | proposed | Inspect and edit shell environment variables, per-shell and global. |
"Proposed" means we've sketched the value but nothing's started. Pick one up by opening a new module proposal issue and starting the conversation.
When designing a new module, think about:
- What's the equivalent of "Reveal in Finder"? Every module should have a one-click escape hatch into the user's familiar tools.
- What's the safety story? What can the module destroy or change? How does it confirm? Where does it log?
- What's the empty / loading state? Loft cares about polish in those states.
- What's the failure-mode messaging? If a permission is missing or a system call fails, the user should know in human language.
- Does it deserve "Ask AI"? If the module shows opaque names the user might want explained (process names, kernel extensions, etc.), wire up
PromptBuilder+AskAIMenu.
Things Loft will probably never ship:
- Network ad blockers, VPN clients, content blockers — out of scope.
- Anything modifying other apps' internals via injection.
- Anything operating on the user's behalf without explicit action.
If you're unsure, propose it. We're early enough that the boundary is still being drawn.