Skip to content

feat: agent reconciliation banner + banner-click flash fix#53

Merged
hiskudin merged 2 commits into
chore/pre-public-readmefrom
feat/agent-reconciliation
May 20, 2026
Merged

feat: agent reconciliation banner + banner-click flash fix#53
hiskudin merged 2 commits into
chore/pre-public-readmefrom
feat/agent-reconciliation

Conversation

@hiskudin
Copy link
Copy Markdown
Collaborator

Stacked on top of #51 (Codex + Gemini auto-wire). Will auto-retarget to `main` once #51 merges.

Summary

1. Agent reconciliation banner (Tier 1 backlog #11 in spirit)

Today, three real cases silently miss hook setup:

  1. User installs a NEW agent (eg Codex) on their Mac after the wizard ran.
  2. A StackNudge release adds support for an agent the user already has installed; existing users auto-update + never see the wizard.
  3. User manually deletes a hook entry then forgets.

Adds a banner at the top of Settings listing agents detected on disk without our `notify.sh` hook:

Wire up Codex?
Detected on this Mac without StackNudge hooks. Set up to start getting banners.
[ Set up ] [ Not now ]

Multi-agent case collapses into one banner ("Wire up 2 agents?" + comma-separated list).

After Set up, the same slot flashes a green confirmation for ~3 s so the user gets feedback rather than the banner just disappearing:

✓ Codex is set up.
New banners will fire when the agent finishes a turn or waits for approval.

Not now dismisses to `~/.stack-nudge/dismissed-agents.json`. Dismissed agents reappear only if they leave + re-enter the unwired set — eg manual edit followed by deletion.

2. Fix: banner-click flash

Banner clicks were briefly showing then hiding the panel. Two AppKit delegate methods both fire on banner-click:

  • `applicationShouldHandleReopen` (added earlier for Dock-icon reopens) → our handler called `showPanel()`.
  • `userNotificationCenter(_:didReceive:)` → handler called `NSApp.hide(nil)`.

The reopen delegate turns out to be part of the banner-activation sequence, not just user reopens. Defer the reopen-driven `showPanel` by 200 ms and let `didReceive` veto via a `bannerActivationUntil` deadline. Real app-icon reopens see no veto (200 ms latency added — acceptable).

Test plan

  • Strip a hook from any agent's config (eg `~/.codex/hooks.json`); restart panel → banner appears in Settings.
  • Click Set up → green confirmation appears + fades after ~3 s + agent's hooks are re-written.
  • Click Not now → banner disappears + `~/.stack-nudge/dismissed-agents.json` records the dismissal + re-opening Settings doesn't re-show.
  • Delete the dismissal file, re-strip the hook, re-open Settings → banner returns (sticky dismissal cleared).
  • Click a notification banner → no panel flash; target editor/terminal focuses cleanly.
  • Click the menubar bell / `open -a StackNudge` → panel appears (after ~200 ms — confirm no regression).

🤖 Generated with Claude Code

hiskudin and others added 2 commits May 20, 2026 21:05
Three real-world cases all silently miss hook setup today:
  1. User installs a new agent (eg Codex) after the bootstrap wizard.
  2. A StackNudge release adds support for an agent the user already
     has installed; existing users auto-update + never see the wizard.
  3. User manually deletes our hook entry then forgets.

Add a reconciliation banner at the top of Settings that lists agents
detected on disk without a notify.sh hook, with "Set up" (wires every
unwired agent) and "Not now" (dismisses, persisted to
~/.stack-nudge/dismissed-agents.json) actions. Refreshes on app launch
and on every Settings open so the surface stays current without polling.

After Set up, the same slot flashes a green "✓ X is set up" confirmation
for ~3 s before clearing — so users get feedback that something happened
beyond the banner just vanishing.

Implementation:
  - Bootstrap.unwiredAgents() / isAgentWired() — read each agent's
    config file, match against the staleHookRegex (same one uninstall
    uses for stale-hook scrubbing).
  - Bootstrap.wireSingleAgent() — exposes the existing per-agent
    wireHooks for one-at-a-time wiring without rerunning install().
  - PanelNav.{refreshUnwiredAgents,wireSingleAgent,dismissUnwiredAgent}
    + recentlyWiredAgents transient state + dismissed-agents.json I/O.
  - Settings.swift unwiredAgentsRow + wiredConfirmationRow occupy the
    same slot above the existing update-available row. Not part of the
    keyboard-indexed nav at v1 (mouse-only).
  - PanelController calls refreshUnwiredAgents() in
    applicationDidFinishLaunching alongside loadFromConfig().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clicking a notification banner triggered visible flash: panel briefly
appeared then disappeared. Two AppKit delegate methods fire on a
banner click and they race —

  1. applicationShouldHandleReopen (added earlier for Dock-icon
     reopens) → our handler called showPanel() → panel appears.
  2. userNotificationCenter(_:didReceive:) → our handler called
     NSApp.hide(nil) → panel disappears.

The reopen delegate is part of the banner-activation sequence, not
just real user-initiated reopens.

Defer the reopen-driven showPanel by 200 ms and let didReceive
veto by bumping a `bannerActivationUntil` deadline. Real app-icon
reopens see no veto and the panel appears (200 ms latency vs
immediate — acceptable trade-off). Banner clicks suppress the show
cleanly: no flash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hiskudin hiskudin merged commit ae1f37b into chore/pre-public-readme May 20, 2026
4 checks passed
@hiskudin hiskudin deleted the feat/agent-reconciliation branch May 20, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant