@team-agent/installer 0.5.115 → 0.5.117
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/mod.rs +143 -12
- package/crates/team-agent/src/cli/shutdown_socket.rs +288 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +11 -4
- package/crates/team-agent/src/contract_runtime/backend.rs +88 -8
- package/crates/team-agent/src/contract_runtime/commands.rs +88 -7
- package/crates/team-agent/src/contract_runtime/discovery.rs +42 -10
- package/crates/team-agent/src/contract_runtime/mcp.rs +3 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +2 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +86 -9
- package/crates/team-agent/src/contract_runtime/workspace.rs +79 -11
- package/crates/team-agent/src/leader/lease.rs +1 -18
- package/crates/team-agent/src/lifecycle/pane_input_lock.rs +86 -8
- package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +101 -72
- package/crates/team-agent/src/lifecycle/restart/selection.rs +30 -8
- package/crates/team-agent/src/messaging/delivery.rs +132 -27
- package/crates/team-agent/src/messaging/enter_rescue.rs +334 -0
- package/crates/team-agent/src/messaging/enter_rescue_tests.rs +566 -0
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/mod.rs +1 -0
- package/crates/team-agent/src/messaging/scheduler.rs +8 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +2 -0
- package/crates/team-agent/src/messaging/types.rs +1 -0
- package/crates/team-agent/src/provider/session/capture.rs +24 -1
- package/crates/team-agent/src/state/persist.rs +72 -2
- package/crates/team-agent/src/state/projection.rs +29 -0
- package/crates/team-agent/src/state/repository/tests.rs +70 -0
- package/crates/team-agent/src/state/repository.rs +52 -1
- package/crates/team-agent/src/tmux_backend/native_input.rs +494 -0
- package/crates/team-agent/src/tmux_backend/native_input_tests.rs +781 -0
- package/crates/team-agent/src/tmux_backend.rs +49 -6
- package/crates/team-agent/src/transport/native_input.rs +594 -0
- package/crates/team-agent/src/transport.rs +19 -0
- package/package.json +4 -4
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
//! Owned legacy-tmux composer evidence. No keys, persistence, or provider admission.
|
|
2
|
+
|
|
3
|
+
use std::cell::RefCell;
|
|
4
|
+
|
|
5
|
+
use serde::{Deserialize, Serialize};
|
|
6
|
+
|
|
7
|
+
use crate::model::enums::Provider;
|
|
8
|
+
|
|
9
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
10
|
+
pub struct NativeInputRequest {
|
|
11
|
+
pub message_id: String,
|
|
12
|
+
pub owner_team_id: String,
|
|
13
|
+
pub recipient: String,
|
|
14
|
+
pub provider: Provider,
|
|
15
|
+
pub pane_id: String,
|
|
16
|
+
pub pane_pid: u32,
|
|
17
|
+
pub session: String,
|
|
18
|
+
pub window: String,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl NativeInputRequest {
|
|
22
|
+
pub fn marker(&self) -> String {
|
|
23
|
+
format!("[team-agent-token:{}]", self.message_id)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub fn supported(&self) -> bool {
|
|
27
|
+
matches!(self.provider, Provider::Pi | Provider::CursorAgent)
|
|
28
|
+
&& self.message_id.starts_with("msg_")
|
|
29
|
+
&& self
|
|
30
|
+
.message_id
|
|
31
|
+
.bytes()
|
|
32
|
+
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
|
|
33
|
+
&& !self.owner_team_id.is_empty()
|
|
34
|
+
&& !self.recipient.is_empty()
|
|
35
|
+
&& self.recipient != "leader"
|
|
36
|
+
&& self.pane_id.starts_with('%')
|
|
37
|
+
&& self.pane_pid > 1
|
|
38
|
+
&& !self.session.is_empty()
|
|
39
|
+
&& !self.window.is_empty()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Carrier birth and framework pane nonce, not a claim of native executable admission.
|
|
44
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
45
|
+
pub struct NativeInputBinding {
|
|
46
|
+
pub endpoint: String,
|
|
47
|
+
pub pane_id: String,
|
|
48
|
+
pub pane_pid: u32,
|
|
49
|
+
pub pane_height: u32,
|
|
50
|
+
pub carrier_birth: String,
|
|
51
|
+
pub binding_nonce: String,
|
|
52
|
+
pub session: String,
|
|
53
|
+
pub window: String,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
|
57
|
+
#[serde(rename_all = "snake_case")]
|
|
58
|
+
pub enum NativeInputPhase {
|
|
59
|
+
Ready,
|
|
60
|
+
MayHavePasted,
|
|
61
|
+
Pasted,
|
|
62
|
+
Seen,
|
|
63
|
+
MayHaveSubmitted,
|
|
64
|
+
Submitted,
|
|
65
|
+
Accepted,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
69
|
+
pub struct NativeInputEvidence {
|
|
70
|
+
pub request: NativeInputRequest,
|
|
71
|
+
pub binding: NativeInputBinding,
|
|
72
|
+
pub payload_sha256: String,
|
|
73
|
+
pub ui_identity: String,
|
|
74
|
+
pub footer_sha256: String,
|
|
75
|
+
pub baseline_sha256: String,
|
|
76
|
+
pub baseline_capsules: Vec<String>,
|
|
77
|
+
pub phase: NativeInputPhase,
|
|
78
|
+
pub capsule: Option<String>,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
82
|
+
pub enum EnterRescueOutcome {
|
|
83
|
+
Consumed,
|
|
84
|
+
NoEffect {
|
|
85
|
+
reason: &'static str,
|
|
86
|
+
reobserve: bool,
|
|
87
|
+
},
|
|
88
|
+
Entered {
|
|
89
|
+
accepted: bool,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type Scope = (Option<NativeInputRequest>, Option<NativeInputEvidence>);
|
|
94
|
+
thread_local! {
|
|
95
|
+
static INPUT_SCOPE: RefCell<Scope> = const { RefCell::new((None, None)) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
struct Restore(Option<Scope>);
|
|
99
|
+
impl Drop for Restore {
|
|
100
|
+
fn drop(&mut self) {
|
|
101
|
+
if let Some(previous) = self.0.take() {
|
|
102
|
+
INPUT_SCOPE.with(|slot| *slot.borrow_mut() = previous);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Existing synchronous delivery owns this scope; no borrowed observer escapes a thread.
|
|
108
|
+
pub fn with_native_input<R>(
|
|
109
|
+
request: Option<NativeInputRequest>,
|
|
110
|
+
f: impl FnOnce() -> R,
|
|
111
|
+
) -> (R, Option<NativeInputEvidence>) {
|
|
112
|
+
let previous = INPUT_SCOPE.with(|slot| slot.replace((request, None)));
|
|
113
|
+
let restore = Restore(Some(previous));
|
|
114
|
+
let result = f();
|
|
115
|
+
let evidence = INPUT_SCOPE.with(|slot| slot.borrow_mut().1.take());
|
|
116
|
+
drop(restore);
|
|
117
|
+
(result, evidence)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
pub(crate) fn current_request() -> Option<NativeInputRequest> {
|
|
121
|
+
INPUT_SCOPE.with(|slot| slot.borrow().0.clone())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pub(crate) fn record_evidence(evidence: &NativeInputEvidence) {
|
|
125
|
+
INPUT_SCOPE.with(|slot| {
|
|
126
|
+
let mut scope = slot.borrow_mut();
|
|
127
|
+
if scope.0.as_ref() == Some(&evidence.request) {
|
|
128
|
+
scope.1 = Some(evidence.clone());
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
134
|
+
pub(crate) struct InputFrame {
|
|
135
|
+
pub composer: String,
|
|
136
|
+
pub transcript: String,
|
|
137
|
+
pub identity: String,
|
|
138
|
+
pub footer: String,
|
|
139
|
+
pub busy: bool,
|
|
140
|
+
pub capsule: Option<String>,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
impl InputFrame {
|
|
144
|
+
pub fn empty(&self) -> bool {
|
|
145
|
+
self.composer.trim().is_empty()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pub fn has_marker(&self, marker: &str) -> bool {
|
|
149
|
+
self.composer
|
|
150
|
+
.lines()
|
|
151
|
+
.map(str::trim)
|
|
152
|
+
.collect::<String>()
|
|
153
|
+
.matches(marker)
|
|
154
|
+
.count()
|
|
155
|
+
== 1
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
pub fn pending(&self, evidence: &NativeInputEvidence) -> bool {
|
|
159
|
+
!self.busy
|
|
160
|
+
&& self.identity == evidence.ui_identity
|
|
161
|
+
&& input_digest(&self.footer) == evidence.footer_sha256
|
|
162
|
+
&& (self.has_marker(&evidence.request.marker())
|
|
163
|
+
|| evidence.capsule.as_ref().is_some_and(|capsule| {
|
|
164
|
+
self.capsule.as_ref() == Some(capsule)
|
|
165
|
+
&& !evidence.baseline_capsules.contains(capsule)
|
|
166
|
+
}))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
pub fn accepted(&self, evidence: &NativeInputEvidence) -> bool {
|
|
170
|
+
if !self.empty() || self.identity != evidence.ui_identity {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if evidence.phase < NativeInputPhase::Seen {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
let marker = evidence.request.marker();
|
|
177
|
+
let joined: String = self.transcript.lines().map(str::trim).collect();
|
|
178
|
+
if joined.matches(&marker).count() == 1 {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
evidence.capsule.as_ref().is_some_and(|capsule| {
|
|
182
|
+
!evidence.baseline_capsules.contains(capsule)
|
|
183
|
+
&& self
|
|
184
|
+
.transcript
|
|
185
|
+
.lines()
|
|
186
|
+
.filter(|line| line.trim() == capsule)
|
|
187
|
+
.count()
|
|
188
|
+
== 1
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
fn input_digest(text: &str) -> String {
|
|
194
|
+
use sha2::{Digest, Sha256};
|
|
195
|
+
format!("{:x}", Sha256::digest(text.as_bytes()))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
pub(crate) fn inspect_input(
|
|
199
|
+
provider: Provider,
|
|
200
|
+
text: &str,
|
|
201
|
+
pane_height: u32,
|
|
202
|
+
) -> Option<InputFrame> {
|
|
203
|
+
match provider {
|
|
204
|
+
Provider::Pi => pi_input(text, pane_height),
|
|
205
|
+
Provider::CursorAgent => cursor_input(text),
|
|
206
|
+
_ => None,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fn cursor_input(text: &str) -> Option<InputFrame> {
|
|
211
|
+
use team_agent_contract::contract::delivery::InputSurface;
|
|
212
|
+
let (surface, rows, capsule, busy) =
|
|
213
|
+
team_agent_contract::providers::cursor::interaction::inspect_composer(text)?;
|
|
214
|
+
let first = rows.first()?.strip_prefix("→ ")?;
|
|
215
|
+
let footer = rows.iter().rposition(|row| {
|
|
216
|
+
row.strip_suffix("Run Everything").is_some_and(|prefix| {
|
|
217
|
+
!prefix.trim().is_empty() && !prefix.trim_start().starts_with("→ ")
|
|
218
|
+
})
|
|
219
|
+
})?;
|
|
220
|
+
if footer == 0 {
|
|
221
|
+
return None;
|
|
222
|
+
}
|
|
223
|
+
let tail: Vec<_> = rows[footer + 1..]
|
|
224
|
+
.iter()
|
|
225
|
+
.filter(|row| !row.is_empty())
|
|
226
|
+
.copied()
|
|
227
|
+
.collect();
|
|
228
|
+
if tail.is_empty() {
|
|
229
|
+
return None;
|
|
230
|
+
}
|
|
231
|
+
let header: Vec<_> = text.lines().map(str::trim).collect();
|
|
232
|
+
let version = header.windows(2).find(|pair| pair[0] == "Cursor Agent")?[1];
|
|
233
|
+
let identity = format!("{version}:{}", input_digest(&tail.join("\n")));
|
|
234
|
+
let footer_row = rows[footer]
|
|
235
|
+
.split_whitespace()
|
|
236
|
+
.collect::<Vec<_>>()
|
|
237
|
+
.join(" ");
|
|
238
|
+
let ready = matches!(surface, InputSurface::ComposerReady | InputSurface::Busy);
|
|
239
|
+
if (ready || capsule.is_some()) && rows[1..footer].iter().any(|row| !row.trim().is_empty()) {
|
|
240
|
+
return None;
|
|
241
|
+
}
|
|
242
|
+
let composer = if ready {
|
|
243
|
+
String::new()
|
|
244
|
+
} else {
|
|
245
|
+
std::iter::once(first)
|
|
246
|
+
.chain(rows[1..footer].iter().copied())
|
|
247
|
+
.collect::<Vec<_>>()
|
|
248
|
+
.join("\n")
|
|
249
|
+
};
|
|
250
|
+
let text_rows: Vec<_> = text.lines().collect();
|
|
251
|
+
let arrow = text_rows
|
|
252
|
+
.iter()
|
|
253
|
+
.rposition(|line| line.trim().starts_with("→ "))?;
|
|
254
|
+
Some(InputFrame {
|
|
255
|
+
composer,
|
|
256
|
+
transcript: text.lines().take(arrow).collect::<Vec<_>>().join("\n"),
|
|
257
|
+
identity,
|
|
258
|
+
footer: footer_row,
|
|
259
|
+
busy,
|
|
260
|
+
capsule: capsule.map(str::to_owned),
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fn border(line: &str) -> bool {
|
|
265
|
+
line.chars().filter(|character| *character == '─').count() >= 8
|
|
266
|
+
&& line.trim_start().starts_with('─')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// A complete native update notice can end inside the editor search window,
|
|
270
|
+
// but its opening must precede every possible editor row. Never discard a
|
|
271
|
+
// ruler belonging to the editable region or an incomplete/unknown notice.
|
|
272
|
+
fn pi_closed_update_notice(rows: &[&str], closing: usize, editor_start: usize, top: usize) -> bool {
|
|
273
|
+
let rule = rows[closing].trim();
|
|
274
|
+
if !rule.chars().all(|character| character == '─')
|
|
275
|
+
|| !rows[closing + 1..top]
|
|
276
|
+
.iter()
|
|
277
|
+
.all(|row| row.trim().is_empty())
|
|
278
|
+
{
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
let Some(opening) = rows[..editor_start]
|
|
282
|
+
.iter()
|
|
283
|
+
.rposition(|row| row.trim() == rule)
|
|
284
|
+
else {
|
|
285
|
+
return false;
|
|
286
|
+
};
|
|
287
|
+
let body = &rows[opening + 1..closing];
|
|
288
|
+
body.len() >= 4
|
|
289
|
+
&& body[0].trim() == "Package Updates Available"
|
|
290
|
+
&& body[1].trim() == "Package updates are available. Run pi update --extensions"
|
|
291
|
+
&& body[2].trim() == "Packages:"
|
|
292
|
+
&& body[3..].iter().all(|row| {
|
|
293
|
+
row.trim()
|
|
294
|
+
.strip_prefix("- ")
|
|
295
|
+
.is_some_and(|package| !package.trim().is_empty())
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
pub(crate) fn pi_paste_capsule(text: &str) -> Option<&str> {
|
|
300
|
+
fn positive(value: &str) -> bool {
|
|
301
|
+
!value.starts_with('0')
|
|
302
|
+
&& value.bytes().all(|byte| byte.is_ascii_digit())
|
|
303
|
+
&& value.parse::<u64>().is_ok()
|
|
304
|
+
}
|
|
305
|
+
let label = text.trim();
|
|
306
|
+
let body = label.strip_prefix("[paste #")?.strip_suffix(']')?;
|
|
307
|
+
let (id, size) = body.split_once(' ')?;
|
|
308
|
+
let count = if let Some(lines) = size.strip_prefix('+') {
|
|
309
|
+
lines.strip_suffix(" lines")?
|
|
310
|
+
} else {
|
|
311
|
+
size.strip_suffix(" chars")?
|
|
312
|
+
};
|
|
313
|
+
(positive(id) && positive(count)).then_some(label)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
fn pi_input(text: &str, pane_height: u32) -> Option<InputFrame> {
|
|
317
|
+
if pane_height < 10 {
|
|
318
|
+
return None;
|
|
319
|
+
}
|
|
320
|
+
let rows: Vec<_> = text.lines().collect();
|
|
321
|
+
let footer = rows
|
|
322
|
+
.iter()
|
|
323
|
+
.rposition(|row| row.contains(" · ") && row.contains("%/"))?;
|
|
324
|
+
if rows[footer + 1..].iter().any(|row| !row.trim().is_empty()) {
|
|
325
|
+
return None;
|
|
326
|
+
}
|
|
327
|
+
let bottom = (0..footer)
|
|
328
|
+
.rev()
|
|
329
|
+
.find(|index| !rows[*index].trim().is_empty())?;
|
|
330
|
+
if !border(rows[bottom]) {
|
|
331
|
+
return None;
|
|
332
|
+
}
|
|
333
|
+
// Current Pi renderer shows at most max(5, floor(pane_rows * .3))
|
|
334
|
+
// editor lines. A task's Unicode ruler is not a native top boundary:
|
|
335
|
+
// ambiguous borders inside that whole possible editor extent refuse.
|
|
336
|
+
let extent = (pane_height as usize * 3 / 10).max(5) + 1;
|
|
337
|
+
let editor_start = bottom.saturating_sub(extent);
|
|
338
|
+
let candidates: Vec<_> = (editor_start..bottom)
|
|
339
|
+
.filter(|index| border(rows[*index]))
|
|
340
|
+
.collect();
|
|
341
|
+
let top = *candidates.last()?;
|
|
342
|
+
if candidates[..candidates.len() - 1]
|
|
343
|
+
.iter()
|
|
344
|
+
.any(|index| !pi_closed_update_notice(&rows, *index, editor_start, top))
|
|
345
|
+
{
|
|
346
|
+
return None;
|
|
347
|
+
}
|
|
348
|
+
let composer = rows[top + 1..bottom].join("\n");
|
|
349
|
+
let transcript = rows[..top].join("\n");
|
|
350
|
+
// Inspect native status adjacent to the editor, never the task body.
|
|
351
|
+
let status = rows[..top]
|
|
352
|
+
.iter()
|
|
353
|
+
.rev()
|
|
354
|
+
.filter(|row| !row.trim().is_empty())
|
|
355
|
+
.take(3)
|
|
356
|
+
.copied()
|
|
357
|
+
.collect::<Vec<_>>()
|
|
358
|
+
.join("\n");
|
|
359
|
+
// Pi embeds its live status/spinner after a 1–3-cell border prefix. Idle
|
|
360
|
+
// (including the observed overflow/title border) has a solid leading run.
|
|
361
|
+
let busy = rows[top]
|
|
362
|
+
.trim_start()
|
|
363
|
+
.chars()
|
|
364
|
+
.take_while(|character| *character == '─')
|
|
365
|
+
.count()
|
|
366
|
+
< 8
|
|
367
|
+
|| status.to_ascii_lowercase().contains("esc to interrupt")
|
|
368
|
+
|| status.to_ascii_lowercase().contains("ctrl+c to stop");
|
|
369
|
+
let capsule = pi_paste_capsule(&composer).map(str::to_owned);
|
|
370
|
+
Some(InputFrame {
|
|
371
|
+
composer,
|
|
372
|
+
transcript,
|
|
373
|
+
identity: rows[footer].split(" · ").next()?.trim().to_string(),
|
|
374
|
+
footer: String::new(),
|
|
375
|
+
busy,
|
|
376
|
+
capsule,
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#[cfg(test)]
|
|
381
|
+
mod tests {
|
|
382
|
+
#![allow(clippy::unwrap_used, clippy::panic)]
|
|
383
|
+
use super::*;
|
|
384
|
+
|
|
385
|
+
fn cursor(composer: &str, transcript: &str) -> String {
|
|
386
|
+
format!("Cursor Agent\nv2026.10.01-e373342\n{transcript}\n→ {composer}\nGrok 4.7 High Run Everything\n/owned/project\n")
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
fn pi(composer: &str, transcript: &str) -> String {
|
|
390
|
+
format!("{transcript}\n──────────────── ↑ 20 more ─ runner-luna ─\n{composer}\n────────────────────────\ngpt-6-luna · max · 0%/272k · 0% · $0.00\n")
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
fn inspect_input(provider: Provider, text: &str) -> Option<InputFrame> {
|
|
394
|
+
super::inspect_input(provider, text, 120)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
#[test]
|
|
398
|
+
fn native_input_scope_restores_nested_and_panicking_calls() {
|
|
399
|
+
let request = NativeInputRequest {
|
|
400
|
+
message_id: "msg_one".into(),
|
|
401
|
+
owner_team_id: "team".into(),
|
|
402
|
+
recipient: "worker".into(),
|
|
403
|
+
provider: Provider::Pi,
|
|
404
|
+
pane_id: "%1".into(),
|
|
405
|
+
pane_pid: 2,
|
|
406
|
+
session: "team".into(),
|
|
407
|
+
window: "worker".into(),
|
|
408
|
+
};
|
|
409
|
+
with_native_input(Some(request.clone()), || {
|
|
410
|
+
assert_eq!(current_request(), Some(request.clone()));
|
|
411
|
+
with_native_input(None, || assert!(current_request().is_none()));
|
|
412
|
+
assert_eq!(current_request(), Some(request));
|
|
413
|
+
let _ = std::panic::catch_unwind(|| with_native_input(None, || panic!("fixture")));
|
|
414
|
+
assert!(current_request().is_some());
|
|
415
|
+
});
|
|
416
|
+
assert!(current_request().is_none());
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#[test]
|
|
420
|
+
fn native_input_pi_requires_editor_and_footer_not_startup_echo() {
|
|
421
|
+
let marker = "[team-agent-token:msg_one]";
|
|
422
|
+
let draft = pi(
|
|
423
|
+
&format!("{}\n{marker}", "line\n".repeat(26)),
|
|
424
|
+
"Warning: startup",
|
|
425
|
+
);
|
|
426
|
+
let frame = inspect_input(Provider::Pi, &draft).unwrap();
|
|
427
|
+
assert!(!frame.empty());
|
|
428
|
+
assert!(frame.has_marker(marker));
|
|
429
|
+
assert!(!frame.busy);
|
|
430
|
+
assert!(inspect_input(Provider::Pi, &format!("Warning: startup\n{marker}")).is_none());
|
|
431
|
+
assert!(inspect_input(Provider::Pi, &pi("", "old history"))
|
|
432
|
+
.unwrap()
|
|
433
|
+
.empty());
|
|
434
|
+
let busy = pi("draft", "history").replace(
|
|
435
|
+
"──────────────── ↑ 20 more ─ runner-luna ─",
|
|
436
|
+
"── ⠋ Working ─────────────────────",
|
|
437
|
+
);
|
|
438
|
+
assert!(inspect_input(Provider::Pi, &busy).unwrap().busy);
|
|
439
|
+
assert!(inspect_input(
|
|
440
|
+
Provider::Pi,
|
|
441
|
+
&pi("foreign draft\n────────\n", "old history")
|
|
442
|
+
)
|
|
443
|
+
.is_none());
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const PI_R6_IDLE: &str = include_str!("../../tests/fixtures/pi-r6-idle-24.txt");
|
|
447
|
+
|
|
448
|
+
#[test]
|
|
449
|
+
fn native_input_pi_r6_idle_update_notice_preserves_empty_editor() {
|
|
450
|
+
assert_eq!(
|
|
451
|
+
input_digest(PI_R6_IDLE),
|
|
452
|
+
"40f31e7476f628a471113c89b76a1ace0246e69f53fb7238999205f62b5d20fa"
|
|
453
|
+
);
|
|
454
|
+
let frame = super::inspect_input(Provider::Pi, PI_R6_IDLE, 24).unwrap();
|
|
455
|
+
assert!(frame.empty());
|
|
456
|
+
assert!(!frame.busy);
|
|
457
|
+
assert_eq!(frame.identity, "gpt-6-luna");
|
|
458
|
+
assert!(frame.transcript.contains("Package Updates Available"));
|
|
459
|
+
assert!(!frame.composer.contains("Package Updates Available"));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#[test]
|
|
463
|
+
fn native_input_pi_r6_notice_does_not_hide_drafts_or_busy_state() {
|
|
464
|
+
let top = " pi-target ─\n\n";
|
|
465
|
+
let marker = "[team-agent-token:msg_one]";
|
|
466
|
+
let draft = PI_R6_IDLE.replace(top, &format!(" pi-target ─\nforeign draft\n{marker}\n"));
|
|
467
|
+
let frame = super::inspect_input(Provider::Pi, &draft, 24).unwrap();
|
|
468
|
+
assert!(!frame.empty());
|
|
469
|
+
assert!(frame.has_marker(marker));
|
|
470
|
+
let busy = PI_R6_IDLE.replace(
|
|
471
|
+
"──────────────────────────────────────────────────────────────────── pi-target ─",
|
|
472
|
+
"── ⠋ Working ──────────────────────────────────────────────────────────────────",
|
|
473
|
+
);
|
|
474
|
+
let frame = super::inspect_input(Provider::Pi, &busy, 24).unwrap();
|
|
475
|
+
assert!(frame.empty());
|
|
476
|
+
assert!(frame.busy);
|
|
477
|
+
for composer in ["foreign draft\n────────\n", "\n────────\n"]
|
|
478
|
+
{
|
|
479
|
+
let foreign = PI_R6_IDLE.replace(top, &format!(" pi-target ─\n{composer}\n"));
|
|
480
|
+
assert!(super::inspect_input(Provider::Pi, &foreign, 24).is_none());
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
#[test]
|
|
485
|
+
fn native_input_pi_r6_incomplete_or_ambiguous_notice_refuses() {
|
|
486
|
+
for frame in [
|
|
487
|
+
PI_R6_IDLE.replace("Package Updates Available", "Unknown notification"),
|
|
488
|
+
PI_R6_IDLE.replace(" - pi-mcp-adapter", "not a native package row"),
|
|
489
|
+
PI_R6_IDLE.replace("\n\n──────────────────────────────────────────────────────────────────── pi-target", "\nforeign draft\n──────────────────────────────────────────────────────────────────── pi-target"),
|
|
490
|
+
] {
|
|
491
|
+
assert!(super::inspect_input(Provider::Pi, &frame, 24).is_none());
|
|
492
|
+
}
|
|
493
|
+
// If the complete box itself could be editor content, geometry cannot
|
|
494
|
+
// distinguish it from a pasted imitation: keep the old refusal.
|
|
495
|
+
assert!(super::inspect_input(Provider::Pi, PI_R6_IDLE, 120).is_none());
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const PI_R7_FOLDED: &str = include_str!("../../tests/fixtures/pi-r7-folded-24.txt");
|
|
499
|
+
|
|
500
|
+
#[test]
|
|
501
|
+
fn native_input_pi_r7_capsule_is_current_composer_not_history() {
|
|
502
|
+
assert_eq!(
|
|
503
|
+
input_digest(PI_R7_FOLDED),
|
|
504
|
+
"b7ea6992c3303743b50714fb55ffc8b91ac5ad168e0acd015428ca5af538ed0e"
|
|
505
|
+
);
|
|
506
|
+
let frame = super::inspect_input(Provider::Pi, PI_R7_FOLDED, 24).unwrap();
|
|
507
|
+
assert!(!frame.empty());
|
|
508
|
+
assert!(!frame.busy);
|
|
509
|
+
assert_eq!(frame.capsule.as_deref(), Some("[paste #1 +30 lines]"));
|
|
510
|
+
assert!(!frame.has_marker("[team-agent-token:msg_one]"));
|
|
511
|
+
let history = format!("[paste #1 +30 lines]\n{PI_R6_IDLE}");
|
|
512
|
+
let frame = super::inspect_input(Provider::Pi, &history, 24).unwrap();
|
|
513
|
+
assert!(frame.empty());
|
|
514
|
+
assert!(frame.capsule.is_none());
|
|
515
|
+
let extra = PI_R7_FOLDED.replace(
|
|
516
|
+
"[paste #1 +30 lines]",
|
|
517
|
+
"[paste #1 +30 lines]\nforeign draft",
|
|
518
|
+
);
|
|
519
|
+
let frame = super::inspect_input(Provider::Pi, &extra, 24).unwrap();
|
|
520
|
+
assert!(!frame.empty());
|
|
521
|
+
assert!(frame.capsule.is_none());
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#[test]
|
|
525
|
+
fn native_input_pi_capsule_grammar_is_canonical_and_provider_specific() {
|
|
526
|
+
for label in ["[paste #1 +30 lines]", "[paste #2 1234 chars]"] {
|
|
527
|
+
assert_eq!(pi_paste_capsule(label), Some(label));
|
|
528
|
+
}
|
|
529
|
+
for label in [
|
|
530
|
+
"[paste #0 +30 lines]",
|
|
531
|
+
"[paste #01 +30 lines]",
|
|
532
|
+
"[paste #1 +0 lines]",
|
|
533
|
+
"[paste #1 +030 lines]",
|
|
534
|
+
"[paste #18446744073709551616 +30 lines]",
|
|
535
|
+
"[paste #1 +18446744073709551616 lines]",
|
|
536
|
+
"[paste #1 -30 lines]",
|
|
537
|
+
"[paste #1 +30 line]",
|
|
538
|
+
"[paste #1 +30 lines]suffix",
|
|
539
|
+
"prefix[paste #1 +30 lines]",
|
|
540
|
+
"[paste #1 +30 lines]\n[paste #2 +40 lines]",
|
|
541
|
+
"[Pasted text #1 +30 lines]",
|
|
542
|
+
] {
|
|
543
|
+
assert_eq!(pi_paste_capsule(label), None, "{label}");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
#[test]
|
|
548
|
+
fn native_input_cursor_separates_folded_draft_from_history_and_busy() {
|
|
549
|
+
let capsule = "[Pasted text #1 +25 lines]";
|
|
550
|
+
let draft = inspect_input(Provider::CursorAgent, &cursor(capsule, "")).unwrap();
|
|
551
|
+
assert!(!draft.empty());
|
|
552
|
+
assert_eq!(draft.capsule.as_deref(), Some(capsule));
|
|
553
|
+
let ready = inspect_input(
|
|
554
|
+
Provider::CursorAgent,
|
|
555
|
+
&cursor("Plan, search, build anything", capsule),
|
|
556
|
+
)
|
|
557
|
+
.unwrap();
|
|
558
|
+
assert!(ready.empty());
|
|
559
|
+
assert!(ready.capsule.is_none());
|
|
560
|
+
let busy = inspect_input(
|
|
561
|
+
Provider::CursorAgent,
|
|
562
|
+
&cursor("Add a follow-up ctrl+c to stop", capsule),
|
|
563
|
+
)
|
|
564
|
+
.unwrap();
|
|
565
|
+
assert!(busy.busy);
|
|
566
|
+
assert!(
|
|
567
|
+
inspect_input(Provider::CursorAgent, &cursor(capsule, "⠘⠆ Working"))
|
|
568
|
+
.unwrap()
|
|
569
|
+
.busy
|
|
570
|
+
);
|
|
571
|
+
assert!(inspect_input(Provider::CursorAgent, "→ [Pasted text #1 +25 lines]").is_none());
|
|
572
|
+
assert!(inspect_input(
|
|
573
|
+
Provider::CursorAgent,
|
|
574
|
+
&cursor("Plan, search, build anything\nextra editable text", "")
|
|
575
|
+
)
|
|
576
|
+
.is_none());
|
|
577
|
+
assert!(inspect_input(
|
|
578
|
+
Provider::CursorAgent,
|
|
579
|
+
&cursor("[Pasted text #1 +25 lines]\nsecond block", "")
|
|
580
|
+
)
|
|
581
|
+
.is_none());
|
|
582
|
+
assert!(inspect_input(
|
|
583
|
+
Provider::CursorAgent,
|
|
584
|
+
"Cursor Agent\nv2026.10.01-e373342\n→ Run Everything\n"
|
|
585
|
+
)
|
|
586
|
+
.is_none());
|
|
587
|
+
let malformed = inspect_input(
|
|
588
|
+
Provider::CursorAgent,
|
|
589
|
+
&cursor("[Pasted text #0 +25 lines]", ""),
|
|
590
|
+
)
|
|
591
|
+
.unwrap();
|
|
592
|
+
assert!(malformed.capsule.is_none());
|
|
593
|
+
}
|
|
594
|
+
}
|
|
@@ -67,6 +67,8 @@ use std::path::{Path, PathBuf};
|
|
|
67
67
|
use serde::{Deserialize, Serialize};
|
|
68
68
|
use thiserror::Error;
|
|
69
69
|
|
|
70
|
+
pub mod native_input;
|
|
71
|
+
|
|
70
72
|
#[cfg(test)]
|
|
71
73
|
pub mod test_support;
|
|
72
74
|
|
|
@@ -727,6 +729,9 @@ pub enum AttachOutcome {
|
|
|
727
729
|
/// transport 的 I/O / 子进程错误。能力性拒绝**不**走这里(用上面的 typed outcome)。
|
|
728
730
|
#[derive(Debug, Error)]
|
|
729
731
|
pub enum TransportError {
|
|
732
|
+
/// Guard refused before a paste/key; the owned evidence carries any later effect floor.
|
|
733
|
+
#[error("native input not ready: {reason}")]
|
|
734
|
+
NativeInputNotReady { reason: String },
|
|
730
735
|
#[error("spawn failed on {backend:?}: {source}")]
|
|
731
736
|
Spawn {
|
|
732
737
|
backend: BackendKind,
|
|
@@ -796,6 +801,13 @@ pub trait Transport: Send + Sync {
|
|
|
796
801
|
false
|
|
797
802
|
}
|
|
798
803
|
|
|
804
|
+
/// This backend implements the owned native composer/submit handshake.
|
|
805
|
+
/// Capability only: no socket discovery, reads, or effects. Unknown/test
|
|
806
|
+
/// backends do not claim a native input protocol by default.
|
|
807
|
+
fn supports_owned_native_input(&self) -> bool {
|
|
808
|
+
false
|
|
809
|
+
}
|
|
810
|
+
|
|
799
811
|
/// Physical tmux endpoint used by this transport when known. For tmux this is
|
|
800
812
|
/// either a full `-S` socket path or a `-L` socket name; non-tmux/test
|
|
801
813
|
/// transports can leave it unknown.
|
|
@@ -977,6 +989,13 @@ pub trait Transport: Send + Sync {
|
|
|
977
989
|
Ok(report)
|
|
978
990
|
}
|
|
979
991
|
|
|
992
|
+
/// Enter-only recovery; unsupported backends have an explicit zero-effect refusal.
|
|
993
|
+
fn rescue_enter(&self, _evidence: &native_input::NativeInputEvidence, _authorize: &dyn Fn() -> bool, _observer: Option<&dyn SubmitObserver>)
|
|
994
|
+
-> Result<native_input::EnterRescueOutcome, TransportError>
|
|
995
|
+
{
|
|
996
|
+
Ok(native_input::EnterRescueOutcome::NoEffect { reason: "unsupported_backend", reobserve: false })
|
|
997
|
+
}
|
|
998
|
+
|
|
980
999
|
fn send_keys(&self, target: &Target, keys: &[Key]) -> Result<(), TransportError>;
|
|
981
1000
|
|
|
982
1001
|
fn capture(&self, target: &Target, range: CaptureRange)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.117",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.117",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.117",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.117"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|