@team-agent/installer 0.5.117 → 0.5.118
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/mod.rs +20 -0
- package/crates/team-agent/src/leader/start.rs +27 -47
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +2 -7
- package/crates/team-agent/src/messaging/delivery.rs +25 -97
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +6 -5
- package/crates/team-agent/src/messaging/tests/spine.rs +27 -28
- package/crates/team-agent/src/tmux_backend/native_input_tests.rs +141 -4
- package/crates/team-agent/src/transport/native_input.rs +131 -9
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -424,6 +424,26 @@ pub mod lifecycle_port {
|
|
|
424
424
|
});
|
|
425
425
|
if let Some(object) = value.as_object_mut() {
|
|
426
426
|
object.extend(facts);
|
|
427
|
+
// Only a plain Pi entry can be retried verbatim with this command.
|
|
428
|
+
// Never drop explicit attach/external intent or provider arguments,
|
|
429
|
+
// clear the caller's environment, or adopt the unverified pane.
|
|
430
|
+
if cfg!(unix)
|
|
431
|
+
&& provider == Provider::Pi
|
|
432
|
+
&& attach == &LeaderLauncherArgs::default()
|
|
433
|
+
{
|
|
434
|
+
let command = "env -u TMUX -u TMUX_PANE team-agent pi";
|
|
435
|
+
object.insert("recovery_command".to_string(), json!(command));
|
|
436
|
+
object.insert(
|
|
437
|
+
ACTION_FIELD.to_string(),
|
|
438
|
+
json!(format!(
|
|
439
|
+
"The inherited TMUX/TMUX_PANE could not be authorized for \
|
|
440
|
+
requested_workspace; this is not an empty-directory error. \
|
|
441
|
+
To explicitly retry from requested_workspace, run `{command}`. \
|
|
442
|
+
This removes the inherited variables only for the new process \
|
|
443
|
+
and does not detach or modify your current tmux pane. {action}"
|
|
444
|
+
)),
|
|
445
|
+
);
|
|
446
|
+
}
|
|
427
447
|
}
|
|
428
448
|
value
|
|
429
449
|
}
|
|
@@ -48,9 +48,6 @@ enum ManagedClientAttachMode {
|
|
|
48
48
|
SwitchClient,
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const DIFFERENT_TMUX_SERVER_PREFIX: &str =
|
|
52
|
-
"managed launcher refuses a different ambient tmux server";
|
|
53
|
-
|
|
54
51
|
impl PreparedLeaderStart {
|
|
55
52
|
pub(crate) fn plan(&self) -> &LeaderStartPlan {
|
|
56
53
|
&self.plan
|
|
@@ -141,8 +138,6 @@ pub(crate) fn prepare_leader_start_with_nested_attach(
|
|
|
141
138
|
let (ambient_authority, managed_client_attach_mode, managed_exec_provider) =
|
|
142
139
|
if explicit_external_path || state_external_path {
|
|
143
140
|
(ambient_pane_authority_preflight(workspace)?, None, false)
|
|
144
|
-
} else if workspace_state_is_managed_provider_reentry(workspace) {
|
|
145
|
-
(ambient_pane_authority_preflight(workspace)?, None, true)
|
|
146
141
|
} else {
|
|
147
142
|
managed_launcher_ambient_route(
|
|
148
143
|
provider,
|
|
@@ -159,7 +154,7 @@ pub(crate) fn prepare_leader_start_with_nested_attach(
|
|
|
159
154
|
confirm_attach,
|
|
160
155
|
attach_session,
|
|
161
156
|
external_leader,
|
|
162
|
-
|
|
157
|
+
ambient_authority.is_some(),
|
|
163
158
|
managed_client_attach_mode,
|
|
164
159
|
managed_exec_provider,
|
|
165
160
|
allow_nested_attach,
|
|
@@ -613,64 +608,49 @@ fn managed_launcher_ambient_route(
|
|
|
613
608
|
),
|
|
614
609
|
PrepareLeaderStartError,
|
|
615
610
|
> {
|
|
616
|
-
let
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
611
|
+
let isolated = || (None, Some(ManagedClientAttachMode::AttachSession), false);
|
|
612
|
+
let authority = match ambient_pane_authority_preflight(workspace) {
|
|
613
|
+
Ok(Some(authority)) => authority,
|
|
614
|
+
Ok(None) => return Ok(isolated()),
|
|
615
|
+
Err(PrepareLeaderStartError::PaneAuthorityRefused(_)) if !allow_nested_attach => {
|
|
616
|
+
// An inherited tuple cannot authorize adoption, but an ordinary
|
|
617
|
+
// managed launch can still create its own workspace pane. Keep
|
|
618
|
+
// explicit external/attach and nested-attach intent fail-closed.
|
|
619
|
+
return Ok(isolated());
|
|
620
|
+
}
|
|
621
|
+
Err(error) => return Err(error),
|
|
622
622
|
};
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
))
|
|
628
|
-
})?;
|
|
629
|
-
let observed_endpoint = validated_ambient_tmux_endpoint(workspace, &tmux)
|
|
630
|
-
.map_err(PrepareLeaderStartError::PaneAuthorityRefused)?;
|
|
631
|
-
if provider == Provider::Pi && provider_args.is_empty() && !allow_nested_attach {
|
|
632
|
-
// Pi's zero-argument managed entry already owns this verified pane as its
|
|
633
|
-
// user-facing terminal. Replacing the current process is safe on either
|
|
634
|
-
// the workspace server or another server; switching/attaching would move
|
|
635
|
-
// the user away from the positively verified workspace pane.
|
|
636
|
-
let authority = verified_ambient_pane_authority(workspace, &tmux)
|
|
637
|
-
.map_err(PrepareLeaderStartError::PaneAuthorityRefused)?;
|
|
623
|
+
if workspace_state_is_managed_provider_reentry(workspace)
|
|
624
|
+
|| (provider == Provider::Pi && provider_args.is_empty() && !allow_nested_attach)
|
|
625
|
+
{
|
|
626
|
+
// Only verified current-pane authority permits replacing the caller.
|
|
638
627
|
return Ok((Some(authority), None, true));
|
|
639
628
|
}
|
|
640
|
-
let target_socket_name = TmuxBackend::for_workspace(workspace)
|
|
641
|
-
.tmux_endpoint()
|
|
642
|
-
.ok_or_else(|| LeaderError::Start("workspace tmux endpoint missing".to_string()))?;
|
|
643
629
|
let target_endpoint = crate::tmux_backend::socket_path_for_workspace(workspace);
|
|
644
630
|
let same_server = target_endpoint.as_ref().is_some_and(|target| {
|
|
645
|
-
let observed = Path::new(&
|
|
631
|
+
let observed = Path::new(&authority.endpoint)
|
|
646
632
|
.canonicalize()
|
|
647
|
-
.unwrap_or_else(|_| PathBuf::from(&
|
|
633
|
+
.unwrap_or_else(|_| PathBuf::from(&authority.endpoint));
|
|
648
634
|
let target = target.canonicalize().unwrap_or_else(|_| target.clone());
|
|
649
635
|
observed == target
|
|
650
636
|
});
|
|
651
637
|
if same_server {
|
|
652
|
-
let authority = verified_ambient_pane_authority(workspace, &tmux)
|
|
653
|
-
.map_err(PrepareLeaderStartError::PaneAuthorityRefused)?;
|
|
654
638
|
return Ok((
|
|
655
639
|
Some(authority),
|
|
656
640
|
Some(ManagedClientAttachMode::SwitchClient),
|
|
657
641
|
false,
|
|
658
642
|
));
|
|
659
643
|
}
|
|
660
|
-
if
|
|
661
|
-
return
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
644
|
+
if allow_nested_attach {
|
|
645
|
+
return Ok((
|
|
646
|
+
Some(authority),
|
|
647
|
+
Some(ManagedClientAttachMode::AttachSession),
|
|
648
|
+
false,
|
|
649
|
+
));
|
|
666
650
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
Ok((
|
|
670
|
-
Some(authority),
|
|
671
|
-
Some(ManagedClientAttachMode::AttachSession),
|
|
672
|
-
false,
|
|
673
|
-
))
|
|
651
|
+
// Do not switch/adopt the unrelated server. The existing managed attach
|
|
652
|
+
// child clears TMUX/TMUX_PANE without changing the caller's environment.
|
|
653
|
+
Ok(isolated())
|
|
674
654
|
}
|
|
675
655
|
|
|
676
656
|
fn validated_ambient_tmux_endpoint(
|
|
@@ -4,7 +4,7 @@ use std::time::Duration;
|
|
|
4
4
|
|
|
5
5
|
use crate::lifecycle::launch::pi_mcp::pi_seat_paths;
|
|
6
6
|
use crate::messaging::delivery::{
|
|
7
|
-
paste_to_submit_floor_for_recipient, recipient_allows_explicit_queue_flush,
|
|
7
|
+
paste_to_submit_floor_for_recipient, recipient_allows_explicit_queue_flush,
|
|
8
8
|
recipient_requires_single_enter,
|
|
9
9
|
};
|
|
10
10
|
use crate::model::enums::{Provider, ProviderEffort};
|
|
@@ -186,17 +186,12 @@ fn pi_delivery_submits_one_enter_and_never_flushes_or_retries() {
|
|
|
186
186
|
paste_to_submit_floor_for_recipient(&idle, "worker-a"),
|
|
187
187
|
Duration::ZERO
|
|
188
188
|
);
|
|
189
|
-
assert!(!recipient_is_busy(&idle, "worker-a"));
|
|
190
|
-
|
|
191
189
|
let busy = serde_json::json!({
|
|
192
190
|
"agents": {
|
|
193
191
|
"worker-a": {"provider": "pi", "status": "busy"}
|
|
194
192
|
}
|
|
195
193
|
});
|
|
196
|
-
|
|
197
|
-
recipient_is_busy(&busy, "worker-a"),
|
|
198
|
-
"known busy must take the existing send.deferred_busy path before paste"
|
|
199
|
-
);
|
|
194
|
+
// Working native CLIs queue the same single-Enter input; no idle admission.
|
|
200
195
|
assert!(recipient_requires_single_enter(&busy, "worker-a"));
|
|
201
196
|
assert!(!recipient_allows_explicit_queue_flush(&busy, "worker-a"));
|
|
202
197
|
}
|
|
@@ -299,34 +299,6 @@ fn prepare_message_state(
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
|
-
if batch
|
|
303
|
-
&& recipient_is_busy(
|
|
304
|
-
prepared.scoped.as_ref().unwrap_or(state),
|
|
305
|
-
&message.recipient,
|
|
306
|
-
)
|
|
307
|
-
{
|
|
308
|
-
event_log.write(
|
|
309
|
-
"send.deferred_busy",
|
|
310
|
-
serde_json::json!({
|
|
311
|
-
"message_id": message_id,
|
|
312
|
-
"sender": message.sender,
|
|
313
|
-
"recipient": message.recipient,
|
|
314
|
-
"reason": "recipient_busy",
|
|
315
|
-
}),
|
|
316
|
-
)?;
|
|
317
|
-
return Ok(Err(DeliveryOutcome {
|
|
318
|
-
ok: false,
|
|
319
|
-
status: DeliveryStatus::Queued,
|
|
320
|
-
message_status: MessageStatusShadow(message.status.clone()),
|
|
321
|
-
message_id: Some(message_id.to_string()),
|
|
322
|
-
verification: None,
|
|
323
|
-
stage: None,
|
|
324
|
-
reason: None,
|
|
325
|
-
channel: None,
|
|
326
|
-
ack_forced_off: false,
|
|
327
|
-
turn_verification: None,
|
|
328
|
-
}));
|
|
329
|
-
}
|
|
330
302
|
Ok(Ok(prepared))
|
|
331
303
|
}
|
|
332
304
|
|
|
@@ -682,34 +654,33 @@ fn deliver_prepared_message(
|
|
|
682
654
|
canonical_owner_team_id.as_deref(),
|
|
683
655
|
resolved.metadata.as_ref(),
|
|
684
656
|
);
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}),
|
|
705
|
-
)
|
|
706
|
-
});
|
|
657
|
+
// v0.5.113 delivery: address/owner and pane serialization are authoritative;
|
|
658
|
+
// composer appearance is not admission. Native CLIs buffer/queue input.
|
|
659
|
+
let inject_attempt = crate::tmux_backend::with_paste_to_submit_floor(
|
|
660
|
+
paste_to_submit_floor_for_recipient(state, &message.recipient),
|
|
661
|
+
|| {
|
|
662
|
+
crate::tmux_backend::with_cursor_single_enter(
|
|
663
|
+
recipient_requires_single_enter(state, &message.recipient),
|
|
664
|
+
|| {
|
|
665
|
+
transport.inject_with_submit_observer(
|
|
666
|
+
&target,
|
|
667
|
+
&payload,
|
|
668
|
+
Key::Enter,
|
|
669
|
+
true,
|
|
670
|
+
Some(&submit_observer),
|
|
671
|
+
)
|
|
672
|
+
},
|
|
673
|
+
)
|
|
674
|
+
},
|
|
675
|
+
);
|
|
707
676
|
let inject_report = match inject_attempt {
|
|
708
677
|
Ok(report) => {
|
|
709
678
|
// grok 忙时第一次回车只入显式队列。默认 send-now:只重按回车顶出去。
|
|
710
679
|
// 无 `Enter:send now` 时零次额外按键,claude/codex 行为不变。
|
|
711
680
|
// cursor:第二下 Enter 打断进行中回合,不走 flush。
|
|
712
|
-
if !
|
|
681
|
+
if !crate::provider::submit_now::keep_provider_queue_requested()
|
|
682
|
+
&& recipient_allows_explicit_queue_flush(state, &message.recipient)
|
|
683
|
+
{
|
|
713
684
|
match crate::provider::submit_now::flush_explicit_queue(transport, &target) {
|
|
714
685
|
Ok(flush) if flush.extra_enters > 0 => {
|
|
715
686
|
let _ = event_log.write(
|
|
@@ -739,28 +710,6 @@ fn deliver_prepared_message(
|
|
|
739
710
|
}
|
|
740
711
|
Err(error) => {
|
|
741
712
|
let reason = format!("inject_failed:{error}");
|
|
742
|
-
if guarded_native {
|
|
743
|
-
let pasted = native_evidence.as_ref().filter(|evidence|
|
|
744
|
-
evidence.phase >= crate::transport::native_input::NativeInputPhase::MayHavePasted);
|
|
745
|
-
let status = if let Some(evidence) = pasted {
|
|
746
|
-
super::enter_rescue::park(store, evidence, attempt, &reason, event_log)?;
|
|
747
|
-
"submitted_unverified"
|
|
748
|
-
} else {
|
|
749
|
-
super::enter_rescue::release_no_effect(store, message_id, attempt)?;
|
|
750
|
-
"target_resolved"
|
|
751
|
-
};
|
|
752
|
-
event_log.write("native_input.deferred", serde_json::json!({
|
|
753
|
-
"message_id": message_id, "reason": reason, "status": status,
|
|
754
|
-
"may_have_pasted": pasted.is_some(),
|
|
755
|
-
}))?;
|
|
756
|
-
return Ok(DeliveryOutcome {
|
|
757
|
-
ok: false, status: DeliveryStatus::Queued,
|
|
758
|
-
message_status: MessageStatusShadow(status.to_string()),
|
|
759
|
-
message_id: Some(message_id.to_string()), verification: Some(reason),
|
|
760
|
-
stage: Some(DeliveryStage::Inject), reason: None, channel: None,
|
|
761
|
-
ack_forced_off: false, turn_verification: None,
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
713
|
if message.recipient == "leader" {
|
|
765
714
|
store.mark(message_id, "failed", Some("leader_not_attached"))?;
|
|
766
715
|
event_log.write(
|
|
@@ -926,11 +875,7 @@ fn deliver_prepared_message(
|
|
|
926
875
|
turn_verification: Some(turn_verification),
|
|
927
876
|
});
|
|
928
877
|
}
|
|
929
|
-
|
|
930
|
-
super::enter_rescue::park(store, evidence, attempt, &reason, event_log)?;
|
|
931
|
-
} else {
|
|
932
|
-
store.mark(message_id, "submitted_unverified", Some(&reason))?;
|
|
933
|
-
}
|
|
878
|
+
store.mark(message_id, "submitted_unverified", Some(&reason))?;
|
|
934
879
|
return Ok(DeliveryOutcome {
|
|
935
880
|
ok: false,
|
|
936
881
|
status: DeliveryStatus::Failed,
|
|
@@ -2305,8 +2250,8 @@ fn only_team_entry(state: &serde_json::Value) -> Option<&serde_json::Value> {
|
|
|
2305
2250
|
}
|
|
2306
2251
|
}
|
|
2307
2252
|
|
|
2308
|
-
///
|
|
2309
|
-
///
|
|
2253
|
+
/// Deliver pending rows without activity/composer admission; native CLIs queue input.
|
|
2254
|
+
/// Returns the message IDs whose post-submit evidence verified delivery.
|
|
2310
2255
|
pub fn deliver_pending_messages(
|
|
2311
2256
|
workspace: &Path,
|
|
2312
2257
|
state: &serde_json::Value,
|
|
@@ -2500,13 +2445,6 @@ fn recipient_provider(state: &serde_json::Value, recipient: &str) -> Option<Prov
|
|
|
2500
2445
|
.and_then(parse_canonical_provider)
|
|
2501
2446
|
}
|
|
2502
2447
|
|
|
2503
|
-
fn recipient_is_cursor_agent(state: &serde_json::Value, recipient: &str) -> bool {
|
|
2504
|
-
matches!(
|
|
2505
|
-
recipient_provider(state, recipient),
|
|
2506
|
-
Some(Provider::CursorAgent)
|
|
2507
|
-
)
|
|
2508
|
-
}
|
|
2509
|
-
|
|
2510
2448
|
/// ---
|
|
2511
2449
|
/// purpose: paste→Enter 地板按收件人 provider 选择
|
|
2512
2450
|
/// contract: CursorAgent 与 Grok 为 1s;其余(含 claude)为 ZERO
|
|
@@ -2549,16 +2487,6 @@ pub(crate) fn recipient_allows_explicit_queue_flush(
|
|
|
2549
2487
|
)
|
|
2550
2488
|
}
|
|
2551
2489
|
|
|
2552
|
-
pub(crate) fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> bool {
|
|
2553
|
-
state
|
|
2554
|
-
.get("agents")
|
|
2555
|
-
.and_then(serde_json::Value::as_object)
|
|
2556
|
-
.and_then(|agents| agents.get(recipient))
|
|
2557
|
-
.and_then(|agent| agent.get("status"))
|
|
2558
|
-
.and_then(serde_json::Value::as_str)
|
|
2559
|
-
== Some("busy")
|
|
2560
|
-
}
|
|
2561
|
-
|
|
2562
2490
|
fn leader_rollout_path(state: &serde_json::Value) -> Option<std::path::PathBuf> {
|
|
2563
2491
|
if let Some(receiver) = leader_receiver_value(state).filter(|receiver| {
|
|
2564
2492
|
receiver.get("provider").and_then(serde_json::Value::as_str) == Some("pi")
|
|
@@ -735,7 +735,7 @@ fn resource188_batch_and_public_single_prepare_once_and_keep_fresh_leader_load()
|
|
|
735
735
|
}
|
|
736
736
|
|
|
737
737
|
#[test]
|
|
738
|
-
fn
|
|
738
|
+
fn resource188_busy_reaches_claim_but_owner_refusal_does_not() {
|
|
739
739
|
use crate::messaging::delivery::preparation_probe::COUNTS;
|
|
740
740
|
let mut case = Case::new("188-busy");
|
|
741
741
|
case.state["agents"] = serde_json::json!({"worker": {"agent_id": "worker", "status": "busy"}});
|
|
@@ -767,14 +767,15 @@ fn resource188_busy_and_refused_candidates_prepare_once_without_claiming() {
|
|
|
767
767
|
Some("missing-team"),
|
|
768
768
|
)
|
|
769
769
|
.unwrap();
|
|
770
|
-
let before = case.message_status(&busy);
|
|
771
770
|
COUNTS.with(|counts| counts.set((0, 0, 0)));
|
|
772
771
|
deliver_pending_messages(&case.workspace, &case.state, &transport, &case.event_log).unwrap();
|
|
773
|
-
assert_eq!(COUNTS.with(|counts| counts.get()), (2, 2,
|
|
774
|
-
|
|
772
|
+
assert_eq!(COUNTS.with(|counts| counts.get()), (2, 2, 1));
|
|
773
|
+
assert_ne!(case.message_status(&busy), "accepted");
|
|
775
774
|
assert_ne!(case.message_status(&refused), "delivered");
|
|
775
|
+
// This leader-only transport has no worker target. Removing idle admission
|
|
776
|
+
// must not also remove target binding: the claimed row still cannot inject.
|
|
776
777
|
assert_eq!(transport.inject_count(), 0);
|
|
777
|
-
assert!(case.events().contains("send.deferred_busy"));
|
|
778
|
+
assert!(!case.events().contains("send.deferred_busy"));
|
|
778
779
|
assert!(case.events().contains("delivery.projection_refused"));
|
|
779
780
|
}
|
|
780
781
|
|
|
@@ -174,6 +174,9 @@ impl Transport for DeliverOkTransport {
|
|
|
174
174
|
fn kind(&self) -> BackendKind {
|
|
175
175
|
BackendKind::Tmux
|
|
176
176
|
}
|
|
177
|
+
fn supports_owned_native_input(&self) -> bool {
|
|
178
|
+
true
|
|
179
|
+
}
|
|
177
180
|
fn spawn_first(
|
|
178
181
|
&self,
|
|
179
182
|
_s: &SessionName,
|
|
@@ -201,6 +204,8 @@ impl Transport for DeliverOkTransport {
|
|
|
201
204
|
_s: Key,
|
|
202
205
|
_b: bool,
|
|
203
206
|
) -> Result<InjectReport, TransportError> {
|
|
207
|
+
assert!(crate::transport::native_input::current_request().is_none(),
|
|
208
|
+
"ordinary sends must use v0.5.113 direct input, not pre-paste idle admission");
|
|
204
209
|
Ok(InjectReport {
|
|
205
210
|
stage_reached: crate::transport::InjectStage::Submit,
|
|
206
211
|
inject_verification: crate::transport::InjectVerification::CaptureContainsToken,
|
|
@@ -344,13 +349,10 @@ fn spine_delivery_skips_queued_statuses() {
|
|
|
344
349
|
);
|
|
345
350
|
}
|
|
346
351
|
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
// gates on `state.agents[recipient].status == "busy"`, NOT on turn-level agent_health=WORKING.
|
|
350
|
-
// (The prior version of this test seeded agent_health=WORKING + status="running" and asserted deferral,
|
|
351
|
-
// misreading golden — that stale lock is the root of the deferred_busy regression; corrected here.)
|
|
352
|
+
// Input is unconditional: native CLIs queue messages while working. Activity
|
|
353
|
+
// metadata must not prevent a pending message from reaching paste + Enter.
|
|
352
354
|
#[test]
|
|
353
|
-
fn
|
|
355
|
+
fn spine_delivery_busy_recipient_is_injected_without_idle_deferral() {
|
|
354
356
|
let ws = tmp_ws("deliver-busy");
|
|
355
357
|
let store = store_for(&ws);
|
|
356
358
|
let log = EventLog::new(&ws);
|
|
@@ -361,33 +363,30 @@ fn spine_delivery_busy_recipient_defers_with_event() {
|
|
|
361
363
|
|
|
362
364
|
let delivered = deliver_pending_messages(&ws, &state, &DeliverOkTransport, &log).unwrap();
|
|
363
365
|
|
|
364
|
-
assert!(
|
|
365
|
-
|
|
366
|
-
"a busy recipient's message must NOT be delivered"
|
|
367
|
-
);
|
|
368
|
-
assert_eq!(
|
|
369
|
-
message_status_of(&store, &mid),
|
|
370
|
-
"accepted",
|
|
371
|
-
"the row must stay 'accepted' (no-drop, left for a later tick)"
|
|
372
|
-
);
|
|
366
|
+
assert!(delivered.contains(&mid));
|
|
367
|
+
assert_eq!(message_status_of(&store, &mid), "delivered");
|
|
373
368
|
let events = read_event_log(&ws);
|
|
374
369
|
let deferred: Vec<_> = events
|
|
375
370
|
.iter()
|
|
376
371
|
.filter(|e| e.get("event").and_then(|v| v.as_str()) == Some("send.deferred_busy"))
|
|
377
372
|
.collect();
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
373
|
+
assert!(deferred.is_empty());
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
#[test]
|
|
377
|
+
fn spine_delivery_pi_cursor_uses_v113_direct_input_without_idle_scope() {
|
|
378
|
+
for provider in ["pi", "cursor_agent"] {
|
|
379
|
+
let ws = tmp_ws(&format!("direct-input-{provider}"));
|
|
380
|
+
let store = store_for(&ws);
|
|
381
|
+
let log = EventLog::new(&ws);
|
|
382
|
+
let mid = store
|
|
383
|
+
.create_message(Some("t"), "leader", "w1", "hi", None, true, None)
|
|
384
|
+
.unwrap();
|
|
385
|
+
let state = serde_json::json!({"agents": {"w1": {"provider": provider, "status": "running"}}});
|
|
386
|
+
let delivered = deliver_pending_messages(&ws, &state, &DeliverOkTransport, &log).unwrap();
|
|
387
|
+
assert!(delivered.contains(&mid), "{provider}");
|
|
388
|
+
assert_eq!(message_status_of(&store, &mid), "delivered");
|
|
389
|
+
}
|
|
391
390
|
}
|
|
392
391
|
|
|
393
392
|
// CONTRACT (shared-root, real-machine-driven; golden = correct-behavior baseline): an ALIVE worker
|
|
@@ -20,6 +20,12 @@ enum Mode {
|
|
|
20
20
|
R6IdleNotice,
|
|
21
21
|
PiFolded,
|
|
22
22
|
PiFoldedUnstable,
|
|
23
|
+
CursorFollowup,
|
|
24
|
+
CursorHeaderEvicted,
|
|
25
|
+
CursorHistoryOnly,
|
|
26
|
+
CursorLongHistory,
|
|
27
|
+
CursorExpanded,
|
|
28
|
+
CursorExpandedDirty,
|
|
23
29
|
}
|
|
24
30
|
struct State {
|
|
25
31
|
mode: Mode,
|
|
@@ -53,7 +59,13 @@ impl CommandRunner for Runner {
|
|
|
53
59
|
};
|
|
54
60
|
let height = if matches!(
|
|
55
61
|
state.mode,
|
|
56
|
-
Mode::R6IdleNotice
|
|
62
|
+
Mode::R6IdleNotice
|
|
63
|
+
| Mode::PiFolded
|
|
64
|
+
| Mode::PiFoldedUnstable
|
|
65
|
+
| Mode::CursorHistoryOnly
|
|
66
|
+
| Mode::CursorLongHistory
|
|
67
|
+
| Mode::CursorExpanded
|
|
68
|
+
| Mode::CursorExpandedDirty
|
|
57
69
|
) {
|
|
58
70
|
24
|
|
59
71
|
} else {
|
|
@@ -168,14 +180,58 @@ impl CommandRunner for Runner {
|
|
|
168
180
|
let stdout = if state.provider == Provider::CursorAgent
|
|
169
181
|
&& argv.iter().any(|arg| arg == "capture-pane")
|
|
170
182
|
{
|
|
183
|
+
let followup = matches!(
|
|
184
|
+
state.mode,
|
|
185
|
+
Mode::CursorFollowup
|
|
186
|
+
| Mode::CursorHeaderEvicted
|
|
187
|
+
| Mode::CursorHistoryOnly
|
|
188
|
+
| Mode::CursorLongHistory
|
|
189
|
+
| Mode::CursorExpanded
|
|
190
|
+
| Mode::CursorExpandedDirty
|
|
191
|
+
);
|
|
192
|
+
let placeholder = if followup {
|
|
193
|
+
"Add a follow-up"
|
|
194
|
+
} else {
|
|
195
|
+
"Plan, search, build anything"
|
|
196
|
+
};
|
|
197
|
+
let header = if matches!(state.mode, Mode::CursorFollowup)
|
|
198
|
+
|| (matches!(state.mode, Mode::CursorHeaderEvicted) && state.pasted > 0)
|
|
199
|
+
{
|
|
200
|
+
""
|
|
201
|
+
} else {
|
|
202
|
+
"Cursor Agent\nv2026.10.01-e373342\n"
|
|
203
|
+
};
|
|
171
204
|
let (composer, history) = if state.entered > 0 {
|
|
172
|
-
(
|
|
205
|
+
(placeholder, "[Pasted text #2 +25 lines]")
|
|
173
206
|
} else if state.pasted > 0 {
|
|
174
207
|
("[Pasted text #2 +25 lines]", "[Pasted text #1 +25 lines]")
|
|
175
208
|
} else {
|
|
176
|
-
(
|
|
209
|
+
(placeholder, "[Pasted text #1 +25 lines]")
|
|
210
|
+
};
|
|
211
|
+
let padding = if matches!(state.mode, Mode::CursorLongHistory) {
|
|
212
|
+
"historical transcript row\n".repeat(70)
|
|
213
|
+
} else {
|
|
214
|
+
String::new()
|
|
177
215
|
};
|
|
178
|
-
format!(
|
|
216
|
+
let frame = format!(
|
|
217
|
+
"{header}{history}\n{padding}→ {composer}\nGrok 4.7 High Run Everything\n/owned/project\n"
|
|
218
|
+
);
|
|
219
|
+
if matches!(state.mode, Mode::CursorExpanded | Mode::CursorExpandedDirty) {
|
|
220
|
+
let expanded = include_str!("../../tests/fixtures/cursor-followup-expanded.txt");
|
|
221
|
+
if matches!(state.mode, Mode::CursorExpandedDirty)
|
|
222
|
+
|| state.pasted > 0 && state.entered == 0
|
|
223
|
+
{
|
|
224
|
+
format!("[Pasted text #1 +30 lines]\n{expanded}")
|
|
225
|
+
} else if state.entered > 0 {
|
|
226
|
+
format!("{marker}\n→ Add a follow-up\nGrok 4.7 256K High · 8.6% Run Everything\n/owned/project\n")
|
|
227
|
+
} else {
|
|
228
|
+
"[Pasted text #1 +30 lines]\n→ Add a follow-up\nGrok 4.7 256K High · 8.6% Run Everything\n/owned/project\n".into()
|
|
229
|
+
}
|
|
230
|
+
} else if matches!(state.mode, Mode::CursorHistoryOnly) {
|
|
231
|
+
format!("{frame}{}", "unknown current row\n".repeat(24))
|
|
232
|
+
} else {
|
|
233
|
+
frame
|
|
234
|
+
}
|
|
179
235
|
} else {
|
|
180
236
|
stdout
|
|
181
237
|
};
|
|
@@ -703,6 +759,87 @@ fn native_input_cursor_owned_new_capsule_transfers_to_transcript_without_token_c
|
|
|
703
759
|
assert_eq!(state.entered, 1);
|
|
704
760
|
}
|
|
705
761
|
|
|
762
|
+
#[test]
|
|
763
|
+
fn native_input_cursor_clean_followup_uses_one_paste_one_enter_across_banner_eviction() {
|
|
764
|
+
for mode in [
|
|
765
|
+
Mode::CursorFollowup,
|
|
766
|
+
Mode::CursorHeaderEvicted,
|
|
767
|
+
Mode::CursorLongHistory,
|
|
768
|
+
] {
|
|
769
|
+
let f = Fixture::new(mode);
|
|
770
|
+
f.state.lock().unwrap().provider = Provider::CursorAgent;
|
|
771
|
+
let (report, evidence) = f.inject();
|
|
772
|
+
let report = report.unwrap();
|
|
773
|
+
assert_eq!(
|
|
774
|
+
report.submit_verification,
|
|
775
|
+
SubmitVerification::EnterSentWithoutPlaceholderCheck
|
|
776
|
+
);
|
|
777
|
+
let evidence = evidence.unwrap();
|
|
778
|
+
assert_eq!(evidence.phase, NativeInputPhase::Accepted);
|
|
779
|
+
assert_eq!(
|
|
780
|
+
evidence.capsule.as_deref(),
|
|
781
|
+
Some("[Pasted text #2 +25 lines]")
|
|
782
|
+
);
|
|
783
|
+
let state = f.state.lock().unwrap();
|
|
784
|
+
assert_eq!((state.pasted, state.entered), (1, 1));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
#[test]
|
|
789
|
+
fn native_input_cursor_expanded_followup_reaches_seen_then_one_enter() {
|
|
790
|
+
let f = Fixture::new(Mode::CursorExpanded);
|
|
791
|
+
f.state.lock().unwrap().provider = Provider::CursorAgent;
|
|
792
|
+
let text = "For a harmless terminal-scroll fixture, reply with exactly 240 numbered lines, one per line, from 001 through 240, each formatted `NNN: amber`; do not use tools or change any files. Then wait.\n\n[team-agent-token:msg_one]";
|
|
793
|
+
let (report, evidence) = with_native_input(Some(f.request()), || {
|
|
794
|
+
f.backend.inject(
|
|
795
|
+
&Target::Pane(PaneId::new("%1")),
|
|
796
|
+
&InjectPayload::Text(text.into()),
|
|
797
|
+
Key::Enter,
|
|
798
|
+
true,
|
|
799
|
+
)
|
|
800
|
+
});
|
|
801
|
+
let report = report.unwrap();
|
|
802
|
+
assert_eq!(
|
|
803
|
+
report.submit_verification,
|
|
804
|
+
SubmitVerification::EnterSentWithoutPlaceholderCheck
|
|
805
|
+
);
|
|
806
|
+
assert_eq!(
|
|
807
|
+
report.inject_verification,
|
|
808
|
+
InjectVerification::CaptureContainsToken
|
|
809
|
+
);
|
|
810
|
+
let evidence = evidence.unwrap();
|
|
811
|
+
assert_eq!(evidence.phase, NativeInputPhase::Accepted);
|
|
812
|
+
assert!(evidence.capsule.is_none());
|
|
813
|
+
assert_eq!(evidence.baseline_capsules, ["[Pasted text #1 +30 lines]"]);
|
|
814
|
+
let state = f.state.lock().unwrap();
|
|
815
|
+
assert_eq!((state.pasted, state.entered), (1, 1));
|
|
816
|
+
assert!(state.stage_captures[1] >= 2);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
#[test]
|
|
820
|
+
fn native_input_cursor_foreign_expanded_followup_is_zero_effect() {
|
|
821
|
+
let f = Fixture::new(Mode::CursorExpandedDirty);
|
|
822
|
+
f.state.lock().unwrap().provider = Provider::CursorAgent;
|
|
823
|
+
let (report, _) = f.inject();
|
|
824
|
+
assert!(report.is_err());
|
|
825
|
+
let state = f.state.lock().unwrap();
|
|
826
|
+
assert_eq!((state.pasted, state.entered), (0, 0));
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
#[test]
|
|
830
|
+
fn native_input_cursor_history_only_composer_cannot_authorize_paste_or_enter() {
|
|
831
|
+
let f = Fixture::new(Mode::CursorHistoryOnly);
|
|
832
|
+
f.state.lock().unwrap().provider = Provider::CursorAgent;
|
|
833
|
+
let (result, _) = f.inject();
|
|
834
|
+
let state = f.state.lock().unwrap();
|
|
835
|
+
assert_eq!(
|
|
836
|
+
(state.pasted, state.entered),
|
|
837
|
+
(0, 0),
|
|
838
|
+
"old history is not current composer authority"
|
|
839
|
+
);
|
|
840
|
+
assert!(result.is_err());
|
|
841
|
+
}
|
|
842
|
+
|
|
706
843
|
#[test]
|
|
707
844
|
fn native_input_buffer_uses_authoritative_message_not_first_quoted_marker() {
|
|
708
845
|
let f = Fixture::new(Mode::Normal);
|
|
@@ -202,16 +202,27 @@ pub(crate) fn inspect_input(
|
|
|
202
202
|
) -> Option<InputFrame> {
|
|
203
203
|
match provider {
|
|
204
204
|
Provider::Pi => pi_input(text, pane_height),
|
|
205
|
-
Provider::CursorAgent => cursor_input(text),
|
|
205
|
+
Provider::CursorAgent => cursor_input(text, pane_height),
|
|
206
206
|
_ => None,
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
fn cursor_input(text: &str) -> Option<InputFrame> {
|
|
210
|
+
fn cursor_input(text: &str, pane_height: u32) -> Option<InputFrame> {
|
|
211
211
|
use team_agent_contract::contract::delivery::InputSurface;
|
|
212
|
+
if pane_height == 0 {
|
|
213
|
+
return None;
|
|
214
|
+
}
|
|
215
|
+
let text_rows: Vec<_> = text.lines().collect();
|
|
216
|
+
// Only the current viewport authorizes input; retain full history below
|
|
217
|
+
// solely for same-message transcript correlation.
|
|
218
|
+
let viewport_start = text_rows.len().saturating_sub(pane_height as usize);
|
|
219
|
+
let viewport = text_rows[viewport_start..].join("\n");
|
|
212
220
|
let (surface, rows, capsule, busy) =
|
|
213
|
-
team_agent_contract::providers::cursor::interaction::
|
|
214
|
-
let first = rows.first()
|
|
221
|
+
team_agent_contract::providers::cursor::interaction::inspect_legacy_composer(&viewport)?;
|
|
222
|
+
let first = match *rows.first()? {
|
|
223
|
+
"→" => "",
|
|
224
|
+
row => row.strip_prefix("→ ")?,
|
|
225
|
+
};
|
|
215
226
|
let footer = rows.iter().rposition(|row| {
|
|
216
227
|
row.strip_suffix("Run Everything").is_some_and(|prefix| {
|
|
217
228
|
!prefix.trim().is_empty() && !prefix.trim_start().starts_with("→ ")
|
|
@@ -228,9 +239,13 @@ fn cursor_input(text: &str) -> Option<InputFrame> {
|
|
|
228
239
|
if tail.is_empty() {
|
|
229
240
|
return None;
|
|
230
241
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
242
|
+
// Startup history can leave Tail(200) between Ready/Pasted/Accepted. The
|
|
243
|
+
// immutable carrier birth/nonce remains in NativeInputBinding; this UI
|
|
244
|
+
// context is the current native cwd/footer tail, not a version attestation.
|
|
245
|
+
let identity = format!(
|
|
246
|
+
"cursor-current-composer-v1:{}",
|
|
247
|
+
input_digest(&tail.join("\n"))
|
|
248
|
+
);
|
|
234
249
|
let footer_row = rows[footer]
|
|
235
250
|
.split_whitespace()
|
|
236
251
|
.collect::<Vec<_>>()
|
|
@@ -247,10 +262,9 @@ fn cursor_input(text: &str) -> Option<InputFrame> {
|
|
|
247
262
|
.collect::<Vec<_>>()
|
|
248
263
|
.join("\n")
|
|
249
264
|
};
|
|
250
|
-
let text_rows: Vec<_> = text.lines().collect();
|
|
251
265
|
let arrow = text_rows
|
|
252
266
|
.iter()
|
|
253
|
-
.rposition(|line| line.trim().starts_with("→ "))?;
|
|
267
|
+
.rposition(|line| line.trim() == "→" || line.trim().starts_with("→ "))?;
|
|
254
268
|
Some(InputFrame {
|
|
255
269
|
composer,
|
|
256
270
|
transcript: text.lines().take(arrow).collect::<Vec<_>>().join("\n"),
|
|
@@ -544,6 +558,114 @@ mod tests {
|
|
|
544
558
|
}
|
|
545
559
|
}
|
|
546
560
|
|
|
561
|
+
// Native Issue #328 current-composer rows; transcript removed and private
|
|
562
|
+
// cwd replaced with /owned/project. Startup banner is genuinely absent
|
|
563
|
+
// from the owned pane's Tail(200), not a fabricated invalid version.
|
|
564
|
+
const CURSOR_FOLLOWUP: &str =
|
|
565
|
+
include_str!("../../tests/fixtures/cursor-followup-headerless.txt");
|
|
566
|
+
|
|
567
|
+
#[test]
|
|
568
|
+
fn native_input_cursor_clean_followup_does_not_require_scrolled_startup_banner() {
|
|
569
|
+
let ready = inspect_input(Provider::CursorAgent, CURSOR_FOLLOWUP).unwrap();
|
|
570
|
+
assert!(ready.empty());
|
|
571
|
+
assert!(!ready.busy);
|
|
572
|
+
assert!(ready.capsule.is_none());
|
|
573
|
+
let header = format!("Cursor Agent\nv2026.10.14-0f9edde\n{CURSOR_FOLLOWUP}");
|
|
574
|
+
let with_header = inspect_input(Provider::CursorAgent, &header).unwrap();
|
|
575
|
+
assert!(with_header.empty() && !with_header.busy);
|
|
576
|
+
assert_eq!(ready.footer, with_header.footer);
|
|
577
|
+
assert_eq!(
|
|
578
|
+
ready.identity, with_header.identity,
|
|
579
|
+
"startup history is not a new UI identity"
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
#[test]
|
|
584
|
+
fn native_input_cursor_headerless_followup_retains_draft_busy_and_footer_fences() {
|
|
585
|
+
let label = "[Pasted text #2 +25 lines]";
|
|
586
|
+
let folded = CURSOR_FOLLOWUP.replace("Add a follow-up", label);
|
|
587
|
+
let draft = inspect_input(Provider::CursorAgent, &folded).unwrap();
|
|
588
|
+
assert!(!draft.empty());
|
|
589
|
+
assert_eq!(draft.capsule.as_deref(), Some(label));
|
|
590
|
+
let dirty = CURSOR_FOLLOWUP.replace("Add a follow-up", "Add a follow-up draft");
|
|
591
|
+
assert!(!inspect_input(Provider::CursorAgent, &dirty)
|
|
592
|
+
.unwrap()
|
|
593
|
+
.empty());
|
|
594
|
+
for dirty in [
|
|
595
|
+
CURSOR_FOLLOWUP.replace("Add a follow-up", "Add a follow-up\nforeign draft"),
|
|
596
|
+
CURSOR_FOLLOWUP.replace("Add a follow-up", &format!("{label}\nforeign draft")),
|
|
597
|
+
] {
|
|
598
|
+
assert!(inspect_input(Provider::CursorAgent, &dirty).is_none());
|
|
599
|
+
}
|
|
600
|
+
let busy = CURSOR_FOLLOWUP.replace("Add a follow-up", "Add a follow-up ctrl+c to stop");
|
|
601
|
+
assert!(inspect_input(Provider::CursorAgent, &busy).unwrap().busy);
|
|
602
|
+
let working = format!("⠘⠆ Working\n{CURSOR_FOLLOWUP}");
|
|
603
|
+
assert!(inspect_input(Provider::CursorAgent, &working).unwrap().busy);
|
|
604
|
+
for unknown in [
|
|
605
|
+
"→ Add a follow-up\n".to_string(),
|
|
606
|
+
CURSOR_FOLLOWUP.replace("Run Everything", "unknown footer"),
|
|
607
|
+
CURSOR_FOLLOWUP.replace(" /owned/project\n", ""),
|
|
608
|
+
] {
|
|
609
|
+
assert!(inspect_input(Provider::CursorAgent, &unknown).is_none());
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
#[test]
|
|
614
|
+
fn native_input_cursor_real_expanded_followup_is_nonempty_current_marker_not_capsule() {
|
|
615
|
+
let expanded = include_str!("../../tests/fixtures/cursor-followup-expanded.txt");
|
|
616
|
+
let marker = "[team-agent-token:msg_one]";
|
|
617
|
+
for prefix in [
|
|
618
|
+
"".to_string(),
|
|
619
|
+
format!("Cursor Agent\nv2026.10.14-0f9edde\n{CURSOR_FOLLOWUP}"),
|
|
620
|
+
] {
|
|
621
|
+
let text = format!("{prefix}{expanded}");
|
|
622
|
+
let frame = inspect_input(Provider::CursorAgent, &text).unwrap();
|
|
623
|
+
assert!(!frame.empty() && !frame.busy);
|
|
624
|
+
assert!(frame.has_marker(marker));
|
|
625
|
+
assert!(frame.capsule.is_none());
|
|
626
|
+
assert!(!frame.transcript.contains(marker));
|
|
627
|
+
let changed = text.replace("/owned/project", "/different/project");
|
|
628
|
+
assert_ne!(
|
|
629
|
+
frame.identity,
|
|
630
|
+
inspect_input(Provider::CursorAgent, &changed)
|
|
631
|
+
.unwrap()
|
|
632
|
+
.identity
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
let duplicate = expanded.replace(marker, &format!("{marker}\n{marker}"));
|
|
636
|
+
assert!(!inspect_input(Provider::CursorAgent, &duplicate)
|
|
637
|
+
.unwrap()
|
|
638
|
+
.has_marker(marker));
|
|
639
|
+
let busy = expanded.replace(" →", "⠘⠆ Working\n →");
|
|
640
|
+
assert!(inspect_input(Provider::CursorAgent, &busy).unwrap().busy);
|
|
641
|
+
for unknown in [
|
|
642
|
+
"→\nGrok 4.7 High Run Everything\n/owned/project\n".to_string(),
|
|
643
|
+
"→\nforeign draft\n→ Add a follow-up\nGrok 4.7 High Run Everything\n/owned/project\n"
|
|
644
|
+
.to_string(),
|
|
645
|
+
expanded.replace("Run Everything", "unknown footer"),
|
|
646
|
+
expanded.replace(" /owned/project\n", ""),
|
|
647
|
+
format!("{expanded}{}", "unknown current row\n".repeat(24)),
|
|
648
|
+
] {
|
|
649
|
+
assert!(super::inspect_input(Provider::CursorAgent, &unknown, 24).is_none());
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
#[test]
|
|
654
|
+
fn native_input_cursor_requires_composer_in_current_viewport_not_old_history() {
|
|
655
|
+
for header in ["", "Cursor Agent\nv2026.10.14-0f9edde\n"] {
|
|
656
|
+
let hidden = format!(
|
|
657
|
+
"{header}{CURSOR_FOLLOWUP}{}",
|
|
658
|
+
"unknown current row\n".repeat(24)
|
|
659
|
+
);
|
|
660
|
+
assert!(super::inspect_input(Provider::CursorAgent, &hidden, 24).is_none());
|
|
661
|
+
}
|
|
662
|
+
assert!(super::inspect_input(Provider::CursorAgent, CURSOR_FOLLOWUP, 0).is_none());
|
|
663
|
+
let current = super::inspect_input(Provider::CursorAgent, CURSOR_FOLLOWUP, 24).unwrap();
|
|
664
|
+
let changed = CURSOR_FOLLOWUP.replace("/owned/project", "/different/project");
|
|
665
|
+
let changed = super::inspect_input(Provider::CursorAgent, &changed, 24).unwrap();
|
|
666
|
+
assert_ne!(current.identity, changed.identity);
|
|
667
|
+
}
|
|
668
|
+
|
|
547
669
|
#[test]
|
|
548
670
|
fn native_input_cursor_separates_folded_draft_from_history_and_busy() {
|
|
549
671
|
let capsule = "[Pasted text #1 +25 lines]";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.118",
|
|
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.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.118",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.118",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.118"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|