@team-agent/installer 0.3.1 → 0.3.3

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.
Files changed (79) hide show
  1. package/Cargo.lock +34 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -1
  4. package/crates/team-agent/src/cli/adapters.rs +234 -26
  5. package/crates/team-agent/src/cli/diagnose.rs +144 -10
  6. package/crates/team-agent/src/cli/emit.rs +289 -54
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +1281 -196
  9. package/crates/team-agent/src/cli/status_port.rs +195 -46
  10. package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
  11. package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
  12. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
  13. package/crates/team-agent/src/cli/tests/run_delegation.rs +59 -3
  14. package/crates/team-agent/src/cli/types.rs +18 -0
  15. package/crates/team-agent/src/compiler.rs +15 -5
  16. package/crates/team-agent/src/coordinator/health.rs +95 -17
  17. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  18. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  19. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  20. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  21. package/crates/team-agent/src/coordinator/types.rs +15 -3
  22. package/crates/team-agent/src/db/schema.rs +37 -2
  23. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  24. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  25. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  26. package/crates/team-agent/src/fake_worker.rs +146 -3
  27. package/crates/team-agent/src/leader/start.rs +121 -23
  28. package/crates/team-agent/src/leader/types.rs +44 -1
  29. package/crates/team-agent/src/lib.rs +3 -0
  30. package/crates/team-agent/src/lifecycle/display.rs +645 -47
  31. package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
  32. package/crates/team-agent/src/lifecycle/mod.rs +2 -0
  33. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  34. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  35. package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
  36. package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
  37. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
  38. package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
  39. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  40. package/crates/team-agent/src/lifecycle/restart.rs +24 -1
  41. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
  43. package/crates/team-agent/src/lifecycle/types.rs +19 -0
  44. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  45. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  46. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  47. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  48. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  49. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  50. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  51. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  52. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  53. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  54. package/crates/team-agent/src/message_store.rs +21 -4
  55. package/crates/team-agent/src/messaging/delivery.rs +470 -59
  56. package/crates/team-agent/src/messaging/mod.rs +9 -6
  57. package/crates/team-agent/src/messaging/results.rs +353 -63
  58. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  59. package/crates/team-agent/src/messaging/send.rs +35 -3
  60. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  61. package/crates/team-agent/src/messaging/types.rs +11 -3
  62. package/crates/team-agent/src/os_probe.rs +119 -0
  63. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  64. package/crates/team-agent/src/packaging/tests.rs +23 -0
  65. package/crates/team-agent/src/provider/adapter.rs +564 -63
  66. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  67. package/crates/team-agent/src/provider/classify.rs +51 -4
  68. package/crates/team-agent/src/provider/helpers.rs +10 -1
  69. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  70. package/crates/team-agent/src/provider/types.rs +47 -0
  71. package/crates/team-agent/src/session_capture.rs +616 -0
  72. package/crates/team-agent/src/state/persist.rs +170 -1
  73. package/crates/team-agent/src/state/projection.rs +141 -8
  74. package/crates/team-agent/src/state/selector.rs +5 -2
  75. package/crates/team-agent/src/tmux_backend.rs +161 -64
  76. package/crates/team-agent/src/transport/test_support.rs +9 -0
  77. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  78. package/crates/team-agent/src/transport.rs +13 -2
  79. package/package.json +4 -4
@@ -27,8 +27,8 @@ use crate::transport::{
27
27
  normalize_capture, tmux_capture_argv, tmux_empty_inject_argv, tmux_inject_text_argv,
28
28
  tmux_query_argv, tmux_send_keys_argv, tmux_spawn_argv, AttachOutcome, BackendKind,
29
29
  CaptureRange, CapturedText, InjectPayload, InjectReport, InjectStage, InjectVerification, Key,
30
- PaneField, PaneId, PaneInfo, PaneMode, SessionName, SetEnvOutcome, SpawnResult, SubmitVerification,
31
- Target, Transport, TransportError, TurnVerification, WindowName,
30
+ PaneField, PaneId, PaneInfo, PaneMode, SessionName, SetEnvOutcome, SpawnResult,
31
+ SubmitVerification, Target, Transport, TransportError, TurnVerification, WindowName,
32
32
  };
33
33
 
34
34
  /// Result of running an external command — the typed output of the OS edge.
@@ -91,22 +91,29 @@ impl RealCommandRunner {
91
91
  };
92
92
  let mut child = std::process::Command::new(program)
93
93
  .args(argv.iter().skip(1))
