Skip to documentation

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 WisentErrorBanner above retained content. Data never disappears because a later read failed.
  • A mutation reports through WisentMutationBar in 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. NavigationLink inside a List did not change the destination when the row was clicked through the accessibility action; the application was navigable by keyboard only. A Button carries one unambiguous action, which is clickable and verifiable.
  • Do not run twelve reads as twelve async let bindings. A task-local allocator is a stack: awaiting children in declaration order frees the oldest allocation first, and the window aborts on launch inside asyncLet_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.