@team-agent/installer 0.5.8 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +82 -1
- package/crates/team-agent/src/cli/emit.rs +30 -1
- package/crates/team-agent/src/cli/leaders.rs +94 -0
- package/crates/team-agent/src/cli/mod.rs +138 -2
- package/crates/team-agent/src/cli/named_address.rs +254 -34
- package/crates/team-agent/src/cli/send.rs +370 -3
- package/crates/team-agent/src/cli/status_port.rs +20 -6
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +9 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +14 -0
- package/crates/team-agent/src/leader/lease.rs +80 -3
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/registry.rs +402 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +79 -0
- package/crates/team-agent/src/mcp_server/tools.rs +8 -1
- package/crates/team-agent/src/messaging/leader_receiver.rs +58 -0
- package/crates/team-agent/src/messaging/mod.rs +2 -1
- package/crates/team-agent/src/messaging/results.rs +53 -0
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/transport/tests/mod.rs +1 -0
- package/crates/team-agent/src/transport.rs +6 -0
- package/package.json +4 -4
|
@@ -18,6 +18,14 @@ pub(crate) enum NamedAddressErrorKind {
|
|
|
18
18
|
NameInvalid,
|
|
19
19
|
WorkspaceNotFound,
|
|
20
20
|
StateNotFound,
|
|
21
|
+
// E6 (0.5.9 offline-mailbox-toname-design §3.1): three-way taxonomy for
|
|
22
|
+
// `--to-name <team>/leader` refusal — required by the E6 RED so the top-level
|
|
23
|
+
// `leader_receiver` fallback is no longer reached for a wrong runtime key.
|
|
24
|
+
// `WorkspaceNoState` mirrors `StateNotFound` semantics but keeps the wire
|
|
25
|
+
// reason distinct for third-party sender copy.
|
|
26
|
+
WorkspaceNoState,
|
|
27
|
+
TeamKeyNotFound,
|
|
28
|
+
LeaderNotAttached,
|
|
21
29
|
NameNotResolvable,
|
|
22
30
|
NameNotLive,
|
|
23
31
|
NameAmbiguous,
|
|
@@ -29,6 +37,9 @@ impl NamedAddressErrorKind {
|
|
|
29
37
|
Self::NameInvalid => "name_invalid",
|
|
30
38
|
Self::WorkspaceNotFound => "workspace_not_found",
|
|
31
39
|
Self::StateNotFound => "state_not_found",
|
|
40
|
+
Self::WorkspaceNoState => "workspace_no_state",
|
|
41
|
+
Self::TeamKeyNotFound => "team_key_not_found",
|
|
42
|
+
Self::LeaderNotAttached => "leader_not_attached",
|
|
32
43
|
Self::NameNotResolvable => "name_not_resolvable",
|
|
33
44
|
Self::NameNotLive => "name_not_live",
|
|
34
45
|
Self::NameAmbiguous => "name_ambiguous",
|
|
@@ -43,6 +54,13 @@ pub(crate) struct NamedAddressError {
|
|
|
43
54
|
pub action: String,
|
|
44
55
|
pub log: String,
|
|
45
56
|
pub candidates: Vec<Value>,
|
|
57
|
+
// E6: when the caller supplied a `<team>/leader` name that isn't a runtime
|
|
58
|
+
// team_key, echo the requested value + the canonical alternatives so
|
|
59
|
+
// spec/display-name confusion is visibly diagnosed. Empty for other
|
|
60
|
+
// refusal kinds.
|
|
61
|
+
pub requested_team: Option<String>,
|
|
62
|
+
pub available_team_keys: Vec<String>,
|
|
63
|
+
pub suggested_name: Option<String>,
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
impl NamedAddressError {
|
|
@@ -53,6 +71,9 @@ impl NamedAddressError {
|
|
|
53
71
|
action: "inspect team-agent status and use an explicit workspace/team name".to_string(),
|
|
54
72
|
log: "named-address resolver refused before injection".to_string(),
|
|
55
73
|
candidates: Vec::new(),
|
|
74
|
+
requested_team: None,
|
|
75
|
+
available_team_keys: Vec::new(),
|
|
76
|
+
suggested_name: None,
|
|
56
77
|
}
|
|
57
78
|
}
|
|
58
79
|
|
|
@@ -66,15 +87,35 @@ impl NamedAddressError {
|
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
pub(crate) fn to_json(&self) -> Value {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
let mut obj = serde_json::Map::new();
|
|
91
|
+
obj.insert("ok".to_string(), Value::Bool(false));
|
|
92
|
+
obj.insert("status".to_string(), Value::String("refused".to_string()));
|
|
93
|
+
obj.insert(
|
|
94
|
+
"reason".to_string(),
|
|
95
|
+
Value::String(self.kind.as_str().to_string()),
|
|
96
|
+
);
|
|
97
|
+
obj.insert("error".to_string(), Value::String(self.message.clone()));
|
|
98
|
+
obj.insert("action".to_string(), Value::String(self.action.clone()));
|
|
99
|
+
obj.insert("log".to_string(), Value::String(self.log.clone()));
|
|
100
|
+
obj.insert("candidates".to_string(), Value::Array(self.candidates.clone()));
|
|
101
|
+
if let Some(requested) = self.requested_team.as_deref() {
|
|
102
|
+
obj.insert("requested_team".to_string(), Value::String(requested.to_string()));
|
|
103
|
+
}
|
|
104
|
+
if !self.available_team_keys.is_empty() {
|
|
105
|
+
obj.insert(
|
|
106
|
+
"available_team_keys".to_string(),
|
|
107
|
+
Value::Array(
|
|
108
|
+
self.available_team_keys
|
|
109
|
+
.iter()
|
|
110
|
+
.map(|k| Value::String(k.clone()))
|
|
111
|
+
.collect(),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if let Some(suggested) = self.suggested_name.as_deref() {
|
|
116
|
+
obj.insert("suggested_name".to_string(), Value::String(suggested.to_string()));
|
|
117
|
+
}
|
|
118
|
+
Value::Object(obj)
|
|
78
119
|
}
|
|
79
120
|
}
|
|
80
121
|
|
|
@@ -226,6 +267,26 @@ pub(crate) fn resolve_name_for_cli(
|
|
|
226
267
|
Ok((resolved, transport))
|
|
227
268
|
}
|
|
228
269
|
|
|
270
|
+
/// E6 wiring helper (`cli::send::maybe_enqueue_offline_leader_mailbox`):
|
|
271
|
+
/// re-parse a `<workspace>::<team>/leader` name and return the resolved
|
|
272
|
+
/// target workspace + canonical team_key so send.rs can enqueue the
|
|
273
|
+
/// mailbox without duplicating the parser. Returns `Ok(None)` for names
|
|
274
|
+
/// that aren't `<team>/leader` shapes (worker sends, session:window,
|
|
275
|
+
/// etc. keep their existing refusal semantics).
|
|
276
|
+
pub(crate) fn parse_leader_target_workspace_and_team(
|
|
277
|
+
sender_workspace: &Path,
|
|
278
|
+
raw_name: &str,
|
|
279
|
+
) -> Result<Option<(PathBuf, String)>, NamedAddressError> {
|
|
280
|
+
let parsed = parse_named_address(raw_name)?;
|
|
281
|
+
let target_workspace = resolve_workspace(sender_workspace, parsed.workspace.as_deref())?;
|
|
282
|
+
match parsed.target {
|
|
283
|
+
ParsedTarget::TeamEntity { team, entity } if entity == "leader" => {
|
|
284
|
+
Ok(Some((target_workspace, team)))
|
|
285
|
+
}
|
|
286
|
+
_ => Ok(None),
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
229
290
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
230
291
|
struct ParsedNamedAddress {
|
|
231
292
|
workspace: Option<PathBuf>,
|
|
@@ -555,12 +616,18 @@ fn leader_advisory_candidates(
|
|
|
555
616
|
transport: &dyn Transport,
|
|
556
617
|
source: &str,
|
|
557
618
|
) -> Vec<Value> {
|
|
558
|
-
let
|
|
619
|
+
let team_scoped_socket = team_entry(state, team)
|
|
559
620
|
.and_then(|entry| entry.get("leader_receiver"))
|
|
560
|
-
.
|
|
561
|
-
|
|
562
|
-
.
|
|
563
|
-
|
|
621
|
+
.and_then(|receiver| string_field(receiver, "tmux_socket"));
|
|
622
|
+
let socket = match team_scoped_socket {
|
|
623
|
+
Some(s) => Some(s.to_string()),
|
|
624
|
+
None => state
|
|
625
|
+
// ALLOWED-LEGACY-SINGLE-TEAM: diagnostic-only socket label for the advisory candidates; never fed back into routing.
|
|
626
|
+
.get("leader_receiver")
|
|
627
|
+
.and_then(|receiver| string_field(receiver, "tmux_socket"))
|
|
628
|
+
.map(str::to_string)
|
|
629
|
+
.or_else(|| transport.tmux_endpoint()),
|
|
630
|
+
};
|
|
564
631
|
let targets = match transport.list_targets() {
|
|
565
632
|
Ok(targets) => targets,
|
|
566
633
|
Err(_) => return Vec::new(),
|
|
@@ -588,17 +655,27 @@ fn resolve_leader(
|
|
|
588
655
|
parsed: &ParsedNamedAddress,
|
|
589
656
|
transport: &dyn Transport,
|
|
590
657
|
) -> Result<ResolvedNamedAddress, NamedAddressError> {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
.
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
658
|
+
// E6 taxonomy split (0.5.9 offline-mailbox-toname-design §3.1): before
|
|
659
|
+
// reading any leader_receiver, decide whether `<team>` is actually the
|
|
660
|
+
// runtime team key. Wrong keys must not fall through to top-level
|
|
661
|
+
// leader_receiver (that was the pre-fix bug where `twitter-autopub/leader`
|
|
662
|
+
// silently reused the `current` team's receiver and reported a stale
|
|
663
|
+
// pane_id). Legacy single-team compat is preserved only when the state
|
|
664
|
+
// has no `teams` map yet and the requested key equals `active_team_key`.
|
|
665
|
+
let receiver = resolve_team_scoped_leader_receiver(state, team, target_workspace)?
|
|
666
|
+
.ok_or_else(|| {
|
|
667
|
+
let mut err = leader_not_attached_third_party(
|
|
668
|
+
target_workspace,
|
|
669
|
+
team,
|
|
670
|
+
None,
|
|
671
|
+
None,
|
|
672
|
+
None,
|
|
673
|
+
transport.tmux_endpoint(),
|
|
674
|
+
);
|
|
675
|
+
err.candidates =
|
|
676
|
+
leader_advisory_candidates(state, team, transport, "state_recorded_socket");
|
|
677
|
+
err
|
|
678
|
+
})?;
|
|
602
679
|
if let Some((mode, transport_kind)) = receiver_transport_conflict(receiver) {
|
|
603
680
|
let mut err = NamedAddressError::new(
|
|
604
681
|
NamedAddressErrorKind::NameNotLive,
|
|
@@ -624,8 +701,14 @@ fn resolve_leader(
|
|
|
624
701
|
let pane_id = string_field(receiver, "pane_id")
|
|
625
702
|
.filter(|pane| !pane.is_empty())
|
|
626
703
|
.ok_or_else(|| {
|
|
627
|
-
let mut err =
|
|
628
|
-
|
|
704
|
+
let mut err = leader_not_attached_third_party(
|
|
705
|
+
target_workspace,
|
|
706
|
+
team,
|
|
707
|
+
None,
|
|
708
|
+
None,
|
|
709
|
+
None,
|
|
710
|
+
transport.tmux_endpoint(),
|
|
711
|
+
);
|
|
629
712
|
err.candidates =
|
|
630
713
|
leader_advisory_candidates(state, team, transport, "state_recorded_socket");
|
|
631
714
|
err
|
|
@@ -660,8 +743,14 @@ fn resolve_leader(
|
|
|
660
743
|
warning: None,
|
|
661
744
|
}),
|
|
662
745
|
0 => {
|
|
663
|
-
let mut err =
|
|
664
|
-
|
|
746
|
+
let mut err = leader_not_attached_third_party(
|
|
747
|
+
target_workspace,
|
|
748
|
+
team,
|
|
749
|
+
Some(pane_id),
|
|
750
|
+
session,
|
|
751
|
+
window,
|
|
752
|
+
socket,
|
|
753
|
+
);
|
|
665
754
|
err.candidates =
|
|
666
755
|
leader_advisory_candidates(state, team, transport, "state_recorded_socket");
|
|
667
756
|
Err(err)
|
|
@@ -835,12 +924,18 @@ fn transport_for_cli_target(
|
|
|
835
924
|
) -> Box<dyn Transport> {
|
|
836
925
|
if let ParsedTarget::TeamEntity { team, entity } = &parsed.target {
|
|
837
926
|
if entity == "leader" {
|
|
838
|
-
|
|
927
|
+
let team_scoped_socket = team_entry(state, team)
|
|
839
928
|
.and_then(|entry| entry.get("leader_receiver"))
|
|
840
|
-
.
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
929
|
+
.and_then(|receiver| string_field(receiver, "tmux_socket"));
|
|
930
|
+
let socket = team_scoped_socket.map(str::to_string).or_else(|| {
|
|
931
|
+
state
|
|
932
|
+
// ALLOWED-LEGACY-SINGLE-TEAM: pre-teams-map compat probe endpoint; canonical taxonomy in `resolve_team_scoped_leader_receiver` still gates identity.
|
|
933
|
+
.get("leader_receiver")
|
|
934
|
+
.and_then(|receiver| string_field(receiver, "tmux_socket"))
|
|
935
|
+
.map(str::to_string)
|
|
936
|
+
});
|
|
937
|
+
if let Some(socket) = socket {
|
|
938
|
+
return Box::new(crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&socket));
|
|
844
939
|
}
|
|
845
940
|
}
|
|
846
941
|
if let Some(entry) = team_entry(state, team) {
|
|
@@ -926,6 +1021,46 @@ fn team_entry<'a>(state: &'a Value, team: &str) -> Option<&'a Value> {
|
|
|
926
1021
|
.and_then(|teams| teams.get(team))
|
|
927
1022
|
}
|
|
928
1023
|
|
|
1024
|
+
/// E6 (0.5.9 offline-mailbox-toname-design §3.1): decide which
|
|
1025
|
+
/// `leader_receiver` object, if any, applies to `<team>/leader` before any
|
|
1026
|
+
/// downstream code reads it. Wrong runtime keys are rejected here with
|
|
1027
|
+
/// `team_key_not_found` — the pre-fix behavior was to silently reuse the
|
|
1028
|
+
/// top-level `leader_receiver` (the `current` team's receiver in the gate
|
|
1029
|
+
/// evidence), producing misleading `expected_pane=<missing>` messages.
|
|
1030
|
+
///
|
|
1031
|
+
/// Legacy single-team compat is preserved only when the state has no
|
|
1032
|
+
/// `teams` map at all AND the requested team equals `active_team_key`.
|
|
1033
|
+
/// That branch is a documented exception to the E6 fallback grep guard;
|
|
1034
|
+
/// the exception is tagged inline with `ALLOWED-LEGACY-SINGLE-TEAM` so
|
|
1035
|
+
/// the guard admits it explicitly (see the R2 rework in
|
|
1036
|
+
/// `.team/artifacts/059-impl-cr-verdict.md §4`). The exception is
|
|
1037
|
+
/// scheduled to be removed when B1 canonical state layout lands
|
|
1038
|
+
/// (`.team/artifacts/next-version-staged-plan.md §5 Phase-Foundation-1`),
|
|
1039
|
+
/// at which point every runtime state carries a `teams` map and the
|
|
1040
|
+
/// legacy branch becomes unreachable dead code.
|
|
1041
|
+
fn resolve_team_scoped_leader_receiver<'a>(
|
|
1042
|
+
state: &'a Value,
|
|
1043
|
+
team: &str,
|
|
1044
|
+
target_workspace: &Path,
|
|
1045
|
+
) -> Result<Option<&'a Value>, NamedAddressError> {
|
|
1046
|
+
if let Some(entry) = team_entry(state, team) {
|
|
1047
|
+
return Ok(entry.get("leader_receiver"));
|
|
1048
|
+
}
|
|
1049
|
+
if is_legacy_single_team_active(state, team) {
|
|
1050
|
+
// ALLOWED-LEGACY-SINGLE-TEAM: pre-teams-map compat; state has no teams map AND request matches active_team_key.
|
|
1051
|
+
return Ok(state.get("leader_receiver"));
|
|
1052
|
+
}
|
|
1053
|
+
Err(team_key_not_found(target_workspace, state, team))
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
fn is_legacy_single_team_active(state: &Value, team: &str) -> bool {
|
|
1057
|
+
state.get("teams").is_none()
|
|
1058
|
+
&& state
|
|
1059
|
+
.get("active_team_key")
|
|
1060
|
+
.and_then(Value::as_str)
|
|
1061
|
+
.is_some_and(|k| k == team)
|
|
1062
|
+
}
|
|
1063
|
+
|
|
929
1064
|
fn string_field<'a>(value: &'a Value, key: &str) -> Option<&'a str> {
|
|
930
1065
|
value
|
|
931
1066
|
.get(key)
|
|
@@ -1004,6 +1139,91 @@ fn name_not_live_worker(
|
|
|
1004
1139
|
err
|
|
1005
1140
|
}
|
|
1006
1141
|
|
|
1142
|
+
/// E6 (0.5.9 offline-mailbox §3.1): requested `<team>` is not a runtime team_key.
|
|
1143
|
+
/// The refusal echoes the requested string plus the canonical alternatives so
|
|
1144
|
+
/// the caller can retry with `::<canonical_key>/leader`. The action copy is
|
|
1145
|
+
/// deliberately neutral — no `claim-leader` / `takeover` — because a third-party
|
|
1146
|
+
/// sender must never be told to seize the target team's leader.
|
|
1147
|
+
fn team_key_not_found(target_workspace: &Path, state: &Value, team: &str) -> NamedAddressError {
|
|
1148
|
+
let available = available_team_keys(state);
|
|
1149
|
+
let workspace = target_workspace.display();
|
|
1150
|
+
let mut err = NamedAddressError::new(
|
|
1151
|
+
NamedAddressErrorKind::TeamKeyNotFound,
|
|
1152
|
+
format!(
|
|
1153
|
+
"team key `{team}` is not a runtime key in target workspace `{workspace}` (spec/display name may differ from runtime key)"
|
|
1154
|
+
),
|
|
1155
|
+
);
|
|
1156
|
+
let suggested_key = available.first().cloned();
|
|
1157
|
+
err.action = match suggested_key.as_deref() {
|
|
1158
|
+
Some(k) => format!(
|
|
1159
|
+
"Retry with the canonical key, e.g. `{workspace}::{k}/leader`; check `team-agent status --workspace {workspace} --json` for team_key"
|
|
1160
|
+
),
|
|
1161
|
+
None => format!(
|
|
1162
|
+
"No runtime team keys are present; check `team-agent status --workspace {workspace} --json`"
|
|
1163
|
+
),
|
|
1164
|
+
};
|
|
1165
|
+
err.log = format!(
|
|
1166
|
+
"requested_team={team} available_team_keys={:?} workspace={workspace}",
|
|
1167
|
+
available
|
|
1168
|
+
);
|
|
1169
|
+
err.requested_team = Some(team.to_string());
|
|
1170
|
+
err.suggested_name = suggested_key
|
|
1171
|
+
.as_deref()
|
|
1172
|
+
.map(|k| format!("{workspace}::{k}/leader"));
|
|
1173
|
+
err.available_team_keys = available;
|
|
1174
|
+
err
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/// E6 (0.5.9 offline-mailbox §3.1): key hit but the leader receiver is either
|
|
1178
|
+
/// absent or not live. `leader_not_attached_third_party` is the neutral variant
|
|
1179
|
+
/// used by the resolver: it never suggests `claim-leader` / `takeover` so the
|
|
1180
|
+
/// caller (which may be a third-party sender) never sees ownership-seizing copy.
|
|
1181
|
+
/// The owner-facing action (attach-leader) belongs to `status_port`
|
|
1182
|
+
/// pending_leader_notifications and diagnose.
|
|
1183
|
+
fn leader_not_attached_third_party(
|
|
1184
|
+
target_workspace: &Path,
|
|
1185
|
+
team: &str,
|
|
1186
|
+
pane_id: Option<&str>,
|
|
1187
|
+
session: Option<&str>,
|
|
1188
|
+
window: Option<&str>,
|
|
1189
|
+
socket: Option<String>,
|
|
1190
|
+
) -> NamedAddressError {
|
|
1191
|
+
let workspace = target_workspace.display();
|
|
1192
|
+
let mut err = NamedAddressError::new(
|
|
1193
|
+
NamedAddressErrorKind::LeaderNotAttached,
|
|
1194
|
+
"target team leader is not attached on its recorded tmux endpoint",
|
|
1195
|
+
);
|
|
1196
|
+
err.action = format!(
|
|
1197
|
+
"Ask the target team owner to run `team-agent attach-leader --workspace {workspace} --team {team}`; the sender-side offline mailbox will requeue the message once attach succeeds"
|
|
1198
|
+
);
|
|
1199
|
+
err.log = format!(
|
|
1200
|
+
"name={team}/leader expected_pane={} expected_session={} expected_window={} tmux_socket={}",
|
|
1201
|
+
pane_id.unwrap_or("<missing>"),
|
|
1202
|
+
session.unwrap_or("<unknown>"),
|
|
1203
|
+
window.unwrap_or("<unknown>"),
|
|
1204
|
+
socket.unwrap_or_else(|| "<unknown>".to_string())
|
|
1205
|
+
);
|
|
1206
|
+
err.requested_team = Some(team.to_string());
|
|
1207
|
+
err
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
pub(crate) fn available_team_keys(state: &Value) -> Vec<String> {
|
|
1211
|
+
let mut keys: Vec<String> = state
|
|
1212
|
+
.get("teams")
|
|
1213
|
+
.and_then(Value::as_object)
|
|
1214
|
+
.map(|teams| teams.keys().cloned().collect())
|
|
1215
|
+
.unwrap_or_default();
|
|
1216
|
+
if keys.is_empty() {
|
|
1217
|
+
if let Some(active) = state.get("active_team_key").and_then(Value::as_str) {
|
|
1218
|
+
if !active.is_empty() {
|
|
1219
|
+
keys.push(active.to_string());
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
keys.sort();
|
|
1224
|
+
keys
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1007
1227
|
fn name_not_live_leader(
|
|
1008
1228
|
team: &str,
|
|
1009
1229
|
pane_id: Option<&str>,
|