• 0.0.7 5191a38f7a

    Fix the window tags: permission, drift, anchoring, and styling

    bdeshi released this 2026-08-05 13:08:11 +06:00 | 11 commits to main since this release

    Four separate defects, each reproduced before being fixed.

    Accessibility was silently denied. AXIsProcessTrusted() returned false,
    so every AX read failed, WindowOverlay.init? failed all twelve retries,
    and attach gave up without a word — indistinguishable from the feature
    being broken. The cause was outside this file: an ad-hoc-signed app has
    its permission pinned to its exact cdhash, which changes on every build,
    so the rebuild-and-replace workflow silently revoked the grant each time
    while the app stayed listed and ticked in System Settings. Now it prompts
    rather than failing mutely, and keeps rechecking — macOS never tells an
    app it has just been granted Accessibility, and overlays were otherwise
    only reconsidered when a Claude window launched, quit or activated, so a
    grant made while running did nothing visible until the user happened to
    touch a Claude window.

    The tag walked off its window on every drag. windowDidMove re-derived and
    persisted the corner offset for the app's own programmatic moves too, on
    the assumption that recomputing an offset it had just positioned against
    was a no-op. It isn't: AppKit posts that notification synchronously from
    inside setFrame, and during a live drag the AX frame read there is
    already newer than the one reposition used, so each event banked the few
    points the window had moved in between. Observed saturating at 952,
    which parks the tag at the far edge of the window or off-screen.

    Placement was anchored to the top-right unconditionally, so any resize
    moving that corner dragged the tag along. It now anchors to whichever
    corner it was dropped nearest. Offsets are no longer written back
    clamped either: a window too small to honour one shows the tag pushed in
    as far as it fits but keeps the stored distance, so widening the window
    restores the chosen spot.

    Styling: thinner (13pt) and pill-shaped so it can sit close to an edge
    without covering window controls; rotated when against a side edge,
    reading bottom-to-top on the left and top-to-bottom on the right so the
    text leans into the window; text colour chosen per profile colour by WCAG
    relative luminance rather than fixed white, which was as low as ~1.1:1 on
    a yellow chip against 4.50:1 worst-case now; and text drawn into a
    one-line-tall rect centred on the chip's midline, fixing its high
    seating. Settings gains a choice between the name chip and a plain
    coloured dot, which is never rotated, having no reading direction.

    Verified live throughout: a 400pt offset squeezed to 260 on a narrowed
    window and returned to 400 on restore with the stored value untouched;
    a drag committed the expected corner and orientation; and contrast was
    measured across the hue wheel.

    Tag placement is also stored per profile rather than once for the whole
    app. Two windows side by side are the case these tags exist for, and
    wanting each one's tag somewhere different is the normal outcome — one
    window's sidebar is not another's. Previously a single shared offset made
    them look independent (only the dragged tag moved at once) while every
    other tag snapped to it on its next reposition.

    A tag is a floating panel, which puts it above every ordinary window on
    the system rather than merely above the window it labels — so it hovered
    over the browser, the editor, and everything else, even when its own
    window was buried or on another Space. There is no cross-process way to
    attach one window above another (addChildWindow is same-process only, and
    the private ordering call window managers use is a one-shot that goes
    stale on the next reorder), so each tag now checks whether the window it
    labels is genuinely visible beneath it. The window server returns its list
    front-to-back, so one pass answers it: anything overlapping the tag before
    we reach our own window is covering it, and never reaching that window
    means it isn't on screen at all.

    Tag position is also clamped into whatever part of the window is on
    screen. A window dragged half off the edge takes its anchored corner with
    it, and a tag that follows it out of view identifies nothing. The clamp is
    presentational only — the stored anchor is untouched, so the tag returns
    to it once the window is fully back.

    Occlusion is judged from the window server's listing, which it returns
    front-to-back. Our own window is identified by its owning process rather
    than by matching rectangles: AX reports a new position the instant a
    window moves while the listing still holds the previous one, so a
    geometric match fails almost continuously mid-drag — measured at 87 of 92
    frames, during which the walk ran past our own window and mistook
    whatever else overlapped the tag for something covering it.

    Only ordinary windows count as occluders. Everything above that band is
    permanently in front and would veto the tag forever — including the
    invisible one-pixel markers some utilities park in a screen corner, which
    is what made a tag vanish at the bottom-left and nowhere else: it takes a
    window moved off two edges at once for the tag to clamp into that pixel.

    Downloads