Conversation
Introduce a new hook architecture in RxCodeCore (Hook, HookController, HookOutcome, HookPayloads) and an AppState-backed HookManager / AppStateHookController. Migrate user hooks, notifications, and response handling into dedicated event hooks (CI, MCP, permission, question, remote config, response, user-added). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Introduces a new, strongly-typed hook architecture in RxCodeCore and wires it into the macOS app via an AppState-backed controller/manager, migrating legacy hook + notification dispatch sites onto dedicated hook events.
Changes:
- Adds
RxCodeCorehook primitives (Hook,HookController,HookOutcome/HookAggregateResult,HookPayloads) to model lifecycle events with Codable/Sendable payloads. - Adds
HookManager+AppStateHookControllerand registers built-in hooks (user-added bash hooks + various notification hooks). - Replaces legacy hook/notification dispatch in
AppStatewith typed hook dispatches across session lifecycle, CI/MCP, permissions/plans, remote config, and project add/clone.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| RxCode/Services/Hooks/hooks/UserAddedHook.swift | Runs persisted user bash hook profiles and persists synthetic hook cards/output. |
| RxCode/Services/Hooks/hooks/ResponseNotificationHook.swift | Posts response-complete notifications via afterSessionEnd. |
| RxCode/Services/Hooks/hooks/RemoteConfigNotificationHook.swift | Posts banners for remote config changes. |
| RxCode/Services/Hooks/hooks/QuestionNotificationHook.swift | Posts banners when an AskUserQuestion tool call arrives. |
| RxCode/Services/Hooks/hooks/PermissionNotificationHook.swift | Posts banners when a permission approval is needed. |
| RxCode/Services/Hooks/hooks/MCPNotificationHook.swift | Posts banners for MCP disconnect events. |
| RxCode/Services/Hooks/hooks/CINotificationHook.swift | Posts banners when CI fails. |
| RxCode/Services/Hooks/HookManager.swift | Registers and sequentially dispatches typed events to enabled hooks. |
| RxCode/Services/Hooks/AppStateHookController.swift | Implements HookController backed by AppState capabilities. |
| RxCode/App/AppState+Stream.swift | Dispatches stop/permission/plan events through the new hook manager. |
| RxCode/App/AppState+Project.swift | Dispatches repository added/cloned events; adjusts add APIs to return Project?. |
| RxCode/App/AppState+MobileRemote.swift | Routes remote-config banners through the remote-config hook event. |
| RxCode/App/AppState+Lifecycle.swift | Routes permission/question notifications through hook events. |
| RxCode/App/AppState+Hooks.swift | Removes legacy runHooks implementation; keeps hook profile persistence utilities. |
| RxCode/App/AppState+CrossProject.swift | Routes session start/end hooks and response-complete notifications through hook dispatch. |
| RxCode/App/AppState+CIStatus.swift | Routes CI-failure notifications through hook dispatch. |
| RxCode/App/AppState+Agents.swift | Routes MCP disconnect notifications through hook dispatch. |
| RxCode/App/AppState.swift | Adds and initializes hookController/hookManager and registers built-in hooks. |
| Packages/Sources/RxCodeCore/Hooks/HookPayloads.swift | Defines event kinds and typed Codable/Sendable payload structs. |
| Packages/Sources/RxCodeCore/Hooks/HookOutcome.swift | Defines per-hook outcomes and folding into an aggregate result. |
| Packages/Sources/RxCodeCore/Hooks/HookController.swift | Defines the host capability surface exposed to hooks. |
| Packages/Sources/RxCodeCore/Hooks/Hook.swift | Defines the hook protocol and default no-op handlers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Posts the "response complete" banner when a turn finishes on its own. Fires | ||
| /// on `afterSessionEnd`, but only for a genuinely-completed, non-errored turn | ||
| /// (a cancelled turn or an errored one is suppressed). The AI summary + post are | ||
| /// done in a detached `Task` so dispatch isn't blocked on the network. |
Comment on lines
+1
to
+16
| import Foundation | ||
| import os | ||
| import RxCodeCore | ||
|
|
||
| /// Registry + dispatcher for `Hook`s. Owned by `AppState`. Dispatch runs each | ||
| /// enabled hook **sequentially in registration order** (not concurrently): | ||
| /// hook cards are inserted one-by-one and the persisted "last hook" status row | ||
| /// is overwritten per hook, so concurrent runs would race the message list and | ||
| /// that row. Each event has its own typed `dispatch*` method so payloads stay | ||
| /// strongly typed end-to-end. | ||
| @MainActor | ||
| final class HookManager { | ||
| private(set) var hooks: [any Hook] = [] | ||
| private let controller: any HookController | ||
| private let logger = Logger(subsystem: "com.claudework", category: "HookManager") | ||
|
|
Comment on lines
+1
to
+14
| import AppKit | ||
| import Foundation | ||
| import os | ||
| import RxCodeCore | ||
|
|
||
| /// Concrete `HookController` backed by `AppState`. This is the *only* type | ||
| /// allowed to reach into app internals on a hook's behalf; hooks themselves | ||
| /// stay decoupled from `AppState`. Holds a `weak` reference so the controller | ||
| /// never keeps the app alive (AppState → HookManager → controller → AppState). | ||
| @MainActor | ||
| final class AppStateHookController: HookController { | ||
| private weak var app: AppState? | ||
| private let logger = Logger(subsystem: "com.claudework", category: "HookController") | ||
|
|
Comment on lines
+133
to
+149
| public struct PermissionDecisionPayload: Codable, Sendable { | ||
| public let toolUseId: String | ||
| public let toolName: String | ||
| public let sessionId: String? | ||
| public let projectId: UUID? | ||
| /// Stable, serializable label for the decision (e.g. "allow", "deny", | ||
| /// "allowSessionTool", "allowAndSetMode", "denyWithReason"). | ||
| public let decision: String | ||
|
|
||
| public init(toolUseId: String, toolName: String, sessionId: String?, projectId: UUID?, decision: String) { | ||
| self.toolUseId = toolUseId | ||
| self.toolName = toolName | ||
| self.sessionId = sessionId | ||
| self.projectId = projectId | ||
| self.decision = decision | ||
| } | ||
| } |
Comment on lines
+526
to
+534
| // Fan the resolved decision out to hooks. Dispatching here (rather than at | ||
| // each UI button) means desktop and mobile responses both fire exactly once. | ||
| let payload = PermissionDecisionPayload( | ||
| toolUseId: request.id, | ||
| toolName: request.toolName, | ||
| sessionId: request.sessionId, | ||
| projectId: window.selectedProject?.id, | ||
| decision: Self.permissionDecisionLabel(decision) | ||
| ) |
| /// Posts the "response complete" banner when a turn finishes on its own. Fires | ||
| /// on `afterSessionEnd`, but only for a genuinely-completed, non-errored turn | ||
| /// (a cancelled turn or an errored one is suppressed). The AI summary + post are | ||
| /// done in a detached `Task` so dispatch isn't blocked on the network. |
Comment on lines
+1
to
+16
| import Foundation | ||
| import os | ||
| import RxCodeCore | ||
|
|
||
| /// Registry + dispatcher for `Hook`s. Owned by `AppState`. Dispatch runs each | ||
| /// enabled hook **sequentially in registration order** (not concurrently): | ||
| /// hook cards are inserted one-by-one and the persisted "last hook" status row | ||
| /// is overwritten per hook, so concurrent runs would race the message list and | ||
| /// that row. Each event has its own typed `dispatch*` method so payloads stay | ||
| /// strongly typed end-to-end. | ||
| @MainActor | ||
| final class HookManager { | ||
| private(set) var hooks: [any Hook] = [] | ||
| private let controller: any HookController | ||
| private let logger = Logger(subsystem: "com.claudework", category: "HookManager") | ||
|
|
Comment on lines
+1
to
+14
| import AppKit | ||
| import Foundation | ||
| import os | ||
| import RxCodeCore | ||
|
|
||
| /// Concrete `HookController` backed by `AppState`. This is the *only* type | ||
| /// allowed to reach into app internals on a hook's behalf; hooks themselves | ||
| /// stay decoupled from `AppState`. Holds a `weak` reference so the controller | ||
| /// never keeps the app alive (AppState → HookManager → controller → AppState). | ||
| @MainActor | ||
| final class AppStateHookController: HookController { | ||
| private weak var app: AppState? | ||
| private let logger = Logger(subsystem: "com.claudework", category: "HookController") | ||
|
|
Comment on lines
+133
to
+149
| public struct PermissionDecisionPayload: Codable, Sendable { | ||
| public let toolUseId: String | ||
| public let toolName: String | ||
| public let sessionId: String? | ||
| public let projectId: UUID? | ||
| /// Stable, serializable label for the decision (e.g. "allow", "deny", | ||
| /// "allowSessionTool", "allowAndSetMode", "denyWithReason"). | ||
| public let decision: String | ||
|
|
||
| public init(toolUseId: String, toolName: String, sessionId: String?, projectId: UUID?, decision: String) { | ||
| self.toolUseId = toolUseId | ||
| self.toolName = toolName | ||
| self.sessionId = sessionId | ||
| self.projectId = projectId | ||
| self.decision = decision | ||
| } | ||
| } |
Comment on lines
+526
to
+534
| // Fan the resolved decision out to hooks. Dispatching here (rather than at | ||
| // each UI button) means desktop and mobile responses both fire exactly once. | ||
| let payload = PermissionDecisionPayload( | ||
| toolUseId: request.id, | ||
| toolName: request.toolName, | ||
| sessionId: request.sessionId, | ||
| projectId: window.selectedProject?.id, | ||
| decision: Self.permissionDecisionLabel(decision) | ||
| ) |
Add HookChoice/secret-file domain types, continuation-based hook UI requests (picker + confirmation), SecretsAutoDownloadHook, and a progress overlay. Wire interactive UI and secrets methods into AppStateHookController and AppState, and surface hidden .env files with NSOpenPanel in AddSecretSheet. Co-Authored-By: Claude Opus 4.8 (1M context) <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
RxCodeCore(Hook,HookController,HookOutcome,HookPayloads)HookManagerandAppStateHookControllerto drive hooks from app stateNotes
AppState+Hooks.swiftin favor of the new system🤖 Generated with Claude Code