feat: codex hooks, listener soft-mute, single-pet refactor#7
Merged
Conversation
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.
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
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 petThe "one
PetInstanceperAITool" model was a v0.1 placeholder — only Claudeever rendered, and
SessionRegistry.activeToolshard-pinned the list. Thiscommit moves to a single global pet owned by
SessionRegistry; sessions fromevery tool aggregate into it.
registry.pets[tool]→registry.petPetInstance.toolremovedSessionRegistry.activeToolsremovedPetWindowController.showAll()/toggleAll()/locate(_:)→show()/toggle()/locate()SceneRouter.toggleAllPets()→togglePet()feat(hookkit,emit)— switch Codex from[notify]to~/.codex/hooks.jsonCodex CLI 0.129.0+ ships fine-grained lifecycle hooks via
~/.codex/hooks.json(features.codex_hooks = true), matching Claude's hookshape. This replaces the v0.1
[notify]-only completion path with the fullevent set so Codex sessions now drive
PetStatetransitions the same wayClaude does.
HookInstallermerges Hopet's entries intohooks.jsonwhile preservingthird-party registrations. On install it also strips any legacy
[notify]block from
config.toml, so a single stop event no longer double-firesthrough both channels.
hopet-emitgains a Codex-format transcript tailparser; the rest of its event plumbing is shared with the Claude path.
feat(core,app,panel)— listener soft-mute toggleToggling a listener in the Hooks tab no longer un/installs hook files. Hooks
are unconditionally installed at boot; the toggle now only flips an
EventRoutergate. When off:EventRoutersilently drops events from that tool. Sync requests(permission_ask / AskUserQuestion) reply
nilso Claude/Codex fall back totheir built-in UI rather than blocking.
SceneRoutersweeps the registry to remove idle bubbles for the muted tool.current decision; once pending clears, the next mutation re-sweeps and drops
them.
HopetConfig.Listenersgains anAIToolsubscript so call sites stopopen-coding the same
switchin three places.AITool.recognizedis thecanonical iteration target for hook install + toggle UI.
feat(theme,panel)— pixel press flash and drop slotPixelButtonStyle's press visual was a single frame in practice: any actionthat immediately rebuilt the view (sheet, modal, state change) ate
configuration.isPressedbefore SwiftUI could render it. The style is splitinto
PixelButtonBody, which latchesflashPressedfor 120 ms on thepress-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 dedicatedPixelDropSlotButtonStylethatreuses the same flash trick, plus a one-runloop delay before opening
NSOpenPanelso the press animation has time to render before the modalswallows mouse events. Also swaps stale
.accentColoruses in ThemesTab forthe explicit
PixelPalettetints used elsewhere.chore(docs)— drop unused statusbar expression assetsstatusbar-expression-omega.pngandstatusbar-expression-seal-mouth.pngwere 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. Callersshould use
registry.pet,PetWindowController.show()/toggle()/locate(),and
SceneRouter.togglePet().