The four surfaces¶
Epic #268 (Phase 7 of #262).
Open ZettelFlow, not Obsidian. The ~12 sidebar views that ZettelFlow grew were consolidated into four surfaces, each hosting the former views as modes behind a segmented control. One front door, everything reachable, nothing deleted.
One ribbon button¶
Purpose-led work (#401) lives inside the existing Home → Cultivate mode. A small inquiry activation intent selects start/resume/ordinary; private purpose text is never put into workspace view state. The runtime singleton owns draft/checkpoint continuity across renderer remounts. No new surface or command is registered; the ordinary mode keeps its recipe and friction settings.
There is a single all-in-one ribbon button ("Open ZettelFlow"). Its menu leads with Create note (also the hotkey-bindable Open workflow command), then the system-adoption actions, then the surfaces. Note creation is no longer its own ribbon icon.
The surfaces and their modes¶
| Surface | Modes | Folds in (former views) |
|---|---|---|
| Home | Home · Cultivate · Think | ZettelFlow Home (+ a "What to do next" recommendation surface, #273) + Cultivate (#309) + Think (#467), the thinking space. The What ZettelFlow changed mode (#454) is gone (#511): undo is offered in the moment, see reversibility |
| Health | Tend · Practice | Tend (#644): one row per note that needs you, handed to This note on the fix — replacing the slip-box health ops console and the knowledge dashboard folded into it (#314) — + Practice (#645): Momentum and Agency merged — the 12-week development strip, the decisions on proposals and the latest decisions. The Timeline mode became This note (#640) |
| (Discovery — dissolved, #504) | — | its four modes answered two questions that already had homes: see below |
| Explore | (one mode, so no mode bar) | Ask your graph + the retired Graph surface (#484), whose 3D view is now one of Explore's lenses |
The count went 4 → 3 → 4 → 3, and the honest reading is not "boxes were saved".
484 absorbed the Graph surface: it hosted one mode, answering what shape is my knowledge? while¶
Explore answered which notes match this? — the same question in two places, with no way to carry an answer across. That merge stands.
487 then gave Explore its own room. Discovery's four modes are narrow lists — a handful of¶
pairs, a few resurfaced notes — which is why people keep that surface in a side panel. Explore is facets, chips, an answer, a lens bar and a 3D graph, and a mode cannot be moved out of a pane without dragging the four lists with it. The principle was never "fewer boxes": it is one home per capability, and no two places answering the same question.
504 then dissolved Discovery, whose four modes were never about discovery:¶
| Mode | What it really was | Where it went |
|---|---|---|
| Connections | findDiscoveries, a ranked pair-finder | Home — which already rendered it, from the same function |
| Questions | a global list of what is unanswered | Home |
| Forgotten | notes near the active note you have not revisited | the note's own view |
| Challenges | the active note's supports and contradictions | the note's own view |
Two were recommendations — what should I do next, which is what Home is for — and two were about the note you had open, which is what the This note mode of Health already was. A surface whose modes answer other surfaces' questions is a door onto four things that live somewhere else.
A surface with a single mode draws no mode bar — a bar offering one choice is not a choice, and an ARIA tablist of one is noise for a screen reader too.
Every mode reuses the retired view's rendering verbatim — same numbers, same behaviour — mounted inside the surface as a KnowledgeModeRenderer (an Obsidian Component, so its listeners are cleaned up on every mode switch).
One primary action per header (#577)¶
A mode's header may carry one control that opens a capability. A refresh, a filter, and moving around inside the mode are not that.
The reason is measured rather than aesthetic. The Lab drew its legend and the mechanic that starts a collision through one private helper, with one class and one weight — so nothing on screen said which of them mattered, and think before you look sat in that row for a release without being found. It is the same shape #509 found in the move buttons ("the thing the picker exists to avoid, put back by hand") and #542 in the 3D view (subtract, then a gear), one level down.
ModeHeader is the shared helper, and its three calls are the three ranks:
| call | what it draws | for |
|---|---|---|
primary() | one mod-cta button, with an icon | the control that opens something. Calling it twice throws |
nav() | a plain, muted button, in the header | next idea, refresh, a filter — navigation inside the mode |
secondary() | an item in one overflow menu | everything else the header used to carry |
What the rule does not reach: a cancel that appears while a composer is armed, an undo drawn where the card was, a clear beside the filter it clears. Those are controls on the thing they act upon, and two of them were placed deliberately, against a toast, for reasons written down at the time.
The three headers it re-ranked:
| mode | primary | header navigation | overflow |
|---|---|---|---|
| Lab | start a collision | — | the legend |
| Cultivate | work on your own question | another idea | think about this instead |
| This note (its own view, #640) | — | — (the story's chips filter inside the story, #642) | share the idea card, reasoning paths |
Crystallize is deliberately not the Lab's primary. It lives in the picked bar, on the selection it acts upon; promoting it would put a permanently inert button in the header, which is exactly the clutter #542 removed.
onePrimaryAction.test.ts counts the primary() calls per renderer. It cannot tell a capability from navigation — that is a judgement — but it makes the judgement singular and visible in a diff, and ModeHeader throws on the second call, so the two halves cover each other.
A dashboard that fills the pane (#620)¶
The Home surface's modes are a minimalist, responsive dashboard rather than a tall single column. The shared primitives live in src/styles/components/dashboard.scss: a root that fills the pane (centred only past a generous rem cap, never a fixed-pixel cage) and a dashboard-grid of repeat(auto-fit, minmax(…, 1fr)) — one column when the pane is docked narrow, two or three when Obsidian's side panels are collapsed, with no breakpoint and no script. The retired cages (.cultivate 44rem, .inquiry 52rem, .lab 780px, each margin: 0 auto) are gone.
- Home leads with a header strip (greeting + a single
Capture a thoughtprimary + a fixed, icon-only Ask your graph door that deep-links to Explore) and three hero tiles — what to do next (the one accent card), cultivate an idea, ready to look at again. Everything else (new ideas, main concepts, review, gaps, open questions, pinned queries, the 3D-graph teaser) folds behind one "show everything" disclosure, collapsed by default. - Cultivate wears the same shape: the idea under cultivation is the one accent card, Notes by stage is a card that is also the stage filter, and the five moves reflow as a grid.
- Think (the Thought Lab) keeps its composer and thread list; the list reflows into grid columns on a wide pane (grid, not CSS
columns, so the newest-first order still reads left-to-right).
The Ask your graph door is an icon-only nav() variant of ModeHeader (icon + tooltip + aria-label, no visible label) — a fixed door where you already are, never a second primary().
A surface as a set (#875)¶
A surface marked continuous in the registry — today only Home — is one set: its modes sit side by side in one strip, in tab order, and the host moves a camera along it.
travel()is the one way between modes, replacing the old empty-and-rebuild. It builds the destination in its own pane, waits for the renderer'sready()(capped by the shot's patience), then pans with the sharedCameraPan(the Community's room pan, #798 — one pan, not two) and lets the mode it left go when the shot lands. One renderer at rest, two during a shot.- A door (
travelByDoor) carries a payload and the card you pressed. The destination'slanding(payload)says where the card lands — ornull, for a plain pan — and the card flies there with the Reader'sfly()on the pan's own beat;landed(el)marks it. - The way back is the reverse shot to the card, once: the host sets the renderer's
backTo, drawn as header navigation (nav()), never a second primary (#577), and takes Esc on the view's scope only when nothing else wants it — no text box, no menu, and the mode'sclaimsEscape()false. The pure rules (panPlan,shotPlan, thedoorsreducer,escapeIsFree,centeredScrollTop) aresurface/modeSet.ts. - No shot for a link. A deep link, a restored layout or
refresh()lands instantly; every surface that is notcontinuous(Health, Explore) travels instantly too, as it always switched. - The camera is never knocked sideways: the set's viewport clips (
overflow: clip) and a landing is scrolled to by number, neverscrollIntoView.
Open as tabs¶
Surfaces open as normal main-area tabs (getLeaf('tab')), not only in the right sidebar — so you can move, split or pin them like any Obsidian document.
No visible breakage (§XI)¶
- The 12 retired
show-*opener commands are kept as aliases that open the owning surface at the right mode (e.g.show-slipbox-health→ Health/Tend,resurface-related-notes→ This note,show-notes-history→ This note). Bind hotkeys to them as before. - The Graph surface's own view type joined that alias list when it was retired (#484), so a workspace saved before the merge reopens Explore instead of an empty pane, and
show-graph/explore-in-3dkeep their ids and their hotkeys. - A mode that moved house is handed over rather than dropped:
relocateModemapszettelflow-discovery:askto the Explore surface (#487), so a workspace saved while Explore was still a Discovery mode reopens Explore — instead of quietly showing Connections, which is the failure that looks like nothing went wrong. - A user's saved or pinned leaf of a retired view type is transparently redirected: a thin
LegacyRedirectViewtransforms that leaf into the surface it now lives in, so no "no view of type X" pane ever appears.
Where it lives¶
- Registry + back-compat maps (pure):
architecture/components/core/surface/{surfaceRegistry,legacyTargets}.ts. - Host + renderer contract:
architecture/components/core/surface/{ModeHostView,KnowledgeModeRenderer,LegacyRedirectView}.ts. - The three surfaces:
architecture/components/core/surface/{Home,Health,Explore}SurfaceView.ts. - Deep-linking to a mode:
activateSurface(app, surfaceType, mode)inarchitecture/plugin/services/ViewActivation.ts. - The dashboard primitives + header:
src/styles/components/dashboard.scss(fluid grid + cards) andarchitecture/components/core/surface/ModeHeader.ts(oneprimary(), plus the icon-onlynav()door, #620).
Standalone views (#640)¶
Not everything is a surface. A standalone view answers one thing in one place and has no modes: the script workbench (#446), and This note (#640) — the per-note companion docked in the right sidebar, following the active note. surfaceCount.test.ts names the allowed ones, so a new view cannot quietly become a fourth surface.
The redirect maps in legacyTargets.ts therefore have two kinds of target: a surface + mode ({ surface, mode, lens? }) and a view ({ view }). Every door that led to Health's Timeline mode — four commands, three retired view types, a saved timeline leaf, Discovery's forgotten and challenges — now leads to { view: "zettelflow-note" }. A restored retired leaf closes and the one companion opens where it lives, through openNoteCompanion, which serialises requests so a workspace holding several retired leaves still ends up with exactly one.
A mode renamed inside its surface (#644)¶
When a surface renames one of its own modes — Health's health became tend — a saved leaf on the old id is switched in place: RELOCATED_MODES maps zettelflow-health:health to { surface: "zettelflow-health", mode: "tend" }, and ModeHostView.setState sees the target is its own surface, shows the new mode and saves the layout. It does not round-trip through setViewState to the same view type, which would land back in setState and leave the stale id in the saved workspace.