Product engineering
The Wisent application shell
The Wisent application shell WisentAppShell.swift ships the layer above the tokens: the shell every Wisent desktop application shares, and the ten rules that make ten applications look like one product without making them say the same thing
The Wisent application shell
WisentAppShell.swift ships the layer above the tokens: the shell every Wisent
desktop application shares, and the ten rules that make ten applications look
like one product without making them say the same thing.
The rules are not preferences. Each one was written after a measurement on a shipped application, and each measurement is named. When a rule looks arbitrary, the evidence is the argument.
1. Severity is the layout
A failure gets WisentAlertPanel: full width, a severity edge, the backend's
own sentence, and the exact command that reproduces it. A healthy signal gets
one line in WisentSignalStrip.
Measured: a client rendered a broken integrity chain — a retroactively edited append-only journal — in the same 210 pt card, in the same grid, as the card reading "Bundled backend". The card that meant someone edited the evidence was sized like the card that meant nothing is wrong.
2. not_configured is never red
Absence by choice is not an incident. A fresh install has switched nothing on, and colouring that like an outage teaches an operator that red means nothing — after which red means nothing when it matters.
Use .neutral and the words "Not configured". Keep .danger for a backend that
was asked and could not answer.
3. Navigate by decision, not by noun
A destination exists because a human has something to decide or verify there. Group them; two to four groups is legible, a flat list of seven is a filing cabinet.
Measured: one client's seven destinations named storage concepts and covered 15 of roughly 70 backend commands. Nine of the twelve real operator journeys — including every emergency and recovery path — had no surface at all. The application was a viewer for a tool that is used for deciding.
Scope — workspace, host, vault, project — is not a destination. Every screen reads it, so it belongs in the sidebar header.
4. The context bar replaces the hero
WisentScreen pins a 44 pt WisentContextBar: destination name, scope chip,
freshness, at most two verbs.
Measured: display(34) plus eyebrow plus detail plus a section header pushed
the first row of data to y≈390 in a 752 pt window. Over half the window was
spent restating the name of the destination the operator had just clicked.
The bar returns roughly 150 pt per screen — about a fifth of the window.
display survives in WisentEmptyState/WisentEmptyPanel and first run. It
does not belong in the shell.
5. Density is respect
Identifiers in WisentTypeScale.identifier(), 28 pt rows, monospacedDigit on
anything that changes, aggregation before enumeration.
A status pill appears only for the minority state. When 558 of 566 rows are "Active", no row gets a pill and the count lives in the facet rail. A pill on every row is 566 pieces of identical ink that make the eight exceptions harder to find, not easier.
6. A data screen is three zones
WisentFacetRail with counts, the table, WisentInspector. Facets are the only
filter surface. The inspector is a pane rather than a sheet, because an operator
compares the selected row against the rest of the list while reading it.
Pass WisentScreen(scrolls: false, constrainsWidth: false) and lay the three
zones out in one HStack(spacing: 0).
Column widths must fit the middle zone, not the window: at a 1280 pt default, 1280 − 236 (sidebar) − 168 (rail) − 320 (inspector) leaves 556 pt. Minimum widths that summed to 662 pt pushed two columns under the inspector; ideals that summed to 742 pt did it again after the minima fitted.
7. An irreversible decision is a dialog
WisentDecisionDialog quotes the backend's own reason code, lists exactly
what would be lost, names any backup written first, gives the safe verb the
primary button and the destructive verb its own red one.
Measured: a backend refused a pull with
reason: local_only_items_would_be_lost, listed the items and named the backup
file. The client decoded only ok and had no representation for any of it, so
the one decision that could cost an operator data was invisible.
8. State the boundary once
If the application reads only metadata, or never opens a file's contents, or never returns a value — say so once, in the sidebar footer.
Measured: one client repeated its boundary sentence on four screens. Repetition turns a contract into wallpaper.
Where a capability is absent by contract, list it. WisentCapabilityList(…, isAvailable: false) renders the absence as inventory. A disabled button implies
a permission that might arrive; an absent capability states a rule.
9. Every state is a designed state
- Loading names what is being read (
WisentLoadingPanel), not just that something is happening. - Empty-because-nothing and empty-because-filter are different states with different remedies, so they are different views. The second offers "Clear filters".
- A refresh that fails is
WisentErrorBannerabove retained content. Data never disappears because a later read failed. - A mutation reports through
WisentMutationBarin the backend's exact words, never a paraphrase and never silence.
Measured: one screen rendered skeleton rows that never resolved, beside a
blank sidebar, with no copy explaining either. Another swallowed every read
error with try?, so a dead socket looked identical to an idle one.
10. A screen occupies exactly the window
WisentScreen forces this with a GeometryReader, because maxHeight: .infinity
is an upper bound on a flexible child, not a ceiling on a greedy one.
Measured: a Table of 172 rows and a ScrollView around 500 asked for the
height of their contents. The detail column grew to 2 012 pt inside an 832 pt
window and pushed the whole split view — sidebar included — off the top edge, by
532, 320 and 188 pt on three different screens. The accessibility frames put a
sidebar title at y = −26 in a window whose own origin was y = 294.
Never work around the GeometryReader.
11. The window must exist
A redesign changes the root view type, and AppKit window restoration is keyed to
the old one. Call wisentEnsureWindow(title:) from
applicationDidFinishLaunching and retain what it returns; it builds a window
only when no usable one is on screen.
Measured: three applications launched from their real bundles on 2026-08-17 —
Oko, Skrzynka, Tama — came up alive and opened no window at all. Each
log says the same thing: hasPersistentStateToRestore=1, a savedIdentifier
naming the previous tree (WisentAuthGate<SkrzynkaRootView>, SwiftUI._FlexFrameLayout in one, Oko.SignInView in another), then
window=0x0. SwiftUI opens nothing after that, and the process sat there with
zero windows for as long as it was left running.
Every operator updating across a redesign reaches that state once, and a window that never appears is indistinguishable from a crash on launch. Clearing saved state is not the fix: it repairs one machine and leaves the next one exposed.
Interaction rules that cost a release
- Sidebar rows are buttons.
NavigationLinkinside aListdid not change the destination when the row was clicked through the accessibility action; the application was navigable by keyboard only. AButtoncarries one unambiguous action, which is clickable and verifiable. - Do not run twelve reads as twelve
async letbindings. A task-local allocator is a stack: awaiting children in declaration order frees the oldest allocation first, and the window aborts on launch insideasyncLet_finish_after_task_completion. Use a task group.
Component reference
| component | use |
|---|---|
WisentAppLayout |
the shared measurements: sidebar 236, facet rail 168, inspector 320, context bar 44, row 28, dialog 620, window minimum 1000 × 700 |
WisentTypeScale |
the type scale a data application needs; no display |
WisentScreen |
a screen: pinned context bar, canvas background, window-bounded content |
WisentContextBar |
destination, scope, freshness, at most two verbs |
WisentAlertPanel |
a failure, sized like one, with the backend's sentence and the reproducing command |
WisentSignalStrip / WisentSignal |
healthy signals, one line each |
WisentQueueRow |
one waiting decision: deadline plus one verb |
WisentFacetRail / WisentFacetGroup / WisentFacet |
the only filter surface, and where majority counts live |
WisentInspector / WisentField |
the detail pane beside a table |
WisentCapabilityList |
what this surface can do, and what it never will |
WisentTableFrame |
a Table in the design system's clothes |
WisentStatusChip |
a pill for the minority state only |
WisentLoadingPanel / WisentEmptyPanel / WisentErrorBanner |
the state canon |
WisentDecisionDialog |
the irreversible decision |
WisentCounterRow / WisentSectionBox |
aggregates and section structure |
WisentMutationBar / WisentMutationOutcome |
what a mutation did, in the backend's words |
WisentAction / WisentActionButton / WisentDestructiveButtonStyle |
verbs, and the one red button |
Adopting applications pin this package by revision and delete their local theme. Two copies of a shell is two sources of truth, and the second one is always the one nobody updates.