@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.
- package/Cargo.lock +34 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +234 -26
- package/crates/team-agent/src/cli/diagnose.rs +144 -10
- package/crates/team-agent/src/cli/emit.rs +289 -54
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +1281 -196
- package/crates/team-agent/src/cli/status_port.rs +195 -46
- package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +59 -3
- package/crates/team-agent/src/cli/types.rs +18 -0
- package/crates/team-agent/src/compiler.rs +15 -5
- package/crates/team-agent/src/coordinator/health.rs +95 -17
- package/crates/team-agent/src/coordinator/mod.rs +4 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
- package/crates/team-agent/src/coordinator/tick.rs +222 -69
- package/crates/team-agent/src/coordinator/types.rs +15 -3
- package/crates/team-agent/src/db/schema.rs +37 -2
- package/crates/team-agent/src/diagnose/comms.rs +226 -0
- package/crates/team-agent/src/diagnose/mod.rs +45 -0
- package/crates/team-agent/src/diagnose/orphans.rs +658 -0
- package/crates/team-agent/src/fake_worker.rs +146 -3
- package/crates/team-agent/src/leader/start.rs +121 -23
- package/crates/team-agent/src/leader/types.rs +44 -1
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +645 -47
- package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
- package/crates/team-agent/src/lifecycle/mod.rs +2 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
- package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
- package/crates/team-agent/src/lifecycle/types.rs +19 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
- package/crates/team-agent/src/mcp_server/mod.rs +3 -74
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
- package/crates/team-agent/src/mcp_server/tools.rs +312 -111
- package/crates/team-agent/src/mcp_server/types.rs +6 -4
- package/crates/team-agent/src/mcp_server/wire.rs +19 -7
- package/crates/team-agent/src/message_store.rs +21 -4
- package/crates/team-agent/src/messaging/delivery.rs +470 -59
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +353 -63
- package/crates/team-agent/src/messaging/selftest.rs +199 -12
- package/crates/team-agent/src/messaging/send.rs +35 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
- package/crates/team-agent/src/messaging/types.rs +11 -3
- package/crates/team-agent/src/os_probe.rs +119 -0
- package/crates/team-agent/src/packaging/migrate.rs +10 -2
- package/crates/team-agent/src/packaging/tests.rs +23 -0
- package/crates/team-agent/src/provider/adapter.rs +564 -63
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
- package/crates/team-agent/src/provider/classify.rs +51 -4
- package/crates/team-agent/src/provider/helpers.rs +10 -1
- package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
- package/crates/team-agent/src/provider/types.rs +47 -0
- package/crates/team-agent/src/session_capture.rs +616 -0
- package/crates/team-agent/src/state/persist.rs +170 -1
- package/crates/team-agent/src/state/projection.rs +141 -8
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +161 -64
- package/crates/team-agent/src/transport/test_support.rs +9 -0
- package/crates/team-agent/src/transport/tests/wire.rs +4 -0
- package/crates/team-agent/src/transport.rs +13 -2
- 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,
|
|
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() {
|
|
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
|
|
100
|
-
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 {
|
|
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(
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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(
|
|
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 {
|
|
249
|
+
Self {
|
|
250
|
+
runner,
|
|
251
|
+
socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())),
|
|
252
|
+
}
|
|
221
253
|
} else if endpoint.is_empty() || endpoint == "default" {
|
|
222
|
-
Self {
|
|
254
|
+
Self {
|
|
255
|
+
runner,
|
|
256
|
+
socket: None,
|
|
257
|
+
}
|
|
223
258
|
} else {
|
|
224
|
-
Self {
|
|
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
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
401
|
-
|
|
402
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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.
|
|
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.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.3.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.3.
|
|
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",
|