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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
//! The runtime helpers a host's own status-line print subcommand composes with.
//!
//! agentgear no longer writes any harness's status-line slot: the automatic wiring
//! (the host declaration, the per-harness slot renderers, the stash-and-restore) is
//! retired. What remains is the render half — the host prints its own rows, then the
//! rows of whatever status line the user already had ([`user_original`] +
//! [`compose`]). A user who wants the host's line in their harness wires the print
//! subcommand into the harness config manually, and their own pre-existing line runs
//! natively beside it through that config — nothing is displaced, so nothing is
//! stashed anymore.
//!
//! The user's original row [`user_original`] returns comes from a legacy stamp
//! marker: only pre-retirement installs stashed one. Every marker written today
//! carries no stash, so the compose step contributes nothing on a fresh install and
//! the host's own rows render alone.
use ;
use ;
use ;
use mpsc;
use Duration;
use Value;
use crateResult;
use crate;
/// How long the user's own status command may run before its row is dropped. The
/// harness already bounds the whole status line and ours is now nested inside that,
/// so a command of theirs that hangs must not take the host's own rows down with it.
const USER_COMMAND_TIMEOUT: Duration = from_secs;
/// Ceiling on what a status command may print. The timeout bounds wall time, not
/// bytes: a command spraying stdout would otherwise have the reader allocating flat
/// out until it fires. Far above any real status line, so this is runaway protection
/// rather than a functional limit — output past it is simply cut.
const MAX_OUTPUT_BYTES: u64 = 1024 * 1024;
/// Presence-only marker that this process is already running inside a status-line
/// render. [`run_with_timeout`] both sets it on the child it spawns — so it reaches the
/// re-entered host binary through the shell — and refuses to spawn while it is already
/// set. Only presence is read; the value carries nothing beyond being non-empty.
const NESTED_RENDER_VAR: &str = "AGENTGEAR_STATUSLINE_NESTED";
/// One status-line command this crate (or a legacy stamp marker) knows about: the
/// shell command whose stdout is the rendered bar (line-oriented — each line is one
/// row), plus the harness's optional padding knob.
///
/// Deliberately open (no `#[non_exhaustive]`): a host constructs one directly, and
/// `..Default::default()` keeps working as fields are added.
/// The status line this machine's user had before this crate's retired automatic
/// wiring displaced it, or `None` when nothing was stashed (or nothing is
/// installed). Only markers written by pre-retirement binaries carry a stash.
///
/// Scope resolution is project-then-user: with `cwd` set, the marker for that
/// project scope is consulted first and the user-scope marker is the fallback, so a
/// project install shadows the user one exactly as the harness's own precedence
/// does. `client` is the backend id the host was invoked for — a host reads it
/// straight off its own `--client` argument.
///
/// A stashed command that would re-enter the host binary from inside itself is not
/// filtered here: this reader answers what the marker HOLDS, and a host may ask for
/// reasons that never spawn anything, so a depth guard here would make it lie. The
/// re-entry is bounded at the spawn boundary instead ([`NESTED_RENDER_VAR`] in
/// [`run_with_timeout`]).
///
/// Ceiling on the project lookup: `cwd` is matched against the project path the
/// install was scoped to, EXACTLY. A session started in a subdirectory of that root
/// keys a different marker and silently falls back to the user-scope stash, so a
/// project-scoped status line of the user's is dropped from the bar. The harness
/// itself walks up to find its project settings; this does not. Widening it means
/// walking `cwd`'s ancestors for a marker.
///
/// # Examples
///
/// ```
/// use agentgear::{Plugin, StatusLineDecl};
///
/// // `Plugin` only comes from `PluginHost::descriptor()`, so this reader is
/// // compile-checked against the live signature without building one.
/// fn original(plugin: &Plugin) -> agentgear::Result<Option<StatusLineDecl>> {
/// agentgear::statusline::user_original(plugin, "claude", None)
/// }
/// let _ = original as fn(&Plugin) -> agentgear::Result<Option<StatusLineDecl>>;
/// ```
/// Render the full status line: the host's own `rows` first, then the rows of
/// whatever status line the user already had (run with `session_json` on its
/// stdin, exactly as the harness would have run it). Concatenation is enough
/// because the surface is line-oriented — one output line is one row.
///
/// The project-or-user scope is taken from the session JSON's `cwd` (falling back
/// to `workspace.current_dir`), then resolved by [`user_original`].
///
/// A user command that cannot be spawned, prints nothing, outlives the internal
/// timeout, or is reached from inside another render contributes nothing: a broken or
/// hung command of theirs must not blank the host's own bar.
///
/// # Examples
///
/// ```
/// use agentgear::Plugin;
///
/// fn render(plugin: &Plugin, session_json: &str) -> agentgear::Result<String> {
/// agentgear::statusline::compose(plugin, "claude", session_json, "mytool main ok")
/// }
/// let _ = render as fn(&Plugin, &str) -> agentgear::Result<String>;
/// ```
/// The scopes [`user_original`] consults, in precedence order: a project scope
/// shadows the user one, matching the harness's own whole-value override.
/// The session's working directory: Claude Code sends a top-level `cwd`, with
/// `workspace.current_dir` as the same value under the workspace block. Anything
/// unparseable resolves to user scope.
/// Run the user's own status-line command through the platform shell with
/// `session_json` on stdin, returning its trailing-newline-trimmed stdout. `None` on
/// a spawn failure, empty output, or `timeout` elapsing (the child is killed and
/// reaped). The exit code is deliberately ignored — the harness itself renders
/// whatever a status command prints.
///
/// stdin and stdout are each drained on their own thread. Writing the whole payload
/// before reading deadlocks as soon as either side outgrows its pipe buffer, and the
/// calling thread has to stay free to enforce `timeout`.
///
/// Also the spawn-depth guard, both halves: [`NESTED_RENDER_VAR`] is read on entry and
/// set on the child, so a render already nested inside another one returns `None` without
/// starting a process. Keeping the pair here means a later caller of this primitive
/// inherits the guard instead of having to remember it.
///
/// The sentinel goes on the child rather than on this process because the whole subtree
/// needs to see it (the shell hands it down to whatever it runs, host binary included),
/// and setting a process-global would need `unsafe`, which this crate forbids.
///
/// Not read in [`user_original`]: that reader is public and answers what the marker
/// holds, which a host may want for reasons that never spawn anything.
///
/// An empty value reads as absent. We only ever write `"1"`, so a blank one cannot be
/// ours — it means something else exported the name — and treating it as present would
/// silently drop the user's row on every render with nothing to observe from outside.
/// Kill (harmless once it has already exited) and reap the child so no zombie
/// outlives a host process that renders a status line every turn.