Skip to content

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.

BehaviorWhy it’s acceptedArchitectural rootEscape hatch / planned fix
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 only a hand-written config or a profile authored on 26 reaches this state.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 “Displays have separate Spaces” on, native Desktop→profile bindings cannot represent an independent Desktop choice on every connected display; KiwiDesk applies one global active profile.Basic tiling remains valid, single-display use is unaffected, and users may want to inspect or prepare bindings before changing the macOS option, so hiding or disabling the controls would overstate the limitation.Native-Space routing resolves one active Desktop number and one active profile for the whole display setup, not a per-display profile tuple (#8).Turn the option off in Desktop & Dock Settings, then log out and back in. Onboarding and the binding-section warning share one gate and fire only in the affected multi-display state, never on a single display. See Shared display Spaces are recommended, not required.
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.
Tiling honors only the configured min_window_size, never an app’s own enforced minimum: in BSP (and any split layout), shrinking a window below the minimum its app enforces (many browsers pin a minimum height — Zen, for example) leaves the window at that minimum while its assigned region keeps shrinking, so the growing neighbor slides underneath — a slight overlap.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. Even with true numbers, BSP ratios are two per-space scalars (splitRatioH/splitRatioV), not per-node — honoring one stubborn window would freeze or distort the shared ratio for every same-orientation split. And honoring large real minimums would flip currently-tiling arrangements into OverlapStack cascades (piles) on ordinary displays — a graceful slight-overlap degrades better. yabai and AeroSpace share the identical limitation.The frame-set is fire-and-forget: FrameApplier hands each assigned region to WindowControl.setFrame with no read-back, so an app silently clamping the AX frame-set is invisible to the engine. 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), or give the stubborn app a float_rules entry so it never competes for a split region.
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.
Mouse-resizing a window in the track layout snaps back on both axes; keyboard/CLI resize covers both knobs.Both track adjustments (the track’s weight, the in-track share) key off the dragged window’s identity — the same windowless mouse-resize seam as the stack height drag above.The mouse-resize translation (MouseResize.translate) is deliberately windowless; track weights are session-scoped resize state (#128).Keyboard/CLI resize (both axes) and move_to_track; revisit together with the stack height drag if mouse parity is asked for.
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 modes (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 modes, but not when a config reload or profile apply rebinds the currently-active mode without changing which mode is active — an open panel keeps its now-stale bindings until reopened.The panel is a per-open snapshot, closed on mode 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.onModeChange, 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.
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.
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 scrollOffset 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.
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, more marker-tracks than limit) 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.
Holding a key to resize a floating window under-accumulates while a slide/resize animation is still in flight.Each step re-bases on the last AX-reported frame; mid-animation the AX echo lags, so rapid repeats read stale geometry.Resize re-bases on live AX state, and AX echoes trail an in-flight animation.Let the frame settle, or press again once the animation completes (#129).
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 desktop by activating Finder. In the narrow case where Finder’s only windows live on non-visible spaces, 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 Space switch.macOS exposes no public “focus the empty desktop” API, and activating Finder while its windows sit only on another space 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 Space 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 a currently-visible space — gauged by AXHelper.normalWindowCount(onScreenOnly:) over the WindowServer (layer-0, desktop 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 desktop to focus it; or keep a Finder window on the space you’re on. A window remaining on the space is refocused instead of the desktop.
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 native desktop is off-screen is reported as reason: vanished, never as a corrective closed; and a real close landing within the ~1 s settle window after a desktop switch can also read vanished.The reason payload (#40) classifies visibility changes at emit time; once a desktop is off-screen, a close there is observationally identical to the vanish that already fired, and inside the settle window a close is indistinguishable from the switch burst. Both self-heal under the documented consumer pattern (events as dirty flags + re-query).macOS AX only reports the current desktop’s windows (the same observation limit behind the SIP-blocked items): KiwiDesk cannot see lifecycle on an off-screen desktop, and the burst is only separable from user closes by time.Consumers filtering vanished refresh on native_space_change — the sketchybar recipe pattern does this already.
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, the final stacking is decided by the WindowServer resolving those activations asynchronously — not by the order KiwiDesk issued the raises — so a float can occasionally settle a notch out of place or, briefly, not on top at all.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, and a time-bounded echo ledger so a raise’s own focus echo doesn’t move the ring onto the float (nor eat a later deliberate float click) — leaving only that irreducible activation asynchrony. 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).Re-raise on focus is the shipping behavior; the transient activation settles on the handoff. 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 a tracked float, while its panels and borders stay ignored by EventLoop.shouldIgnoreOwnWindow (#177).Move the ignored app’s window by hand, remove its rule, or don’t run a bar over it.
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 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 focus of that same app’s main window — before any other app is focused — is suppressed too.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 drop that flag on the next focus — it cannot tell the dismiss re-report from a genuine one. Suppressing a stray follow beats hijacking the user to another space.The flag (ignoredPanelActive) is set when the event loop filters the panel’s own focus report and consumed by the next managed-window focus of that app; a dismiss emitting no main-window report leaves it for the following focus.Focus any other window or app first (clears the flag), then the main window; or simply focus it a second time.
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.
The Layout Defaults schematics are fixed illustrative diagrams (a handful of tiles, capped with a “+N” chip), not a render of your actual window count or arrangement.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. 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.None needed — the preview is for judging values pre-Save; Save and observe the real windows (#125).
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 (MouseResize.bspSide), 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 scrolling focus steps backward (up/left) toward a window pinned behind the leading edge, keystrokes still reach the previously focused app until the pan settles (one animation length, 50–1000 ms). Forward (down/right) focus and the handoff after closing a window raise immediately, so only the backward slide has the delay. A genuine click on a window KiwiDesk just raised, before that raise’s focus echo lands and while focus has already moved to another window in the same scrolling space, is read as KiwiDesk’s own echo, so focus re-asserts to that other window.Raising a pinned-behind row first pops it over the whole screen before the slide starts (#143); deferring only that direction keeps the pinned row reading as a real scroll, while forward moves and closes lay the target 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 — but AppKit gives a click and a raise echo the same shape, so the window focus has already moved to wins the tie over a still-unechoed self-raise. Normal for scroll-style window managers.AppKit keyboard status only moves with the real AX raise, and the backward 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.
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 (KeyModeOverride-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 the quick menu’s checked layout shows no “not saved to profile” subtitle when the session layout drifts from the profile; 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).The drift hint rides a system menu affordance that arrived mid-major-release; the same drift is still visible in Settings (layout caption + footer lines).None needed — macOS 14.4+ shows it; older point releases read the drift in Settings.
While the Settings window is open, a set_mode issued from a hotkey, Lua, or the CLI does not update the layout-drift captions (Spaces caption, footer lines, Revert enablement) until the window is reopened or a quick-menu layout action fires.The captions render a snapshot refreshed on window show() and by the quick menu’s own actions; wiring the GUI into the core event bus for one cosmetic caption would add a GUI↔engine subscription seam nothing else needs (#123).Drift is a transient computed by direct comparison (never latched into isDirty/profileDirty), so nothing marks the view model stale on external commands; the quick menu always recomputes on open and is never stale.Reopen the Settings window (every show() reloads), or switch via the quick menu, which refreshes the captions; revisit if the GUI ever subscribes to core events for another feature.
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 the border (or run border.fit_gaps), or lower the width; the default 2 pt border is imperceptible at any typical gap.
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 native Spaces at the identical tiled frame (suppressed by a post-space-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 beginner ladder seeds five spaces per display (10 on two monitors, 15 on three, and 5 × N for more), but only the first ten get a default digit shortcut — spaces 11+ ship with no ⌃⌥/⌃⌥⇧/⌃⌥⌘ binding.The number row has exactly ten keys (19, 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”).DefaultKeybindings.numbered caps the per-space rows at ten and maps the tenth to ⌃⌥0; the ladder itself (StarterLadder) scales to any display count, so on 3+ monitors it 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 native macOS Space switch is never animated in either direction — windows simply appear in place once the desktop settles — while a virtual 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 native desktops never appear in kAXWindowsAttribute, so handleNativeSpaceChange re-tracks and retiles with animated: false — the same OS visibility boundary behind the SIP-adjacent native-Space entries (#25/#26, see Blocked by macOS (SIP)).Use virtual spaces where animated switching matters — macOS already animates its own native swipe; KiwiDesk will never ask you to disable SIP to change this.
Sidebar search does not reach the controls inside a per-Space Overrides… popover — searching a label that appears only there finds nothing (the same words on the Layout Defaults tabs are found normally).A search result navigates to a sidebar destination and then reveals a target inside it. A popover is opened from a row button, not from a destination, so there is nowhere for a result to send the user — a stronger bar than the Bars switch or a per-layout override drawer, both of which are indexed because they live inside a pane. The override rows also mirror layout_params.* labels the Layout Defaults tabs already carry, so the user’s word stays findable (#277).The index is a per-destination catalog and a reveal selects only destination-level surfaces (SettingsSurface: a layout mode tab, the Bars switch); “open this popover for you” is a behavior nothing else in the app has, every popover here being user-invoked.Search the same setting’s global home (Layout Defaults) and use the per-Space Overrides… popover to deviate from it; revisit if the per-control catalog ever gains a popover surface.
KiwiDesk has two independent auto-start paths that don’t know each other’s on/off: the Open at Login toggle (Settings ▸ General, SMAppService, shown in System Settings ▸ Login Items) and the advanced kiwidesk service LaunchAgent (RunAtLoad). Enable the service and the app opens at login even while the toggle reads off; the toggle reflects only the login item.The two solve different problems and both are wanted: SMAppService is the mainstream login item; the service adds KeepAlive crash-restart supervision, which genuinely needs RunAtLoad to recover from a login-time crash — so the login item cannot subsume it. Notarization/App-Store reinforces the split (a sandbox can’t write a LaunchAgent or spawn launchctl). Coupling the mainstream toggle to launchctl state would drag CLI-only complexity into the pane the GUI curates.The overlap is safe because of the #196 single-instance lock — main.swift acquires it before anything else, so two login-time launch triggers dedupe to one process. That lock is load-bearing here, not incidental (see the Open at login decision, #575).None planned — accepted. kiwidesk service status reports the login-item state and service start notes when both are on; the user guide and this row explain the overlap. Turn one off if you want a single path.
Open-or-Focus cycling (a repeat press of pull_or_spawn while one of the app’s windows is focused) walks only the windows KiwiDesk currently tracks — every KiwiDesk space on the current native desktop. The app’s windows on other native macOS desktops are not part of the ring while you are away, 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).AX only reports the current desktop’s windows — the same observation limit behind the native-Space rows above (#25): an away window is untracked, so a deterministic ring cannot contain it, and guessing at unseen windows would make the cycle’s order unpredictable, which is the property it exists for.cycleToNextWindow reads tracked state only (windows keyed by bundle id, canonical space order then slot order, #637); windows leave tracking when their desktop goes off-screen (reconcileAll) and re-enter it on return.Switch to the other desktop — its windows rejoin the cycle immediately — or use macOS’s own app window cycling / Mission Control for cross-desktop windows of one app.
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 resize shortcut (⌃⌥⌘ + arrows), which makes the same change with the smooth motion. 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.

© 2026 KiwiCanopy