@team-agent/installer 0.3.2 → 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 +196 -19
- package/crates/team-agent/src/cli/diagnose.rs +144 -10
- package/crates/team-agent/src/cli/emit.rs +286 -52
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +799 -316
- package/crates/team-agent/src/cli/status_port.rs +25 -2
- 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 +57 -3
- package/crates/team-agent/src/cli/types.rs +17 -0
- package/crates/team-agent/src/compiler.rs +15 -5
- package/crates/team-agent/src/coordinator/health.rs +89 -20
- 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 +818 -116
- 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 +177 -83
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +443 -9
- package/crates/team-agent/src/lifecycle/restart/remove.rs +22 -6
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +4 -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 +87 -37
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +153 -16
- 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 +483 -67
- 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/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 +57 -0
- package/crates/team-agent/src/state/projection.rs +32 -23
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +97 -60
- 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
|
@@ -19,6 +19,13 @@ use crate::state::persist::{load_runtime_state, save_runtime_state_with_deleted_
|
|
|
19
19
|
/// `team_state_key`(`state.py:93`):从 team_dir(.name)/spec_path(.parent.name)派生 team key,
|
|
20
20
|
/// 跳过 `.team`/`runtime`;兜底 `session_name` 或 `"current"`。
|
|
21
21
|
pub fn team_state_key(state: &Value) -> String {
|
|
22
|
+
if let Some(team_key) = state
|
|
23
|
+
.get("team_key")
|
|
24
|
+
.and_then(Value::as_str)
|
|
25
|
+
.filter(|key| !key.is_empty())
|
|
26
|
+
{
|
|
27
|
+
return team_key.to_string();
|
|
28
|
+
}
|
|
22
29
|
for field in ["team_dir", "spec_path"] {
|
|
23
30
|
// Python `if not value: continue` —— None/空串 falsy 跳过。
|
|
24
31
|
let value = match state.get(field).and_then(Value::as_str) {
|
|
@@ -68,9 +75,7 @@ pub fn resolve_owner_team_id(state: &Value, owner_team_id: &str) -> OwnerTeamRes
|
|
|
68
75
|
}
|
|
69
76
|
let teams = state.get("teams").and_then(Value::as_object);
|
|
70
77
|
if teams.is_some_and(|teams| teams.contains_key(requested)) {
|
|
71
|
-
|
|
72
|
-
return OwnerTeamResolution::Canonical(requested.to_string());
|
|
73
|
-
}
|
|
78
|
+
return OwnerTeamResolution::Canonical(requested.to_string());
|
|
74
79
|
}
|
|
75
80
|
if teams.is_none_or(Map::is_empty) {
|
|
76
81
|
let active = state.get("active_team_key").and_then(Value::as_str).unwrap_or("");
|
|
@@ -113,21 +118,6 @@ pub fn resolve_owner_team_id(state: &Value, owner_team_id: &str) -> OwnerTeamRes
|
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
fn has_top_level_runtime_content(state: &Value) -> bool {
|
|
117
|
-
[
|
|
118
|
-
"session_name",
|
|
119
|
-
"team_dir",
|
|
120
|
-
"spec_path",
|
|
121
|
-
"workspace",
|
|
122
|
-
"agents",
|
|
123
|
-
"tasks",
|
|
124
|
-
"leader_receiver",
|
|
125
|
-
"team_owner",
|
|
126
|
-
]
|
|
127
|
-
.into_iter()
|
|
128
|
-
.any(|key| state.get(key).is_some_and(super::json_truthy))
|
|
129
|
-
}
|
|
130
|
-
|
|
131
121
|
fn legacy_owner_team_aliases(entry: &Value) -> impl Iterator<Item = String> + '_ {
|
|
132
122
|
let scalar_paths = [
|
|
133
123
|
"/team/name",
|
|
@@ -357,11 +347,7 @@ pub fn select_runtime_state(workspace: &Path, team: Option<&str>) -> Result<Valu
|
|
|
357
347
|
}
|
|
358
348
|
let matches: Vec<&String> = alive
|
|
359
349
|
.iter()
|
|
360
|
-
.filter(|(key, value)|
|
|
361
|
-
let session = value.get("session_name").and_then(Value::as_str).unwrap_or("");
|
|
362
|
-
let dir = value.get("team_dir").and_then(Value::as_str).unwrap_or("");
|
|
363
|
-
team == key.as_str() || team == session || team == dir
|
|
364
|
-
})
|
|
350
|
+
.filter(|(key, value)| team_selector_matches(team, key, value))
|
|
365
351
|
.map(|(k, _)| k)
|
|
366
352
|
.collect();
|
|
367
353
|
if matches.len() == 1 {
|
|
@@ -398,6 +384,29 @@ pub fn select_runtime_state(workspace: &Path, team: Option<&str>) -> Result<Valu
|
|
|
398
384
|
))
|
|
399
385
|
}
|
|
400
386
|
|
|
387
|
+
fn team_selector_matches(team: &str, key: &str, value: &Value) -> bool {
|
|
388
|
+
if team == key {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
let session = value.get("session_name").and_then(Value::as_str).unwrap_or("");
|
|
392
|
+
if team == session {
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
if let Some(stripped) = session.strip_prefix("team-") {
|
|
396
|
+
if team == stripped {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
let dir = value.get("team_dir").and_then(Value::as_str).unwrap_or("");
|
|
401
|
+
if team == dir {
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
std::path::Path::new(dir)
|
|
405
|
+
.file_name()
|
|
406
|
+
.and_then(|name| name.to_str())
|
|
407
|
+
.is_some_and(|name| team == name)
|
|
408
|
+
}
|
|
409
|
+
|
|
401
410
|
/// `ambiguous_team_target_result`(`state.py:226`):无显式 team 且多候选 → 拒绝 dict;否则 None。
|
|
402
411
|
pub fn ambiguous_team_target_result(state: &Value) -> Option<Value> {
|
|
403
412
|
let alive = team_state_candidates(state);
|
|
@@ -98,8 +98,11 @@ fn spec_workspace_from_state(state: &Value) -> Option<PathBuf> {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
fn selected_team_key(state: &Value, team: Option<&str>) -> String {
|
|
101
|
-
|
|
101
|
+
state
|
|
102
|
+
.get("active_team_key")
|
|
103
|
+
.and_then(Value::as_str)
|
|
104
|
+
.filter(|s| !s.is_empty())
|
|
102
105
|
.map(ToString::to_string)
|
|
103
|
-
.or_else(||
|
|
106
|
+
.or_else(|| team.filter(|s| !s.is_empty()).map(ToString::to_string))
|
|
104
107
|
.unwrap_or_else(|| team_state_key(state))
|
|
105
108
|
}
|
|
@@ -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 {
|
|
@@ -611,11 +650,12 @@ impl Transport for TmuxBackend {
|
|
|
611
650
|
}
|
|
612
651
|
return Ok(InjectReport {
|
|
613
652
|
stage_reached: InjectStage::Submit,
|
|
614
|
-
inject_verification:
|
|
653
|
+
inject_verification:
|
|
654
|
+
InjectVerification::CaptureContainsNewPastedContentPrompt,
|
|
615
655
|
submit_verification: if cleared {
|
|
616
656
|
SubmitVerification::PastedContentPromptAbsentAfterSubmit
|
|
617
657
|
} else {
|
|
618
|
-
|
|
658
|
+
SubmitVerification::PastedContentPromptStillPresentAfterSubmit
|
|
619
659
|
},
|
|
620
660
|
turn_verification: TurnVerification::NotYetObserved,
|
|
621
661
|
attempts,
|
|
@@ -656,7 +696,10 @@ impl Transport for TmuxBackend {
|
|
|
656
696
|
) -> Result<CapturedText, TransportError> {
|
|
657
697
|
let pane = pane_from_target(target);
|
|
658
698
|
let argv = self.tmux_argv(&tmux_capture_argv(&pane, range));
|
|
659
|
-
let output = self
|
|
699
|
+
let output = self
|
|
700
|
+
.runner
|
|
701
|
+
.run(&argv)
|
|
702
|
+
.map_err(|source| TransportError::Capture { source })?;
|
|
660
703
|
if !output.success {
|
|
661
704
|
return Err(subprocess_error(argv, output));
|
|
662
705
|
}
|
|
@@ -666,11 +709,7 @@ impl Transport for TmuxBackend {
|
|
|
666
709
|
})
|
|
667
710
|
}
|
|
668
711
|
|
|
669
|
-
fn query(
|
|
670
|
-
&self,
|
|
671
|
-
target: &Target,
|
|
672
|
-
field: PaneField,
|
|
673
|
-
) -> Result<Option<String>, TransportError> {
|
|
712
|
+
fn query(&self, target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
|
|
674
713
|
let pane = pane_from_target(target);
|
|
675
714
|
let argv = self.tmux_argv(&tmux_query_argv(&pane, field));
|
|
676
715
|
let output = self.runner.run(&argv)?;
|
|
@@ -693,7 +732,11 @@ impl Transport for TmuxBackend {
|
|
|
693
732
|
if output.success {
|
|
694
733
|
return Ok(PaneLiveness::Live);
|
|
695
734
|
}
|
|
696
|
-
if output
|
|
735
|
+
if output
|
|
736
|
+
.stderr
|
|
737
|
+
.to_ascii_lowercase()
|
|
738
|
+
.contains("can't find pane")
|
|
739
|
+
{
|
|
697
740
|
Ok(PaneLiveness::Dead)
|
|
698
741
|
} else {
|
|
699
742
|
Ok(PaneLiveness::Unknown)
|
|
@@ -736,10 +779,7 @@ impl Transport for TmuxBackend {
|
|
|
736
779
|
Ok(output.success)
|
|
737
780
|
}
|
|
738
781
|
|
|
739
|
-
fn list_windows(
|
|
740
|
-
&self,
|
|
741
|
-
session: &SessionName,
|
|
742
|
-
) -> Result<Vec<WindowName>, TransportError> {
|
|
782
|
+
fn list_windows(&self, session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
743
783
|
// golden runtime.py:1023-1029 `_tmux_window_exists`: `tmux list-windows -t <s> -F #{window_name}`;
|
|
744
784
|
// returncode != 0 -> false (here: an empty window set), else the window names by line.
|
|
745
785
|
let argv = self.tmux_argv(&[
|
|
@@ -800,10 +840,7 @@ impl Transport for TmuxBackend {
|
|
|
800
840
|
self.run_ok(&argv)
|
|
801
841
|
}
|
|
802
842
|
|
|
803
|
-
fn attach_session(
|
|
804
|
-
&self,
|
|
805
|
-
session: &SessionName,
|
|
806
|
-
) -> Result<AttachOutcome, TransportError> {
|
|
843
|
+
fn attach_session(&self, session: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
807
844
|
let argv = [
|
|
808
845
|
"tmux".to_string(),
|
|
809
846
|
"attach-session".to_string(),
|
|
@@ -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",
|