94
- .stdin(if stdin_text.is_some() { Stdio::piped() } else { Stdio::null() })
94
+ .stdin(if stdin_text.is_some() {
95
+ Stdio::piped()
96
+ } else {
97
+ Stdio::null()
98
+ })
95
99
  .stdout(Stdio::piped())
96
100
  .stderr(Stdio::piped())
97
101
  .spawn()?;
98
102
  if let Some(text) = stdin_text {
99
- let mut stdin = child.stdin.take().ok_or_else(|| {
100
- std::io::Error::other("stdin pipe missing")
101
- })?;
103
+ let mut stdin = child
104
+ .stdin
105
+ .take()
106
+ .ok_or_else(|| std::io::Error::other("stdin pipe missing"))?;
102
107
  stdin.write_all(text.as_bytes())?;
103
108
  }
104
- let stdout = child.stdout.take().ok_or_else(|| {
105
- std::io::Error::other("stdout pipe missing")
106
- })?;
107
- let stderr = child.stderr.take().ok_or_else(|| {
108
- std::io::Error::other("stderr pipe missing")
109
- })?;
109
+ let stdout = child
110
+ .stdout
111
+ .take()
112
+ .ok_or_else(|| std::io::Error::other("stdout pipe missing"))?;
113
+ let stderr = child
114
+ .stderr
115
+ .take()
116
+ .ok_or_else(|| std::io::Error::other("stderr pipe missing"))?;
110
117
  let stdout_thread = std::thread::spawn(move || read_pipe(stdout));
111
118
  let stderr_thread = std::thread::spawn(move || read_pipe(stderr));
112
119
  let deadline = Instant::now() + COMMAND_TIMEOUT;
@@ -173,7 +180,10 @@ impl TmuxBackend {
173
180
  /// Backend bound to the real `tmux` subprocess on the SHARED default socket (no `-L`).
174
181
  /// Non-team callers + existing argv/unit tests stay unaffected.
175
182
  pub fn new() -> Self {
176
- Self { runner: Box::new(RealCommandRunner), socket: None }
183
+ Self {
184
+ runner: Box::new(RealCommandRunner),
185
+ socket: None,
186
+ }
177
187
  }
178
188
 
179
189
  /// CP-1 team backend: bound to the real `tmux` subprocess on a PER-WORKSPACE socket, derived
@@ -182,7 +192,9 @@ impl TmuxBackend {
182
192
  pub fn for_workspace(workspace: &Path) -> Self {
183
193
  Self {
184
194
  runner: Box::new(RealCommandRunner),
185
- socket: Some(TmuxSocketEndpoint::Name(socket_name_for_workspace(workspace))),
195
+ socket: Some(TmuxSocketEndpoint::Name(socket_name_for_workspace(
196
+ workspace,
197
+ ))),
186
198
  }
187
199
  }
188
200
 
@@ -190,7 +202,10 @@ impl TmuxBackend {
190
202
  if socket.is_empty() || socket == "default" {
191
203
  Self::new()
192
204
  } else {
193
- Self { runner: Box::new(RealCommandRunner), socket: Some(TmuxSocketEndpoint::Name(socket.to_string())) }
205
+ Self {
206
+ runner: Box::new(RealCommandRunner),
207
+ socket: Some(TmuxSocketEndpoint::Name(socket.to_string())),
208
+ }
194
209
  }
195
210
  }
196
211
 
@@ -198,7 +213,10 @@ impl TmuxBackend {
198
213
  if endpoint.is_empty() || endpoint == "default" {
199
214
  Self::new()
200
215
  } else if Path::new(endpoint).is_absolute() {
201
- Self { runner: Box::new(RealCommandRunner), socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())) }
216
+ Self {
217
+ runner: Box::new(RealCommandRunner),
218
+ socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())),
219
+ }
202
220
  } else {
203
221
  Self::new()
204
222
  }
