@team-agent/installer 0.5.117 → 0.5.119

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.
@@ -0,0 +1,432 @@
1
+ #![allow(clippy::unwrap_used, clippy::panic)]
2
+ use super::*;
3
+ use crate::model::enums::Provider;
4
+ use std::sync::atomic::{AtomicUsize, Ordering};
5
+ use std::sync::{Arc, Mutex};
6
+
7
+ #[derive(Clone, Copy)]
8
+ enum Mode {
9
+ Normal,
10
+ Busy,
11
+ Unknown,
12
+ Capsule,
13
+ History,
14
+ Changed,
15
+ UnknownAfterEnter,
16
+ UnknownWithReplacement,
17
+ }
18
+ struct State {
19
+ mode: Mode,
20
+ nonce: String,
21
+ endpoint: String,
22
+ pid: u32,
23
+ captures: usize,
24
+ entered: usize,
25
+ calls: Vec<Vec<String>>,
26
+ }
27
+ struct Runner(Arc<Mutex<State>>);
28
+ fn pi(composer: &str, history: &str, busy: bool) -> String {
29
+ format!("{history}\n{}\n{composer}\n────────────────────────\ngpt-6-luna · max · 0%/272k · 0% · $0.00\n",if busy{"── ⠋ Working ──────────────────"}else{"────────────────────"})
30
+ }
31
+ impl CommandRunner for Runner {
32
+ fn run(&self, argv: &[String]) -> Result<CommandOutput, std::io::Error> {
33
+ let mut s = self.0.lock().unwrap();
34
+ s.calls.push(argv.to_vec());
35
+ let stdout = if argv.iter().any(|a| a == "display-message") {
36
+ let format = argv.last().unwrap();
37
+ if format.contains("socket_path") {
38
+ let nonce = if (matches!(s.mode, Mode::UnknownAfterEnter) && s.entered > 0)
39
+ || (matches!(s.mode, Mode::UnknownWithReplacement) && s.captures > 0)
40
+ {
41
+ "replaced"
42
+ } else {
43
+ &s.nonce
44
+ };
45
+ format!("{}|%1|{}|session|worker|{nonce}|0|120\n", s.endpoint, s.pid)
46
+ } else if format == "#{pane_height}" {
47
+ "120\n".into()
48
+ } else if format == "#{pane_pid}" {
49
+ format!("{}\n", s.pid)
50
+ } else {
51
+ "0\n".into()
52
+ }
53
+ } else if argv.iter().any(|a| a == "set-option") {
54
+ assert!(s.nonce.is_empty());
55
+ assert!(argv.iter().any(|a| a == "-o"));
56
+ s.nonce = argv.last().unwrap().clone();
57
+ String::new()
58
+ } else if argv.iter().any(|a| a == "show-options") {
59
+ format!("{}\n", s.nonce)
60
+ } else if argv.iter().any(|a| a == "capture-pane") {
61
+ s.captures += 1;
62
+ let marker = "[team-agent-token:msg_one]";
63
+ match s.mode {
64
+ Mode::Unknown | Mode::UnknownWithReplacement => "unknown native screen".into(),
65
+ Mode::Capsule => pi("[paste #1 +30 lines]", "", false),
66
+ Mode::History => pi("", marker, false),
67
+ Mode::Changed if s.captures >= 3 => pi("different draft", "", false),
68
+ Mode::Busy => pi(marker, "", true),
69
+ _ if s.entered > 0 => pi("", marker, false),
70
+ _ => pi(marker, "", false),
71
+ }
72
+ } else if argv.iter().any(|a| a == "send-keys") {
73
+ assert_eq!(argv.last().unwrap(), "Enter");
74
+ s.entered += 1;
75
+ String::new()
76
+ } else {
77
+ panic!("unexpected transport operation {argv:?}")
78
+ };
79
+ Ok(CommandOutput {
80
+ success: true,
81
+ code: Some(0),
82
+ stdout,
83
+ stderr: String::new(),
84
+ })
85
+ }
86
+ }
87
+ struct Fixture {
88
+ path: std::path::PathBuf,
89
+ backend: TmuxBackend,
90
+ state: Arc<Mutex<State>>,
91
+ }
92
+ impl Fixture {
93
+ fn new(mode: Mode) -> Self {
94
+ static NEXT: AtomicUsize = AtomicUsize::new(0);
95
+ let path = std::env::temp_dir().join(format!(
96
+ "ta-independent-rescue-backend-{}-{}",
97
+ std::process::id(),
98
+ NEXT.fetch_add(1, Ordering::SeqCst)
99
+ ));
100
+ std::fs::create_dir(&path).unwrap();
101
+ let state = Arc::new(Mutex::new(State {
102
+ mode,
103
+ nonce: "original-nonce".into(),
104
+ endpoint: "/tmp/owned.sock".into(),
105
+ pid: std::process::id(),
106
+ captures: 0,
107
+ entered: 0,
108
+ calls: vec![],
109
+ }));
110
+ let backend =
111
+ TmuxBackend::with_runner_for_workspace(Box::new(Runner(state.clone())), &path);
112
+ Self {
113
+ path,
114
+ backend,
115
+ state,
116
+ }
117
+ }
118
+ fn context(&self) -> EnterRescueContext {
119
+ EnterRescueContext {
120
+ message_id: "msg_one".into(),
121
+ owner_team_id: "team".into(),
122
+ recipient: "worker".into(),
123
+ payload_sha256: digest("rendered payload"),
124
+ target: Some(EnterRescueTarget::selected(
125
+ Provider::Pi,
126
+ "%1".into(),
127
+ std::process::id(),
128
+ "session".into(),
129
+ "worker".into(),
130
+ )),
131
+ proof: None,
132
+ }
133
+ }
134
+ fn snapshot(&self) -> EnterRescueContext {
135
+ self.backend
136
+ .snapshot_rescue_context(
137
+ &Target::Pane(PaneId::new("%1")),
138
+ &self.context(),
139
+ "rendered payload",
140
+ )
141
+ .unwrap()
142
+ }
143
+ fn entered(&self) -> usize {
144
+ self.state.lock().unwrap().entered
145
+ }
146
+ }
147
+ impl Drop for Fixture {
148
+ fn drop(&mut self) {
149
+ let _ = std::fs::remove_dir_all(&self.path);
150
+ }
151
+ }
152
+
153
+ #[test]
154
+ fn original_boundary_snapshot_records_only_exact_visible_current_marker() {
155
+ let f = Fixture::new(Mode::Normal);
156
+ let c = f.snapshot();
157
+ assert!(c.target.as_ref().unwrap().bound());
158
+ let proof = c.proof.unwrap();
159
+ assert!(proof.visible_marker && proof.transcript_marker_absent);
160
+ assert!(proof.owned_capsule.is_none());
161
+ assert_eq!(f.entered(), 0);
162
+ for mode in [Mode::Unknown, Mode::Capsule, Mode::History] {
163
+ let f = Fixture::new(mode);
164
+ let c = f.snapshot();
165
+ assert!(c.proof.is_none());
166
+ assert_eq!(f.entered(), 0);
167
+ }
168
+ }
169
+
170
+ #[test]
171
+ fn snapshot_rejects_wrong_payload_or_selected_carrier_and_creates_nonce_only_when_owned() {
172
+ let f = Fixture::new(Mode::Normal);
173
+ let c = f.context();
174
+ let t = Target::Pane(PaneId::new("%1"));
175
+ assert!(f
176
+ .backend
177
+ .snapshot_rescue_context(&t, &c, "edited")
178
+ .unwrap()
179
+ .proof
180
+ .is_none());
181
+ assert!(f.state.lock().unwrap().calls.is_empty());
182
+ let mut c = f.context();
183
+ c.target.as_mut().unwrap().pane_pid += 1;
184
+ assert!(f
185
+ .backend
186
+ .snapshot_rescue_context(&t, &c, "rendered payload")
187
+ .unwrap()
188
+ .proof
189
+ .is_none());
190
+ let f = Fixture::new(Mode::Capsule);
191
+ f.state.lock().unwrap().nonce.clear();
192
+ assert!(f.snapshot().proof.is_none());
193
+ assert!(f.state.lock().unwrap().nonce.is_empty());
194
+ let f = Fixture::new(Mode::Normal);
195
+ f.state.lock().unwrap().nonce.clear();
196
+ assert!(f.snapshot().target.unwrap().bound());
197
+ assert!(!f.state.lock().unwrap().nonce.is_empty());
198
+ assert_eq!(f.entered(), 0);
199
+ }
200
+
201
+ #[test]
202
+ fn watchdog_rechecks_carrier_and_never_adopts_replacement_or_missing_receipt() {
203
+ for changed in ["endpoint", "nonce", "birth", "pid"] {
204
+ let f = Fixture::new(Mode::Normal);
205
+ let mut c = f.snapshot();
206
+ match changed {
207
+ "endpoint" => f.state.lock().unwrap().endpoint = "/tmp/other.sock".into(),
208
+ "nonce" => f.state.lock().unwrap().nonce = "other".into(),
209
+ "birth" => c.target.as_mut().unwrap().carrier_birth = "reused".into(),
210
+ _ => c.target.as_mut().unwrap().pane_pid += 1,
211
+ };
212
+ let _ = f.backend.rescue_guarded_enter(&c, &|| true, None);
213
+ assert_eq!(f.entered(), 0, "{changed}");
214
+ }
215
+ let f = Fixture::new(Mode::Normal);
216
+ assert!(matches!(
217
+ f.backend
218
+ .rescue_guarded_enter(&f.context(), &|| true, None)
219
+ .unwrap(),
220
+ EnterRescueOutcome::NoEffect { .. }
221
+ ));
222
+ assert_eq!(f.entered(), 0);
223
+ }
224
+
225
+ #[test]
226
+ fn busy_unstable_unknown_or_final_authorization_failure_sends_no_key() {
227
+ for mode in [Mode::Busy, Mode::Changed] {
228
+ let f = Fixture::new(mode);
229
+ let c = f.snapshot();
230
+ assert!(matches!(
231
+ f.backend.rescue_guarded_enter(&c, &|| true, None).unwrap(),
232
+ EnterRescueOutcome::NoEffect { .. }
233
+ ));
234
+ assert_eq!(f.entered(), 0);
235
+ }
236
+ let f = Fixture::new(Mode::Normal);
237
+ let c = f.snapshot();
238
+ f.state.lock().unwrap().mode = Mode::Unknown;
239
+ assert_eq!(
240
+ f.backend.rescue_guarded_enter(&c, &|| true, None).unwrap(),
241
+ no_effect("surface_unknown", true)
242
+ );
243
+ assert_eq!(f.entered(), 0);
244
+ let f = Fixture::new(Mode::Normal);
245
+ let c = f.snapshot();
246
+ assert!(matches!(
247
+ f.backend.rescue_guarded_enter(&c, &|| false, None).unwrap(),
248
+ EnterRescueOutcome::NoEffect { .. }
249
+ ));
250
+ assert_eq!(f.entered(), 0);
251
+ }
252
+
253
+ #[test]
254
+ fn unknown_observation_still_checks_original_carrier_before_zero_effect_refusal() {
255
+ let f = Fixture::new(Mode::Normal);
256
+ let c = f.snapshot();
257
+ {
258
+ let mut state = f.state.lock().unwrap();
259
+ state.mode = Mode::UnknownWithReplacement;
260
+ state.captures = 0;
261
+ }
262
+ assert!(matches!(
263
+ f.backend.rescue_guarded_enter(&c, &|| true, None),
264
+ Err(TransportError::NativeInputNotReady { reason }) if reason == "rescue_binding_changed"
265
+ ));
266
+ assert_eq!(f.entered(), 0);
267
+ }
268
+
269
+ #[test]
270
+ fn required_input_lock_cannot_be_omitted_or_bypassed() {
271
+ let f = Fixture::new(Mode::Normal);
272
+ let c = f.snapshot();
273
+ let _lock = acquire_required(PaneInputLockRequest {
274
+ workspace: Some(&f.path),
275
+ target_key: "%1",
276
+ operation: "fixture-held",
277
+ })
278
+ .unwrap();
279
+ assert_eq!(
280
+ f.backend.rescue_guarded_enter(&c, &|| true, None).unwrap(),
281
+ no_effect("input_lock", true)
282
+ );
283
+ assert_eq!(f.entered(), 0);
284
+ let f = Fixture::new(Mode::Normal);
285
+ let c = f.snapshot();
286
+ let backend = TmuxBackend::with_runner(Box::new(Runner(f.state.clone())));
287
+ assert_eq!(
288
+ backend.rescue_guarded_enter(&c, &|| true, None).unwrap(),
289
+ no_effect("input_lock", true)
290
+ );
291
+ assert_eq!(f.entered(), 0);
292
+ }
293
+
294
+ struct Observer(AtomicUsize);
295
+ impl SubmitObserver for Observer {
296
+ fn after_physical_submit(&self) {
297
+ self.0.fetch_add(1, Ordering::SeqCst);
298
+ }
299
+ }
300
+ #[test]
301
+ fn one_logical_enter_notifies_before_verification_and_never_pastes() {
302
+ let f = Fixture::new(Mode::Normal);
303
+ let c = f.snapshot();
304
+ let observer = Observer(AtomicUsize::new(0));
305
+ assert_eq!(
306
+ f.backend
307
+ .rescue_guarded_enter(&c, &|| true, Some(&observer))
308
+ .unwrap(),
309
+ EnterRescueOutcome::Entered { accepted: true }
310
+ );
311
+ assert_eq!(f.entered(), 1);
312
+ assert_eq!(observer.0.load(Ordering::SeqCst), 1);
313
+ let s = f.state.lock().unwrap();
314
+ assert!(!s
315
+ .calls
316
+ .iter()
317
+ .flatten()
318
+ .any(|a| matches!(a.as_str(), "paste-buffer" | "load-buffer" | "set-buffer")));
319
+ }
320
+
321
+ #[test]
322
+ fn already_consumed_transition_never_sends_another_enter_or_fakes_observer() {
323
+ let f = Fixture::new(Mode::Normal);
324
+ let c = f.snapshot();
325
+ f.state.lock().unwrap().mode = Mode::History;
326
+ let observer = Observer(AtomicUsize::new(0));
327
+ assert_eq!(
328
+ f.backend
329
+ .rescue_guarded_enter(&c, &|| true, Some(&observer))
330
+ .unwrap(),
331
+ EnterRescueOutcome::Consumed
332
+ );
333
+ assert_eq!(f.entered(), 0);
334
+ assert_eq!(observer.0.load(Ordering::SeqCst), 0);
335
+ }
336
+
337
+ #[test]
338
+ fn foreign_token_or_bare_capsule_is_not_current_message_key_authority() {
339
+ let f = Fixture::new(Mode::Normal);
340
+ let mut c = f.snapshot();
341
+ let mut frame = f
342
+ .backend
343
+ .rescue_frame(c.target.as_ref().unwrap())
344
+ .unwrap()
345
+ .unwrap();
346
+ frame.composer.push_str("\n[team-agent-token:msg_foreign]");
347
+ assert!(!pending(&c, &frame));
348
+ frame.composer = "[paste #1 +30 lines]".into();
349
+ frame.capsule = Some(frame.composer.clone());
350
+ assert!(!pending(&c, &frame));
351
+ let scope_sha256 = c.scope_sha256().unwrap();
352
+ let proof = c.proof.as_mut().unwrap();
353
+ proof.visible_marker = false;
354
+ proof.owned_capsule = Some(crate::transport::enter_rescue::OwnedCapsule {
355
+ scope_sha256,
356
+ label: frame.composer.clone(),
357
+ baseline_sha256: digest("original baseline"),
358
+ baseline_capsules: vec![],
359
+ observed_after_owned_paste: true,
360
+ });
361
+ assert!(pending(&c, &frame));
362
+ for field in [
363
+ "message",
364
+ "payload",
365
+ "owner",
366
+ "recipient",
367
+ "nonce",
368
+ "birth",
369
+ "endpoint",
370
+ ] {
371
+ let mut reassigned = c.clone();
372
+ match field {
373
+ "message" => reassigned.message_id = "msg_other".into(),
374
+ "payload" => reassigned.payload_sha256 = digest("other payload"),
375
+ "owner" => reassigned.owner_team_id = "other".into(),
376
+ "recipient" => reassigned.recipient = "other".into(),
377
+ "nonce" => reassigned.target.as_mut().unwrap().binding_nonce = "other".into(),
378
+ "birth" => reassigned.target.as_mut().unwrap().carrier_birth = "other".into(),
379
+ _ => reassigned.target.as_mut().unwrap().endpoint = "/tmp/other.sock".into(),
380
+ }
381
+ assert!(!pending(&reassigned, &frame), "{field}");
382
+ }
383
+ c.proof
384
+ .as_mut()
385
+ .unwrap()
386
+ .owned_capsule
387
+ .as_mut()
388
+ .unwrap()
389
+ .baseline_capsules
390
+ .push(frame.composer.clone());
391
+ assert!(!pending(&c, &frame));
392
+ }
393
+
394
+ #[test]
395
+ fn unknown_after_enter_is_terminal_uncertainty_not_no_effect() {
396
+ let f = Fixture::new(Mode::UnknownAfterEnter);
397
+ let c = f.snapshot();
398
+ let observer = Observer(AtomicUsize::new(0));
399
+ assert_eq!(
400
+ f.backend
401
+ .rescue_guarded_enter(&c, &|| true, Some(&observer))
402
+ .unwrap(),
403
+ EnterRescueOutcome::Entered { accepted: false }
404
+ );
405
+ assert_eq!(f.entered(), 1);
406
+ assert_eq!(observer.0.load(Ordering::SeqCst), 1);
407
+ }
408
+
409
+ #[test]
410
+ fn transcript_transfer_requires_empty_not_whitespace_composer_and_unique_marker() {
411
+ let f = Fixture::new(Mode::Normal);
412
+ let c = f.snapshot();
413
+ let proof = c.proof.as_ref().unwrap();
414
+ let mut frame = InputFrame {
415
+ composer: "".into(),
416
+ transcript: c.marker(),
417
+ identity: proof.ui_identity.clone(),
418
+ footer: "changed while working".into(),
419
+ busy: true,
420
+ capsule: None,
421
+ };
422
+ assert!(accepted(&c, &frame));
423
+ frame.composer = " ".into();
424
+ assert!(!accepted(&c, &frame));
425
+ frame.composer.clear();
426
+ frame.transcript = format!("{}{}", c.marker(), c.marker());
427
+ assert!(!accepted(&c, &frame));
428
+ frame.transcript = c.marker();
429
+ let mut c = c;
430
+ c.proof.as_mut().unwrap().transcript_marker_absent = false;
431
+ assert!(!accepted(&c, &frame));
432
+ }
@@ -38,6 +38,25 @@ pub(crate) fn digest(text: &str) -> String {
38
38
  }
