main
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ffbd1536b6
|
Let the menu's click and shift-click actions be swapped
Clicking a profile switched to it exclusively and shift-click opened it
alongside, with no way round for anyone who opens alongside far more often
than they switch. Settings now carries the choice, storing only the plain
click's action — the other gesture gets whatever is left, so the two can't
both end up meaning the same thing, and no migration is needed for anyone
who never touches it.
Switching also quits the other profiles again when the profile clicked is
already up, which
|
||
|
|
e7777d5dfc |
Don't quit the neighbour when switching to an already-open profile
Clicking a profile quit every other running one first, unconditionally — including when the profile clicked was already open. The menu lists every running profile at once, so the click that reaches for the second one is almost always someone moving between two open windows, and it cost them a relaunch and whatever was on screen to carry out an instruction nobody gave. Switching now only quits anything when the target isn't up yet. An already-running profile is simply brought forward, which is what shift-click did and what the click on a running profile always looked like it would do. |
||
|
|
cf7d9a4583 |
Draw the real icon, and give the menu bar its own glyph
Replaces the placeholder coat silhouette with the app's actual subject: Claude's asterisk above a rack of overcoats, on Claude's own coral. The wardrobe is the app in one picture — several coats to pick between, one currently worn. The menu bar stops scaling the app icon down and draws its own glyph instead. At 18pt the icon's three coats, lapels and pockets collapse into a smudge, and its colours ignore the menu bar entirely. The glyph is a template image, so macOS tints it for light and dark and for the open-menu inversion, and re-tints it itself when the theme changes. That also drove its shape: six rays rather than eight, because at that size eight have under a pixel between arms and merge into a blob. The coloured dots beside it don't get that for free — ProfileColor picks a brightness for the current appearance at the moment it's asked, and the title is only rebuilt when profiles start or stop. Observing effectiveAppearance rebuilds it, so a theme switch doesn't leave dots mixed for the old appearance sitting in the menu bar. Everything is still drawn from vector paths rather than exported from a design tool, so each size can be tuned — small sizes need fatter strokes than a straight downscale gives — and nothing binary but the derived assets is committed. The README gains a hero banner, composed from the generated icon so it can't drift out of step with it. About gains the tagline under the icon, names itself in the description rather than opening with a bare verb, and drops the author line. |
||
|
|
5191a38f7a |
Fix the window tags: permission, drift, anchoring, and styling
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. |
||
|
|
1d3a539edd |
Add the window identification overlay
Every profile launches the same Claude Desktop binary with the same icon and title, so with two or more open there was no way to tell which window belonged to which profile at a glance. WindowOverlay pins a small tag — profile name, in that profile's own dot colour — to each running managed profile's window, tracked live via AXObserver notifications rather than polling. Only a persistent process can hold a watcher like that, which is exactly the gap the old one-shot CLI could never close. Shown for every running managed profile, not just when two or more are up: the tag is an identity marker, not merely a disambiguator. Never shown on "default", which isn't a shannoncoat profile at all. Positioned top-right, since top-left is where a window's close/minimize/ zoom controls live. Draggable in case that still obstructs something in a particular layout, on a .nonactivatingPanel so dragging it doesn't steal focus from the window underneath, with the offset persisted. Attachment retries for ~3s rather than requiring the target's first window to already exist: a cold Electron launch can take a second or more between the process starting — which is what triggers the update, via NSWorkspace's launch notification — and its first window existing. Losing that race previously meant the tag silently never appeared at all. Also ignores CHECKPOINT.md, a local session-handoff note. |
||
|
|
9673dc2792 |
Redesign the Manage window and fold About into it as a tab
The previous layout was a fixed 460x360 window regardless of tab or state, with a plain unbordered table and text +/- buttons — it read as a generic cross-platform port rather than something native. Several rows also had no real width anchor at all, so they floated: flush against the window's left edge with no margin, and in the add form's case wide enough to overflow past the right edge. - The window is no longer manually resizable. Each tab reports its own preferredContentSize, recomputed in viewDidLayout and after every state change, and NSTabViewController resizes the window to match. That is what actually fixes the large empty areas: a tab with less content gets a smaller window rather than sitting inside a fixed larger one. - Every row now anchors its width to the one element with a real absolute width, fixing both the edge-touching and the overflow. - Table gains a bezel border and alternating rows; form labels get a fixed trailing-aligned width so fields line up; the error label wraps instead of truncating; tab padding is consistent. - About moves out of a separate panel into a third tab, with the app icon centred and enlarged to 96px. Verified visually with a standalone harness that drives the real window and screenshots it, rather than simulating input — scripting clicks into the app itself needs an Accessibility grant this environment lacks. |
||
|
|
c6a7da54ca |
Show profile paths, report the build version, and colour each profile
- Selecting a row in the Profiles tab shows its Claude Desktop and Claude Code dirs inline and selectable, instead of requiring a right-click "Reveal in Finder" to find out where a profile actually lives. - build.sh stamps Contents/Resources/COMMIT with the short HEAD SHA, left empty when HEAD sits exactly on a tag (how a real release is built), since the version number alone is unambiguous there. About appends it when present, so two dev builds off the same VERSION are distinguishable. The update checker still compares plain semver. - Profile dot colours are picked per NSApp.effectiveAppearance at render time: same hue either way, but full saturation with brightness split per background (1.0 dark, 0.55 light), so they read as bold hues rather than washing out on white or muddying against a dark menu. |
||
|
|
135db9b308 |
Rewrite shannoncoat as a native Swift menu-bar app
Replaces the original shannoncoat.sh entirely with in-process Swift. The script stopped earning its keep once the rest went native, and being a persistent process rather than a one-shot CLI is what later makes live window tracking possible at all. - ProfileStore: JSON config at ~/.shannoncoat/<name>.json (was hand-rolled YAML), with ~ expansion, paths relative to the config dir, and name/dir-collision validation in one place. - ProcessInspector: sysctl(KERN_PROC_ALL/KERN_PROCARGS2) enumeration instead of shelling out to pgrep. - ClaudeControl: launch via Process, focus via direct Accessibility calls (unhide, un-minimize, poll-for-window, AXRaise) instead of AppleScript, and a quit that respects Claude's own termination handling rather than unconditionally SIGKILLing after a flat 5s timeout. - LiveState: NSWorkspace notification-driven state with no polling timer, so the menu bar reflects reality within about a second — including changes made outside the app entirely. - ManageWindow: one non-modal window replacing what would otherwise be a string of separate popup alerts, with inline add/remove and directory pickers. - LaunchAtLogin / UpdateChecker: SMAppService login-item toggle, and a minimal VERSION-file update check that alerts on a manual check and posts a quiet notification on the automatic one. Also drops the standalone CLI in favour of GUI-only, and adds a placeholder app icon drawn from vector shapes rather than composited on top of Claude's own icon assets. Claude Desktop is located at runtime rather than assumed: a path the user picked previously, then /Applications, then a per-user ~/Applications install, each accepted only if it actually contains the executable. If none match, launching a profile asks the user to locate it once and remembers the answer — a launch that silently does nothing gives them no way to work out what's wrong. |