@@ -206,25 +224,50 @@ impl TmuxBackend {
206
224
 
207
225
  /// Backend with an injected runner (tests: canned/recording tmux output). Shared default socket.
208
226
  pub fn with_runner(runner: Box<dyn CommandRunner>) -> Self {
209
- Self { runner, socket: None }
227
+ Self {
228
+ runner,
229
+ socket: None,
230
+ }
210
231
  }
211
232
 
212
233
  /// Backend with an injected runner bound to a per-workspace socket (tests: assert the `-L` is in
213
234
  /// the recorded argv for a workspace-bound backend).
214
235
  pub fn with_runner_for_workspace(runner: Box<dyn CommandRunner>, workspace: &Path) -> Self {
215
- Self { runner, socket: Some(TmuxSocketEndpoint::Name(socket_name_for_workspace(workspace))) }
236
+ Self {
237
+ runner,
238
+ socket: Some(TmuxSocketEndpoint::Name(socket_name_for_workspace(
239
+ workspace,
240
+ ))),
241
+ }
216
242
  }
217
243
 
218
- pub(crate) fn with_runner_for_tmux_endpoint(runner: Box<dyn CommandRunner>, endpoint: &str) -> Self {
244
+ pub(crate) fn with_runner_for_tmux_endpoint(
245
+ runner: Box<dyn CommandRunner>,
246
+ endpoint: &str,
247
+ ) -> Self {
219
248
  if Path::new(endpoint).is_absolute() {
220
- Self { runner, socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())) }
249
+ Self {
250
+ runner,
251
+ socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())),
252
+ }
221
253
  } else if endpoint.is_empty() || endpoint == "default" {
222
- Self { runner, socket: None }
254
+ Self {
255
+ runner,
256
+ socket: None,
257
+ }
223
258
  } else {
224
- Self { runner, socket: None }
259
+ Self {
260
+ runner,
261
+ socket: None,
262
+ }
225
263
  }
226
264
  }
227
265
 
266
+ /// Build the exact argv that a workspace-bound tmux backend will execute.
267
+ pub fn argv_for_workspace(workspace: &Path, argv: &[String]) -> Vec<String> {
268
+ Self::for_workspace(workspace).tmux_argv(argv)
269
+ }
270
+
228
271
  /// THE RUN CHOKEPOINT: every executed `tmux` argv is funneled through here. When a per-team
229
272
  /// socket is set, inject `-L <socket>` right after the leading "tmux" token; otherwise pass argv
230
273
  /// through unchanged. Non-`tmux` argv (e.g. the spawned provider command) is never rewritten.
@@ -257,10 +300,7 @@ impl TmuxBackend {
257
300
  if self.socket.is_none() {
258
301
  return;
259
302
  }
260
- let argv = self.tmux_argv(&[
261
- "tmux".to_string(),
262
- "kill-server".to_string(),
263
- ]);
303
+ let argv = self.tmux_argv(&["tmux".to_string(), "kill-server".to_string()]);
264
304
  let _ = self.runner.run(&argv);
265
305
  }
266
306
  }