39
39
 
40
40
  impl TmuxBackend {
41
+ pub(super) fn observe_current_native_input(
42
+ &self,
43
+ target: &Target,
44
+ provider: Provider,
45
+ ) -> Result<Option<InputFrame>, TransportError> {
46
+ if !crate::transport::native_input::supports_input_observation(provider) {
47
+ return Ok(None);
48
+ }
49
+ // Pure observation: no nonce initialization, paste, or key.
50
+ let binding = self.query_native_binding(target, true)?;
51
+ let capture = self.capture(target, CaptureRange::Tail(200))?;
52
+ let frame = inspect_input(provider, &capture.text, binding.pane_height)
53
+ .ok_or_else(|| refused("native_surface_unknown"))?;
54
+ if self.query_native_binding(target, true)? != binding {
55
+ return Err(refused("binding_changed"));
56
+ }
57
+ Ok(Some(frame))
58
+ }
59
+
41
60
  fn native_binding(&self, target: &Target) -> Result<NativeInputBinding, TransportError> {
42
61
  self.query_native_binding(target, false)
43
62
  }
@@ -20,6 +20,12 @@ enum Mode {
20
20
  R6IdleNotice,
21
21
  PiFolded,
22
22
  PiFoldedUnstable,
23
+ CursorFollowup,
24
+ CursorHeaderEvicted,
25
+ CursorHistoryOnly,
26
+ CursorLongHistory,
27
+ CursorExpanded,
28
+ CursorExpandedDirty,
23
29
  }
24
30
  struct State {
25
31
  mode: Mode,
@@ -53,7 +59,13 @@ impl CommandRunner for Runner {
53
59
  };
54
60
  let height = if matches!(
55
61
  state.mode,
56
- Mode::R6IdleNotice | Mode::PiFolded | Mode::PiFoldedUnstable
62
+ Mode::R6IdleNotice
63
+ | Mode::PiFolded
64
+ | Mode::PiFoldedUnstable
65
+ | Mode::CursorHistoryOnly
66
+ | Mode::CursorLongHistory
67
+ | Mode::CursorExpanded
68
+ | Mode::CursorExpandedDirty
57
69
  ) {
58
70
  24
59
71
  } else {
@@ -168,14 +180,58 @@ impl CommandRunner for Runner {
168
180
  let stdout = if state.provider == Provider::CursorAgent
169
181
  && argv.iter().any(|arg| arg == "capture-pane")
170
182
  {
183
+ let followup = matches!(
184
+ state.mode,
185
+ Mode::CursorFollowup
186
+ | Mode::CursorHeaderEvicted
187
+ | Mode::CursorHistoryOnly
188
+ | Mode::CursorLongHistory
189
+ | Mode::CursorExpanded
190
+ | Mode::CursorExpandedDirty
191
+ );
192
+ let placeholder = if followup {
193
+ "Add a follow-up"
194
+ } else {
195
+ "Plan, search, build anything"
196
+ };
197
+ let header = if matches!(state.mode, Mode::CursorFollowup)
198
+ || (matches!(state.mode, Mode::CursorHeaderEvicted) && state.pasted > 0)
199
+ {
200
+ ""
201
+ } else {
202
+ "Cursor Agent\nv2026.10.01-e373342\n"
203
+ };
171
204
  let (composer, history) = if state.entered > 0 {
172
- ("Plan, search, build anything", "[Pasted text #2 +25 lines]")
205
+ (placeholder, "[Pasted text #2 +25 lines]")
173
206
  } else if state.pasted > 0 {
174
207
  ("[Pasted text #2 +25 lines]", "[Pasted text #1 +25 lines]")
175
208
  } else {
176
- ("Plan, search, build anything", "[Pasted text #1 +25 lines]")
209
+ (placeholder, "[Pasted text #1 +25 lines]")
210
+ };
211
+ let padding = if matches!(state.mode, Mode::CursorLongHistory) {
212
+ "historical transcript row\n".repeat(70)
213
+ } else {
214
+ String::new()
177
215
  };
178
- format!("Cursor Agent\nv2026.10.01-e373342\n{history}\n→ {composer}\nGrok 4.7 High Run Everything\n/owned/project\n")
216
+ let frame = format!(
217
+ "{header}{history}\n{padding}→ {composer}\nGrok 4.7 High Run Everything\n/owned/project\n"
218
+ );
219
+ if matches!(state.mode, Mode::CursorExpanded | Mode::CursorExpandedDirty) {
220
+ let expanded = include_str!("../../tests/fixtures/cursor-followup-expanded.txt");
221
+ if matches!(state.mode, Mode::CursorExpandedDirty)
222
+ || state.pasted > 0 && state.entered == 0
223
+ {
224
+ format!("[Pasted text #1 +30 lines]\n{expanded}")
225
+ } else if state.entered > 0 {
226
+ format!("{marker}\n→ Add a follow-up\nGrok 4.7 256K High · 8.6% Run Everything\n/owned/project\n")
227
+ } else {
228
+ "[Pasted text #1 +30 lines]\n→ Add a follow-up\nGrok 4.7 256K High · 8.6% Run Everything\n/owned/project\n".into()
229
+ }
230
+ } else if matches!(state.mode, Mode::CursorHistoryOnly) {
231
+ format!("{frame}{}", "unknown current row\n".repeat(24))
232
+ } else {
233
+ frame
234
+ }
179
235
  } else {
180
236
  stdout
181
237
  };
@@ -703,6 +759,87 @@ fn native_input_cursor_owned_new_capsule_transfers_to_transcript_without_token_c
703
759
  assert_eq!(state.entered, 1);
704
760
  }
705
761
 
762
+ #[test]
763
+ fn native_input_cursor_clean_followup_uses_one_paste_one_enter_across_banner_eviction() {
764
+ for mode in [
765
+ Mode::CursorFollowup,
766
+ Mode::CursorHeaderEvicted,
767
+ Mode::CursorLongHistory,
768
+ ] {
769
+ let f = Fixture::new(mode);
770
+ f.state.lock().unwrap().provider = Provider::CursorAgent;
771
+ let (report, evidence) = f.inject();
772
+ let report = report.unwrap();
773
+ assert_eq!(
774
+ report.submit_verification,
775
+ SubmitVerification::EnterSentWithoutPlaceholderCheck
776
+ );
777
+ let evidence = evidence.unwrap();
778
+ assert_eq!(evidence.phase, NativeInputPhase::Accepted);
779
+ assert_eq!(
780
+ evidence.capsule.as_deref(),
781
+ Some("[Pasted text #2 +25 lines]")
782
+ );
783
+ let state = f.state.lock().unwrap();
784
+ assert_eq!((state.pasted, state.entered), (1, 1));
785
+ }
786
+ }
787
+
788
+ #[test]
789
+ fn native_input_cursor_expanded_followup_reaches_seen_then_one_enter() {
790
+ let f = Fixture::new(Mode::CursorExpanded);
791
+ f.state.lock().unwrap().provider = Provider::CursorAgent;
792
+ let text = "For a harmless terminal-scroll fixture, reply with exactly 240 numbered lines, one per line, from 001 through 240, each formatted `NNN: amber`; do not use tools or change any files. Then wait.\n\n[team-agent-token:msg_one]";
793
+ let (report, evidence) = with_native_input(Some(f.request()), || {
794
+ f.backend.inject(
795
+ &Target::Pane(PaneId::new("%1")),
796
+ &InjectPayload::Text(text.into()),
797
+ Key::Enter,
798
+ true,
799
+ )
800
+ });
801
+ let report = report.unwrap();
802
+ assert_eq!(
803
+ report.submit_verification,
804
+ SubmitVerification::EnterSentWithoutPlaceholderCheck
805
+ );
806
+ assert_eq!(
807
+ report.inject_verification,
808
+ InjectVerification::CaptureContainsToken
809
+ );
810
+ let evidence = evidence.unwrap();
811
+ assert_eq!(evidence.phase, NativeInputPhase::Accepted);
812
+ assert!(evidence.capsule.is_none());
813
+ assert_eq!(evidence.baseline_capsules, ["[Pasted text #1 +30 lines]"]);
814
+ let state = f.state.lock().unwrap();
815
+ assert_eq!((state.pasted, state.entered), (1, 1));
816
+ assert!(state.stage_captures[1] >= 2);
817
+ }
818
+
819
+ #[test]
820
+ fn native_input_cursor_foreign_expanded_followup_is_zero_effect() {
821
+ let f = Fixture::new(Mode::CursorExpandedDirty);
822
+ f.state.lock().unwrap().provider = Provider::CursorAgent;
823
+ let (report, _) = f.inject();
824
+ assert!(report.is_err());
825
+ let state = f.state.lock().unwrap();
826
+ assert_eq!((state.pasted, state.entered), (0, 0));
827
+ }
828
+
829
+ #[test]
830
+ fn native_input_cursor_history_only_composer_cannot_authorize_paste_or_enter() {
831
+ let f = Fixture::new(Mode::CursorHistoryOnly);
832
+ f.state.lock().unwrap().provider = Provider::CursorAgent;
833
+ let (result, _) = f.inject();
834
+ let state = f.state.lock().unwrap();
835
+ assert_eq!(
836
+ (state.pasted, state.entered),
837
+ (0, 0),
838
+ "old history is not current composer authority"
839
+ );
840
+ assert!(result.is_err());
841
+ }
842
+
706
843
  #[test]
707
844
  fn native_input_buffer_uses_authoritative_message_not_first_quoted_marker() {
708
845
  let f = Fixture::new(Mode::Normal);
@@ -73,6 +73,7 @@ use crate::transport::{
73
73
  SubmitVerification, Target, Transport, TransportError, TurnVerification, WindowName,
74
74
  };
75
75
 
76
+ mod enter_rescue;
76
77
  mod native_input;
77
78
 
78
79
  pub const PANE_BINDING_NONCE_METADATA_KEY: &str = "TEAM_AGENT_PANE_BINDING_NONCE";
@@ -3848,6 +3849,32 @@ impl Transport for TmuxBackend {
3848
3849
  })
3849
3850
  }
3850
3851
 
3852
+ fn observe_native_input(
3853
+ &self,
3854
+ target: &Target,
3855
+ provider: crate::model::enums::Provider,
3856
+ ) -> Result<Option<crate::transport::native_input::InputFrame>, TransportError> {
3857
+ self.observe_current_native_input(target, provider)
3858
+ }
3859
+
3860
+ fn snapshot_enter_rescue(
3861
+ &self,
3862
+ target: &Target,
3863
+ context: &crate::transport::enter_rescue::EnterRescueContext,
3864
+ rendered_payload: &str,
3865
+ ) -> Result<crate::transport::enter_rescue::EnterRescueContext, TransportError> {
3866
+ self.snapshot_rescue_context(target, context, rendered_payload)
3867
+ }
3868
+
3869
+ fn rescue_enter_guarded(
3870
+ &self,
3871
+ context: &crate::transport::enter_rescue::EnterRescueContext,
3872
+ authorize: &dyn Fn() -> bool,
3873
+ observer: Option<&dyn SubmitObserver>,
3874
+ ) -> Result<crate::transport::enter_rescue::EnterRescueOutcome, TransportError> {
3875
+ self.rescue_guarded_enter(context, authorize, observer)
3876
+ }
3877
+
3851
3878
  fn rescue_enter(&self, evidence: &crate::transport::native_input::NativeInputEvidence, authorize: &dyn Fn() -> bool, observer: Option<&dyn SubmitObserver>)
3852
3879
  -> Result<crate::transport::native_input::EnterRescueOutcome, TransportError>
3853
3880
  {