Master & child windows — every state, every route between them

SUPERSEDED 19 Aug 2026 — the master/child model was deleted the day after it shipped. It was replaced by peer windows (348b5122): every window keeps its project list and takes its study from the serve. Read this sheet as history, not as spec.

Dead — do not build from these: §2 (two window types), §4 (the ⌥⌘N gate — NewWindowGate is deleted), §5 (menu states by window type — there are no window types), and the study-axis dimming throughout. WindowRoster.Role, role(for:) and the role: parameter on every menu site went with them.

Still live, and the reason this file is kept: §1 "The surfaces a user judges by" — the framing that caught a window disagreeing with itself in three places, and the method the peer work was reviewed by. The context menus (Open in New Window on the lens row; Open Project in New Window on the project row) are still specced and still unbuilt. §7's video pop-out defect (Finding 22) is still open.

What replaces it: peer today, and Stage 3b next — every window shows any project, any lens, or Welcome, independently. Under 3b the sheet's central question (which window may change the study?) dissolves: all of them may, and each changes only itself.

Stage 3a as decided 18 Aug 2026. One serve, one project at a time, N windows, N lenses. Masters get projects, children get lenses. This sheet exists to be judged the way the app is judged — by the surfaces a researcher can actually see.

Master — has the project list, picks the study Child — lens rail only, inherits the served study shipsas specified bugshipped today, wrong decideopen

The one rule everything else falls out of: a child has no project of its own. Its title is read from what is being served, never from a per-window selection. A child therefore cannot name a study it isn't showing — the failure is unrepresentable rather than guarded.

1 · The surfaces a user judges by

Every claim below is a thing on screen. If two of them can disagree, that's the bug.

SurfaceMaster reads it fromChild reads it fromCan it lie?
Window titleits own selectionthe serve Master: only during a switch (guarded). Child: never.
Window subtitlelens counts over the bridgesame No — the bridge is per-window and reports what that window shows.
Sidebar — project listpresent, one row selectedabsent Today: yes. Master's highlight and title share one source, so they agree and are wrong together.
Sidebar — lens railpresent, own lenspresent, own lensNo
Window menu rowtitle + subtitle, as renderedsame Inherits whatever the title does.
Content panethe servethe serveNo — it is the source of truth
View ▸ Show Projectslivelive No — a child has a sidebar (the lens rail) and hiding it is a real thing to want. The name is right for 3b; only 3a makes it read oddly.

The correction this sheet is built on. The design doc argued a master is safer than a child because a wrong title is contradicted by the sidebar. It isn't: in a master the title and the sidebar highlight both derive from the same per-window selection, while the content comes from the shared serve. They agree with each other and are wrong together. Two cues from one source are one cue drawn twice — which makes the master the more deceptive shape, because it looks corroborated.

2 · The two window types

master · project selected ships
IKEA Study
16 Sessions · 18h 23m
Studies
IKEA Study
Nokia Diary
Views
Sessions
Quotes
Analysis
Sessions
child · ⌥⌘N from the master to build
IKEA Study
16 Sessions · 18h 23m
Views
Sessions
Quotes
Analysis
Codebook
Project
Sessions
child · re-pointed (step 2 of 2)
IKEA Study
312 quotes
Views
Sessions
Quotes
Analysis
Codebook
Project
Quotes

A new child opens at its parent's lens, then gets re-pointed. Two steps, and the first one is never a surprise — the gesture reads as "duplicate this view, now change it". Opening at a fixed default would cost the same two steps but start somewhere nobody asked for.

The child is one flag, not a second window type: ProjectSidebarOutline already folds the lens rows into the same NSOutlineView as the project list, so this is a section omitted.