@@ -362,10 +402,13 @@ impl TmuxBackend {
362
402
 
363
403
  fn run_spawn(&self, argv: &[String]) -> Result<CommandOutput, TransportError> {
364
404
  let argv = self.tmux_argv(argv);
365
- let output = self.runner.run(&argv).map_err(|source| TransportError::Spawn {
366
- backend: BackendKind::Tmux,
367
- source,
368
- })?;
405
+ let output = self
406
+ .runner
407
+ .run(&argv)
408
+ .map_err(|source| TransportError::Spawn {
409
+ backend: BackendKind::Tmux,
410
+ source,
411
+ })?;
369
412
  if output.success {
370
413
  Ok(output)
371
414
  } else {
@@ -373,16 +416,12 @@ impl TmuxBackend {
373
416
  }
374
417
  }
375
418
 
376
- fn run_inject_stage(
377
- &self,
378
- argv: &[String],
379
- stage: InjectStage,
380
- ) -> Result<(), TransportError> {
419
+ fn run_inject_stage(&self, argv: &[String], stage: InjectStage) -> Result<(), TransportError> {
381
420
  let argv = self.tmux_argv(argv);
382
- let output = self.runner.run(&argv).map_err(|source| TransportError::Inject {
383
- stage,
384
- source,
385
- })?;
421
+ let output = self
422
+ .runner
423
+ .run(&argv)
424
+ .map_err(|source| TransportError::Inject { stage, source })?;
386
425
  if output.success {
387
426
  Ok(())
388
427
  } else {
@@ -397,10 +436,10 @@ impl TmuxBackend {
397
436
  stdin: &str,
398
437
  ) -> Result<(), TransportError> {
399
438
  let argv = self.tmux_argv(argv);
400
- let output = self.runner.run_with_stdin(&argv, stdin).map_err(|source| TransportError::Inject {
401
- stage,
402
- source,
403
- })?;
439
+ let output = self
440
+ .runner
441
+ .run_with_stdin(&argv, stdin)
442
+ .map_err(|source| TransportError::Inject { stage, source })?;
404
443
  if output.success {
405
444
  Ok(())
406
445
  } else {
@@ -494,6 +533,14 @@ fn submit_verification_for_key(key: Key) -> SubmitVerification {
494
533
  }
495
534
  }
496
535
 
536
+ fn capture_has_pasted_content_prompt(text: &str) -> bool {
537
+ let lower = text.to_ascii_lowercase();
538
+ lower.contains("pasted content") || lower.contains("pasted text")
539
+ }
540
+
541
+ const PASTED_CONTENT_APPEAR_POLLS: u32 = 5;
542
+ const PASTED_CONTENT_SUBMIT_ATTEMPTS: u32 = 3;
543
+
497
544
  fn shell_command(argv: &[String], cwd: &Path, env: &BTreeMap<String, String>) -> String {
498
545
  let mut parts = Vec::new();
499
546
  parts.push("cd".to_string());
@@ -579,7 +626,41 @@ impl Transport for TmuxBackend {
579
626
  self.run_inject_stage(&argv, stage)?;
580
627
  }
581
628
  }
629
+ let mut saw_pasted_prompt = false;
630
+ for _ in 0..PASTED_CONTENT_APPEAR_POLLS {
631
+ let captured = self.capture(target, CaptureRange::Tail(80))?;
632
+ if capture_has_pasted_content_prompt(&captured.text) {
633
+ saw_pasted_prompt = true;
634
+ break;
635
+ }
636
+ std::thread::sleep(Duration::from_millis(25));
637
+ }
582
638
  let submit_argv = tmux_send_keys_argv(&pane, &[submit]);
639
+ if saw_pasted_prompt {
640
+ let mut attempts = 0;
641
+ let mut cleared = false;
642
+ for _ in 0..PASTED_CONTENT_SUBMIT_ATTEMPTS {
643
+ attempts += 1;
644
+ self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
645
+ let captured = self.capture(target, CaptureRange::Tail(80))?;
646
+ if !capture_has_pasted_content_prompt(&captured.text) {
647
+ cleared = true;
648
+ break;
649
+ }
650
+ }
651
+ return Ok(InjectReport {
652
+ stage_reached: InjectStage::Submit,
653
+ inject_verification:
654
+ InjectVerification::CaptureContainsNewPastedContentPrompt,
655
+ submit_verification: if cleared {
656
+ SubmitVerification::PastedContentPromptAbsentAfterSubmit
657
+ } else {
658
+ SubmitVerification::PastedContentPromptStillPresentAfterSubmit
659
+ },
660
+ turn_verification: TurnVerification::NotYetObserved,
661
+ attempts,
662
+ });
663
+ }
583
664
  self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
584
665
  }
585
666
  }
@@ -615,7 +696,10 @@ impl Transport for TmuxBackend {
615
696
  ) -> Result<CapturedText, TransportError> {
616
697
  let pane = pane_from_target(target);
617
698
  let argv = self.tmux_argv(&tmux_capture_argv(&pane, range));
618
- let output = self.runner.run(&argv).map_err(|source| TransportError::Capture { source })?;
699
+ let output = self
700
+ .runner
701
+ .run(&argv)
702
+ .map_err(|source| TransportError::Capture { source })?;
619
703
  if !output.success {
620
704
  return Err(subprocess_error(argv, output));
621
705
  }
@@ -625,11 +709,7 @@ impl Transport for TmuxBackend {
625
709
  })
626
710
  }
627
711
 
628
- fn query(
629
- &self,
630
- target: &Target,
631
- field: PaneField,
632
- ) -> Result<Option<String>, TransportError> {
712
+ fn query(&self, target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
633
713
  let pane = pane_from_target(target);
634
714
  let argv = self.tmux_argv(&tmux_query_argv(&pane, field));
635
715
  let output = self.runner.run(&argv)?;
@@ -652,7 +732,11 @@ impl Transport for TmuxBackend {
652
732
  if output.success {
653
733
  return Ok(PaneLiveness::Live);
654
734
  }
655
- if output.stderr.to_ascii_lowercase().contains("can't find pane") {
735
+ if output
736
+ .stderr
737
+ .to_ascii_lowercase()
738
+ .contains("can't find pane")
739
+ {
656
740
  Ok(PaneLiveness::Dead)
657
741
  } else {
658
742
  Ok(PaneLiveness::Unknown)
@@ -674,7 +758,10 @@ impl Transport for TmuxBackend {
674
758
  }
675
759
  let mut panes = Vec::new();
676
760
  for line in output.stdout.lines().filter(|line| !line.is_empty()) {
677
- if let Some(pane) = parse_pane_info_line(line) {
761
+ if let Some(mut pane) = parse_pane_info_line(line) {
762
+ if pane.pane_pid.is_none() {
763
+ pane.pane_pid = query_pane_pid(self, &pane.pane_id)?;
764
+ }
678
765
  panes.push(pane);
679
766
  }
680
767
  }
@@ -692,10 +779,7 @@ impl Transport for TmuxBackend {
692
779
  Ok(output.success)
693
780
  }
694
781
 
695
- fn list_windows(
696
- &self,
697
- session: &SessionName,
698
- ) -> Result<Vec<WindowName>, TransportError> {
782
+ fn list_windows(&self, session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
699
783
  // golden runtime.py:1023-1029 `_tmux_window_exists`: `tmux list-windows -t <s> -F #{window_name}`;
700
784
  // returncode != 0 -> false (here: an empty window set), else the window names by line.
701
785
  let argv = self.tmux_argv(&[
@@ -737,29 +821,26 @@ impl Transport for TmuxBackend {
737
821
  }
738
822
 
739
823
  fn kill_session(&self, session: &SessionName) -> Result<(), TransportError> {
740
- let argv = vec![
824
+ let argv = self.tmux_argv(&[
741
825
  "tmux".to_string(),
742
826
  "kill-session".to_string(),
743
827
  "-t".to_string(),
744
828
  session.as_str().to_string(),
745
- ];
829
+ ]);
746
830
  self.run_ok(&argv)
747
831
  }
748
832
 
749
833
  fn kill_window(&self, target: &Target) -> Result<(), TransportError> {
750
- let argv = vec![
834
+ let argv = self.tmux_argv(&[
751
835
  "tmux".to_string(),
752
836
  "kill-window".to_string(),
753
837
  "-t".to_string(),
754
838
  target_name(target),
755
- ];
839
+ ]);
756
840
  self.run_ok(&argv)
757
841
  }
758
842
 
759
- fn attach_session(
760
- &self,
761
- session: &SessionName,
762
- ) -> Result<AttachOutcome, TransportError> {
843
+ fn attach_session(&self, session: &SessionName) -> Result<AttachOutcome, TransportError> {
763
844
  let argv = [
764
845
  "tmux".to_string(),
765
846
  "attach-session".to_string(),
@@ -771,6 +852,22 @@ impl Transport for TmuxBackend {
771
852
  }
772
853
  }
773
854
 
855
+ fn query_pane_pid(backend: &TmuxBackend, pane: &PaneId) -> Result<Option<u32>, TransportError> {
856
+ let argv = backend.tmux_argv(&[
857
+ "tmux".to_string(),
858
+ "display-message".to_string(),
859
+ "-p".to_string(),
860
+ "-t".to_string(),
861
+ pane.as_str().to_string(),
862
+ "#{pane_pid}".to_string(),
863
+ ]);
864
+ let output = backend.runner.run(&argv)?;
865
+ if !output.success {
866
+ return Ok(None);
867
+ }
868
+ Ok(parse_optional_u32(output.stdout.trim()))
869
+ }
870
+
774
871
  fn parse_pane_info_line(line: &str) -> Option<PaneInfo> {
775
872
  let fields = line.split('\t').collect::<Vec<_>>();
776
873
  if fields.len() < 11 {
@@ -786,7 +883,7 @@ fn parse_pane_info_line(line: &str) -> Option<PaneInfo> {
786
883
  current_command: non_empty(fields[6]).map(str::to_string),
787
884
  active: fields[7] == "1",
788
885
  current_path: non_empty(fields[8]).map(PathBuf::from),
789
- pane_pid: None,
886
+ pane_pid: fields.get(11).and_then(|raw| parse_optional_u32(raw)),
790
887
  leader_env: BTreeMap::new(),
791
888
  })
792
889
  }
@@ -21,6 +21,7 @@ pub struct SpawnRecord {
21
21
  #[derive(Debug, Clone, Default)]
22
22
  struct OfflineState {
23
23
  session_present: bool,
24
+ session_absent_after_spawn_first: bool,
24
25
  targets: Vec<PaneInfo>,
25
26
  windows: Vec<WindowName>,
26
27
  calls: Vec<&'static str>,
@@ -44,6 +45,11 @@ impl OfflineTransport {
44
45
  self
45
46
  }
46
47
 
48
+ pub fn with_session_absent_after_spawn_first(self) -> Self {
49
+ self.with_state(|state| state.session_absent_after_spawn_first = true);
50
+ self
51
+ }
52
+
47
53
  pub fn with_targets(self, targets: Vec<PaneInfo>) -> Self {
48
54
  self.with_state(|state| state.targets = targets);
49
55
  self
@@ -100,6 +106,9 @@ impl OfflineTransport {
100
106
  let pane_index = self.with_state(|state| {
101
107
  state.calls.push(kind);
102
108
  state.spawns.push(SpawnRecord { kind: kind.to_string(), argv: argv.to_vec() });
109
+ if kind == "spawn_first" && !state.session_absent_after_spawn_first {
110
+ state.session_present = true;
111
+ }
103
112
  state.spawns.len().saturating_sub(1)
104
113
  });
105
114
  SpawnResult {
@@ -103,6 +103,10 @@
103
103
  submit_verification_wire(SubmitVerification::PastedContentPromptAbsentAfterSubmit),
104
104
  "pasted_content_prompt_absent_after_submit"
105
105
  );
106
+ assert_eq!(
107
+ submit_verification_wire(SubmitVerification::PastedContentPromptStillPresentAfterSubmit),
108
+ "pasted_content_prompt_still_present_after_submit"
109
+ );
106
110
  assert_eq!(
107
111
  submit_verification_wire(SubmitVerification::SendKeysFailed),
108
112
  "send_keys_failed"
@@ -165,6 +165,7 @@ pub enum Key {
165
165
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
166
166
  pub enum CaptureRange {
167
167
  Tail(u32),
168
+ Head(u32),
168
169
  Full,
169
170
  }
170
171
 
@@ -290,6 +291,8 @@ pub enum SubmitVerification {
290
291
  EnterSentWithoutPlaceholderCheck,
291
292
  /// `pasted_content_prompt_absent_after_submit`。
292
293
  PastedContentPromptAbsentAfterSubmit,
294
+ /// `pasted_content_prompt_still_present_after_submit`。
295
+ PastedContentPromptStillPresentAfterSubmit,
293
296
  /// `{key}_sent_after_visible_token`(key 由 variant 携带)。
294
297
  KeySentAfterVisibleToken { key: Key },
295
298
  /// `send_keys_failed`。
@@ -580,9 +583,10 @@ pub fn tmux_cancel_mode_argv(pane: &PaneId, mode: PaneMode) -> Vec<String> {
580
583
  pub fn tmux_capture_argv(pane: &PaneId, range: CaptureRange) -> Vec<String> {
581
584
  let spec = match range {
582
585
  CaptureRange::Tail(lines) => format!("-{lines}"),
586
+ CaptureRange::Head(_) => "0".to_string(),
583
587
  CaptureRange::Full => "-".to_string(),
584
588
  };
585
- vec![
589
+ let mut argv = vec![
586
590
  "tmux".to_string(),
587
591
  "capture-pane".to_string(),
588
592
  "-p".to_string(),
@@ -590,7 +594,11 @@ pub fn tmux_capture_argv(pane: &PaneId, range: CaptureRange) -> Vec<String> {
590
594
  spec,
591
595
  "-t".to_string(),
592
596
  pane.as_str().to_string(),
593
- ]
597
+ ];
598
+ if let CaptureRange::Head(lines) = range {
599
+ argv.extend(["-E".to_string(), lines.saturating_sub(1).to_string()]);
600
+ }
601
+ argv
594
602
  }
595
603
 
596
604
  /// PaneField → `display-message -p -t <target> [-F] <fmt>`。
@@ -748,6 +756,9 @@ pub fn submit_verification_wire(v: SubmitVerification) -> String {
748
756
  SubmitVerification::PastedContentPromptAbsentAfterSubmit => {
749
757
  "pasted_content_prompt_absent_after_submit".to_string()
750
758
  }
759
+ SubmitVerification::PastedContentPromptStillPresentAfterSubmit => {
760
+ "pasted_content_prompt_still_present_after_submit".to_string()
761
+ }
751
762
  SubmitVerification::KeySentAfterVisibleToken { key } => {
752
763
  format!("{}_sent_after_visible_token", tmux_key_name(key))
753
764
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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.3.1",
24
- "@team-agent/cli-darwin-x64": "0.3.1",
25
- "@team-agent/cli-linux-x64": "0.3.1"
23
+ "@team-agent/cli-darwin-arm64": "0.3.3",
24
+ "@team-agent/cli-darwin-x64": "0.3.3",
25
+ "@team-agent/cli-linux-x64": "0.3.3"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",