Skip to content

feat: codex hooks, listener soft-mute, single-pet refactor#7

Merged
BinaryFroggy merged 5 commits into
mainfrom
feat/codex-hooks-and-listener-mute
May 11, 2026
Merged

feat: codex hooks, listener soft-mute, single-pet refactor#7
BinaryFroggy merged 5 commits into
mainfrom
feat/codex-hooks-and-listener-mute

Conversation

@BinaryFroggy
Copy link
Copy Markdown
Owner

Summary

Five commits packaging the v0.2 work that was sitting uncommitted in the working tree.

refactor(pet)! — collapse per-tool pets into a single global pet

The "one PetInstance per AITool" model was a v0.1 placeholder — only Claude
ever rendered, and SessionRegistry.activeTools hard-pinned the list. This
commit moves to a single global pet owned by SessionRegistry; sessions from
every tool aggregate into it.

  • registry.pets[tool]registry.pet
  • PetInstance.tool removed
  • SessionRegistry.activeTools removed
  • PetWindowController.showAll()/toggleAll()/locate(_:)show()/toggle()/locate()
  • SceneRouter.toggleAllPets()togglePet()

feat(hookkit,emit) — switch Codex from [notify] to ~/.codex/hooks.json

Codex CLI 0.129.0+ ships fine-grained lifecycle hooks via
~/.codex/hooks.json (features.codex_hooks = true), matching Claude's hook
shape. This replaces the v0.1 [notify]-only completion path with the full
event set so Codex sessions now drive PetState transitions the same way
Claude does.

HookInstaller merges Hopet's entries into hooks.json while preserving
third-party registrations. On install it also strips any legacy [notify]
block from config.toml, so a single stop event no longer double-fires
through both channels. hopet-emit gains a Codex-format transcript tail
parser; the rest of its event plumbing is shared with the Claude path.

feat(core,app,panel) — listener soft-mute toggle

Toggling a listener in the Hooks tab no longer un/installs hook files. Hooks
are unconditionally installed at boot; the toggle now only flips an
EventRouter gate. When off:

  • EventRouter silently drops events from that tool. Sync requests
    (permission_ask / AskUserQuestion) reply nil so Claude/Codex fall back to
    their built-in UI rather than blocking.
  • SceneRouter sweeps the registry to remove idle bubbles for the muted tool.
  • Bubbles with pending permission/askUser are kept so the user can finish the
    current decision; once pending clears, the next mutation re-sweeps and drops
    them.

HopetConfig.Listeners gains an AITool subscript so call sites stop
open-coding the same switch in three places. AITool.recognized is the
canonical iteration target for hook install + toggle UI.

feat(theme,panel) — pixel press flash and drop slot

PixelButtonStyle's press visual was a single frame in practice: any action
that immediately rebuilt the view (sheet, modal, state change) ate
configuration.isPressed before SwiftUI could render it. The style is split
into PixelButtonBody, which latches flashPressed for 120 ms on the
press-down edge so the chrome actually flashes.

ThemesTab's per-state GIF drop slot was rolled by hand on top of
PixelChrome; it's replaced with a dedicated PixelDropSlotButtonStyle that
reuses the same flash trick, plus a one-runloop delay before opening
NSOpenPanel so the press animation has time to render before the modal
swallows mouse events. Also swaps stale .accentColor uses in ThemesTab for
the explicit PixelPalette tints used elsewhere.

chore(docs) — drop unused statusbar expression assets

statusbar-expression-omega.png and statusbar-expression-seal-mouth.png
were exploration screenshots for a menu bar expression slot that was never
built and is not on the current roadmap. devDocs no longer references either
file.

Breaking changes

The refactor(pet)! commit removes the per-tool pet API surface. Callers
should use registry.pet, PetWindowController.show()/toggle()/locate(),
and SceneRouter.togglePet().

The PetInstance per AITool model was a v0.1 placeholder: only Claude
ever rendered, and registry.activeTools hard-pinned the list. Move to
one global PetInstance owned by SessionRegistry; sessions from every
tool aggregate into it. Drops AITool from PetInstance, replaces
registry.pets[tool] with registry.pet, renames showAll/toggleAll on
the window controller and SceneRouter to show/togglePet.

BREAKING CHANGE: SessionRegistry.pets, SessionRegistry.activeTools,
PetInstance(tool:), PetWindowController.showAll()/toggleAll()/
locate(_:), and SceneRouter.toggleAllPets() are removed. Callers
should use registry.pet, PetWindowController.show()/toggle()/locate(),
and SceneRouter.togglePet().

See devDocs/architecture.md §3, devDocs/features.md §2.
Codex CLI 0.129.0+ ships fine-grained lifecycle hooks via
~/.codex/hooks.json (features.codex_hooks=true), matching Claude's
hook shape. Replace the v0.1 [notify]-only completion path with the
full event set so Codex sessions now drive PetState transitions the
same way Claude does.

HookInstaller merges Hopet's entries into hooks.json while preserving
third-party tool registrations, and on install strips any legacy
[notify] block from config.toml so a single stop event no longer
double-fires through both channels. hopet-emit gains a Codex-format
transcript tail parser; the rest of its event plumbing is shared.

See devDocs/hooks-and-priority.md.
Replace the v0.1 listener model (toggle = install/uninstall hooks)
with a soft-mute: hooks are now unconditionally installed at boot,
and the Hooks tab toggle only flips an EventRouter gate. When off,
EventRouter silently drops events from that tool (sync requests
reply nil so Claude/Codex fall back to their built-in UI) and
SceneRouter sweeps the registry to remove that tool's idle bubbles.
Bubbles with pending permission/askUser are kept so the user can
finish the current decision; once pending clears, the next mutation
re-sweeps and drops them.

HopetConfig.Listeners gains a subscript over AITool so call sites
stop open-coding the same switch in three places. `AITool.recognized`
becomes the canonical iteration target for hook install + toggle UI.

See devDocs/preferences.md §11.6.
PixelButtonStyle's press visual was a single frame in practice: any
action that immediately rebuilt the view (sheet, modal, state change)
ate configuration.isPressed before SwiftUI could render it. Split the
style into PixelButtonBody and latch flashPressed for 120ms on the
press-down edge so the chrome actually flashes.

ThemesTab's per-state GIF drop slot was rolled by hand on top of
PixelChrome; replace it with a dedicated PixelDropSlotButtonStyle that
reuses the same flash trick, plus a one-runloop delay before opening
NSOpenPanel so the press animation has time to render before the
modal swallows mouse events. Also swap stale .accentColor uses in
ThemesTab for the explicit PixelPalette tints used elsewhere.

See devDocs/preferences.md §11.4.
statusbar-expression-omega.png and statusbar-expression-seal-mouth.png
were spec exploration screenshots for a menu bar expression slot that
was never built and is not on the current roadmap. devDocs no longer
references either file.
@BinaryFroggy BinaryFroggy merged commit d185f8c into main May 11, 2026
1 check passed
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