3 · The happy path

  1. Launch. No windows → master, welcome screen, project list present.
  2. Pick IKEA Study. Serve starts. Title and sidebar agree because there is one window.
  3. ⌥⌘N → child on IKEA Study at Sessions (the master's lens). No project list.
  4. Click Quotes in the child. Now: Sessions in the master, Quotes in the child. Two lenses, one study, both titles true. This is the feature.
  5. ⌥⌘N ×3 more → three more children. Five transcripts side by side is the observed case.
  6. Close a child. Nothing else moves.
  7. Close the last window. Serve stops (stopServeIfLastProjectWindow).

4 · What ⌥⌘N makes, and why the condition is not "no windows"

State⌥⌘N makesWhy
Zero windowsmaster the menu bar outlives windows, so this is the documented way back from empty
Welcome master, nothing serveddisabled you would simply get two welcome screens
Master with a projectchildthe normal case
Master + childrenchild
Children only — master closedchild ⌥⌘N means "another lens window", always — see the note
Welcome master + children (serve still up)child something is served — the children are showing it
Only Settings / Import openmasterthe roster deliberately doesn't count them

The condition is what is served, not what windows exist (decided 18 Aug 2026). ⌥⌘N means one thing everywhere — another lens window on the study I am looking at — and it needs a study to be looking at. So it is disabled exactly when nothing is served and a window is already open, which is the two-welcomes case and nothing else. With no windows at all it still makes a master, because the menu bar outlives windows and that is the way back from empty.

Note the case this quietly handles. A master that returns to the welcome screen while children are open does not stop the serve — stopServeIfLastProjectWindow keeps it up while any other window still shows a project, and the children do. So the study stays served, the children keep working, and ⌥⌘N stays live because there is still something to be a child of. "Is anything served" is the right question; "is the front window a welcome screen" is not.

5 · Menu states, by window type

Dimmed, never hidden — except where the item is gone for good.

File · from a master
File · from a child
View · master / child
Window · two windows, one study

decided View ▸ Show Projects keeps its name and stays live in a child. An earlier draft dimmed it — written when a child was going to have no sidebar at all. The child we settled on keeps the lens rail, so it has a sidebar and hiding it to widen the content is a real thing to want; dimming would remove a capability to make a label read better. The name stays because it is right for the destination: at 3b it will still be called Projects in a window that has them. 3a is the temporary state, not the name.

Why the whole New/Add/Import family dims in a child. They all act on which study — the axis a child deliberately doesn't have. Dimming them is the same statement the missing project list makes, in the place a Mac user checks when a click does nothing. The lens rows stay live, because that is the axis a child owns.

Measured, not assumed: AppKit's automatic Window-menu path renders Title (Subtitle) — verified 18 Aug 2026 with two throwaway binaries. So two windows on one study at different lenses are already distinguishable without an ordinal.

Context menus

right-click a project row — master only
right-click a lens row — master and child
right-click the content pane — child

decided Both ship, and the project one is renamed. Right-click a lens → Open in New Window (a child at that lens — the most direct form of the gesture, and it beats ⌥⌘N-then-re-point for the deliberate case). Right-click a project → Open Project in New Window. Two commands, two labels, no collision — and the longer label is on the item that does the bigger thing, which is the right way round. The project one stays dimmed until 3b; the lens one works today.

6 · Transitions

TriggerWhat movesWhat the user sees
T1⌥⌘N from a masternew child, parent's lens a second window, same title, same view — then they re-point it
T2Master switches studyevery child follows all titles change together; all panes change together; nothing disagrees
T3Child switches lensthat child only its subtitle changes; nothing else moves
T4Master closes, children remainnothing — no promotion children keep working; ⌥⌘N brings a master back
T5Last window closesserve stops
T6Dock icon, no windowsmaster the roster answers, not AppKit's hasVisibleWindows

T2 is the accepted oddity. Switching the master to another study takes the children with it, so you cannot peek at study B while keeping study A's transcripts open. Accepted 18 Aug 2026 — odd, manageable through beta, and better than any alternative before 3b. It is the Stage 3a constraint stated honestly rather than hidden behind a window that lies. Removing it is exactly what 3b buys, which gives that stage a one-line headline: "you can now have two studies open at once."

T4 is why promotion was rejected. Promotion would exist only to paper over there being one serve, would be deleted at 3b, and would make a window change shape while you're looking at it because you closed a different one. An orphaned child is a working window, and the recovery is a keystroke already in the researcher's fingers.

7 · Edge cases

E1 Two windows, two studies ships today — the bug

today: master + master
IKEA Study
16 Sessions · 18h 23m
Studies
IKEA Study
Nokia Diary
Nokia Diary's
quotes
as specified: master + child
Nokia Diary
204 quotes
Views
Sessions
Quotes
Nokia Diary's
quotes

The title says IKEA, the sidebar highlights IKEA, the pane shows Nokia — and Send to Miro exports Nokia's quotes to a board named IKEA. The child shape makes it unrepresentable: no selection, no second source, title read from the serve.

E2 Master mid-switch the master still needs a guard

Between "user clicks study B" and "sidecar for B is up", the master's title already says B while the pane still shows A. Short, real, and the one place a master can lie. Guard the mount on serveManager.currentProjectPath == project.path; show the boot state, never a sibling's report. Children are immune — they follow the serve, so they simply arrive when it does.

E3 Two children, same study, same lens

the only case an ordinal fires

Different studies are told apart by name; different lenses by subtitle. The ordinal is a last resort for genuinely byte-identical rows — the case Finder and Mail both leave stuck. That is a better justification than the one window-menu-naming.html rejected it on ("two views of one project, not two projects"), which was only ever true of Stage 3a. That mockup's E4 card needs correcting, not just reconciling with its own summary.

E4 ⌥⌘N while nothing is served decided — dims

A child of nothing is an empty lens rail, and making a second master just gives you two welcome screens. So the item dims. It is the one state where ⌥⌘N has nothing to mean. With no windows at all it stays live and makes a master, because the menu bar outlives windows and that is the documented way back from empty.

E4b Children only — the master was closed accepted, with a cost

⌥⌘N makes another child. The rule stays one sentence — another lens window on the study I am looking at — rather than gaining a special case, and the researcher gets what they almost certainly wanted, which is one more lens.

What it costs, stated plainly: with the master closed there is no project list anywhere, New Project / New Folder / Add Files / Import are all dimmed (they act on the study axis a child does not have), and Open in New Window is dimmed until 3b. So the only route back to a project list is close every window, then ⌥⌘N.

accepted 18 Aug 2026 Not nice, and lived with until 3b — at which point every window can serve its own study and the state stops existing. Every alternative is worse, and the reasons are worth keeping so none of them gets re-proposed:

AlternativeWhy not
Refuse to close the master You can always close a Mac window. A window that refuses is one of the few genuinely un-Mac behaviours — even sheets and panels close.
Close the children with it The nearest real idiom (closing a document closes its inspectors) is the opposite move — close more, not refuse. And it destroys a deliberate arrangement: five transcript windows the researcher placed by hand.
Promote a child to master Already rejected — exists only to paper over the single serve, deleted at 3b, and makes a window change shape while you are looking at it because a different one closed.
Keep ⌘N New Project live in a child Workable, and the closest call: NewItemFallback already almost does it. But it breaks the one clean rule a child has — every study-axis command dims — for a state that disappears at 3b.

E5 A run is in progress decided — narrate on main

The subtitle narrates the run, and the rule was .key-only — so opening Settings meant no window narrated, every subtitle dropped back to its resting count, and it read as the run having stopped. The intent was always "one window narrates, not five", and main expresses that where key does not: AppKit keeps mainWindow on the frontmost main-capable window when a panel takes key.

Measured, and it went against the guess. The Settings window cannot become main — the package overrides it (SettingsWindowController.swift:168, override var canBecomeMain: Bool { false }). So gating on isMainWindow fixes the Settings case in one line, with no roster change and no most-recently-key list — the structure the design declined for command routing stays declined.

FrontmostA project window narrates?
Settingsyesfree — it cannot take main
Import · Shoal · System Healthno plain Window scenes, so ordinary NSWindows that do take main — accepted

Import is a busy foreground task of its own; the other two are decorative or debug. Revisit only if it bites — then UtilityWindow (macOS 26, makes an NSPanel) for Shoal and System Health but not Import, or the roster MRU, earned rather than anticipated. The cheap test for the next window that raises this: open it over a project window and watch that window's title bar — main draws at full contrast, not-main goes grey.

E6 The video pop-out regression

It is a plain NSWindow, not a scene, so @FocusedValue(\.bridge) goes nil and every Video menu item dims exactly when the video window is what you're looking at. Master/child doesn't fix this — the pop-out is a third shape, and "front window" routing has no answer for a key window that isn't a scene.

parked 18 Aug 2026 — solve later Deliberately not fixed with this work, and deliberately written down in three places so it cannot be lost: here, Finding 22 of the workspace review log, and docs/design-workspace.md. It is a regression from b72c23eb (the per-window bridge), not a pre-existing wart — the Video menu worked before, when one app-level handler served every window. The general case is the same one E-series finding 26 names: a key window that is not a scene, which also covers open/save panels. Fix the pop-out first, then re-check whether the general case still bites.

E7 Relaunch

Children have no selection to restore — they take the served study. So only the master's selection needs persisting, which makes the restore story simpler under this shape than under all-masters. Today one global @AppStorage("selectedProjectID") means every restored window lands on the same study; harmless once only one window reads it.

8 · What this sheet asks you to decide

Still openCheapest coherent answer
E4bThe orphan dead end. Master closed, children only: no project list, every study-axis command dimmed, so the way back is close-everything then ⌥⌘N. Reachable by ordinary tidying. Accept it and say so — or keep ⌘N New Project live in a child, which already has a fallback that opens a window (NewItemFallback) and would be the one study-axis command that stays lit. Costs the clean "the whole family dims" rule.
E5Who narrates a run when Settings or Import is frontmost — today nobody does, and every subtitle drops to its resting count mid-run. Either keep one narrator (the last key project window) or accept silence and state it. Nobody decided the no-window case; it currently reads as the run having stopped.
E3window-menu-naming.html's E4 card still argues against the ordinal, on a premise ("two views of one project, not two projects") that was only ever true of Stage 3a. Correct the card, don't just reconcile it with its own summary — the reasoning is what the next reader will cite.
Pin vs hide-by-default-switchable, deferred on purpose by the design doc. Its own test still stands and needs the child to exist first: spin off a Quotes window and go ten minutes without reaching for the project list.

Settled 18 Aug 2026 — not reopened here. A child holds a lens, not a project and reads its title from the serve. Its sidebar is the master's minus the Studies section — same width, same metrics, Views simply moves up. ⌥⌘N is gated on what is served: child when something is, master when no window is open, dimmed otherwise; children-only still gives a child. Promotion rejected. A new child opens at its parent's lens. Switching the master takes the children. View ▸ Show Projects keeps its name and dims. Lens row gets Open in New Window, project row gets Open Project in New Window. Video pop-out parked. All in docs/design-workspace.md §"What a child window is" and its changelog.