Skip to content

fix: Codex + Gemini auto-wire, agent reconciliation banner, banner-flash fix#51

Merged
hiskudin merged 6 commits into
mainfrom
chore/pre-public-readme
May 20, 2026
Merged

fix: Codex + Gemini auto-wire, agent reconciliation banner, banner-flash fix#51
hiskudin merged 6 commits into
mainfrom
chore/pre-public-readme

Conversation

@hiskudin
Copy link
Copy Markdown
Collaborator

@hiskudin hiskudin commented May 20, 2026

Three connected pieces of agent-handling work landing together.

1. Codex + Gemini auto-wire

Bootstrap previously only auto-wired Claude Code and Cursor. Codex wasn't in the BootstrapAgent enum at all; Gemini was a deliberate no-op masquerading as supported. README ✅ (experimental) for both overstated reality.

Codex (docs): config at `~/.codex/hooks.json`, matcher-group JSON shape identical to Claude Code, same `Stop` + `PermissionRequest` event names, JSON payload on stdin. Implementation: add `.codex` to `BootstrapAgent`, route through the renamed `wireClaudeShapedHooks` generic writer.

Gemini CLI (docs): config at `~/.gemini/settings.json`, matcher-group shape, events renamed — `AfterAgent` (turn end) + `Notification` (with `notification_type=ToolPermission`). One critical difference: Gemini's `timeout` is in milliseconds (default 60000), not seconds. We multiply by 1000 in the Gemini case so the on-disk value is 30000; otherwise Gemini kills the hook after 30 ms before the shell forks. Caveat: Gemini's Notification event is observability-only — banner surfaces the prompt but Allow/Deny has to happen in the terminal.

2. Agent reconciliation banner

Three real-world cases silently miss hook setup today:

  1. User installs a new agent (Codex, Aider, etc.) 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 a hook entry then forgets.

Adds a banner at the top of Settings listing detected-but-unwired agents:

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

After Set up, the same slot flashes a green confirmation for 3 s so users get feedback. Not now persists to `/.stack-nudge/dismissed-agents.json`; dismissed agents reappear only if they leave + re-enter the unwired set (manual edit, then deletion).

Reconciliation runs on app launch + every Settings open, so post-update and post-agent-install scenarios surface naturally without polling.

3. Banner-click flash fix

Clicking a notification banner flashed the panel briefly. Two AppKit delegates both fire on banner-click:

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

Defer the reopen-driven `showPanel` by 200 ms; let `didReceive` veto via a `bannerActivationUntil` deadline. Real app-icon reopens see no veto (200 ms latency added — acceptable).

image image

Other in this PR

  • README polish from the earlier branch life: badges row, StackOne attribution line, non-political Terms of use section, fixed install snippet, expanded license footer.
  • `ui_improvements.md` removed from repo (moved to workspace docs folder).
  • Wizard pre-selects all detected agents (the Gemini exclusion is gone now that it actually wires).
  • Uninstall scrubs the four new event keys (`AfterAgent`, `Notification`) alongside Claude's two.

Test plan

  • Codex stop event from real `codex exec` fires a banner (verified locally).
  • Gemini AfterAgent fires a banner after timeout-units fix (verified locally).
  • Strip Gemini hooks → banner appears → Set up → green flash → hooks re-written (verified).
  • Banner click no longer flashes panel (verified).
  • Fresh-install wizard wires all 4 detected agents on a Mac with `/.codex`, `/.gemini`, `/.claude`, `/.cursor` (verified by Hisku's machine state).
  • Uninstall removes all 4 event keys from all 4 configs cleanly.

🤖 Generated with Claude Code

hiskudin and others added 4 commits May 20, 2026 19:52
Codex and Gemini CLI both ship documented, stable hook systems that
StackNudge can wire into. Bootstrap previously only auto-wired Claude
Code and Cursor; Codex wasn't in the BootstrapAgent enum at all, and
Gemini was a deliberate no-op masquerading as supported. README claimed
✅ (experimental) for both, which overstated reality.

Codex (https://developers.openai.com/codex/hooks):
  - Config at ~/.codex/hooks.json
  - Matcher-group JSON shape, identical to Claude Code's
  - Stop + PermissionRequest event names, identical to Claude Code's
  - JSON payload on stdin, identical to Claude Code's
  Implementation: add `.codex` to BootstrapAgent and route through the
  existing matcher-group writer (renamed to wireClaudeShapedHooks).
  Pass `codex` as the agent label so notify.sh's `agent_label` switch
  shows "Codex" on banners (already handled).

Gemini CLI (https://geminicli.com/docs/hooks/):
  - Config at ~/.gemini/settings.json
  - Same matcher-group shape; events renamed: `AfterAgent` = turn end,
    `Notification` (with notification_type=ToolPermission on stdin) =
    waiting for tool approval.
  - JSON payload on stdin.
  Implementation: wire AfterAgent + Notification through the same
  generic writer.
  Caveat: Gemini's Notification is observability-only — our hook can
  surface the banner but can't return an allow/deny decision the way
  Claude's PermissionRequest can. Documented inline + in README.

Wizard pre-selects all four detected agents now that none are no-ops.
The Gemini-specific "info-only row" UI is replaced by the standard
checkbox path. `availableAgents()` already discovered Codex via the
new detectionDirectory; no change there.

Uninstall scrubs the four new event keys alongside the existing two,
so a Gemini install is cleanly torn down.

README:
  - Updated Supports table: all four ✅, with a footnote explaining
    the Gemini permission-event caveat.
  - Rewrote the Manual setup example to reflect that the four
    primary agents are now auto-wired, and showed a correct
    Claude-shaped block instead of the previous wrong "stop" example
    that wouldn't have worked for any of them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gemini's `timeout` field is documented in milliseconds; we were writing
30 (== 30 ms), which killed the hook before the shell even forked.
Hence no banners from `gemini -p ...` despite the file looking correct.

Multiply by 1000 in the .gemini case so the on-disk value is 30_000.
Claude Code and Codex stay in seconds — same writer, different unit
per agent. Inline note added so the next person to touch this doesn't
get burned the same way.

Source: https://geminicli.com/docs/hooks/reference — "Execution
timeout in milliseconds (default: 60000)".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 changed the title feat: auto-wire Codex and Gemini CLI hooks feat: Codex + Gemini auto-wire, agent reconciliation banner, banner-flash fix May 20, 2026
hiskudin and others added 2 commits May 20, 2026 21:18
The hero image is the abstract green mark on its own — visitors to the
GitHub page would see no "StackNudge" text until the maintained-by
footer. Add an h1 below the image so the name is the most prominent
thing on the page, and align the image's alt text with the displayed
brand. Shrink the mark to 200 px since the wordmark below now carries
the brand weight.

Backlog: replace the mark + h1 with a proper combined logo-with-
wordmark asset once a designer produces one. The bare mark stays
useful for square uses (favicon, menubar, App Store).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@StuBehan StuBehan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@hiskudin hiskudin changed the title feat: Codex + Gemini auto-wire, agent reconciliation banner, banner-flash fix fix: Codex + Gemini auto-wire, agent reconciliation banner, banner-flash fix May 20, 2026
@hiskudin hiskudin merged commit 362a340 into main May 20, 2026
4 checks passed
@hiskudin hiskudin deleted the chore/pre-public-readme branch May 20, 2026 20:31
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.

2 participants