Accepted Limitations
Some behaviors are bugs by design — accepted consequences of a settled architectural trade, not defects to fix. This page is the one place that says, for each: it’s known, here’s why it’s accepted, here’s the architectural root, and here’s the real fix where one is planned. Rows link to their full reasoning in Design Decisions (or to the issue that owns it). OS-blocked-by-SIP items are a separate class — see Blocked by macOS (SIP).
Convention: when a review or manual pass classifies a behavior as
accepted-by-architecture, it adds a row here in the same change
set — the user-facing twin of the AGENTS.md §5 guardrail rule.
A row needs an architectural root and, where one exists, the
planned escape hatch; it is not a wontfix dumping ground.
| Behavior | Why it’s accepted | Architectural root | Escape hatch / planned fix |
|---|---|---|---|
| Deleting a macOS Desktop leaves its profile binding dormant rather than moving it: 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 same holds for the one Desktop macOS destroys when you unplug a screen — though that one 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 deleting the Desktop deletes the stamp. Re-attaching the record to some Desktop would mean guessing which, and the only available guess is the Mission Control number — which is exactly the silent wrong-Desktop behaviour this replaced (a Desktop is its stamp, not its number). | DesktopKey.identity keys profile_bindings; a record whose Desktop no reading can name is held rather than pruned, because absence is not proof — 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. | After a switch a carried sticky window is absent from every reading KiwiDesk has for a beat — its AX element dies as it leaves the visible Space — on a native app that can be before KiwiDesk hears of the switch at all, on an Electron app about two seconds after — and the compositor has not drawn it on the arriving Desktop yet — and so is a window that was just closed. The two are indistinguishable at that moment, 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 same recheck budget the #1157 episode already spends, each refusal arming the follow-up reconcile that converges a true close; CarriedRemovalTests pins the bound. | Wait it out — the rechecks drop the tile within a second or two; focusing the app reconciles it sooner. The mirror residue: the arm opens only for a window the carry has moved or a switch KiwiDesk itself dispatched will move (focus_desktop, move_to_desktop_and_follow — #1213), so on a gesture switch (a swipe, Ctrl+arrow, Mission Control) a sticky window whose AX element dies before the switch handler carried it was never in flight — it takes the ordinary departure, stays on the Desktop it was on, and comes back through the arrival rule when you return, with its sticky scope restored from the intent memory but its slot position and reach pin not kept. |
| 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: it is absent from the app’s Accessibility window list and from the on-screen census while the compositor still hosts it. Read as a close, that beat lost the window’s slot and moved the focus to a neighbor on every fullscreen exit (#1272) — 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 exactly the same at that moment. | 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 same recheck budget the #1157 episode already spends; 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, filing a window somewhere the user asked for long ago (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 rather than picking a stand-in. 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 — the moment it is addressable again — and an unbounded debt would fire minutes later, at a switch it was never about. Paying it to some other window would be the first-in-row jump this fix removes, so the settle prefers macOS’s own choice to a guess (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. |
| 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 return keeps today’s behaviour, where 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 — the departing Space and the previous Desktop number it would have to remember under are not in hand there, and the binding authority is the main screen’s by #888’s ruling. Ruled a residue rather than a scope on 2026-09-02 (#1207). #1230 has since amended the arm to move that screen’s Space — the departing Desktop and what it showed are threaded from the one reading switchedDisplays takes — so this row is now about the FOCUS alone. | moveSwitchedDisplaySpaces moves that screen’s Space, 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, because 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 the user has turned on System Settings ▸ Keyboard ▸ Keyboard navigation, which was 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. | The behavior is correct and the destination is real; what is missing is a system-wide user preference. Working around it would mean routing focus to whatever text field happens to be nearest — which IS the top-of-window outcome the rule forbids — or drawing text-field-shaped controls purely to be focusable. No test can see the difference: a source needle can prove a destination was named, 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 re-reveal the element it is on: it scrolls that element back into view and reads it out again. Keyboard focus was not seen to move, and nothing is lost; the page has scrolled away from what was just opened. Whether any other layout change in Settings does the same is suspected but unmeasured — that is #989’s question, and this row does not answer it. | The behaviour is VoiceOver’s own cursor handling over a focus model that is the platform’s, rather than anything KiwiDesk drives. There is no repro with VoiceOver off, which rules out our own scroll machinery — the detail pane’s reveal driver runs identically either way — and removing the drawer’s expansion animation, on the theory that animating the insert stretched the tree change across every frame, changed nothing at all and was reverted unmerged (#988); the mechanism column says why that theory could not have worked. The one fix that would close it — forcing keyboard focus onto a clicked button — is refused deliberately: making a click focus a control macOS does not focus is non-standard behaviour, and the north star binds behaviour to the platform (the same reasoning that keeps the focus ring the platform’s, #833). Other applications were heard re-announcing on a state change the same way (device session 2026-08-25, macOS 26.6.2). | The click is what leaves the cursor behind (device session 2026-08-28, macOS 26.6.2, Full Keyboard Access ON): SwiftUI on macOS does not give a Button or a Menu keyboard focus when it is clicked — observed on the drawer’s own header button and on Layout Defaults’ anchor menu — so focus stays wherever it last was, and the VoiceOver cursor, which follows keyboard focus, stays parked there too. Revealing the drawer then rebuilds the accessibility element list under the window (that rebuild is what opening a drawer is, and no arrangement of SettingsDisclosureStyle avoids it), VoiceOver answers a changed tree by re-resolving the element under its cursor, and scrolling that stale element into view is how it re-reveals it. 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 never re-asserts it, so nothing here is ours holding focus against the click; a CLICKED navigation now states no destination at all, which is this row’s own reasoning applied one level up (#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 deliberate 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. | Starting VoiceOver activates the system’s accessibility control process, and when it yields, macOS re-activates the most recent regular app — KiwiDesk is an accessory (menu bar) app, so its focused window is skipped and activation lands on the previous app 3–8 s later (#958). That misdirected handoff is indistinguishable at OS level from a deliberate clickless focus in the same window, so the correction eats one; it is single-shot and recoverable with one more press. | 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: 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 exactly the affected class; an app whose windows are merely on another Desktop, on another space, or minimized is warmed eagerly. | The skip is the largest single boot win: 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 rather than 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 — whose warm-on-reconcile promise StartupWarmupSkipTests pins (#662). Since #801 that sweep is itself chunked, so the ceiling is 1 s plus the sweep’s own duration for the last app in its queue — and one the sweep’s budget defers waits for the post-pass drain on top of that. | 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 rather than 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 full reasoning is Design Decisions ▸ Boot. The affected class is an app whose AX calls block: the measured case cost 5011 ms by itself. | The alternative is every other app on the desk waiting for that one — the whole ~10 s boot the chunking exists to end, since one app’s AX work cannot be divided. Users read a long total as “KiwiDesk is slow”, not as “that helper’s Accessibility is slow”, and deferring is the only bound available from inside a blocking call. | 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 — rather than running inside boot. Where the profile’s rules differ from the ones already installed, that shows as re-classification a beat late: 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 there is nothing to re-classify, but 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. | The alternative is what shipped before #836: the profile reload ran a second full reconcileAll inside boot, unchunked and unbudgeted, which paid one AX-unresponsive app’s cost a second time and doubled a heavy boot — a measured 10.3 s scan figure against a 4.9 s scan. A visible re-settle in a narrow case is cheaper than a frozen menu bar in every heavy one. | reconcileAll is deliberately never cut short (accessibility.md — the per-app budget is raised for a queued step, never for a pass), so it cannot simply be chunked in place; 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, which is two callers and no others: 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). Why it is not widened past those two is 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.) | The menu answering at all during boot is the point (#801), and the quick menu greys exactly the two rows that read incomplete state — but hotkeys are armed with the config, before the scan, and a bound key is not a surface that can be greyed. Why queuing was rejected instead is 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. | The event paths can all go silent at once for a fresh-launch app — 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. AX cannot list another Desktop’s windows at all, the same 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 — a tracked overlay must not shadow a missed window) 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 since #1037, so it reads only apps tracking or showing a window, and the Desktop settle sweeps 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, and it may only ever refuse a removal, never cause one — it omits other-Desktop windows exactly as readily as AX does, so trusting its absence would untrack every window parked on a Desktop the user is not standing on (the same one-way trust the hidden-app drop rules, #913). 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 — the glass material does not appear there. | Liquid Glass is a macOS 26 API (NSGlassEffectView); the boolean must still round-trip so a shared profile stays portable, so an older machine shows the underlying boxed/plain shape rather than an unbacked strip. The GUI never offers the toggle below 26 (an OS-capability gate, absent not greyed), so a setup that carries the finish on reaches this state invisibly there — the solid shape is exactly what it draws — and carries the stored value wherever it is imported. | 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 bar’s glass still picks its own light or dark variant from what lies behind it, so on a very dark wallpaper the two bars could in principle render different shades of glass. Not observed; reasoned from the mechanism. A dark fill (the default, and every other bundled palette) pins both bars to the dark variant. | macOS decides a Liquid Glass view’s variant per view from the backdrop it samples and holds the verdict. KiwiDesk pins the dark variant from a dark fill (#1308), but only dark can be pinned publicly: .aqua is the appearance the bars already carry and the material keeps adapting under it, and a light pin would take a private _adaptiveAppearance write with no public fallback, which is refused. A bright tint dominates what the glass samples, so the light case is expected to hold on its own (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 ▸ App Bar / Space Bar 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 rather than auto-recovering. | Re-issuing the set on an unmoved pointer cannot be told apart from the user having already gestured back within the verify window — auto-recovery would yank them to a Desktop they just left, fighting a real gesture to guard against a failure class never observed on this operation. A log line that makes the next report self-diagnosing is the proportionate answer (#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. The deferred re-query reads the per-display space pointer, which 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 — no recommendation to change a system setting, no per-display profile machinery. A secondary Desktop’s binding not firing is honest residue: the alternative readings (coordinated switching, per-display profiles) were weighed and rejected in the design decision. | 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: since #934/#1430 the first BSP split of each orientation and the Stack master/stack split move at retile to draw a learned floor. What remains is a floor no ratio reaches — a window inside a deeper BSP split (the shared ratio scales every same-orientation split, so the first is all it can size, exactly what a resize press can move), a window’s own share inside a Stack zone (#944), a Grid cell, and a floor not yet learned — and the case where the yield cannot fit: a same-axis neighbour already at its own floor. There the window keeps its minimum and overlaps that neighbour, cued once with the neighbour-minimum pill pair (the neighbour that binds marks itself), and the overflow lands inward — toward the screen’s centre rather than past its edge (a window wider than the whole region keeps its leading edge on screen). (Scrolling and Monocle left this row with #677: they consume the learned bound — the row re-packs, the slot centers. Interactive resizing left it with #933: a keyboard or mouse resize clamps at the learned floor, cued with a bounce and a pill. Track’s automatic count left it with #1355: the count fits as many tracks as their apps’ learned minimums allow and the track weights are re-shared at retile, while a fixed limit stays the user’s number.) The floor is still learned from the refused asks themselves — the corroboration the row below describes — 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 — so the only discovery would be probe-resize + read-back: slow synchronous IPC per window, visible jitter, and Electron/WebKit apps answer lazily, so even the probe misleads. The engine learns the bound from its own asks instead, which is where the latency comes from. BSP ratios are two per-space scalars (splitRatioH/splitRatioV), not per-node — the flat-array trade (#56) — so a heal for one window moves every same-orientation split, accepted since #934 because a resize press by hand moves the same number the same way, and unable to reach a deeper split at all. And where two floors cannot share the span, honouring them would flip the arrangement into an OverlapStack cascade (a pile) — a cued, inward overlap degrades better. yabai and AeroSpace share the identical limitation. | The frame-set itself is fire-and-forget — FrameApplier hands each assigned region to WindowControl.setFrame with no read-back — but since #677 the engine learns the effective bound from its own asks (the settled, echo-fed frame is the app’s answer), 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 renders it at the first refusal’s answer — rendering may be provisional; geometry may not). | Discovery requires asking: 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), so a single-observation confirm would learn wrong bounds. | 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 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 — the compliance echo proves the window truly held the size, so the revoke that follows 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 — owner-observed after #618 removed the main-actor freezes, on drags where fast-redraw apps (Ghostty) track perfectly. A second cause of this symptom was inside our own process until #1084 (2026-08-29): every applied frame emitted an AX notification whose window id was resolved by a blocking round-trip on the main thread, starving the frame clock that drives the overlay — measured at 42 stalls in ten seconds of held resize, worst 607 ms, against 1 at 134 ms after. What remains in this row is the app-side half only. | 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. A slow app’s window genuinely trails the pointer, and the ring faithfully hugs a trailing window — leading it would detach the ring from what is on screen, trading an honest lag for a visible lie. | 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, deliberately. | 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 — and since #1055 an un-forced retile answers asks beyond a corroborated bound from the ledger, so the lift is discovered only by an explicit apply (which deliberately probes past corroborated bounds) or a genuine resize of the window. | Nothing announces a constraint change — AX has no notification for it — and re-probing on a timer would reintroduce the visible dance the row above bounds. 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, because 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. |
Dragging a stack window’s height with the mouse snaps back; only keyboard/CLI resize("y") actually moves the vertical share. | Vertical weights are a windowless keyboard/CLI concept; the mouse-drag seam has no window to anchor a weight against. | Per-window vertical weights are session-scoped and keyboard-only by design (#67). | Use keyboard/CLI resize("y"); the mouse asymmetry is deliberate. See Stack resize is focus-aware. |
| 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, so a ring around it still points at a real thing; the bars and mark are space/layout-attached, and the overview has torn that layout apart, so hiding them is correct. jankyborders redraws its borders on the same settle handler for the same reason. 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 that reads worse than a steady ring. | The ring is a space-pinned SkyLight WindowServer window — a normal member of its space that the compositor re-composites into the overview — whereas the AppKit bars/mark carry .transient (hidden by Exposé at the compositor level). A raw WindowServer window cannot take .transient, so the two overlays hide by different mechanisms. | 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 Bar’s icon styles offer System default and Glyphs — never the system’s Dark, Clear, or Tinted icon looks as distinct in-app choices. | A synthesized tinted mode was built and stripped (2026-07-17): a luminance ramp over the flattened bitmap can’t match Apple’s plate-plus-glyph regeneration, and the system-wide Icon & widget style already tints what “System default” shows. Shipping a knock-off would misrepresent the real styles. | 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 (rather than 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 (the common case) rather than continuously live-bound; a reload that rewrites the active mode’s own bindings while the panel happens to be open is rare, and the “reopen to refresh” contract already governs the panel. 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 genuinely different bindings and behave that way; only their drawn glyph collides. macOS offers no keypad-distinguishing glyph to borrow, and minting an English Num prefix inside Core would be neither localizable nor Apple-native (#1074). Keypad Clear prints nothing, so it takes a fixed ⌧ rather than falling through to its key name. | 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 a deliberate pre-release tradeoff (one detector shared by ownership and adoption); these shapes are unusual in hand-written configs. The design fails safe: any structural anomaly, or a foreign token the final hasForeignCode re-check still sees, reverts to commenting everything, so ownership is never broken — worst case is the old whole-file behavior. 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 instead of fitting; the first BSP split and the Stack split make room for it since #934. | The Settings window tiles like any other since #678 item 18, and the layout’s only floor is the profile-wide min_window_size; a per-window floor would put a second, invisible size authority inside every layout calculation, for one window. 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 the honest answer is the same slight overlap every stubborn app gets, not a special case in the engine. | SettingsWidthClass.minimum is enforced by AppKit on the window; since #677 the engine learns that refusal as the window’s effective bound — 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 slot geometry never consumes it and 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. | It is the honest answer to “more tracks than can hold the minimum side by side”: the fitting tracks stay tiled (the layout keeps its identity), 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). One collector reads better than scattering each surplus track into its own buried slot. | 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; re-applying it resets runtime topology, exactly as it re-centers a scrolling viewport. A same-session wake/unlock restore is different — it is involuntary, so it 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 by design. | 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, and the zone’s own overflow vocabulary — never the whole-space collapse the track heal exists to prevent; 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 — its slices have no marker identity, so exchanging them would re-derive a different composition after the swap (windows leaking between visible tracks). Rewriting markers to pin the view would destroy the grandfathered partition instead. | 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. Any offset — even a nudge — makes the float directionally reachable again, and it always stays one click or focus cycle away. | 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). Glow is an opt-in flourish; a correct bloom beats the exotic ordering path. | 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 deliberately floored at 2×2 (#197 spec): one placement rule for every window count reads predictably, and a quit-time special case would be the only layout math that branches on N == 1. | 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, not a visual preference (#281): it interacts with minimum window size, cascade reachability, and display geometry, and no live manager remains after quit to correct an unreachable pile. 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 possible window; both overlay hexes clear 3:1 on near-white and near-black (the common cases) — 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 without giving that 3:1 up. The 5px stroke lets shape aid legibility even where hue contrast is weak. Green is the deliberate brand-identity choice (the ring reads as “kiwi”) accepting this occasional collision with content-green. | The overlay color is a fixed value with no per-window content sampling — dynamic contrast would need live sampling of the covered pixels, deliberately 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. | #129’s general form closed with #1056 — a press mid-animation accumulates against the in-flight animation’s target instead of the lagging echo, which was the reported, visible case. The held case, which #1082’s per-frame glide made far sharper (most frames re-based on the same stale echo, so the window crawled at echo rate instead of the ramp’s — measured at 29% of the asked travel), is no longer accepted: #1090 gave the path a commanded record of its own, so a glide frame accumulates in every configuration. What remains is the per-press residue, and it is the price of that record’s bound. A commanded frame stored here must die on a schedule nothing can extend — the animation target dies at settle, and #1090’s record is readable only by a glide step and retired at the start of the next press — because 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 instead: the loss is at most one echo’s lag per press, and a press issued from rest is always exact. | 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 an edge against the boundary is pinned so the whole delta goes to the other side (#1091). A window with less room on one side than half a step gets that side’s share capped and the remainder spilled to the other, which pins it; the following shrink then comes entirely off the free side and lands short of the origin. Every steady state round-trips exactly — middle of the screen, against one edge, wall to wall — so this is confined to the transition into contact. The obvious fix is worse than the defect: remembering which way the last grow went is state that must be invalidated by every move, mode change and display change, and it buys back 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 — an earlier single-fixture clause asserted the ceiling with a literal that could not fail). | 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 remain now that slot math is display-resolved, 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 — accepted deliberately: the delivery rides the stash-restore retry loop, and animating it would either restart per retile (jerky) or give up the retry (silent stranding on a dropped frame-set). | Resize / mouse-resize / float-nudge / border math resolve the space’s own display, and 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). The spring drop is a pointer-owned placement — the user chose that spot mid-drag, and yanking the window across monitors at release would fight the gesture. A dead display leaves no source bounds to translate from, so the OS-relocated frame is the best remaining truth. | TilingEngine.screen(for:in:) / screen(containing:) resolve displays; KiwiCore.reanchorFloat seeds the translated frame as the stash original and restoreStashed delivers it. The spring path (springSwitchSpace) performs no re-anchor by design, 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. In the narrow case where Finder’s only windows are off screen, KiwiDesk deliberately does nothing instead — so the moved window’s app can briefly keep key focus (stray keystrokes are swallowed until the next click), rather than risk a Desktop switch. | 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). Leaving no key window matches stock macOS after its last window closes — mild and recoverable — whereas a Desktop teleport contradicts the move the user just made (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 by design: 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. Narrowing to exactly that signature means any other click, cmd-tab, or command is never fought; the residue is single-shot and recoverable with one more cmd-tab. | 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 — following that would teleport the user to the space (or display) the move explicitly chose not to visit. Suppressing follows for the moved window for ~1 s (lazy Electron/WebKit re-reports trail by several hundred ms) blocks the teleport deterministically; the residue is single-shot and recoverable with one more click. | 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), which is what makes vanished honest; a window that dies while away sends nothing KiwiDesk can hear, so the corrective closed waits for the ledger’s next census, and a census on a timer is bounded rather than live by design. | 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 pre-#1146 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. | A floating window has no tile slot: there is no home slot for a ghost to preview and no swap a drop could perform, so a highlight would promise an action that cannot happen. A once-planned opt-in toggle (drag.ghost.show_for_floating) was rejected as a no-op for the same reason (#161); earlier 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. Because AX couples a raise with app activation, the raised float’s app briefly activates and steals focus, so 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 it cannot separate “raise” from “activate” or make the resulting activation synchronous. KiwiDesk removes every self-inflicted race around it — a generation-guarded focus handoff so a stale sequence can’t steal focus back, a time-bounded echo ledger so a raise’s own focus echo doesn’t move the ring onto the float — a click that actually 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 — and a sequence that reads the WindowServer’s real stacking between raises instead of trusting the AX call, which returns when the app has accepted the raise and not when it has performed it (#684) — leaving only what no caller controls: each app performs the raise, and the activation it is coupled to, on its own schedule, so a raise that lands after the sequence has stopped waiting settles wherever it lands. The fully-correct alternative — pinning the float’s window-server level so the compositor keeps it above regardless of focus — can only be written for windows your own connection owns; doing it for foreign windows needs code injected into Dock.app via a scripting addition (SIP disabled), which KiwiDesk will not require (§5, and the SIP-blocked list below). An own-connection SLSSetWindowLevel fast path was built and removed once measurement confirmed it can only level KiwiDesk’s own windows, never the foreign floats the feature targets. | The raise + focus handoff is raiseFloatsAbove / raiseFloatsAndSticky (generation-guarded); 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). ZOrderDrain 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), and why a missed landing is left to the next restore instead of retried. | 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, yet macOS still raises the window above its mark — so the WindowServer .reorder notification is the only signal for that gesture, and there is no public-API substitute (AX cannot observe a same-window re-raise). With the WS stream live this is fixed; without it the mark degrades to the pre-fix behavior on that one gesture only. | 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; an ignored window is deliberately outside management entirely — no tracking, no events, no frame assertion — so there is no seam at which to correct it. Clamping it would mean tracking it, defeating the point of ignoring. 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 since QA 2026-07-19) runs over tracked floats only (#242). KiwiDesk’s Settings window is tiled like any other window since #678 item 18, so the clamp is moot for it — a tiled window’s 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. That is where it is meant to be — but it is not a free choice: it is the one window whose position the grid has to work around. 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. | Nothing KiwiDesk can call moves it. 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 the alternative that would work, activating that app, is the one thing a teardown must not do: it would hand the user’s last app away at the moment KiwiDesk exits. So the grid stops fighting the constraint and places that window where being in front is correct, instead of leaving it to cover the pile-mates the circle wanted above it (#688). | The restack is the last thing that runs before management stops, so unlike every other z-order restore there is no later pass to diff against reality and correct it. 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, since no raise could move it anyway. The measurement is on raiseFloor (#684); 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. |
| A floating window taller (or wider) than the free span between bars on opposite edges settles clear of one bar and stays under the other — deterministically, no wobble. | The clamp is a position-only nudge by design; a window that doesn’t fit between two opposing strips cannot satisfy both, and silently resizing the user’s float would be a larger intrusion than the residual overlap. | Each edge’s clamp is a monotonic position push (AppBarGeometry.clampClear); the fold over all strips is idempotent, so the frame reaches a fixed point clear of the later-folded edge (floatFrameClampedClearOfBars). | Shrink the float or move one bar to a different edge; tiled windows never hit this (bars reserve their edge in layout). |
| 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 on-screen #21 distrust, extended across the dismiss transition (#244). 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 grace exists because the disarm used to read arrival order as ground truth and lost a race: live capture measured the stale re-report landing 125–200 ms after the user’s click on another window, which had already cleared the flag, so the panel’s app stole the click’s focus. Suppressing a stray follow beats hijacking the user to another space, and an eaten cmd-tab is single-shot and recoverable. The full argument is 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): an accessory app’s permanent NSStatusItem window sits at the status level and a wallpaper utility’s backdrop below normal — 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. And 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 rather than with your monitor. | They 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 re-introduce exactly the live-apply coupling #123 rejects. Making the count an input (#678 turn 10) buys the part that needed no live state — the count is what makes Cascade overflow and Cascade all diverge, and what opens a track past the limit — without reaching for the part that does. The display quantities are the same trade one level down: 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 (#708; the ruling is in design decisions). Monocle’s diagram shows its focus-cycle navigation model, not tiling geometry (it has none). | 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 already the one click target, and it already reveals the full contents (switch to it). A per-glyph or per-badge disclosure would make part of the row clickable, reopening 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); deferring exactly the stationary reveals keeps them reading as a real scroll, while a moving target — its motion is the show — and the close handoff lay the window on top at once. Echo provenance (#152) tells KiwiDesk’s own raise echoes apart from user focus — tracking every raise whose echo is still in flight — and lazy apps report a raise’s focus twice, the duplicate landing after the user’s next fast step, so the echo classification holds for a full second rather than ending at the first report (#887); inside that second, a clickless refocus of the window just left has the same shape as the duplicate, and the window focus has already moved to wins the tie — the ruling is in 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 right after you left it. 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 since #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: the GUI raises it through the focus command, so its report arrives intended (#1281). Two more reports are honored rather than bounced since #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. Honoring it panned the row back, warped the pointer onto a window nobody chose, flew the user back to the Space they had just left, and left KiwiDesk’s focus anchor on a window the OS did not front, which is the state in which every focus shortcut is refused by the foreground preflight until the app reports again. Where the window landed discriminates nothing — the emulator complied within 9 pt and bounced regardless — so the trade is priced once: 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 rather than bounced. | Raising a window the compositor is not drawing makes macOS switch to the Desktop that holds it, so a raise across Desktops is a Desktop switch nobody asked for — and 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. The window macOS restores on a return has exactly a bounce’s shape (clickless, just placed), 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, which reads that list, sees nothing gone. The alternative is a Finder-specific census read inside the sweep, and the sweep is deliberately census-blind during the switch grace (#1157): the census double-exposes both Desktops while the compositor settles, and a removal it caused could never be corrected. 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 rather than 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. | The slide is the reveal: with both edges walled the jump target sits pinned at its wall, fully covered by the free window, which slides off it — raising the target early would pop it up through its still-moving cover, the #143 artifact. And the restack for jumped-over windows must consume settled frames: armed before the retile, it restacks against pre-pan geometry (#153). A cover-fraction threshold was considered and rejected — it trades the principled rule for a cliff, and this case is 100 % covered, so no threshold would change it. | 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); standing up the sparse behavior-override seam (KeyLayerOverride-style) for one bool is exactly 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; a hand-rolled attributed-title fake would fight the system menu rendering for a cosmetic hint (#123). One #available(macOS 14.4, *) gate in addModeRows serves every list the menu builds, so the per-screen rows inherit the same limit rather than adding one. | The subtitle rides a system menu affordance that arrived mid-major-release, and since #1179 it is the app’s ONLY statement that a layout is temporary — Settings deliberately narrates the profile rather than the session, so there is no second readout to fall back on. What still answers on 14.0–14.3 is the Keep row’s own enablement: it arms whenever any screen is running a temporary layout, so the row being clickable is the signal, even where the per-entry subtitle cannot draw. | 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, moving the pointer to the top of the screen does not reveal the menu bar while a KiwiDesk window is the active one. The Settings window is where you meet it, being the one KiwiDesk window you keep open and work in; click any other app’s window and the reveal returns immediately. This is #329’s symptom by a second route — that report was a .regular KiwiDesk with no NSApp.mainMenu installed and was fixed by installing one, and the permanent accessory policy that landed afterwards removed the promotion instead, so there is now no KiwiDesk menu bar to reveal. (Owner-observed on macOS 26.6.2, 2026-08-24.) | Only a .regular app owns a menu bar, so the only fix is promoting KiwiDesk while a content window is up — which is exactly 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. A reliable focus model for every session is not worth trading for a strip that hides itself, and no presentation option can show a menu bar the process does not own. | main.swift sets .accessory once at launch and nothing else may change it — ActivationPolicySeamTests’ allowed map is the one copy of who may, and its two entries are that launch and the SingleInstanceGuard alert, which exit(1)s before any window can outlive the promotion. MainMenu is still built, but only so AppKit can route key equivalents to the key window (the Settings text fields’ Cut/Copy/Paste/Undo); under .accessory it is never displayed. 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, 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 same index Finder/Dock/Spotlight read, so it localizes from KiwiDesk’s English-only process where a bundle-local lookup can’t. When Spotlight has no entry there is no public API that 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 directly 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. Disk apps are already frozen for process life; only the running-app union is time-sensitive, and a settings picker rarely needs an app launched mid-edit. | 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 because it is the only stable, locale- and rename-proof identity — the display name is presentation, not identity. A name-based fallback for id-less apps would reintroduce exactly the ambiguity (two apps sharing a display name, locale drift) that keying on the id removes, and split the vocabulary into two identity schemes. | 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 (coupling would leak a visual setting into pure layout math). Its configured width is the full 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, while square reaches deeper under the corner reveal). The failure mode is a soft visual merge, never window-content clipping. An automatic minimum-gap floor was rejected: silently widening a user’s gap = 0 the moment borders turn on is paternalistic and changes a value the user set; 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 rather than 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 — and, reading each window’s real corner radius, it hugs the corner cleanly. The above-order alternative (#357) is crisper and shadowless but flickers on those apps; the drop-shadow reads as subtle depth and was preferred to a browser-specific special case. | 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. Failing closed until the OS foreground genuinely matches the managed focused window is the safe answer: a transient rejected shortcut during activation is far less harmful than a hidden mutation. 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 — the deliberately preserved limit from the Ghostty panel work. | 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 rather than 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; splitting a tab into its own tile would need cross-process NSWindow reparenting KiwiDesk cannot perform. Managing the group as one slot that re-keys to the active tab is the honest model — one tile, one App Bar item, and no spurious tile or focus jump on switch/close. Expanding tabs as App Bar sub-items was rejected: the app’s own tab bar already does that, and it 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 deliberately 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 need the vanish and appear in one pass and self-heal 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. Keeping special state windows as individual item slots ensures their corner state badges (pin/layer) remain explicitly identifiable. | 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. Resolving resize against the local focus keeps the write 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 the messiest sub-feature of #414 v2 and was cut as a non-goal; Space.swap/move membership guards make the no-op safe rather than corrupting. Reorder it on its home space — the derived slot follows everywhere. | Injection is derived, never stored: only the home array is real, and every Space mutation guards on membership. | 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 by definition, so there is nothing to exempt the sticky into. 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, and the visiting traveler becomes the (implicit) head; its id carries no stored weight, and storing one under a non-member id would orphan (never pruned, recycled-id hazard #308) — so resize writes instead key the first local member, which applies 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 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), the mismatch surfaces only as an occasionally over- or under-cautious refusal at the folded-overflow boundary, and translating the gauge to the local array is the non-home-reorder non-goal’s territory. | The gauge and the mutation deliberately 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 the dominant use — 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 is what caps a wedged receiver at one stuck child per command instead of thousands (#467). It edges past “Lua is open” (§2.7) only for the rare case of deliberately running two identical commands at once, 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); there is no eleventh digit to bind, and inventing 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”). This is also why the budget itself stops growing at ten: we run out of keys before we run out of spaces. | 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 by design. | 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) and clearing the entry would be wrong: a later EOF reap of that child would then find no entry and leak its Lua callback ref. Not SIGTERM-ing a child after management has torn down (children are fire-and-forget, §5) outweighs reclaiming the corner (#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 only become addressable after macOS’s own swipe already played. Animating whichever fragment happens to be reachable would recreate exactly 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. The override editor is not a destination — it 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 also mirror layout_params.* labels the Layout Defaults cards already 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 by design — nothing for search or the #326 bridge to reveal — so “open this space’s editor for you” is a behavior the search path cannot express; 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. | Reachable only on purpose since #1071: Settings offers no crash supervision, so a user assembles this by running kiwidesk service start while the login item is also on — which service start says is happening, and tells them to run one. The two paths both remain wanted: SMAppService is the mainstream login item, and the service adds KeepAlive supervision that genuinely needs RunAtLoad to recover from a login-time crash, so the login item cannot subsume it — and notarization/App-Store reinforces the split, a sandbox being unable to write a LaunchAgent or spawn launchctl. Making launchd win the race would mean quitting and relaunching the app the user is standing in; deciding it the other way would mean the GUI reaching into the power layer. Neither is worth it for a state you have to assemble deliberately. | The #196 single-instance lock is what keeps two launch triggers to one process, and is load-bearing here rather than incidental. launchd can supervise only a process it spawned itself, so whichever launch loses leaves its job idle; 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 deliberately 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 a caller makes — that every window in the pass is spring-sized — and only a caller that knows exactly what it changed can make it. 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). Marking that path wholesale would trade a real visual defect for a cosmetic one; splitting a gap-only save out of it would fork the apply path for an animation detail. | 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 a reboot is where stale replays did real damage (random windows filed into old spaces); gating on boot time removes the dominant case 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. Polling or delaying the retile to dodge it 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, which is what makes a pin survive an unplug and re-plug at all — the identity has to be stable across sessions, and macOS’s CGDirectDisplayID is not (it is reassigned across reconnects and reboots). EDID serial numbers would disambiguate but are unreliable in exactly the population that needs them (zeros for virtual, AirPlay and projector displays, missing on real panels). Changing the identity is a config-shape change, not a picture change: every stored pin is keyed by that string. | 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 invalidates existing pins, and that is not a reason to delay — pre-release, no compat shims (§5), so the derivation changes, the changelog says so, and people re-pin. The open question is only which field is both stable and distinct, which needs a device test with two identical panels. |
© 2026 KiwiCanopy