Skip to content

Accepted Limitations

Some behaviors are bugs by design — accepted consequences of a settled architectural trade, not defects to fix — and each row below names the trade, its architectural root and the real fix where one is planned. OS-blocked-by-SIP items are a separate class — see Blocked by macOS (SIP).

BehaviorWhy it’s acceptedArchitectural rootEscape hatch / planned fix
Deleting a macOS Desktop leaves its profile binding dormant: the row stays, labelled with the number the Desktop was last seen at and badged not present, and does nothing until you re-point it. The one Desktop macOS destroys when you unplug a screen comes back, binding and all, when the screen returns.A binding is filed under a private identifier KiwiDesk stamps into each Desktop’s own settings, and the only guess for re-attaching a record whose stamp is gone is the Mission Control number — the silent wrong-Desktop behaviour it replaced (a Desktop is its stamp, not its number).DesktopKey.identity keys profile_bindings; a record whose Desktop no reading can name is held, not pruned, since an unplugged screen’s Desktops come back with their stamps (#1147, DesktopBindingIdentityTests).Pick the profile again on that row in Profiles per macOS Desktop, or clear it. On a Mac where the stamp cannot be written, bindings key by the Mission Control number and behave as they did before.
A sticky window with Stay visible across Desktops on that is closed within a few seconds after a Desktop switch keeps its tile for a few reconcile beats — bounded — before KiwiDesk drops it.For a beat after a switch a carried sticky window is absent from every reading KiwiDesk has — its AX element dies as it leaves the visible Space, on a native app before KiwiDesk hears of the switch and on an Electron app about two seconds after, and the compositor has not drawn it on the arriving Desktop yet — so it is indistinguishable from a window that was just closed, and refusing the removal briefly is what keeps a carried window’s slot, sticky scope and reach pin through the switch (sticky reach).The removal-distrust gate’s carried arm (EventLoop.carriedWindows, #1145) refuses a carried window’s vanish census-blind on the #1157 episode’s recheck budget, each refusal arming the follow-up reconcile that converges a true close (CarriedRemovalTests pins the bound); the reach-departure arm (EventLoop.reachAwaitsCarry, #1215) refuses a gesture departure’s vanish the same way while the WindowServer still hosts the window on the Desktop you just left and KiwiDesk has not yet heard of the switch (ReachDepartureRemovalTests).Wait it out — the rechecks drop the tile within a second or two; focusing the app reconciles it sooner. A sticky window closed in the fraction of a second before KiwiDesk hears of a switch waits out the same rechecks. A switch KiwiDesk itself dispatches promises the carry ahead of the OS (#1213); on a gesture switch (a swipe, Ctrl+arrow, Mission Control) the window is carried like any other (#1215).
Returning to a macOS Desktop that holds other windows puts the focus on the last window that was on that Desktop, not on a sticky window with Stay visible across Desktops on that had the focus when you left; the sticky window follows you back a moment later without taking the focus.Reach is a carry, a move that runs after macOS has already restored the arriving Desktop’s own focus, and KiwiDesk honors the focus macOS reports (sticky reach, #1322).The carry (KiwiCore+StickyReach) moves and never raises (#1145, #1215); a Desktop with no other window has nothing for macOS to restore, so there the sticky window keeps the focus.Click the sticky window or focus it by keyboard; the focus you left a Desktop with is remembered per Desktop by macOS for windows that stayed on it. Planned: re-asserting the carried window’s focus at the settle when it was the one you left with (#1564).
A window closed while in native fullscreen (⌘W on the last tab of a fullscreen browser window, say) keeps its tile for a beat or two after macOS drops back to the Desktop — bounded — before KiwiDesk drops it and hands focus on.Some apps (Zen, and Firefox behind it) order the real window out for the fullscreen transition’s beat on both ends — absent from the app’s Accessibility window list and from the on-screen census while the compositor still hosts it — so a vanish of a window last read in fullscreen, whatever its app, or one the compositor hosts on a fullscreen Space, is refused briefly, and a window that really closed while fullscreen looks the same at that moment (#1272).The removal-distrust gate’s fullscreen arm (EventLoop.fullscreenRemovalArmIsOpen — the loop’s last fullscreen reading, or the compositor’s fullscreenSpaceHosts seam — #1272) refuses the vanish census-blind on the #1157 episode’s recheck budget; FullscreenRemovalTests pins the bound.Wait it out — the rechecks drop the tile within a second or two; focusing another app reconciles it sooner.
A Space named on move_to_desktop for a hidden Desktop is dropped, silently, if the window has not left within about five seconds — a slow app, or a move the bridge accepted and never applied — and the window then rejoins the Space it left.The name is a debt paid at the window’s departure, and an unbounded one would attach to a close minutes later (a Desktop move’s explicit Space is paid at the departure).PendingSpaceAssignment, bounded at FollowFocusIntent.drainWindow like the follow’s own debt (#1150, #1007; PendingSpaceAssignmentTests).move_to_space once that Desktop is shown, or move_to_desktop_and_follow, which takes you there with the window.
Returning to a macOS Desktop whose previously focused window takes more than about five seconds to come back — or never does, because it was closed from another Desktop — restores the space but not that focus: whatever macOS itself brought forward keeps it, and KiwiDesk’s own post-switch refocus stands down. Until you next leave that Desktop with a window focused, each return holds the same stale entry for the same few seconds.The remembered focus is a debt paid at the window’s own arrival, an unbounded debt would fire minutes later at a switch it was never about, and paying it to some other window would be the first-in-row jump the fix removed (a Desktop switch is not a close).DesktopMemory.returnFocus is a second FollowFocusIntent, bounded like the follow’s at drainWindow (#1007, #1207); the create fold pays it and desktopSettle stands down while it is owed (DesktopFocusPaymentTests).Focus the window yourself; the next departure from that Desktop records the fresh focus and the return is right again.
In a Track space set to focused track, with Desktops bound to different profiles that share a Space name, a column can gain an extra divider right after a Desktop switch — one track drawn as two — and it lasts until you next leave that Desktop. Own track — the default — is unaffected.For a beat after a switch the departing Desktop’s windows are still in the row beside the arriving ones, a departing track head hands its divider to whatever sits next to it, and a handed break is never handed on, so the stray divider is bounded to one stay (#1387, owner ruling 2026-09-15).Space.handedBreaks marks a handed break; the one hand-off door drops it when the holder leaves, and its head takes its own back on return (ReturningSlotTrackFoldTests); refusing the hand-off across Desktops would need the state fold to know which Desktop a window is on, which #1230 rules it never stores.Switch Desktops and back. Moving the window that holds the stray divider to a neighbouring track drops it too, merging the column behind it until its own head returns. Not measured on a device yet — file the log if it shows.
With Displays have separate Spaces on, a Desktop switch on a secondary screen (one without the menu bar) does not restore that Desktop’s focused window: the first window to re-list may take the focus. The main screen’s Desktops are the ones that remember.The switch handler’s secondary arm knows only which screen changed, and the binding authority is the main screen’s (#888; ruled a residue, not a scope, #1207); #1230 moves that screen’s Space from the same arm, so the residue is the focus alone.moveSwitchedDisplaySpaces moves that screen’s Space from the one reading switchedDisplays takes, but the #1207 focus debt is recorded at the focus REPORT against the native Space it was honored on and paid by the create fold when the owed window re-lists — neither of which the secondary arm reaches, since the debt is owed per Space while the arm moves a display (SecondaryDisplaySpaceTests; design decisions).Make the screen you switch Desktops on the main one (System Settings ▸ Displays, drag the menu bar), or turn Displays have separate Spaces off so every switch is the main screen’s.
Every keyboard focus destination in Settings that is not a text field — the layout picker a space deletion lands on, the Monitors space chips, buttons and pop-up menus generally — is unreachable unless System Settings ▸ Keyboard ▸ Keyboard navigation is on (off by default as observed on macOS 26.6.1, 2026-08-11). With it off, an assignment lands nowhere and focus falls to the search field at the top of the window.What is missing is a system-wide user preference; routing focus to the nearest text field instead is the top-of-window outcome the rule forbids, and no test can see the difference, since a source needle proves a destination was named and never that focus arrived.macOS decides which control classes may become first responder from AppleKeyboardUIMode; an app may read the user’s choice but must not set it, and @FocusState assignments to a control the system refuses are dropped silently (#678 Phase 4 pass 10; the obligation is in .claude/rules/gui.md ▸ The keyboard path).Turn Keyboard navigation on — every documented keyboard path then works. The user guide says so in Using Settings from the Keyboard; VoiceOver users need nothing, its cursor being independent of the setting.
With VoiceOver on, opening a Settings drawer while the VoiceOver cursor sits on some other element — a row near the top of the page, the drawer further down — makes VoiceOver scroll that element back into view and read it out again; keyboard focus was not seen to move, and nothing is lost, but the page has scrolled away from what was just opened. Whether any other layout change in Settings does the same is unmeasured — #989’s question.The behaviour is VoiceOver’s own cursor handling over a focus model that is the platform’s — there is no repro with VoiceOver off, removing the drawer’s expansion animation changed nothing (#988), other applications were heard re-announcing on a state change the same way (device session 2026-08-25, macOS 26.6.2) — and forcing keyboard focus onto a clicked button is refused because the north star binds behaviour to the platform (the same reasoning that keeps the focus ring the platform’s, #833).SwiftUI on macOS does not give a Button or a Menu keyboard focus when it is clicked (device session 2026-08-28, macOS 26.6.2, Full Keyboard Access ON; observed on the drawer’s own header button and on Layout Defaults’ anchor menu), so the VoiceOver cursor stays parked where focus last was; revealing the drawer rebuilds the accessibility element list under the window — which no arrangement of SettingsDisclosureStyle avoids — and VoiceOver answers a changed tree by re-resolving the element under its cursor. KiwiDesk sets keyboard focus at most once per navigation — the content pane, on a push the platform would itself have moved focus for (SettingsView, #996, #991) — and a CLICKED navigation states no destination at all (#961, #989).Move the VoiceOver cursor to the drawer with VoiceOver’s own commands and open it from there, so the cursor and the change are in the same place, rather than clicking with the mouse while the cursor is parked elsewhere. Stated as the shape of the repro, not as a measured workaround: what was observed was the cursor on a row at the top and a click at the bottom, and the cursor-on-the-drawer case has not been tried.
Within the first ~12 s after starting VoiceOver while a KiwiDesk window (Settings) is focused, one clickless cross-app focus change (cmd-tab, app-driven) is returned once — focus comes back to the KiwiDesk window. A click is always honored and clears the return; focus coming back to KiwiDesk on its own clears it too, and it fires at most once per VoiceOver start.When the accessibility control process VoiceOver starts yields, macOS re-activates the most recent regular app — KiwiDesk is an accessory (menu bar) app, so activation lands on the previous app 3–8 s later — and that handoff is indistinguishable at OS level from a deliberate clickless focus in the same window (#958).KiwiCore+AccessibilityReturn.swift owns the one-shot debt, armed only when an accessibility system process takes the ignored-panel flag while a window of KiwiDesk’s own pid is focused; the click escape, fulfilment, expiry and one-shot are pinned by AccessibilityReturnTests.Click the window you want — a click always wins — or wait out the grace; a returned cmd-tab needs one more press.
An app owning no normal (layer-0) window at all — on any Desktop, on any space — when KiwiDesk boots has its Accessibility tree materialized up to ~1 s late, plus the startup sweep’s own duration for the last app in its queue: its windows can appear and get tiled a beat after apps the boot scan warmed eagerly. Chromium/Electron apps that are slow to show their first window are the affected class; an app whose windows are merely on another Desktop, on another space, or minimized is warmed eagerly.The AX warmup is the slowest part of boot (Electron/WebKit answer lazily, 100–300 ms each), and paying it for every windowless helper made time-to-managed scale with apps running, not windows showing.The boot scan warms only apps the WindowServer reports as owning a normal window (AXHelper.pidsWithNormalWindows); everything else keeps its observer and waits for the next reconcile — at latest the startup sweep, which begins 1 s after boot and is itself chunked (#801); an app the sweep’s budget defers waits for the post-pass drain on top — whose warm-on-reconcile promise StartupWarmupSkipTests pins (#662).None planned — the ceiling is the sweep’s schedule plus its length, and an app that shows a window sooner is warmed by the reconcile its own activation triggers.
An app whose boot work exceeded the 500 ms per-app budget has its windows tiled after its pass, not in the first arrangement — a beat later, and one turn per deferred app, so the second slow app on a session waits behind the first. The affected class is an app whose AX calls block: the measured case cost 5011 ms by itself. The full reasoning is Design Decisions ▸ Boot.One app’s AX work cannot be divided, so deferring is the only bound available from inside a blocking call, and the alternative is every other app on the desk waiting for that one — the whole ~10 s boot the chunking exists to end.A chunked pass raises a per-app budget consulted between blocking AX calls (EventLoop.openAppBudget); past it the app’s remaining work is dropped, its name logged, and a post-boot drain reconciles it unbudgeted, one per turn (BootAppBudgetTests, #803).None planned — the deferral completes on its own, and activating the app heals it sooner still. The boot log names the app and its eventual cost, which is what a report needs.
When KiwiDesk boots, the window-rule re-check that the matched profile’s apply would run is postponed to a pass beginning ~1 s after boot — plus that pass’s own duration, since it is chunked. Where the profile’s rules differ from the ones already installed, a window the profile floats is tiled by the boot’s first arrangement and floats a beat later; where they resolve to what is already installed, the postponement still applies to the rest of that pass — a window only the later pass discovers returns to its Space but not to its captured slot.A second full reconcileAll inside boot, unchunked and unbudgeted, pays one AX-unresponsive app’s cost a second time — a measured 10.3 s scan figure against a 4.9 s scan (#836) — and a visible re-settle in a narrow case is cheaper than a frozen menu bar in every heavy one.reconcileAll is never cut short (accessibility.md — the per-app budget is raised for a queued step, never for a pass), and a float verdict is stored per window at reconcile time (recheckFloat), not read from the rules at retile, so the rule write alone cannot re-classify. setResolvedWindowRules skips only where a pass it can defer TO is already queued — boot until its tail arms the startup sweep, with drainDeferredBootApps covering an app the scan gave up on (defersWindowRuleReconcileToSweep), and loadConfig mid-write, which runs one itself on the way out (defersWindowRuleReconcile) — the bound argued once on mayReconcileWindowRulesNow; BootWindowRuleReconcileTests pins the four arms, StartupSweepWiringTests the tail still arming the sweep, BootDeferralWiringTests the flag’s own four writes.None planned — the ceiling is the sweep’s schedule plus its length, the same band as the windowless-app warmup skip above. Switching profiles by hand after boot re-checks immediately.
A shortcut pressed while the menu-bar mark is dimmed acts on the windows scanned so far: it may tile a partial desk, which the first full arrangement then corrects. The CLI cannot reach this — the socket only starts in the boot tail, so a command sent mid-scan fails to connect.Hotkeys are armed with the config, before the scan, and a bound key is not a surface that can be greyed the way the quick menu greys its two incomplete-state rows (#801; queuing is ruled out in Design Decisions ▸ Boot).Boot defers event-driven retiles per pass chunk and runs one arrangement in its tail (defersEventRetiles, KiwiCore+Boot); a command dispatched mid-scan retiles the state that exists at that moment.Wait for the mark to return to full strength — that is the ready signal (#802) — or re-issue the command, which then acts on the whole desk.
A window that every event-driven adoption path missed (a fresh-launch app whose AX notifications never registered, a document window dropped by a mid-launch transient filter) floats unmanaged for up to ~5 s before the periodic heal adopts it, and a missed window sitting on another macOS Desktop waits until that Desktop is next shown.A refused AXObserverAddNotification emits nothing, ever, so a timed backstop is the only pass guaranteed to come — the ~5 s cadence keeps a healthy tick at one ~1 ms WindowServer census with zero AX reads — and AX cannot list another Desktop’s windows at all, the observation limit behind the Desktop-switch animation row below (the WindowServer can, which is what the away ledger reads — #1146 — but adoption still needs AX).EventLoop.healSweep reconciles only where the on-screen census names a window id that is not tracked (id membership, never a count) and repairs failed observer registrations (AdoptionHealTests, #675); the transient filters queue a ~0.75 s one-shot re-track of their own; the native-space-change reconcileAll covers the other-Desktop case, census-gated (#1037) so it reads only apps tracking or showing a window, with the Desktop settle sweeping any arrival the switch notification beat.Activating the app heals it immediately — its activation reconcile runs the same repair and adoption, ahead of the sweep.
Under fast focus churn, a never-closed window whose app transiently under-reports its AX window list and which the WindowServer’s on-screen census also fails to vouch for — a raised-layer window, or one the compositor is mid-transition on — can still lose its tile as a spurious close, until the adoption heal re-tracks it (the row above’s band). A layer-0 window the census still shows is protected: its removal is refused.The census is the only second witness available, it may only ever refuse a removal, never cause one — it omits other-Desktop windows exactly as readily as AX does, the same one-way trust the hidden-app drop rules (#913) — and it reads layer-0 on-screen windows only, so what it cannot vouch for keeps the pre-gate behavior.The removal-distrust gate in the reconcile sweep checks a non-hidden, non-minimized close candidate (outside the brief Desktop-switch grace, where the census double-exposes both Desktops) against one on-screen census and refuses a listed window’s removal, one log line and bounded follow-up reconciles per continuous-absence episode (RemovalDistrustTests, #1157); the heal (EventLoop.healSweep, #675) stays the backstop for what the census cannot see.Focusing the app reconciles it sooner — its activation reconcile re-adopts ahead of the sweep.
A bar with the liquid_glass finish on shows its plain solid shape on macOS earlier than 26.Liquid Glass is a macOS 26 API (NSGlassEffectView); the boolean round-trips so a shared profile stays portable, an older machine shows the underlying boxed/plain shape, and the GUI never offers the toggle below 26, so an imported setup carrying the finish reaches this state invisibly there.The stored liquid_glass value is portable; only the render path is #available(macOS 26)-gated (glassEnabled), painting the solid shape without rewriting the field (#390).Use macOS 26+ to see the glass; there the fill_color tints it (a colored backdrop the glass refracts, #408).
With the liquid_glass finish on and a light fill_color — the bundled Clean Light palette ships one — each glass surface picks its own light or dark variant from what lies behind it, so on a very dark wallpaper two glass surfaces could render different shades. Not observed; reasoned from the mechanism. A dark fill (the default, and every other bundled palette) pins every bar surface to the dark variant.macOS decides a Liquid Glass view’s variant per view from the backdrop it samples and holds the verdict, and only the dark variant can be pinned publicly — .aqua is the appearance the bars already carry and the material keeps adapting under it, while a light pin would take a private _adaptiveAppearance write with no public fallback (#1308; the glass variant is pinned from the Fill).GlassTint.apply sets the glass view’s appearance to .darkAqua for a fill under the wantsLightInk threshold and to nil above it — the OS’s own scheme (GlassTintPinTests).Pick a darker Fill (Advanced Colors) or any bundled palette other than Clean Light. A sighting with the wallpaper, reported on #1308, is what would turn “unmeasured” into a number.
A Desktop switch whose bridge set was accepted but silently declined by the WindowServer — a class never yet observed for this operation, but documented for sibling bridge operations — would leave that display’s origin space hidden with the target never made current: a mostly blank Desktop until the next genuine Space gesture. KiwiDesk logs <verb>: the switch did not land … ~600 ms later and does not auto-recover.Re-issuing the set on an unmoved pointer cannot be told apart from the user having already gestured back within the verify window, so auto-recovery would fight a real gesture to guard against a failure class never observed on this operation (#1023).“Performed is not applied” is the bridge’s contract (.claude/rules/os-private-apis.md): an asynchronous operation returns nothing, so acceptance proves dispatch, never application, and the deferred re-query of the per-display space pointer distinguishes a dropped set but not who moved the pointer since.Any genuine Space gesture (swipe, Ctrl+arrow) restores a consistent state; the log line names the case for a report.
With “Displays have separate Spaces” on, profiles follow the main screen’s Desktop (#888): a swipe on a secondary screen retiles its arrived windows but never selects a profile, and a binding on a Desktop that lives on a secondary screen never fires while that arrangement stands.One active profile means one binding authority, and the main screen is the smallest ruling that keeps bindings well-defined under the macOS default, with no per-display profile machinery.Desktop routing resolves one active profile for the whole display setup, not a per-display profile tuple (#8); the trigger is the main screen’s current Desktop (NativeSpaces.activeDesktopNumber).Bind the Desktops your main screen shows — the card’s intro names the semantics. Per-display active profiles are a possible future shape, taken up only on demonstrated demand. See Desktop→profile bindings key to the main screen’s Desktop.
In BSP, the inner window of a nested pair can’t grow — a “grow” press (or edge-drag) widens its outer neighbor instead.Its width r·(1−r)·W is already maximized at the default ratio, so no resize direction can widen it.All same-orientation splits share the one per-space ratio; per-node ratios would need a container tree the flat-array model forbids (#56 trade).Shipped: the track layout (#128) — set_mode(space, "track") gives every window one true resize target. See BSP resize is focus-aware in direction only.
In the split layouts (BSP, Stack, Grid, Track), tiling honors the configured min_window_size everywhere, and an app’s own enforced minimum only where a stored ratio can reach it: the first BSP split of each orientation and the Stack master/stack split move at retile to draw a learned floor (#934/#1430). A floor no ratio reaches — a window inside a deeper BSP split, a window’s own share inside a Stack zone (#944), a Grid cell, a floor not yet learned — and a yield that cannot fit because a same-axis neighbour is already at its own floor leave the window at its minimum, overlapping that neighbour, cued once with the neighbour-minimum pill pair (the neighbour that binds marks itself), the overflow landing inward — toward the screen’s centre, never past its edge, a window wider than the whole region keeping its leading edge on screen. Scrolling and Monocle consume the learned bound — the row re-packs, the slot centers (#677); a keyboard or mouse resize clamps at the learned floor, cued with a bounce and a pill (#933); Track’s automatic count fits as many tracks as their apps’ learned minimums allow and the track weights are re-shared at retile — each track at least its members’ minimum and no wider than their learned maximum, the remainder staying empty where every track is at its maximum (#1355, #1488) — while a fixed limit stays the user’s number. The floor is learned from the refused asks themselves (the row below), so a window arriving, or a press landing before it, walks the region past the floor for about a second before the heal brings it back (#1254, #1439).macOS AX exposes no minimum-size attribute for a foreign window — NSWindow.minSize never crosses the process boundary, and a probe-resize plus read-back would be slow synchronous IPC per window that Electron/WebKit apps answer lazily — so the engine learns the bound from its own asks, which is where the latency comes from; BSP ratios are two per-space scalars (splitRatioH/splitRatioV), not per-node (#56), so a heal for one window moves every same-orientation split, as a resize press by hand does (#934), and cannot reach a deeper split; and where two floors cannot share the span, a cued, inward overlap degrades better than an OverlapStack cascade. yabai and AeroSpace share the identical limitation.The frame-set is fire-and-forget — FrameApplier hands each assigned region to WindowControl.setFrame with no read-back — and the engine learns the effective bound from its own asks (#677), so a twice-refused target stops being re-issued and the focus ring renders the real frame; KiwiCore.healSplitFloors runs at every retile beside the Track heal and moves the stored ratio through the same capped writers a press uses, so a neighbour yields only to its own floor; SplitOverflow.placed is the post-pass on the frames the retile issues that places the residue; the retile tolerance (±2 pt) absorbs small app-side clamping, and the split-range clamp (#383, SplitDomain) caps ratios at the configured minimum only.Raise min_window_size toward realistic app minimums (the shared floor then reserves enough room everywhere), give the stubborn app a float_rules entry so it never competes for a split region, or — for a window in a deeper BSP split — use the track layout, where every window has its own resizable track.
Learning an app’s size bound costs one visible probe pass (#677): on a window’s first encounter with a refused size, the retile animates toward the full slot — the focus ring rides out and snaps back once. When the ask was issued from a settled state — the common case — the window’s unchanged pre-ask size counts as the first observation, so the bound confirms and the residue places about one probe grace (~0.6 s) after the dance settles; otherwise a second, ring-invisible probe runs first, the ring rendering the first refusal’s answer.macOS AX exposes no minimum/maximum-size attribute (the row above), so the only evidence a size is unreachable is having asked and been refused, and one refusal alone could be a transient (a mid-flight cancel, a laggy answer).SizeBoundLearner’s twice-in-a-row confirm ladder: the settled state frame is the answer to the last recorded ask, and a candidate must repeat to become a bound; one comply-then-revoke pair counts as a repeat (#1049) — an app that performs the asked size and then snaps back (the Android emulator holds the ask ~0.4 s) has answered definitively, so the revoke confirms in one dance instead of two.None needed — the dance self-limits to one visible pass per window and ask (asks are remembered per layout, so each layout pays it once). Once two distinct asks corroborate a bound, a new ask beyond it is answered from the ledger with no dance at all (#1055) — a scrolling resize dances at most through corroboration, never per press, and an evicted ask beyond a still-corroborated bound consumes without re-dancing. The engine sends the corroborating ask itself (#1439): the moment an entry confirms on an axis nothing corroborates yet, one probe a step past the refused ask goes out in place of the ask the entry already answers — ring-invisible like the second probe — so the pill, Scrolling’s re-pack and Track’s count read a corroborated bound about a probe grace after the first confirmation. Otherwise it recurs only when the ledger loses the entry: an invalidation (a genuine resize; a gone window — destroyed or hidden — instead parks its believed entry in a pid-checked tombstone for 30 s, so an AX flap or an unhide re-tiles straight to the learned size without re-dancing, #1049), an eviction (more distinct asks per axis than the ledger’s cap — sized past every real producer), or a settled compliance that contradicts the entry (the grid-snap edge, deliberately traded for never pinning a window at a size its app stopped insisting on — a transient compliance the app itself revokes clears nothing).
During a live mouse resize of a slow-redraw app (JVM apps like Android Studio, Qt apps like Telegram, and at times System Settings), the focus ring trails the cursor in stutters, on drags where fast-redraw apps (Ghostty) track perfectly. This row covers the app-side half only; the in-process half — every applied frame’s AX notification resolved by a blocking main-thread round-trip, measured at 42 stalls in ten seconds of held resize, worst 607 ms, against 1 at 134 ms after — is fixed by #1084.The ring follows the window’s committed frames — under WindowServer tracking it renders bounds the app has actually drawn, and macOS live-resize only commits a frame when the app redraws — so a slow app’s window genuinely trails the pointer, and leading it would detach the ring from what is on screen.The overlay frame writers take committed geometry (the WS stream, AX echoes, the animation tick for our own motion); no channel carries the pointer’s intended frame during an app-owned live resize.None planned — the REMAINING lag is the app’s own resize cadence. Before reopening, take two measurements rather than one: the #618 sampler (CGWindowListCopyWindowInfo at 200 Hz) for whether the ring detaches from the window’s drawn edge, and the frame clock stalled …ms lines (#1084) for whether the clock is being serviced at all. A stall line means the cause is ours again and this row does not cover it — which is precisely what a reader of the pre-#1084 version of this row would have concluded wrongly.
A learned bound can go stale in one narrow case: an app whose size constraint lifts without the window resizing (no pane change, no user resize) keeps its learned bound until something re-asks past it, so the window can rest smaller than the app would now allow; an un-forced retile answers asks beyond a corroborated bound from the ledger (#1055), so the lift is discovered only by an explicit apply (which probes past corroborated bounds) or a genuine resize of the window.AX has no notification for a constraint change, re-probing on a timer would reintroduce the visible dance the row above bounds, and every real observed case (System Settings switching panes) resizes the window, which invalidates the ledger.The ledger invalidates on any genuine (non-echo) resize and migrates on rekey; a gone window (destroy, hide) parks its believed entry in a pid-checked 30 s tombstone, revived on the same window’s re-add (#1049, SizeBoundReviveTests); a settled compliance with a contradicting ask also clears the axis — only a settled read may, since a raw echo’s compliance can be the transient half of a comply-then-snap-back (SizeBoundLearnerTests, SizeBoundTransientComplianceTests).Resize the window by hand (any genuine resize re-probes), or re-apply the layout — an explicit apply forces a re-issue, and since #1055 it also probes past a corroborated bound, so it is a reliable clear.
In stack, when the master zone lines up along the split axis (e.g. horizontal masters beside a right stack), the masters’ individual shares can’t be resized: that axis always moves the split, and the other axis beeps.The split ratio owns its whole axis — giving the same keypress two meanings (split vs weight) by focus zone would make “grow” unpredictable at the boundary.One knob per axis per arrangement (#222); weights live on a zone’s own lineup axis by construction.Pick the orthogonal (vertical) master orientation — since the 2026-07-16 default flip the standard side-by-side arrangement sits inside this limitation once master_count exceeds one — or put the windows that need individual shares in the stack zone.
With a leading stack and parallel master lineup, cascade_overflow piles the array-earliest masters at the master zone’s trailing edge instead of the latest.Mirroring the master render order keeps the promote/demote boundary beside the stack seam; preserving one trailing-edge, downward-cascade vocabulary matters more than which seniority subset enters that pile.StackLayout.mirrorsMasterZone reverses the master render order before the shared zone-overflow path takes its trailing suffix (#313).Use a trailing stack (right/bottom), an orthogonal master orientation, or cascade_all if the subset distinction matters. See The master zone fills from the stack seam.
In the settled Mission Control overview the focus ring stays visible (it vanishes instantly with the swipe, then redraws once the overview settles), while the App Bar, Space Bar, and sticky mark do not appear.The ring is window-attached — the annotated window is itself shown, scaled, in the overview — while the bars and mark are space/layout-attached and the overview has torn that layout apart; hiding the ring too would mean re-adding the Mission Control observer this design deleted, and doing it at settle time produces a redraw-then-vanish flash. jankyborders redraws its borders on the same settle handler.The ring is a space-pinned SkyLight WindowServer window the compositor re-composites into the overview, whereas the AppKit bars/mark carry .transient (hidden by Exposé at the compositor level), which a raw WindowServer window cannot take.None planned. A future timeboxed spike could look for a stable CGS Exposé-exclude window tag (the SkyLight equivalent of .transient); adopt only if one proves stable and fail-safe across macOS releases — jankyborders’ choice to redraw instead suggests it may not exist reliably on app-owned windows.
The app glyph style offers the app’s own icon or a monochrome symbol — never the system’s Dark, Clear, or Tinted icon looks as distinct in-app choices.A synthesized tinted mode (a luminance ramp over the flattened bitmap) cannot match Apple’s plate-plus-glyph regeneration, and the system-wide Icon & widget style already tints the app icons KiwiDesk shows.macOS exposes no public API that hands an app another app’s (or even its own) styled icon rendering or its icon layers — Apple DTS calls it unsupported (#294).A private-IconServices probe with public fallback, the SkyLight dlsym pattern (#362); if viable the picker grows the true system styles.
While window management is paused (no Accessibility permission), the read-only shortcuts panel shows base gui.json bindings without the active profile’s sparse keybinding override applied.The panel is a “defined, not live right now” glance while paused, a profile that overrides layers and not only tiling is rare, and reading the authored base avoids the empty-live-space-list that would otherwise misfile every space shortcut into Custom.Without AX the live resolved snapshot (liveKeybindingSnapshot) is nil, so the paused path reads persistedGuiConfig() (authored gui.json) directly instead of resolving base⊕profile (#326).Grant Accessibility — the live resolved snapshot then drives the panel. The divergence exists only while paused and only for a profile carrying a keybinding override.
The open shortcuts panel auto-closes when you switch layers, but not when a config reload or profile apply rebinds the currently-active layer without changing which layer is active — an open panel keeps its now-stale bindings until reopened.The panel is a per-open snapshot closed on layer change, and continuously re-binding an open glance surface to live config would re-flow its layout under the reader’s eyes.Auto-close hangs off KeybindingManager.onLayerChange, which fires on the active mode changing, not on the active mode’s bindings changing (#603).Reopen the panel (⌃⌥K) to see the current bindings; the divergence exists only while it was left open across a reload/apply that touched the active mode.
A shortcut on a keypad key that prints a character shows the same glyph as the main-block key printing it — ⌃⌥ + keypad . and ⌃⌥. both read ⌃⌥. in the recorder, the Shortcuts panel and the menu.The two are different bindings and only their drawn glyph collides — macOS offers no keypad-distinguishing glyph to borrow, and an English Num prefix inside Core would be neither localizable nor Apple-native (#1074). Keypad Clear prints nothing, so it takes a fixed ⌧.ComboSymbols.keyGlyph resolves a printing key through the active layout, which answers with the character and not with which physical key produced it.Bind the keypad key you want and read the row you created; conflict detection treats the two as distinct, so neither shadows the other.
Adopt into the GUI keeps custom Lua live by commenting only the managed statements, but its statement classifier inherits the token scanner’s per-line blind spots: a foreign construct split across physical lines (app_rules\n= {…}) or reached through an aliased receiver (local K = KiwiDesk; K.bind(…)) is kept live, and a managed token mentioned only inside a string in otherwise-custom code forces the whole file to the full-comment fallback (silencing that file’s hooks).Token scanning, not full Lua parsing, is the one detector shared by ownership and adoption, and it fails safe: any structural anomaly, or a foreign token the final hasForeignCode re-check still sees, reverts to commenting everything, and a live set_* left uncommented is benign (it doesn’t trip hasForeignCode, and gui.json/profile settings are applied wholesale after init.lua, overwriting it).The selective pass (ManagedConfig+Adopt.swift) segments statements by bracket/function-end depth and classifies each by its head line; the foreign net (hasForeignCode) and the classifier both scan per line, inheriting lineMatchesToken’s limitations (#355).The original is always preserved as the commented backup — un-comment or restructure the affected statement by hand. Writing foreign calls one-per-line (the conventional style) avoids the split-token case.
KiwiDesk’s own Settings window is subject to the split-layout app-minimum row above, and it is the one window whose minimum KiwiDesk authors itself: 720 pt wide, hard. In a layout whose slot for it is narrower and that no ratio can widen — a three-column grid on a laptop display, a deeper BSP split — the window stops at 720 and overlaps its neighbour; the first BSP split and the Stack split make room for it (#934).The Settings window tiles like any other (#678 item 18), the layout’s only floor is the profile-wide min_window_size, and the 720 pt minimum is itself a ruling — below it every Settings row is two lines and nothing is comparable (docs/design-decisions.md ▸ narrow windows) — so it gets the same slight overlap every stubborn app gets.SettingsWidthClass.minimum is enforced by AppKit on the window; the engine learns that refusal as the window’s effective bound (#677) — the re-issue stops and the focus ring renders the real frame — but grid has no ratio to move and a deeper BSP split is out of the shared ratio’s reach, so there the overlap stands.Float the Settings window (toggle_floating, or a float_rules entry for KiwiDesk), give its space a layout with fewer columns, or widen the display.
In the track layout, when more tracks exist than fit side by side at min_window_size, the fitting prefix tiles and every surplus track merges into one far-edge overflow track whose windows then pile among themselves.The fitting tracks stay tiled and the surplus collects into a single overflow track whose windows keep a reachable title bar via the downward cascade offset (the app-wide reveal convention).The overflow track is the cap-merge with the cap set to the geometric fit count (TrackLayout.fitCap + counts(cap:)), rendered by trackFrames per overflow_style; a fully-degenerate span still falls back to the whole-region OverlapStack.frames (#192).Widen the display or raise nothing — it is read-time: the overflow track appears and grows as the fit boundary moves. Adjust its pile with track.set_overflow_style (cascade_all default).
reload_config (and re-issuing set_mode(space, "track")) reseeds a track space’s partition to one window per track, dropping a hand-merged arrangement and its track weights. In-track window shares (stackWeights) survive.Reloading re-runs the declarative config, whose set_mode is a statement of the space’s declared default arrangement, so re-applying it resets runtime topology as it re-centers a scrolling viewport; a same-session wake/unlock restore is involuntary and preserves the partition (carried in the state snapshot).The break markers/track weights are session-scoped runtime state (#128), the scrollRest precedent; an explicit set_mode re-apply reseeds.Rebuild the arrangement after a reload (a few move_to_track presses); the wake/unlock path already survives it.
A stack column’s per-window weights (resize("y")) keep only the write-time clamp: a window spawning into the column, or master_count moving the boundary, can leave a stored extreme weight that piles that zone’s cascade — unlike the track layout, whose session weights are healed at the next layout pass (#944).A stack zone’s overflow degrades to a cascade inside the zone — recoverable by a resize — never the whole-space collapse the track heal exists to prevent, and healing it too would rewrite stored weights for a milder symptom nobody has measured live.healTrackSessionWeights iterates only .track spaces by ruling; the stack write path keeps the #933 clamps (StackLayout.weightStep).Re-issue resize("y") on the squeezed window — the clamped write renormalizes the shares — and the heal joins the stack zones if live QA ever measures this class (the re-entry condition in Session weights are healed at retile).
track.swap refuses a swap that would touch the overflow track while it folds two or more marker-tracks together — under a fixed limit (auto_tracks off, as many marker-tracks as limit or more) or a geometric fold on a display too narrow for the tracks at min_window_size.The folded slot is a read-time merge over the marker partition whose slices have no marker identity, so exchanging them would re-derive a different composition after the swap (windows leaking between visible tracks), and rewriting markers to pin the view would destroy the grandfathered partition.The guard gauges the fold against the render’s own effective cap — the fixed limit AND the geometric fit (TrackLayout.overflowCap / geometricCap, shared with the layout math) — and rejects only a swap whose own or target track is the folded slot; two normal tracks still swap (#182 review, widened by #198).Raise the track limit, turn Auto track limit on, or widen the display, then swap; move_to_track still works under the merge.
A floating window parked exactly on a tiled slot (what a float toggle leaves behind) can’t be reached by directional focus — and navigating out of it skips the tile it covers.Directional navigation is center-to-center: two windows sharing one center have zero forward offset, so no direction points from one at the other.Navigation.neighbor requires a positive forward offset between centers; the float tier of the two-tier focus search (#488) navigates floats by live frame, which a coincident park degenerates.Click it, cycle to it, or nudge it off the slot; distinct-center floats are reachable since #488’s two-tier search.
With border.glow on, the focused ring ignores draw_order: "front" and stacks behind its window; the choice takes effect again when glow turns off.The WindowServer-backed SkyLight context drops every shadow colour (the bloom rendered as a grey smear), so a glow ring must render on the public AppKit panel, which cannot express the SkyLight sub-level that makes front-order occlusion-correct and is below-only (#320/#357).A glow ring always swaps to the AppKit backend (BorderOverlay.ensureBackend, #533); plain rings keep SkyLight and its front support.Turn glow off to restore front-order; if a stable coloured-shadow path on SLS surfaces ever appears, the swap becomes unnecessary.
A lone window left behind at quit lands in a quarter-display top-left grid cell instead of keeping its size centered.The quit grid’s dimension formula is floored at 2×2 (#197 spec), so one placement rule reads predictably for every window count.QuitGridLayout.dimension(for:targetDepth:) clamps to 2…4; teardown placement is one-shot, with no live manager to refine it afterwards.Future quit.layout strategies (center, columns, …) slot into the same enum seam; until then, resize the window after quit.
Very large window sets exceed the quit grid’s density target: past 4×4 the grid stops growing and cells keep cascading deeper, however high quit.grid_target_depth is set.The 4×4 cap is a teardown safety boundary (#281) — it interacts with minimum window size, cascade reachability and display geometry, and no live manager remains after quit to correct an unreachable pile — and the density target only moves the 2×2→3×3→4×4 growth thresholds.QuitGridLayout.maxDimension is a constant; the target (quit.set_grid_target_depth, GUI “Target windows per cell”, default 5, range 1–20) feeds only the dimension formula.Raising the cap would need a separate architecture change deriving a safe per-display limit; until then the cascade keeps every title bar reachable via the pinned offsets.
The focus ring / drag ghost can be low-contrast against — or visually confusable with — window content that shares its green hue (a green terminal theme, an editor’s build-success green, git-diff additions).Overlays paint over arbitrary content, so no static hex can guarantee contrast against every window: both overlay hexes clear 3:1 on near-white and near-black — the ring’s bright green #4A9816 and the ghost’s emerald #347957, which diverged from it in #511 to separate from the drop-zone amber under red-green vision loss — the 5px stroke lets shape aid legibility where hue contrast is weak, and green is the brand-identity choice (the ring reads as “kiwi”).The overlay color is a fixed value with no per-window content sampling, which is out of scope for the brand convergence (#439).Recolor the ring/ghost (border.set_focused_color, drag.set_ghost_border_color) if your work lives in green-heavy apps; a content-sampling signal is a possible future spike.
A rapid keyboard resize of a floating window can under-accumulate between AX echoes wherever no animation is in flight: with resize animation off, under system Reduce Motion, and in the brief window between an animation settling and its echo arriving. A second press landing before the first one’s echo re-asks the same target instead of adding to it, so it is swallowed.A commanded frame stored here must die on a schedule nothing can extend — the animation target at settle, #1090’s glide record at the start of the next press — since a record every press could read would be re-armed by every press, and an app that silently refuses every ask would bank commanded growth without ceiling (the #1057 class, and the reason the #881 instant stamp was refused as this base); re-basing a press on the echo-fed frame is self-correcting, the loss at most one echo’s lag per press, and a press issued from rest is always exact. #1056 closed #129’s mid-animation case, and the held case — sharper under #1082’s per-frame glide, measured at 29% of the asked travel — is fixed by #1090, leaving the per-press residue.resizeFloating bases on AnimationEngine.commandedFrame(window:includingHeldGlide:), else the echo-fed state frame; the animation target is nil at settle and absent entirely under Reduce Motion, and the glide record is withheld from a press by that argument (#129, #1056, #1090; FloatResizeAccumulationTests pins the press arms as a ruling, FloatGlideAccumulationTests the held case that is now fixed).Press once more if a rapid burst comes up short — each settled press is exact. Holding the chord is exact in every configuration, Reduce Motion included.
Growing a floating window and then shrinking it back does not always return it to exactly where it started — but only on the one step that first brings it into contact with a screen edge or a bar, and never by more than half a step.A keyboard float resize splits its delta between both edges and pins an edge against the boundary (#1091), so a side with less room than half a step has its share capped and the remainder spilled to the other, which pins it, and the following shrink comes entirely off the free side; every steady state round-trips exactly, and remembering which way the last grow went would be state invalidated by every move, mode change and display change, for at most half a step.FloatResize.resized caps each side by the room it has and spills the remainder (FloatSymmetricResizeTests ▸ The step into contact spills, and costs half a step, which sweeps every offset that makes contact on one press and holds the residue inside half a step across all of them).Nudge the window back with a move shortcut, or ignore it — at the default step it is under 25 pt, once, on the press that reaches the edge.
Cross-display float residues, all pointer- or loss-shaped: a floating window dropped via the Space-Bar spring onto a space shown on another monitor stays exactly where the pointer released it (it does not teleport to that monitor); a space relocation landing mid-drag skips the float under the pointer entirely (it can restore to the old monitor on a later activation); and a parked float whose removed display’s space never re-resolves elsewhere restores to wherever the OS relocated it — when the space does re-resolve (the common re-dock), the float is delivered to the new display at a main-relative guess of its old position. (Mirror case: a space that first gains a display at runtime is a first assignment, not a relocation, so floats parked at the main-screen fallback stay main-anchored until moved.) Delivery itself may flicker once on a cross-display move — instant placement, a macOS display-handoff re-clamp, then the retry winning.Every space-relocation path (window moves, move/pin_space_to_display, profile applies, monitor re-dock, config reload, space-delete rehome) re-anchors floats (#449 / #444), but the spring drop and a hand drop on another display (#1686) are pointer-owned placements the user chose, a dead display leaves no source bounds to translate from, and the delivery rides the stash-restore retry loop, where animating it would either restart per retile or give up the retry.Resize / mouse-resize / border math resolve the space’s own display through TilingEngine.screen(for:in:) / screen(containing:); KiwiCore.reanchorFloat seeds the translated frame as the stash original and restoreStashed delivers it; the spring path (springSwitchSpace) performs no re-anchor, and restoreStashed consumes a capture whose display is gone.Move the float with move_to_space (which re-anchors), or just drag it — a user placement always wins.
Moving the only window off the focused display’s space (without follow) hands key focus to the wallpaper, by activating Finder. Where Finder’s only windows are off screen, KiwiDesk does nothing instead, so the moved window’s app can briefly keep key focus (stray keystrokes are swallowed until the next click).macOS exposes no public API for focusing the bare wallpaper, and activating Finder while its windows sit only on another Desktop can teleport the user there (the “switch to a Space with open windows” setting) — a Desktop teleport contradicts the move the user just made, while leaving no key window matches stock macOS after its last window closes (ui-designer call, option 3).KiwiCore.yieldFocusToDesktop (installed as desktopFocusYield in start()) activates Finder only when it has no document windows anywhere, or at least one on screen right now — gauged by AXHelper.normalWindowCount(onScreenOnly:) over the WindowServer (layer-0, wallpaper elements excluded); otherwise it skips. moveWindow’s no-follow branch fires it only when the active space empties and the moved window held key focus (#446); focus_space fires the same yield when the target space has nothing to focus and the stashed previous window’s app is still frontmost (#463).Click the wallpaper to focus it; or keep a Finder window on the space you’re on. A window remaining on the space is refocused instead of Finder.
A space switch’s ~300 ms settle re-raises the target once when the pre-switch app is still frontmost — so a cmd-tab back to that same app within 300 ms of switching can be re-raised over (macOS’s MRU order puts exactly that app first). Also skipped: a dropped window-level raise inside an already-frontmost app, and a dropped handoff detected while a sticky traveler holds cross-app focus.The detector cannot distinguish “the cooperative activate() never landed” from “it landed and the user immediately returned” — both leave the pre-switch app frontmost with the anchor’s app behind — and narrowing to exactly that signature means no other click, cmd-tab, or command is ever fought.reassertSwitchFocus (KiwiCore+SpaceFocusHandoff.swift) compares the settle-time frontmost pid against a capture taken before the switch (frontmostPIDProvider seam) and the focus anchor’s pid; fires at most once per switch (#463).Cmd-tab again (the re-raise never repeats), or wait out the 300 ms settle before switching apps.
Clicking a window within ~1 s of moving it to another space without follow does not switch to that space — that one focus report is dropped; recovery is the next focus report — after ~1 s a click on it follows normally, and if the window silently kept OS key focus (so a re-click emits no report), focusing another window and coming back does. move_to_space_and_follow is unaffected.Inside that second a genuine click is indistinguishable from the dropped-raise re-report — when the OS quietly drops the no-follow move’s origin-refocus raise (#463’s cooperative activate), the moved window keeps key focus and its app re-reports it AX-focused, and following that would teleport the user to the space (or display) the move chose not to visit — and the ~1 s covers lazy Electron/WebKit re-reports that trail by several hundred ms.MoveIntentLatch stamps every no-follow moveWindow; scheduleFocusFollow drops reports for latched ids, age-bounded like the sibling-report distrust. A companion move settle re-raises the origin’s focus once when the drop is provable (#482 / #483).Focus another window and back (or click the moved window again after ~1 s), or use move_to_space_and_follow when you mean to go.
A window closed while its macOS Desktop is off-screen is reported closed only at the next reading of the Desktops — the next Desktop switch, or up to about five seconds later while any window is away — after the vanished it already sent; and on a Mac where the per-Desktop window list cannot be read, the reason falls back to a timer that can call a fast app’s departure closed.The WindowServer is asked which Desktop hosts a window at every destroy (#1146), a window that dies while away sends nothing KiwiDesk can hear, so the corrective closed waits for the ledger’s next census, which is bounded, not live.KiwiCore.handleWindowGone reads the compositor through DesktopMemory.readWindowSpace against one topology reading and WindowGoneReason.classify decides from that presence; the away ledger is re-read against one DesktopCensus at the Desktop settle and on the 5 s awayCensus task (GoneReasonEventTests, AwayLedgerTests); without the symbol the settle timer stands (LifecycleReasonTests).Consumers filtering on closed now hear about it a beat late; the sketchybar recipe re-query on desktop_change remains the safe shape.
Dragging a floating window shows no drag ghost and no snap zone, and dropping it over a tiled slot does nothing — in every layout mode, though a drop on another display moves it into that display’s Space (#1686).A floating window has no tile slot, so there is no home slot for a ghost to preview and no swap a drop could perform; an opt-in toggle (drag.ghost.show_for_floating) is a no-op for the same reason (#161), and the reports of drag visuals on floating windows were #160 — float state silently reverting to tiled on reopen.Layout algorithms run over the flat array of tiled windows only; floating windows are filtered out before slot computation, so no slot geometry exists for them.make_tiled returns the window to the grid; drag visuals resume immediately.
A floating window is kept above the tiled plane by re-raising it (kAXRaiseAction) whenever focus lands on a tiled window. AX couples a raise with app activation, so the raised float’s app briefly activates and steals focus, and focus and the active ring are handed back to the window you actually reached; for a moment during that handoff the float holds keyboard focus. On rapid focus changes across apps a float can still settle a notch out of place, or briefly not on top at all — but only where a raise lands after KiwiDesk has stopped waiting for it, and a float left behind is never retried on the spot.AX is the only public way to reorder a foreign window’s z-order and cannot separate “raise” from “activate” or make the activation synchronous, so each app performs the raise and the activation it is coupled to on its own schedule, and the AX call returns when the app has accepted the raise and not when it has performed it (#684). Pinning the float’s window-server level can only be written for windows your own connection owns — foreign windows need code injected into Dock.app via a scripting addition (SIP disabled), which KiwiDesk will not require (§5, and the SIP-blocked list below) — and an own-connection SLSSetWindowLevel fast path was built, measured to level only KiwiDesk’s own windows, and removed.The raise + focus handoff is raiseFloatsAbove / raiseFloatsAndSticky (generation-guarded, so a stale sequence cannot steal focus back); the raised floats — and the overflow-pile members a cascade restore re-raises (#425) — are stamped in the shared zOrderRaiseEchoes ledger within zOrderRaiseEchoWindow so their focus echoes revert to the real focus instead of moving the ring onto that window (#418) — a click that reached a stamped window escapes that revert on click provenance (#687), so only a deliberate clickless focus of one (cmd-tab, app-driven) inside the ~1 s echo window can be eaten; ZOrderDrain reads the WindowServer’s real stacking between raises and owns the per-raise verification and the budget it stops checking at (landingLimit per window, and per sequence whatever ZOrderDrain.Policy the caller names — .restore on this path), a missed landing left to the next restore.Re-raise on focus is the shipping behavior; the transient activation settles on the handoff. A float left a notch out of place is re-raised by the next focus change onto a tiled window. Foreign-window level pinning is filed as a SIP-blocked item (needs the injected scripting addition), the only way to make the stacking deterministic.
When the private WindowServer event stream is unavailable (its symbols are absent or a lookup failed), the on-window sticky mark can be briefly obscured by its own window after a re-click on the already-focused window, until the next retile re-asserts it. Every genuine focus change still re-stacks the mark immediately.A re-click on the focused window fires no AX focus event while macOS still raises the window above its mark, so the WindowServer .reorder notification is the only signal for that gesture and AX cannot observe a same-window re-raise.The mark re-asserts its z-order from BorderManager’s onWindowReordered tee, fed by the SkyLight event stream both the ring and the mark ride; its public fallback (the AX move/resize echoes driving follow) covers frame-following but cannot cover a focus-less re-raise (#414).None needed while the WS stream is available (the shipping case); when it isn’t, focus another window and back, which re-asserts via AX.
A window KiwiDesk ignores — Ghostty’s quick terminal or any user ignore_rules app — can sit under a bar on any edge with part of it (on a top bar: the title bar, its grab handle) hidden, and is never pushed clear the way a tracked floating window is. The bar being invisible over it is the visible symptom.The bar clamp reaches only windows KiwiDesk tracks, and an ignored window is outside management entirely — no tracking, no events, no frame assertion — so clamping it would mean tracking it; user-configurable ignore rules (#176) widen the set of windows this applies to.Ignored windows are filtered before any state or layout (FloatDetection.shouldIgnore); the clamp (clampFloatsClearOfBars, all four edges) runs over tracked floats only (#242). KiwiDesk’s Settings window is tiled like any other window (#678 item 18), so a tiled slot already excludes the bar strip; its panels and borders stay ignored by EventLoop.shouldIgnoreOwnWindow (#177), and its floating chrome windows (the tour, Config Issues, update alerts) are tracked floats the clamp does reach.Move the ignored app’s window by hand, remove its rule, or don’t run a bar over it.
On quit, the window you were last working in is placed in the LAST slot of its grid cell and ends up in front of that cell’s pile. In a grid whose window count does not fill the cells evenly, its cell is not the last cell, so a later cell’s windows are raised after it; they never overlap it, unless a very deep pile cascades out of its own cell.AX’s quiet raise (kAXRaiseAction without activation) cannot lift any window above the frontmost app’s key window — measured on device, 0 of 7 windows displaced over 600 ms — and activating that app is the one thing a teardown must not do, so the grid places that window where being in front is correct (#688).The restack is the last thing that runs before management stops, so no later pass diffs it against reality; WindowGather.collect moves the id trustedFrontmostFocusedWindowID() resolves to the end of its display’s list — last in the list is last in its cell’s cascade, the slot QuitGridLayout.raiseOrder raises last — and KiwiCore+TeardownRaise leaves that same id out of the circle; the measurement is on raiseFloor (#684), and GatherWindowsFrontmostTests pins the slot.None needed for the common case. It does not arise when the frontmost app has no window in the grid. KiwiDesk’s own Settings window stopped being an exception when it began tiling (#678 item 18): quitting from it places it last like any other window.
Dismissing an ignored panel (Ghostty’s quick terminal) suppresses one clickless focus report to the app’s main window. Normally that is the spurious post-dismiss report; but if the panel closes without re-reporting the main window, the next genuine clickless focus of that window (cmd-tab, app-driven) is eaten once instead — and because the distrust survives reports for other apps through a ~1 s dismissal grace (#951), focusing elsewhere first clears it only once the grace has expired. A click on the main window is always honored: click provenance escapes the distrust and clears it (#687).The panel is untracked, so KiwiDesk can only flag “an ignored panel was active” and consume the app’s next clickless focus — it cannot tell the dismiss re-report from a genuine one (the on-screen #21 distrust, extended across the dismiss transition in #244) — and the grace exists because the stale re-report was measured landing 125–200 ms after the user’s click on another window (Design Decisions ▸ An ignored panel’s dismissal is a race).The distrust is armed when the event loop filters the panel’s own focus report; shouldConsumeIgnoredPanelReport (KiwiCore+IgnoredPanel.swift) owns the consume, the click-provenance escape, and the grace deadline (IgnoredPanelGraceTests).Click the window you want — a click always wins. A suppressed cmd-tab needs one more press, and the grace expires on its own within ~1 s.
An auto-ignored command bar whose window rides at or above the main-menu window level (some launchers raise their bar to cover native-fullscreen apps) escapes the transient focus-distrust while it is up: a focus-follow can act on the app’s stale main-window report during that moment. The bar itself is still never managed.The visible-ignored-panel scan counts only the raised panel band (above normal, below the main-menu level), since an accessory app’s permanent NSStatusItem window sits at the status level and a wallpaper utility’s backdrop below normal, and counting either would latch the distrust forever and kill focus-follow for every menu-bar app’s managed windows; a band miss fails toward the pre-#448 status quo, transiently.FloatDetection.isPanelBandLayer bounds the hasVisibleIgnoredPanel scan; the track/reconcile ignore gate is keyed by AX-tracked windows (status items and backdrops never are) and carries no band.None needed in practice — the moment passes with the bar; ignore_rules remains the whole-app hatch for a chronically misbehaving launcher.
A layout schematic never draws your actual windows or their arrangement: in Layout Defaults it simulates a window count you choose on the preview’s slider, and a mount with no slider beside it — the preset preview sheet (#859) — draws a fixed stand-in count instead. Where a rule of the layout is a function of your display (how many windows fit in one track, how many tracks fit across the screen, how many cells an auto-sized grid gets), the drawing answers with a fixed stand-in number, not with your monitor. Monocle’s diagram shows its focus-cycle navigation model, not tiling geometry (it has none).The schematics answer “what does this value look like” from the staged config alone — a faithful desktop simulation would need live window state (an AX read) and the live-apply coupling #123 rejects — and a mini-canvas has neither your screen nor your minimum window size, so the preview keeps the engine’s rule and substitutes only the number, which is what lets it teach fill-then-spill at all (#678 turn 10, #708; the ruling is in design decisions).The schematics are pure SwiftUI over the config model (LayoutSchematicKit), by the #123 never-live-apply principle; the count is view state on the preview panel and is never saved, and each substituted display quantity is a named constant in LayoutSchematicStandIns.None needed — the preview is for judging values pre-Save; Save and observe the real windows (#125). The preset sheet’s claim is narrower on purpose and needs none: it answers which layout each Space opens in, never what your desktop will measure.
A Space with more app groups than the configured glyph cap shows only the cap’s worth of glyphs plus a +n count; there is no way to see which apps are hidden without switching to the Space.Space Bar glyphs are a status read, not a click-through inventory — the whole Space item is the one click target — and a per-glyph or per-badge disclosure would reopen the “glyphs aren’t click targets” decision (#293) for a rare case.Grouping then the cap render only space_bar.glyph_cap slots (default 5, range 1–12); everything past that folds into the +n overflow, counting hidden windows (#293 stage 2, #376).Switch to the Space (one click) to see everything, or raise glyph_cap if the overage is small and common.
At deep BSP splits under extreme ratios, the screen-midpoint side rule can misread which side a “grow” acts on.Mouse parity is the spec: keyboard matches the mouse’s midpoint reading exactly, warts included, so the two never diverge.The sign is inferred from the focused window’s screen-midpoint side (BspSplit.side), shared with the mouse for parity.Shipped: the track layout (#128) gives each resize one true target; within BSP the parity is intentional (#122).
When a scrolling focus move merely reveals a stationary window — the target already sits at its final frame, pinned at the top screen border (macOS’s own wall) or at an edge walled by a neighboring screen (#878) — keystrokes still reach the previously focused app until the pan settles (one animation length, 50–1000 ms). A move whose target itself changes frame — sliding in from an open edge’s void, or traveling to its seat under a start/center/end anchor — and the handoff after closing a window raise immediately, so only stationary reveals have the delay. A clickless refocus (cmd-tab, an in-app window switch) of a window KiwiDesk raised within the last second, while focus has already moved to another window in the same scrolling space, is read as KiwiDesk’s own echo: keystrokes go to the window you switched to, but the ring, the pan and the pointer stay on that other window until the next focus event. In every layout, the same clickless refocus inside that second is honored but treated as KiwiDesk’s own echo — no pointer warp under mouse follows focus, no float re-raise above it, and no return of a VoiceOver-start focus steal onto it — again until the next focus event. A click escapes on its own provenance (#687).Raising a pinned-behind row first pops it over the whole screen before the slide starts (#143), so only the stationary reveals are deferred; echo provenance (#152) tells KiwiDesk’s own raise echoes apart from user focus, and lazy apps report a raise’s focus twice with the duplicate landing after the user’s next fast step, so the echo classification holds for a full second (#887) and the window focus has already moved to wins the tie (design decisions). Normal for scroll-style window managers.AppKit keyboard status only moves with the real AX raise, and the deferred raise waits on the animation-settle signal (shared with the z-order restore).Global Carbon hotkeys are unaffected (they reach KiwiDesk regardless of the key app); animations.set_on_scrolling(false) disables the slide and restores instant transfer.
Within ~1 s of KiwiDesk raising one of an app’s windows, a clickless focus (cmd-tab, an in-app window cycle) of a sibling window of the same app that sits on a hidden space or on another screen is distrusted for the rest of that second — KiwiDesk keeps and re-asserts the window you were on — while a click on the sibling is always honored, and so is the sibling’s report when the window you were on sits on a Desktop no screen is showing, since re-asserting it would switch Desktops (#1345).Raising a window activates its app, and lazy apps (Electron-class) then re-report their old window as focused — same app, different window — which would yank you to that window’s space or screen; inside that second the two reports have one shape, a sibling raised after the reported window is the only discriminator, and a click is provenance no re-report can forge (design decisions).siblingRaiseOutranks, distrustsSiblingSpace and recentClickInside in KiwiCore+FocusDistrust.swift, over selfRaiseStamps, read through selfRaiseStamp(_:now:)’s one-second bound (#465, #496, #887; ActivationReReportTests).Click the sibling, or switch to it again once the second has passed.
A window KiwiDesk moved less than two seconds ago that then reports focus without a click — cmd-tab, or the app focusing itself — is bounced in the scrolling Space you are on, whether the pan left it on screen or off, and so is the window a focus command stepped off less than two seconds ago; anywhere else — the corner a hidden Space’s windows are parked in, a scrolling Space you are not on, monocle’s park — only a window placed past the edge that refused it by position is. Bounced means the app comes forward for an instant and focus snaps back to the window you were on; the row does not pan, the focus ring does not move, and the pointer is not warped under mouse follows focus. A click on it is always honored, KiwiDesk’s own Settings window included (#1281); outside a scrolling Space a window that went where it was placed keeps its cmd-tab. Opening Settings from the menu bar, the App menu or the open_settings chord is never bounced (#1281). Two more reports are honored (#1345): the window a macOS Desktop switch just restored as that Desktop’s last focused window, and any report whose bounce would bring back a window sitting on a Desktop no screen is showing. A bounce renews the two seconds while the placement itself is less than two seconds old, so an app that keeps bouncing keeps being bounced — and a lockout, your own retries included, ends at most four seconds after KiwiDesk last moved the window or stepped off it.Some apps answer being moved by focusing themselves — the Android Emulator’s Qt shell does, 0.7–1.5 s after any pan that moves its window, on screen or off, after a focus command steps off it with nothing asked of it, and 1 s after a Space switch parks it (measured 2026-09-05), and it retries — and that report has exactly a cmd-tab’s shape; where the window landed discriminates nothing (the emulator complied within 9 pt and bounced regardless), so a genuine cmd-tab onto a scrolling window KiwiDesk moved less than two seconds ago, or back onto the one you stepped off, is bounced for those seconds too (a placement bounce is the app’s answer).TilingEngine.placements (PlacementLedger, stamped at applyFrame / setFrame, the displacement noted by focusWindow, age-bounded, never consumed, renewed through its bounded renew door) and KiwiCore.placementBounce, consulted by handleWindowFocused above the focus-follow (#1161, PlacementBounceTests, PlacementDisplacementTests, PlacementLedgerTests, PlacementBounceSeamTests).Click the window — a click is always honored — or cmd-tab again once four seconds have passed since KiwiDesk last moved it or stepped off it. For the Settings window, open it from the menu bar or the chord, which are never bounced.
After a macOS Desktop swipe, KiwiDesk raises no stand-in for the window that left with its Desktop — whatever macOS brings forward keeps the focus until the next focus report — and none of its focus corrections (the placement bounce, the same-app sibling re-report, the accessibility-steal return) is performed when the window it would bring back is not on screen: that report is honored instead. The residue: an app whose window was the Desktop’s last focused window and that focuses itself, clicklessly, within half a second of returning is honored, not bounced.Raising a window the compositor is not drawing makes macOS switch to the Desktop that holds it, the window such a correction names is the one the user just left — still in state because a slow app’s destroy notification trails the swipe by seconds — and the window macOS restores on a return has exactly a bounce’s shape, so the record of which window each Desktop last had focused, read only for a fresh return, is the discriminator, and inside that half second it cannot tell the OS’s restore from that same window’s own bounce (a raise across Desktops is a Desktop switch in disguise).KiwiCore.raiseCrossesDesktops reads the compositor’s on-screen flag and whether the window’s hosted Space is one a display shows — either refuses (#1410) — and focusWindow refuses the verb whole on it; the three re-asserts stand down through reassertCrossesDesktops; departedWithDesktop is the close-return stand-down’s arm and isRestoredDesktopFocus the placement distrust’s (#1345; DesktopRaiseGateTests, DesktopRaiseGateArmTests, DesktopDepartureStandDownTests).Click the window you want; to reach a window on another Desktop or change Desktops on purpose, use focus_desktop or Open-or-Focus, which switch rather than raise.
After a macOS Desktop swipe, a Finder window on the Desktop you left can stay in its space’s row for a while — it is still listed, still counted, and comes off at a later sweep or when Finder itself closes it. Nothing visible follows: it is not raised, and the Desktop does not switch back. Measured 2026-09-13 with a compositor probe and the event stream beside the log.Finder keeps reporting a window in its accessibility list after macOS hides the window’s Desktop, where every other app drops it, so the removal sweep sees nothing gone, and the sweep is census-blind during the switch grace (#1157) because the census double-exposes both Desktops while the compositor settles — a stale row is bookkeeping, a wrong removal moves a window.The compositor’s two reads are the one authority a raise consults (KiwiCore.raiseCrossesDesktops, #1345/#1410), so the residue can never become a Desktop switch; the settle’s own arm (#1364) never re-asserts a window the switch removed either.None needed. The row corrects itself at the next reconcile of Finder — a click into Finder, or its own destroy notification for the window. A Finder-aware removal is not planned unless the stale row is ever seen to act (the settle never re-asserts a focus the switch removed).
A multi-slot focus jump (an App Bar click) in a Scrolling space walled on both sides plays as a sequence, not an instant swap: the free middle window visibly slides between the wall piles — behind the focused plane — and the pile restack and focus front land only when that slide settles (#878, owner QA 2026-08-18). Adjacent-step flips between wall-pinned windows stay instant, because there nothing moves at all.With both edges walled the jump target sits pinned at its wall, fully covered by the free window, so raising it early would pop it up through its still-moving cover (the #143 artifact), and the restack for jumped-over windows must consume settled frames, since armed before the retile it restacks against pre-pan geometry (#153).The stationary-reveal raise (row above) and the multi-slot jump restack (#674) both ride the animation-settle signal (KiwiCore+Settle), each for its own reason.animations.set_scroll_duration shortens the slide; animations.set_on_scrolling(false) makes jumps instant frame-sets with an immediate restack.
mouse.follows_focus is a per-profile setting: switching profiles can silently flip mouse-follows-focus, and a profile saved before the toggle existed loads with it off.It lives on the settings root profiles serialize — the same home as animations.* and mouse_resize (#186) — and a sparse behavior-override seam (KeyLayerOverride-style) for one bool is the premature generic override AGENTS.md §5 forbids until a second client exists.Profiles serialize TilingSettings wholesale, and missing keys decode to their defaults by the profile contract.Set the toggle in each profile you use (re-saving captures it); revisit the placement if a real global-behavior tier ever emerges.
On macOS 14.0–14.3 no checked layout in the quick menu shows a “not saved to profile” subtitle when the session layout drifts from the profile — neither the flat single-screen list nor, with several screens connected, any screen’s own list (#752); the menu itself still works.NSMenuItem.subtitle is a macOS 14.4 API and the deployment target is 14.0 (#123); one #available(macOS 14.4, *) gate in addModeRows serves every list the menu builds, so the per-screen rows inherit the same limit.The subtitle is the app’s ONLY statement that a layout is temporary (#1179) — Settings narrates the profile, not the session — and what still answers on 14.0–14.3 is the Keep row’s own enablement, which arms whenever any screen is running a temporary layout.Read the Keep row: enabled means at least one screen is on a layout the profile does not have. Which screen is what the subtitle would have told you, and on 14.0–14.3 it does not — open the per-screen lists and compare the checkmark against what the profile holds.
With macOS “Automatically hide and show the menu bar” on, a pointer at the top of the screen reveals no KiwiDesk menu bar while a KiwiDesk window is the active one — the Settings window, in practice, being the one you keep open (#329’s symptom by a second route). On macOS 26 nothing reveals. On macOS 27 the previous app’s bar reveals: macOS activates the last regular app to have one to show, and the note below the table covers the focus that moves with it (#1532). (Owner-observed on macOS 26.6.2, 2026-08-24, and on 27.0, 2026-09-20.)Only a .regular app owns a menu bar, and promoting KiwiDesk while a content window is up is the promote-on-open / demote-on-close rule Permanent accessory mode retired after one reachable close order stranded the app .regular with nothing on screen and broke the front == focused.pid test every focus command denial rests on.main.swift sets .accessory once at launch and nothing else may change it — ActivationPolicySeamTests’ allowed map is the one copy of who may, its two entries that launch and the SingleInstanceGuard alert, which exits before any window can outlive the promotion; MainMenu is built only so AppKit can route key equivalents to the key window (the Settings text fields’ Cut/Copy/Paste/Undo) and is never displayed under .accessory; the tiled slot reaching the top edge is not the cause — GeometryUtils.axVisibleFrame reclaims the auto-hidden strip for every tiled window, not only KiwiDesk’s own.Click another app’s window, which restores the reveal at once on macOS 26, or turn the auto-hide off (System Settings ▸ Control Center). None planned — the fix and the ruling are the same lever pulled in opposite directions.
If Spotlight indexing is disabled for an app’s volume (mdutil -i off, a locked-down/managed Mac) or the app was just installed and isn’t indexed yet, the picker shows its English disk name instead of the localized one (e.g. “System Settings”, not “Systemeinstellungen”). Normally every app is localized correctly.App names come from Spotlight’s kMDItemDisplayName — the index Finder/Dock/Spotlight read, so it localizes from KiwiDesk’s English-only process where a bundle-local lookup can’t — and without an entry no public API localizes the name from such a process (the limit AeroSpace also hits, naming only running apps), so it falls back to FileManager.displayName, which resolves in the process’s English locale.localizedName(url:) reads kMDItemDisplayName, falling back to FileManager.displayName; the picker stores the bundle id (AppRef) as identity, so the name is presentation only.Re-enable Spotlight indexing (mdutil -i on), or wait for indexing to catch up; the rule keys on the locale-independent bundle id regardless (#263).
The picker’s installed-app list is a one-shot snapshot taken the first time any picker opens: an app launched mid-session from outside the scanned disk roots (a running-only app like Finder that wasn’t running at first open) won’t appear until KiwiDesk relaunches. Disk-installed apps are unaffected.The list is read from a process-cached snapshot so the popover renders fully populated the instant it opens — staging it through view @State on open raced the popover presentation and rendered blank until a keystroke — and only the running-app union is time-sensitive.The picker reads KeybindingCatalog.installedAppsSnapshot (a static let computed once), not a per-render installedApps query, avoiding both the empty-then-fill race and a rebuild per keystroke (#263).Relaunch KiwiDesk to re-snapshot; disk-installed apps (the bulk) never need it.
An app with no bundle identifier (a rare unbundled or legacy helper process) can’t be targeted by an app rule (float_rules, ignore_rules, app_rules) or by pull_or_spawn; its windows match no rule and tile normally.App rules key on the bundle identifier, the only stable, locale- and rename-proof identity, and a name-based fallback for id-less apps would reintroduce the ambiguity (two apps sharing a display name, locale drift) that keying on the id removes.Window identity is NSRunningApplication.bundleIdentifier, captured once at attach and stored on ManagedWindow.appBundleID (AppRef); a nil id stays nil and matches no rule (#262).None — effectively every user-facing app is bundled and has an identifier; the blind spot is a handful of window-less helper processes not worth managing.
A thick focus border at small gaps makes neighbouring windows’ rings touch or visually merge, and at 0 pt gaps they overlap.The ring is a pure post-layout overlay with no gap coupling; the stroke’s configured width is its outward reach, a renderer-only overlap sits behind the target and does not count toward visible thickness or gap fitting (rounded keeps a small seam allowance, square reaches deeper under the corner reveal), the failure mode is a soft visual merge, never window-content clipping, and an automatic minimum-gap floor would silently widen a user’s gap = 0 the moment borders turn on, so border.fit_gaps stays the opt-in escape hatch.The border is rendered independently of layout (#278); the width is bounded to 20 pt, but gaps and border width are set separately.Set gaps at least as wide as twice the border (or run border.fit_gaps), or lower the width. The shipped defaults sit exactly on that line rather than clear of it: a 5 pt ring reaching into a 10 pt inner gap meets its neighbour’s edge-to-edge with nothing to spare, which is why 5 pt is the widest default that tiles cleanly and 6 pt would already overlap. So the merge starts the moment gaps narrow, not at some later threshold.
The focus ring is drawn below each window (its lower reach picks up the window’s own drop-shadow, and the corner is a filled seam, not a floating hairline), not on top.Below-order is immune to the per-keystroke compositor reorder storms some apps (Firefox/Zen) emit — the ring’s order(.below) re-stack is flicker-free, unlike the SkyLight above-order transaction (#357), which is crisper and shadowless but flickers on those apps — and, reading each window’s real corner radius, it hugs the corner cleanly, the drop-shadow reading as subtle depth.The ring is a separate overlay stacked relative to the target: below-order (AppKitBorderOverlay, also the mandatory fallback) sits beneath it, above-order (SkyLightBorderOverlay) on top. Below is the default; the above path stays in the tree, and the square ring’s hidden overlap is derived from the real radius so large-radius corners don’t gap (BorderGeometry.squareHiddenOverlap, #361).border.set_draw_order exposes above-order for anyone who prefers the crisp, shadowless corner and can accept the flicker on Gecko browsers.
A focused-window shortcut (focus, swap, resize, make_floating, move_to_space, move_to_track, stack.promote/demote, …) is rejected without acting while an ignored panel (Ghostty’s quick terminal) or an unmanaged/ignored app holds the foreground — and, for the brief moment during an app-activation or self-raise race before the OS frontmost app catches up to KiwiDesk’s focused window.Acting on the implicit focused window while a different window is frontmost would silently mutate a window the user cannot see, so the command fails closed until the OS foreground genuinely matches the managed focused window; an unmanaged panel that never emits any focus notification cannot set the ignored-panel latch, so a shortcut fired while it is up relies on the frontmost-pid check alone.One semantic preflight at KiwiCore.execute (FocusedCommandPolicy classifies the focused commands; focusedCommandDenial requires the OS frontmost pid to equal the focused managed window’s pid, the event loop to still observe it, and no ignored-panel latch for it) — shared by Lua, CLI, and IPC, else no managed window is currently focused (#292). The “focused managed window” is the focus anchor (focusedWindowID), so a frontmost sticky traveler — tiled (#431/#435) or floating, keyed on its render space (#416) — is recognized as the foreground owner, not mismatched against the stale local slot it can never occupy.Bring the managed window back to the foreground (click it, or dismiss the panel so its app re-reports a managed window) and re-issue. Global config, focus_space, spawns, profile ops, and explicit App Bar clicks are never gated.
A native-tab window (Finder, Terminal, Ghostty) is managed as one window that follows whichever tab is active — its tabs can’t be split into separate tiles, and there is one App Bar item for the group, not one per tab.Native tabs are separate NSWindows the app owns, with only the active tab ever visible to AX, and splitting a tab into its own tile would need cross-process NSWindow reparenting KiwiDesk cannot perform; the app’s own tab bar already expands the tabs, so App Bar sub-items would be un-Mac-like.Background tabs never appear in kAXWindowsAttribute and mint a fresh CGWindowID per switch (#308 probe); the event loop coalesces the same-frame vanish/appear into .windowRekeyed (TabReconciler), preserving the flat one-slot-per-group state. Detection is temporal (a tab group on either side + same frame within tolerance, in one reconcile pass), so two narrow same-frame false-merge edges are accepted — same-app windows stacked in an OverlapStack pile, and a tab carrier spanning two macOS Desktops at the identical tiled frame (suppressed by a post-Desktop-switch grace window) — both needing the vanish and appear in one pass and self-healing on the next reconcile.None in-app — a cross-process split is impossible. Whole-app opt-out via ignore_rules if a specific app’s tab behavior misbehaves.
Floating and sticky windows never merge into multi-window same-app groups in Space Bar items; they always render as individual single-window item slots.Merging a floating or sticky window into a multi-window group (e.g. 2 Terminal windows merged into a group item with count “2”) hides which individual window is floating or sticky behind an aggregate count, and its corner state badges (pin/layer) are identifiable only while the window is its own item slot.KiwiCore.adjacentRuns(of:specials:) treats isFloating and isSticky windows as group terminators, isolating each into its own 1-window run (#414).Tiled windows of the same app still group adjacent runs as expected; un-grouping applies only when a window holds a special state (floating or sticky).
resize issued while a traveling sticky window — tiled or floating — is frontmost adjusts the local focus’s slot (its master/stack/track ratio or per-window weight), not the traveler’s — unlike focus, swap, toggle_sticky, toggle_floating/make_*, and move_to_space, which all act on the traveler via the focus anchor (#431/#416).A per-space weight is keyed by window id, writing one under a non-member traveler would orphan it (never pruned, recycled-id hazard #308), and a traveler carries no stored ratio to nudge, so the write lands on a real member and applies once the traveler departs — a transient reference mismatch, never data loss.Implicit-focused verbs resolve against StateCoordinator.focusAnchor (the OS-frontmost traveler, tiled-injected or floating-rendered), and the #292 guard vets the same anchor (focusedWindowID) so the command is no longer denied cross-app (#431/#435); resize alone stays on space.focused, for the orphan reason.Re-issue resize on the traveler’s home space, where its id is a real member the space can own and prune.
Swapping or drag-reordering a traveling tiled-sticky window on a non-home space does nothing to its position (the reorder partner stays put; a bar drag of the traveler is a no-op; a mouse drag — or mouse resize — of the traveler snaps back to its injected slot on drop). The move is not silent: the traveler’s mark briefly flashes a pill naming its home space — on the mouse path when the traveler is dragged (#421) or another window is dropped onto its slot, and on the keyboard path for every reorder verb that targets or originates at the traveler — directional swap, track.swap, stack.promote/demote, move_to_track — where the pill lands on the traveler (the window that can’t move), not the trier (#435). The keyboard path has no snap-back motion, so the pill’s own entrance carries a small scale overshoot as the acknowledgement.Translating a foreign-space reorder back into a home-array move is a non-goal of #414 v2, and Space.swap/move membership guards make the no-op safe.Injection is derived, never stored: only the home array is real, and every Space mutation guards on membership.Reorder it on its home space — the derived slot follows everywhere. A possible future home-array translation (#414 notes it as out of scope for v2). Keyboard-swap-onto-a-traveler is a semantic refusal, so it belongs to the pill (#435), not the dead-end ring rubber-band (#436) — that cue fires only on a true no-candidate boundary (.fail("no window … of focus")), which a found-but-unswappable traveler is not.
A tiled-sticky window caught in a whole-region cascade (cascade_all, or the emergency min-size cascade every layout falls back to) piles with the rest — the sticky exemption applies only to partial tile-then-pile overflow (Stack zones, track columns, the grid’s last cell).A whole-region cascade has no fully-tiled slot to give — every window is reduced to a title-bar sliver — so there is nothing to exempt the sticky into, and its title bar stays reachable like every other cascade member.OverlapStack.frames is the physics fallback when not even one min-size window fits; OverlapStack.stickyExempt clamps indexes across a tiled/piled boundary, which that fallback does not have.None planned — resize the space or close windows; the pile is already the emergency shape.
While a tiled-sticky traveler visits a track space at index 0 (or at any track head slot), that track’s custom width temporarily reverts to the default share, snapping back when the traveler leaves.Track head weights are keyed by the head window, the visiting traveler becomes the (implicit) head, and storing a weight under a non-member id would orphan it (never pruned, recycled-id hazard #308), so resize writes key the first local member and apply once the traveler departs — a transient wobble, not data loss.Space.trackWeights is keyed by the track’s head window (#128), and injection is derived — the traveler is never a member whose entries the space could own or prune.Resolve the head weight against the first local member at read time if the wobble ever matters in practice.
track.swap’s overflow-block gauge can misjudge by one track at the edge — an over- or under-cautious refusal, never a wrong swap — while a tiled-sticky traveler visits a track space (the gauge reads the injected list, the mutation partitions the local array).The mutation stays safe (membership guards; the focused window is always local), and translating the gauge to the local array is the non-home-reorder non-goal’s territory.The gauge and the mutation consume different derivations: reads see the rendered (injected) space, writes own only the local array.Re-derive the gauge from localTiledMembers if the edge ever bites in practice.
KiwiDesk.exec (and os.execute) dedups identical commands by default: while one copy of the exact command string is in flight, a second exec of it is skipped — it returns nil and its callback never fires.For per-event hook pokes to a receiver that re-queries full state (the sketchybar bridge), a second poke while one is pending is redundant, and deduping caps a wedged receiver at one stuck child per command instead of thousands (#467); it edges past “Lua is open” (§2.7) only for running two identical commands at once on purpose, which the opt-out covers.The launcher tracks an in-flight count per exact command string and skips a dedup launch while it is non-zero (ExecLauncher.inFlight); the raw launch primitive defaults dedup off, the opinionated default-on lives at the Lua boundary (KiwiCore+ExecAPI).Pass dedup = false (the 4th exec argument) to run identical commands in parallel; the callback then fires normally.
The first-run starter setup can seed more spaces than the number row has keys — its budget reaches ten, and min-one-per-screen pushes past that on more than ten displays — but only the first ten get a default digit shortcut; spaces 11+ ship with no ⌃⌥/⌃⌥⇧/⌃⌥⌘ binding.The number row has exactly ten keys (1…9, 0), and a two-key or lettered default for the overflow would be less predictable than leaving it to the user; every space stays reachable and bindable, only the default shortcut is absent (#466, “approachable by default, powerful on demand”), and the budget stops growing at ten for the same reason.DefaultKeybindings.numbered caps the per-space rows at ten and maps the tenth to ⌃⌥0; StarterAllocation soft-caps the budget at ten but lets min-one-per-screen outrank it, so a very wide display setup still out-runs the digit keys.Reach spaces 11+ from the Space Bar, or bind them yourself in the Keybindings editor (any space is bindable, by name).
If an Accessibility-permission revoke (stop()) fires while an exec child is genuinely wedged (a grandchild holds its output pipe past EOF), that child’s bookkeeping — and its in-flight dedup tally — leaks for the process life, so that exact command string stays dedup-blocked even if its receiver later recovers.The window is tiny (the stop must land in the interval a child holds the pipe past EOF), clearing the entry would leak the Lua callback ref when the child’s later EOF reap finds no entry, and a child is never SIGTERM-ed after management has torn down (children are fire-and-forget, §5) (#467 / #37).cancelWatchdogs() cancels the timeout watchdog but leaves the running/inFlight entry, since only the child’s own eventual termination can safely release it (ExecLauncher).Reload the config (a fresh VM and launcher) if a hook command stays silently dedup-blocked after an AX-permission cycle.
A macOS Desktop switch is never animated in either direction — windows simply appear in place once the Desktop settles — while a KiwiDesk space switch (with animations.on_space_change on) runs the full coordinated out+in slide.macOS stops reporting an inactive Desktop’s windows to Accessibility the moment the Desktop changes — the outgoing side is unreachable before an exit slide could run, and the incoming side’s windows become addressable only after macOS’s own swipe already played — so animating whichever fragment is reachable would recreate the asymmetric half-transition the coordinated switch removed (#207).Windows on other Desktops never appear in kAXWindowsAttribute, so handleDesktopChange re-tracks and retiles with animated: false — the same OS visibility boundary behind the Desktop rows in this table — one that focus_desktop and move_to_desktop do not change, since driving a Desktop does not change what Accessibility reports of it.Use KiwiDesk’s spaces where animated switching matters — macOS already animates its own Desktop swipe; KiwiDesk will never ask you to disable SIP to change this.
Settings search does not reach the controls inside a per-Space Customize… override editor — searching a label that appears only there finds nothing (the same words on the Layout Defaults layout cards are found normally).A search result navigates to a destination and then reveals a target inside it, and the override editor is a view-state branch the Spaces list pushes over itself (model.nav.spaceOverridesFocus), reached only by clicking a space’s override cell, so there is nowhere for a result to send the user; the override rows mirror layout_params.* labels the Layout Defaults cards carry, so the user’s word stays findable (#277).The index is static — the census’s settings plus each destination’s control catalog — and a reveal selects only destination-level surfaces (SettingsSurface.layoutMode: one layout’s card); the per-space override fields have no SettingsSurface and are uncataloged, so nothing for search or the #326 bridge can reveal them, and a .spaceOverrides(SpaceID) surface would be added only if a deep-link into one space’s editor is later wanted.Search the same setting’s global home (Layout Defaults) and use a space’s Customize… override editor to deviate from it; revisit if the per-control catalog ever indexes the per-space fields.
Running BOTH auto-start paths — the Start at login item and the kiwidesk service LaunchAgent — makes crash supervision a coin flip. Both launch KiwiDesk at login, the instance lock keeps that to one process, and only the launch that WON is the one launchd can supervise; if the login item won, the agent’s job sits loaded with no process and nothing is watched.Settings offers no crash supervision, so this state is assembled on purpose by running kiwidesk service start while the login item is also on (#1071) — which service start says is happening, and tells you to run one; both paths stay wanted, since SMAppService is the mainstream login item and the service’s KeepAlive supervision needs RunAtLoad to recover from a login-time crash (a sandbox being unable to write a LaunchAgent or spawn launchctl), and making launchd win the race would mean quitting and relaunching the app the user is standing in.The #196 single-instance lock keeps two launch triggers to one process; launchd can supervise only a process it spawned itself, so whichever launch loses leaves its job idle, and AutoStartManager.level reads isLoaded, which is launchctl print succeeding, so an idle job still reports as loaded (#341’s own case).Run one path. kiwidesk service stop leaves the login item; turning Start at login off leaves the service. While the service is loaded the Settings switch says so and stays inert.
Open-or-Focus cycling (a repeat press of pull_or_spawn while one of the app’s windows is focused) reaches the app’s windows on other Desktops only where the Desktop bridge is present; on a Mac without it the ring is the windows KiwiDesk currently tracks, and a repeat press while the app is already frontmost does not pull those Desktops forward (activate() on an already-active app is a no-op). Either way a window found at boot on an away Desktop that no Space has filed yet is not in the ring until that Desktop is first shown.The ring holds each Space’s row in the order it will return in, away windows included (#1146), and cycling onto an away window is a Desktop switch, which only the bridge can perform (the bridge is not a SIP escape hatch); an unfiled window belongs to no row and a deterministic ring cannot place it.cycleToNextWindow reads the rows with withAwayMembers and reaches through reachAwayWindow when canDriveDesktops (#637, OpenOrFocusReachTests).On a Mac without the bridge, switch to the other Desktop — its windows rejoin the cycle immediately — or use macOS’s own app window cycling.
Open or Focus restores a minimized window only when the app has nothing open on any Desktop — but on a Mac where the per-Desktop window list cannot be read, a window on another Desktop is invisible again and the shortcut un-parks the local minimized window instead of switching to the Desktop that already has one.The un-park gate reads the app’s kAXWindows on the shown Desktop plus the away ledger’s windows that the per-Desktop list reports UP (#1146); without that list there is no second witness, and restoring a window where the user is standing is the gentler wrong guess (#673).launch reads awayReach(bundleID:) beside readAppWindowCensus and un-parks only when both are empty (OpenOrFocusReachTests); the AX census is not the WindowServer’s .optionOnScreenOnly count, which would also hide the windows of an app hidden with ⌘H.Re-minimize the restored window (⌘M) and switch to the Desktop that holds the visible one; the wrong guess costs one keystroke and never loses window state.
Editing gaps, min_window_size or a layout ratio in the Settings app shrinks a pane with the old first-frame snap, while the same edit from Lua or the CLI slides its shared edge smoothly (#593). Growing is smooth on both paths.The smoothing is a promise that every window in the pass is spring-sized, which only a caller that knows exactly what it changed can make, and a Settings save applies whole config — it can change the layout mode or window membership in the same pass, where a gradual shrink leaves a newly-placed window overlapping the sibling still vacating its room (#45).GUI edits land through KiwiCore.applyProfileScopedState, one apply for the whole scoped config, which retiles without a sizing promise; the Lua/CLI setters raise the promise beside each individual ratio or gap write (BatchSizing).Ratios only: use the Grow / Shrink width (or height) shortcut, which makes the same change with the smooth motion — press ⌃⌥K for the chord your own setup uses, since an install seeded before the ⌥⌘ size layer keeps the chord it was given. Gaps and min_window_size have no GUI-side workaround — the Lua/CLI setter animates smoothly but is not one for a GUI-managed user, because the next Settings save assigns tiler.settings wholesale (applyProfileScopedState) and overwrites it. The resulting geometry is identical on every path; only the shrink animation differs.
A window-arrangement snapshot from before the current boot is discarded at launch (windows rediscover fresh) — and the boot gate has two residual edges: a logout → login on the same boot still replays a snapshot whose window ids the new login session may remint, and the first launch after a manual backward clock change can wrongly admit a pre-boot snapshot.Window ids are only meaningful within one login session, and gating on boot time removes the dominant case — a reboot, where stale replays filed random windows into old spaces — with one cheap comparison; the two edges fail toward the pre-gate status quo at worst, and the forward-clock case fails benign (fresh rediscovery).The gate compares StateSnapshot.capturedAt against kern.boottime (SystemBoot.time, #633); both are wall-clock, and login-session identity is not part of the comparison.General ▸ Advanced ▸ Discard Saved Window Arrangement clears a suspect snapshot on demand; a session-identity gate could replace the boot gate if the logout edge ever bites in practice.
Taking a window into native fullscreen can flash the App/Space Bar onto the fullscreen screen for a split second before it disappears.The fullscreen flip retiles immediately (the layout must re-partition around the window’s now-exempt slot), and during macOS’s transition the per-display query can still answer with the Desktop the animation is leaving — a stale “still on a Desktop” verdict for one beat — while delaying the retile would trade a cosmetic flash for real staleness in the layout.The bar builders read NativeSpaces.currentSpaceIsUser(display:) live per retile (#670); the desktopChanged that always follows the transition re-syncs both bars, which is what retires the flash.None planned — the window is bounded by macOS’s own transition animation; the follow-up sync is guaranteed by the same event that completes the switch.
Two connected displays of the same model at the same resolution are one identity to KiwiDesk: a space pinned to one may open on the other, both draw the same chips in Settings ▸ Monitors, and only one of them can carry the “main” badge.A display is recognised by name:WxH because the identity has to be stable across sessions and macOS’s CGDirectDisplayID is reassigned across reconnects and reboots, EDID serial numbers are unreliable in exactly the population that needs them (zeros for virtual, AirPlay and projector displays, missing on real panels), and every stored pin is keyed by that string, so changing the identity is a config-shape change.Display.fingerprint (Sources/KiwiDeskCore/Models/DisplayModel.swift) is name:WxH; the Monitors picture, the pin map and the main badge all key off it, and MonitorsFamilyRows.hasAmbiguousDisplays is what detects the collision.The picture says so rather than letting it read as a bug — a note appears under the arrangement whenever two connected displays share a fingerprint. Real fix planned in #734: a display identity that survives reconnects without colliding. Changing it re-keys every stored pin, so the change owes a one-shot ConfigMigration step (AGENTS.md §5), and the changelog says so. The open question is which field is both stable and distinct, which needs a device test with two identical panels.
The Monocle focus flip turns a plate carrying the two apps’ icons, never the windows’ own pixels — and cycling between two windows of the same app turns one icon into the same icon; the direction of the turn is the only tell.A live-image flip needs the window’s pixels, which reach no process without the Screen Recording permission, and the owner ruled no new permission prompt (#1391; the ruling is in design decisions); the plate shows icons alone because the turn is below reading time.MonocleFlipPlate draws a CALayer wash with the app icon over the compositor’s own behind-window blur (NSVisualEffectView); SLSSetWindowTransform/SLSSetWindowAlpha on another app’s window perform and apply nothing from an ordinary connection.None planned — closed, not deferred. animations.set_on_monocle_focus(false) turns the flip off.

The sticky-window row above covers the gesture switch too: a sticky window whose app drops its Accessibility element before KiwiDesk hears of a swipe is carried onto the arriving Desktop with its slot, sticky scope and reach pin kept (#1215), rather than staying on the Desktop you left until you return.

A window you closed and then reopened — an app whose close hides the window and whose reopen shows the same one again, Telegram’s main window for one — comes back like a new window: to the Space its app rule names, else the Space you are on, taking the focus there in every layout, rather than to the Space you closed it in with the slot it had there (#1414, #1561; the ruling is in design decisions); the #1161 row’s bounce no longer reaches it. What remains: an app that re-shows a closed window on its own, with nothing you did behind it, takes the focus once, where you are.

On macOS 27 the focus that a menu-bar reveal moves to the previous app comes back: KiwiDesk returns it to its own window once per reveal, and the previous app’s bar stays revealed above the still-key Settings window until the pointer leaves the edge (#1532; the ruling is in design decisions). What remains: a deliberate ⌘-Tab away from a KiwiDesk window while the pointer is parked at the top edge is returned the same way — only a second one within about a second goes through — so move the pointer off the edge first; a click is honored as choosing that app.

The rejection in the row above has a second form. Issued while the Space KiwiDesk shows is empty — after move_to_desktop and a focus_desktop to that Desktop (Lua reference ▸ move_to_desktop) — a focused-window shortcut is rejected, and the refusal names it: the active Space 2 is empty; the focused window (Finder) is in Space 1 — focus_space 1 first, which the log carries too (#1336; the ruling is in design decisions). What remains: the verb acts only on the Space you are on, so the window that get_state marks focused in another Space is parked, not focused, until focus_space brings its Space back.

Dragging a stack window’s edge along its zone’s own axis — a height drag beside a left or right stack, a width drag beside a top or bottom one — moves the dragged window’s share of its zone, as the keyboard resize does (#941; the ruling is in design decisions). What remains: a master zone lined up along the split has no cross-axis share to move, by the row above on masters’ shares, so that drag snaps back.

Opening an app whose app rule sends its window to another space takes you there only when the app came forward within about a second of a click or key press with no window of its own showing — a launch, a reopen, an un-minimize — or through pull_or_spawn (#1599; the ruling is in design decisions). macOS reports that an app became active, never why, so the timing of your last press and whether the app showed a window stand in for the cause. What remains: an app a script opens other than through pull_or_spawn (open -a, say), or one that takes longer than that second to come forward, leaves you where you are, and so does a first window that appears more than about ten seconds after the launch. An app with no window showing that brings itself forward within a second of a press you made elsewhere takes you along as if you had opened it. Use focus_space to reach the window when you are left behind.

With the Space Bar off, switching a Space between a layout that shows an App Bar (Monocle, Scrolling) and one that does not moves its windows by the bar strip’s depth: KiwiShelf reserves its edge only in a layout where a bar draws (#1517; the ruling is in design decisions). Reserving it in every layout instead would leave an empty strip on screen in every layout that draws nothing there. With the Space Bar on, the strip is reserved in every layout and no switch moves a window.

The light-fill_color glass row above also reaches surfaces that are not bars: the sticky mark on its default Automatic color, and a sticky or drag color light enough to pin nothing, are unpinned glass too. Over dark window content macOS may draw such a mark’s glass dark while its glyph — the system label color under KiwiDesk’s Appearance — stays dark. Not observed; reasoned from the mechanism (#1621). A dark sticky.set_color pins the glass dark with a light glyph; sticky.set_liquid_glass(false) returns the badge with its disc.

© 2026 KiwiCanopy