1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
//! Cross-process hook activity emit.
//!
//! Why: Claude Code's hook commands (`UserPromptSubmit` → `prompt-context`,
//! `SessionStart` → `inbox-check`) run as ephemeral CLI subprocesses, not
//! inside the long-lived daemon. They cannot call `state.emit` directly
//! because they hold no `AppState`. Prior to this module they had no way
//! to populate the activity feed, which led directly to the user
//! complaint "the TUI activity feed is always empty in a normal Claude
//! Code session" — because in a normal session the only daemon traffic
//! is hooks, and hooks emitted nothing.
//!
//! What: this module exposes [`post_hook_event`] — a best-effort async
//! helper that resolves the running daemon's HTTP address via
//! `trusty_common::read_daemon_addr` and POSTs the hook payload to
//! `POST /api/v1/activity/hook`. Failures are swallowed (warn-logged to
//! stderr) so the hook never fails because of a missing or unresponsive
//! daemon — that contract matches the prompt-context handler's "always
//! exit 0" rule. The receiving daemon side lives in `web.rs` and
//! forwards the payload to `state.emit(DaemonEvent::HookFired { … })`.
//!
//! Test: `post_hook_event_no_daemon_is_noop` (the no-daemon branch);
//! the live-daemon round trip is covered in the prompt-context /
//! inbox-check integration tests.
use crate::;
use Duration;
/// HTTP path for the hook ingestion endpoint.
///
/// Why: kept as a constant so tests can target it without copy-pasting
/// the string. Mounted under `/api/v1/activity/hook` so it sits next to
/// the existing `GET /api/v1/activity` history endpoint (#96).
pub const HOOK_EVENT_PATH: &str = "/api/v1/activity/hook";
/// Connect + total timeout for the hook emit POST.
///
/// Why: hooks run in front of every user prompt; the budget here must be
/// tighter than the prompt-context fetch budget so a slow daemon never
/// adds noticeable latency to the user's typing flow. 1.5 s is enough
/// for a healthy local daemon plus a wide margin and tight enough that
/// a hung daemon doesn't block Claude Code by more than a moment.
const HOOK_EMIT_TIMEOUT: Duration = from_millis;
/// JSON payload posted to `POST /api/v1/activity/hook`.
///
/// Why: deliberately separate from `DaemonEvent` itself so we can evolve
/// the wire format (add fields, rename) without breaking the SSE consumer
/// schema. The daemon-side handler maps this into the canonical
/// `DaemonEvent::HookFired` variant. Forwards-compatible: serde
/// `#[serde(default)]` on every optional field means a future client can
/// add fields without breaking older daemons.
/// What: serde-encoded as snake_case JSON.
/// Test: round-trip exercised by `post_hook_event_no_daemon_is_noop` (the
/// payload encode is the only thing that runs).
/// Post a hook event to the running daemon, best-effort.
///
/// Why: the contract for every hook handler is "never block the user's
/// prompt because of a daemon problem". This function therefore swallows
/// every error path — no daemon address discovered, HTTP client build
/// error, POST send error, non-2xx response — and warn-logs the failure
/// to stderr so the hook command itself continues to print whatever
/// stdout the user expected.
///
/// What: resolves the daemon address via
/// `trusty_common::read_daemon_addr("trusty-memory")`, builds a short-
/// timeout `reqwest::Client`, POSTs the payload as JSON. Returns `()`
/// regardless of outcome.
///
/// Test: `post_hook_event_no_daemon_is_noop` confirms the no-daemon
/// branch is a no-op; the live-daemon path is exercised by
/// `hook_fired_activity_emit_smoke` in `commands::prompt_context`.
pub async