Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/command-contract/src/facets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1232,12 +1232,14 @@ pub enum ResumeSource {

/// Portable `/resume` import receipt. The handler renders
/// `Imported foreign session as {truncated_id} ({entry_count} entries, leaf
/// {leaf_display})`.
/// {leaf_display})`, and carries `sync` so the engine adopts the imported
/// conversation instead of staying on the previous one.
#[derive(Clone, Debug, PartialEq)]
pub struct ResumeImportReceipt {
pub truncated_id: String,
pub entry_count: usize,
pub leaf_display: String,
pub sync: SessionSyncPayload,
}

/// `/rename` success receipt; the title is the sanitized persisted value so
Expand Down
1 change: 1 addition & 0 deletions crates/command-contract/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2297,6 +2297,7 @@ fn control_facet_is_object_safe_and_transports_every_outcome() {
truncated_id: "imp-9".to_string(),
entry_count: 12,
leaf_display: "leaf-3".to_string(),
sync: lifecycle_sync_payload(Some("imp-9")),
})),
sanitized_title: Some("Renamed".to_string()),
rename: Some(Ok(SessionTitleReceipt {
Expand Down
8 changes: 8 additions & 0 deletions crates/tui/src/commands/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,14 @@ fn import_session_container(
.map(|journal| journal.entries.len())
.unwrap_or(0),
leaf_display: imported.leaf_id.as_deref().unwrap_or("(none)").to_string(),
sync: SessionSyncPayload {
session_id: Some(new_id.clone()),
messages: app.api_messages.as_ref().clone(),
system_prompt: app.system_prompt.clone(),
model: app.model.clone(),
workspace: app.workspace.clone(),
mode: to_command_mode(app.mode),
},
})
}

Expand Down
42 changes: 34 additions & 8 deletions crates/tui/src/commands/groups/session/resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,22 @@ pub(in crate::commands) fn resume_portable(
};
match control.resolve_resume_source(raw) {
Ok(ResumeSource::File(path)) => match control.import_session_file(path) {
Ok(receipt) => CommandResult::message(format!(
"Imported foreign session as {} ({} entries, leaf {})",
receipt.truncated_id, receipt.entry_count, receipt.leaf_display
)),
Ok(receipt) => CommandResult::with_message_and_action(
format!(
"Imported foreign session as {} ({} entries, leaf {})",
receipt.truncated_id, receipt.entry_count, receipt.leaf_display
),
super::sync_session_action(receipt.sync),
),
Err(error) => CommandResult::error(error),
},
Ok(ResumeSource::Imported(receipt)) => CommandResult::message(format!(
"Imported foreign session as {} ({} entries, leaf {})",
receipt.truncated_id, receipt.entry_count, receipt.leaf_display
)),
Ok(ResumeSource::Imported(receipt)) => CommandResult::with_message_and_action(
format!(
"Imported foreign session as {} ({} entries, leaf {})",
receipt.truncated_id, receipt.entry_count, receipt.leaf_display
),
super::sync_session_action(receipt.sync),
),
Ok(ResumeSource::Session {
load_path,
truncated_id,
Expand Down Expand Up @@ -133,13 +139,24 @@ mod tests {
truncated_id: "imp-9".to_string(),
entry_count: 12,
leaf_display: "leaf-3".to_string(),
sync: super::super::lifecycle_test_support::sync_payload("imp-9"),
}));
let result = resume_portable(&mut fake, Some("/tmp/import.json"));
assert!(!result.is_error);
assert_eq!(
message(&result),
"Imported foreign session as imp-9 (12 entries, leaf leaf-3)"
);
// The engine must adopt the imported conversation; a message-only
// result would leave it on the previous session.
assert!(
matches!(
result.action,
Some(crate::tui::app::AppAction::SyncSession { ref session_id, .. })
if session_id.as_deref() == Some("imp-9")
),
"{result:?}"
);

let mut fake = control_fake();
fake.resume = Some(Ok(ResumeSource::File(PathBuf::from("/tmp/x.json"))));
Expand All @@ -158,12 +175,21 @@ mod tests {
truncated_id: "c-1".to_string(),
entry_count: 0,
leaf_display: "(none)".to_string(),
sync: super::super::lifecycle_test_support::sync_payload("c-1"),
})));
let result = resume_portable(&mut fake, Some("inline-json"));
assert_eq!(
message(&result),
"Imported foreign session as c-1 (0 entries, leaf (none))"
);
assert!(
matches!(
result.action,
Some(crate::tui::app::AppAction::SyncSession { ref session_id, .. })
if session_id.as_deref() == Some("c-1")
),
"{result:?}"
);
}

#[test]
Expand Down
38 changes: 38 additions & 0 deletions crates/tui/src/session_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,44 @@ mod tests {
);
}
#[test]
fn stamped_rebranch_keeps_prefix_identity_and_suffix_stamps() {
let stamp = |secs: i64| DateTime::from_timestamp(secs, 0).expect("stamp");
let mut j = SessionJournal::new();
j.append_stamped(
SessionEntryKind::Message {
message: msg("user", "a"),
},
stamp(100),
);
j.append_stamped(
SessionEntryKind::Message {
message: msg("assistant", "b"),
},
stamp(200),
);
let a_id = j.entries[0].id.clone();
j.rebranch_active_messages_stamped(
&[msg("user", "a"), msg("assistant", "b2")],
&[stamp(100), stamp(300)],
);
assert_eq!(j.entries.len(), 3);
assert_eq!(j.entries[0].id, a_id, "shared prefix keeps its id");
assert_eq!(j.entries[0].created_at, stamp(100));
let path = j.root_to_leaf();
assert_eq!(path.len(), 2);
assert_eq!(
path[1].created_at,
stamp(300),
"suffix keeps the live stamp"
);
assert!(
j.entries
.iter()
.any(|e| e.kind.as_message().as_ref() == Some(&msg("assistant", "b"))),
"replaced suffix survives as a sibling"
);
}
#[test]
fn compaction_fits() {
let mut j = SessionJournal::new();
let id = j.append_compaction("summary".into(), Some(1000), Some(100), None);
Expand Down
Loading