Repository navigation
feat(companion): a floating Stacky that reacts to your agents - #233
Open
adefreitas wants to merge 12 commits into
Open
adefreitas wants to merge 12 commits into
adefreitas wants to merge 12 commits into
Conversation
A skin draws the companion as Core Animation layers, so the app does no per-frame work: the render server runs the animations. Art is baked PNG parts plus a manifest per skin under assets/skins, copied into the bundle by build.sh. BakedRig plays back each group's recorded keyframes, so a skin adds no motion code of its own. CompanionSkins is the registry, so further skins slot in beside Stacky. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
STACKNUDGE_COMPANION puts Stacky in a borderless window on the desktop. Click opens the panel beside it, drag moves it, and the spot is saved as its bottom-right corner so a size change never makes it jump. Without a display for the saved spot it is pulled to the nearest one. The pill widget gives way while it is on and comes back with its settings when it is off (widgetPreferred keeps the user's choice), and right-click shows the menu-bar menu at the pointer. Its pose follows what the app knows, highest first: muted, a quota past the alert threshold, a queued event or permission prompt, a busy agent, live sessions, then asleep after five quiet minutes. A permission prompt is a waiting pose like any queued event, not an alert, since it is the most routine thing the character sees. Animations pause while the window is covered, the display sleeps or the screen is locked. Settings gets a Companion category (on/off and Size). On/off acts on Space or Enter and Size picks, as every other row does since the key rules were unified. Turning the companion on or off flips compactMode and compactExpanded together, whose layout sinks run in willSet, so the second one laid out against a half-updated pair and shrank the panel to a pill. The layout is run once more after both have landed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The menu-bar menu, which the character's right-click shows at the pointer, gets a Companion submenu while the companion is on. It holds the size for now, saved the way the Settings row saves it. The menu object is kept by MenuBarController because a menu item does not retain its target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Existing installs predate the companion and keep the pill, so the post-update screen shows a card with a Turn on button, once. STACKNUDGE_COMPANION_OFFERED records that it was seen; leaving the screen any way counts, so the offer never returns. New installs start with the companion on from notify.conf.example and never see it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README gets a Companion section with the two settings and how the pose follows the app, the menu-bar table gets the submenu, and notify.conf.example turns the companion on for new installs, since install.sh copies it as the first config. CONTRIBUTING lists where the code and the art live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…osition The character now shows how many events wait on you: an app-icon style badge at its corner, grey while muted, switched off with Count badge (STACKNUDGE_COMPANION_BADGE). A new finished agent turn plays one small nod when the character is calm before and after (idle or asleep), notifications aren't muted, Reduce Motion is off and the session isn't muted; a burst earns one. The pose afterwards is still the waiting one: the wave means something wants you to act, and a finished turn does, so it still waves. Only live arrivals nod; events restored at launch never reach onAppend. Settings gets Preview pose, which forces any pose on the character without saving it and outranks every real signal, and Reset position, which forgets the saved spot. Preview pose picks and Reset position acts on Space or Enter, per the key rules. CompanionInputs is where a source of reactions publishes a value for CompanionController to read into CompanionSignals; it holds only the nod request for now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Companion submenu gets Reset position. Holding Option while the menu is open reveals a Debug submenu: Pose preview (any pose, or Off to show the real state) and Info (nod), which plays the nod once. While a pose is previewed the menu's first item says so and stops it on a click. An open menu runs its own tracking loop, which event monitors never see, so a 50 ms timer in the event-tracking run loop mode reads the Option key and shows or hides Debug live. It exists only while the menu is open. The previews are views only: nothing appends an event or writes a log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README and notify.conf.example cover the count badge, the nod, the waiting wave, Reset position, Preview pose and the Option-revealed Debug menu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tab and Shift+Tab move focus between Turn on and Got it while the companion card shows, and Return presses the focused button. Focus starts on Got it, so Return still dismisses. The focused button gets the accent ring Settings rows use for the selection, and the action bar lists Tab (Move) only while the card shows. Esc still hides the panel through the global handler. The key decision is the pure PanelController.postUpdateKeyAction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion or neither Appearance gets a Floating widget picker (Pill, Companion, Off) in place of the Widget toggle, and the Companion tab drops its on/off row. While the companion is off the tab shows a note and a Turn on companion row, and its settings are dimmed. The setting is STACKNUDGE_FLOATING_WIDGET. A config without it is migrated on load: STACKNUDGE_COMPANION=true gives companion, else STACKNUDGE_COMPACT_MODE (on unless false) gives pill, else off, and the result is written back. The post-update Turn on card goes through the same setter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live sessions drove the polling pose, so with any agent session open the character looped its scanning animation even though nothing was happening. Now only a busy session changes the pose (thinking); an idle open session leaves it idle and only keeps it from napping. Polling is left for a later signal that means something is actually being checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the companion: a small animated Stacky that floats on the desktop, takes the pill widget's place, and reacts to what stack-nudge knows (a waiting prompt, a busy agent, a finished turn, mute, quiet). It is on for new installs, offered once to existing installs after an update, and can be switched in Settings → Appearance → Floating widget. Builds on #227 (Settings… in the menu bar menu) and #228 (the widget no longer squeezes the panel into a pill).
Screen.Recording.2026-10-29.at.18.36.12.mov
Screen.Recording.2026-10-29.at.18.39.55.mov
Changes
panel/Companion/): a skin is Core Animation layers built from baked PNG parts and a manifest, so the render server runs the animation and the app does no per-frame work.CompanionSkinsis the registry, so more skins slot in beside Stacky. Only regular Stacky ships (assets/skins/stacky, 3.7 MB), copied into the bundle bybuild.sh.STACKNUDGE_COMPANION_BADGE.CompanionInputsis where a later source of reactions publishes a value thatCompanionControllerreads intoCompanionSignals; it holds only the nod request today, and nothing here depends on feeds, GitHub, calendar or AI.widgetPreferredremembers the user's choice).STACKNUDGE_FLOATING_WIDGETholds it; a config without it is migrated on load (STACKNUDGE_COMPANION=truegives companion, elseSTACKNUDGE_COMPACT_MODEon unless false gives pill, else off) and the result is written back. Both old keys are still read, never written. The post-update Turn on card sets it too.Space/⏎, Floating widget, Size and Preview pose are value rows that pick, arrows alone never change anything. All are inselectedRowIsPickerandSettingsRowTests.⌘,, the sameshowSettings), with a Companion submenu (Size, Reset position). Holding⌥while it is open reveals Debug: Pose preview and Info (nod). A 50 ms timer in the event-tracking run loop mode reads the key live, only while the menu is open.STACKNUDGE_COMPANION_OFFERED). It is keyboard-reachable:Tab/⇧Tabmove focus between Turn on and Got it (accent ring, the Settings selection style),⏎presses the focused one, and focus starts on Got it so⏎still dismisses. The decision is the purePanelController.postUpdateKeyAction; the Tab hint shows only while the card does.Escstill hides the panel, via the global handler. Following fix(panel): swallow a held key after recording a shortcut #226, pressing⏎on the card arms the global key router to swallow that key's repeats, so a held⏎does not carry on into the Events page (GlobalKeyRouter.swallowRepeats).compactModeandcompactExpandedtogether. fix(widget): turning the widget on no longer squeezes the panel into a pill #228 now defersapplyCompactLayoutin both sinks, andsetFloatingWidget(which drives them for Pill, Companion and Off) relies on that. The core adds no sink of its own, so each change lays out once.STACKNUDGE_COMPANION(on innotify.conf.example, so new installs start with it),_SIZE,_BADGE,_POS,_OFFERED. README, CONTRIBUTING andnotify.conf.exampleupdated. Nothing is read or sent over the network, so PRIVACY.md needs no change.Testing
With
SDKROOT=/Library/Developer/CommandLineTools/SDKs/MacOSX26.5.sdk:./build.sh: builds, only the existing ImplicitStrongCapture warnings.make test-without-xcode: 1510 tests, 0 failures.make typecheck-tests: OK.Manual (the window and animation can't run in CI; I did not run the app):
Space): it returns bottom right.⌥with the menu open: Debug appears and disappears with the key. Pose preview shows each pose; the first menu item says a pose is previewed and stops it. Info (nod) plays one nod.Spacethen→steps Pill, Companion, Off; Pill returns at full panel size (not squeezed) and the pill's rows are dimmed otherwise. In the Companion tab→on Size does nothing;Spacethen←/→steps it. With the companion off the tab shows the note and Turn on companion.STACKNUDGE_FLOATING_WIDGET,STACKNUDGE_COMPANIONandSTACKNUDGE_COMPANION_OFFEREDfrom the config, update from an older build: the post-update screen shows the card once.Tabrings Turn on,⇧Tabgoes back to Got it,⏎on Turn on turns the companion on and the card goes,⏎on Got it leaves. Hold⏎on Turn on: nothing happens on the next page. Without the card,Tabdoes nothing and no Tab hint shows.⌘,) opens Settings.STACKNUDGE_COMPANION=trueand noSTACKNUDGE_FLOATING_WIDGETin the config, launch: the key is written ascompanion.Related issues
None.
🤖 Generated with Claude Code