A native Mac application that collapses the pile a working day accumulates — several terminals, several browser windows, PDFs, images — into one window organised by project. Coding agents run inside it, so their processes can be seen, paused and killed rather than abandoned.
Claude keeps opening a million Chrome browsers and a million local servers that it then abandons.
Those processes are not only lost — they cost something, and you cannot see what. An application that owns the processes but hides their cost has solved half the problem. offline owns them and shows the bill.
Every feature is measured against one test: does it reduce the number of windows, or make what is running more visible? If it does neither, it was cut.
A project's state is recomputed from the world, never stored as a flag. It is broken because its folder is missing right now.
A missing folder is searched for, then reported, then left alone. A match is offered, never assumed — same-named folders are not the same folder. Nothing is destroyed: the layout, the manifest and the history all survive. It asks once, then stops, because a nag is the fastest way to teach someone to ignore warnings.
Each project opens with a sensible default: the agent always visible on the left, whatever you need to see on the right. Drag a tab to the other half to rearrange it, and the arrangement saves with that project — never globally.
The defaults are the feature, not a placeholder. offline reads what a folder actually contains and opens accordingly: a repository with a dev server does not want the same arrangement as a folder of documents. Detection is deliberately conservative — a wrong guess is worse than no guess, because it teaches that the app is unpredictable — and every project can be reset to its default.
| Shipping | What it does |
|---|---|
| Project rail | Folder-backed, four states, context menu for pause, resume, reset and locate. |
| Sessions | Four agent kinds through a login shell, so your PATH resolves the way it does in Terminal. |
| Restore on open | Brings back panes — tree, documents, preview address, arrangement. Instant, and it starts nothing. |
| Nothing runs unasked | No session starts on launch, on opening a project, or by restore. An idle offline runs no timer: the readout samples only while something is live and stops when the last session ends. |
| Closing a session | Every tab has a close control. Closing a session ends the process and returns its memory — the count going down means the process actually went away. |
| Pause / resume | Suspends the process group. CPU drops immediately; memory returns when a session ends — and the app says exactly that rather than implying a paused session is free. |
| Resource readout | Live, attributed by cause, and it warns before the fans do. |
| Viewers | File tree, markdown, PDF, images, live web preview. |
| MAP | Rings, force and groups over one data model. Filter by kind. Ask what connects two nodes. |
| Chat sidebar | Standalone conversation, always present, history kept as plain markdown in the vault. Retrieval works with no network and no key. No model is connected, and none is faked. |
| Capture | Through the chat; writes plain markdown into the vault's _inbox. One place to type, not two. |
| Publishes to Milo | The scrubbed index goes to Milo's plan tier so the phone can search the brain with this Mac asleep. Refuses without a sync code, refuses a partial index, and names the workspace rather than defaulting it. |
| Milo can drive it | showMode hands the window back; captureFile gives Milo's photo capture somewhere to put bytes. Filenames from a phone are treated as hostile. |
| Volumes remember themselves | A folder on a drive is remembered by the volume's identity, not its path — so it resolves itself whenever the disk is back, at whatever mount point macOS gives it. A same-named different disk does not match. Connecting never starts an index; it offers. |
| Nothing blocks on a share | Every filesystem call that could touch a network mount has a deadline. "Did not answer" is a distinct state from "gone" — one means plug it in, the other means it is missing. |
| Slow sources are honest | A vault on a network mount takes minutes. Live file count, and Stop actually stops. A cancelled index is discarded — a partial index is not a small one, it is a wrong one. |
| Tested | 131 checks over the scrubbed index, sensitivity inheritance, search withholding, the orphan-task rule, merge and tombstones. Packaging refuses to proceed if they fail. The first test written found a live leak. |
| Headless modes | --scrub builds the index without a window; --find searches from the command line. How the withholding rule is tested rather than asserted. |
| Scrubbed index | Titles, paths, tags and links only. No contents, nothing sensitive, and it verifies itself before writing. |
| Synthesize | Deferred. Overlap-finding across projects needs the index first. |
| A model behind the chat | Retrieval ships. Reasoning needs a decision about keys and endpoints that belongs to Milo, so the chat says so rather than inventing answers. |
| Phone client | Not built here, by agreement. The phone surface for the whole system is Milo; offline exposes the scrubbed index for it to read. |
Drag offline.app to /Applications. That is the
install.
1 · Gatekeeper. This beta is not notarised, so macOS refuses the first double-click. Right-click the app ▸ Open ▸ Open, once. After that it opens normally. Notarisation needs a paid Apple developer account.
2 · Documents access. macOS asks whether offline may read your Documents folder. It needs this to index a vault kept there. Nothing is uploaded — the index stays on the machine, and nothing is published to any address.
Drag offline.app to the Trash, then: rm -rf ~/Library/Application\ Support/offline
That is everything. There is no daemon, no login item, and no background server — closing the window ends the process, and that is a design rule rather than an accident.
Your vault is untouched. offline reads and writes plain files in a folder it does not own, the way Obsidian does, so your editor and your terminal stay valid views of exactly the same data. There is no app-only datastore, which means there is nothing offline can lose that nothing else can read.
HOME and WORK are separate vaults and separate projects. They are never merged and nothing crosses between them. Switching is a button you press — offline never infers which life you are in from what you happened to open.
Each side remembers its own vault, its own projects, and the project you had open. Sessions running on the side you leave keep running; they belong to their project, not to the window, and come back exactly as they were.
offline is the building half of a two-application system. Milo — the doing half — owns the plan, the timeline and the phone. It has not been built yet, and offline is already ready for it.
MILO is a mode in this window, not a link to a browser. Point it at wherever Milo lives and it loads there:
dev:http://localhost:5173 a dev server, while Milo is being built build:/path/to/dist a built folder, no server needed https://milo.wokegod.world the deployed site
Milo gets window.offline injected before its own scripts run —
no integration work on either side:
| Call | Returns |
|---|---|
projects() | the shared Project records, with live state |
sessions() | what is running right now |
startSession(id, agent) | starts one — Milo may drive offline |
pauseProject(id) | frees the machine |
index() | titles, paths, tags, links — never contents |
capture(text) | writes a plain file into the vault |
createTask(title, goal) | queued; orphan tasks are refused |
resources() | what this window costs, by cause |
status() | live or snapshot, and how stale |
A session Milo starts appears in the window. One started remotely that showed up nowhere would be precisely the abandoned process this application exists to abolish.
offline publishes an index of the vault that carries titles, paths, tags, modified dates and the link graph — and no file contents at all. Anything marked sensitive, at file level or source level, is withheld entirely rather than redacted.
{
"schema": "offline.scrubbed-index/1",
"counts": { "files": 0, "withheld": 0, "links": 0 },
"fields": ["path","domain","title","modified","tags"],
"files": [ ["path/to/doc.md","20-Projects","A Title",1755561600,[]] ],
"links": [ ["a.md","b.md","wikilink","[[b]]"] ]
}
Exclusion is inherited and cannot be overridden upward: a sensitive file inside a normal folder stays sensitive, and marking a source sensitive marks everything in it. The safe answer always wins. A verification pass re-reads the finished index and refuses to write it if a sensitive path appears — that check should always pass, which is exactly why it runs.