#[cfg(test)]
use bcode_session_models::{SessionEvent, SessionEventKind};
use bcode_session_models::{ToolArtifact, ToolInvocationResult};
#[cfg(test)]
use bcode_session_view::SessionView;
use bcode_session_view_models::{
InteractionViewSummary, TextFormat, TextStreamViewStatus, ToolInvocationView,
ToolInvocationViewStatus, ToolResultView, TranscriptViewItem, TranscriptViewItemKind,
};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ToolTiming {
pub started_at_ms: Option<u64>,
pub finished_at_ms: Option<u64>,
pub timeout_ms: Option<u64>,
pub timed_out: Option<bool>,
pub duration_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TranscriptItemKind {
UserMessage,
AssistantMessage,
ReasoningMessage,
ToolRequest {
tool_call_id: String,
producer_plugin_id: Option<String>,
tool_name: String,
working_directory: Option<std::path::PathBuf>,
active: bool,
status: Option<ToolInvocationViewStatus>,
timing: ToolTiming,
},
ToolResult {
tool_call_id: String,
tool_name: Option<String>,
arguments_json: Option<String>,
working_directory: Option<std::path::PathBuf>,
result: String,
artifact: Option<Box<ToolArtifact>>,
is_error: bool,
timing: ToolTiming,
},
PermissionRequest {
permission_id: String,
tool_call_id: String,
tool_name: String,
arguments_json: String,
policy_source: Option<String>,
policy_reason: Option<String>,
},
PermissionResult {
approved: bool,
},
System,
Meta,
Skill,
SkillError,
ToolRequestDraft {
draft: Box<bcode_session_view_models::ToolRequestDraftView>,
},
ToolContribution {
contribution: Box<bcode_session_models::ToolContributionEvent>,
placement: bcode_session_models::ToolContributionPlacement,
invocation: Option<Box<ToolInvocationView>>,
},
Interaction {
interaction: Box<InteractionViewSummary>,
},
Generic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TranscriptItemId(u64);
impl TranscriptItemId {
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TranscriptStreamIntegrity {
Incomplete,
Degraded,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TranscriptItem {
id: TranscriptItemId,
revision: u64,
source_view_item_id: Option<bcode_session_view_models::TranscriptViewItemId>,
source_view_revision: Option<bcode_session_view_models::ViewRevision>,
stream_integrity: Option<TranscriptStreamIntegrity>,
pub role: &'static str,
pub text: String,
pub streaming: bool,
text_format: TextFormat,
display_label: Option<String>,
event_sequence: Option<u64>,
timestamp_ms: Option<u64>,
kind: TranscriptItemKind,
}
impl TranscriptItem {
#[cfg(test)]
pub fn new(role: &'static str, text: String) -> Self {
Self::with_identity(
role,
text,
false,
TextFormat::PlainText,
kind_for_role(role),
)
}
pub(crate) fn with_format(role: &'static str, text: String, text_format: TextFormat) -> Self {
Self::with_identity(role, text, false, text_format, kind_for_role(role))
}
pub(crate) fn with_kind(
role: &'static str,
text: String,
streaming: bool,
kind: TranscriptItemKind,
) -> Self {
Self::with_identity(role, text, streaming, TextFormat::PlainText, kind)
}
pub(crate) fn with_identity(
role: &'static str,
text: String,
streaming: bool,
text_format: TextFormat,
kind: TranscriptItemKind,
) -> Self {
static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
Self {
id: TranscriptItemId(NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)),
revision: 0,
source_view_item_id: None,
source_view_revision: None,
stream_integrity: None,
role,
text,
streaming,
text_format,
display_label: None,
event_sequence: None,
timestamp_ms: None,
kind,
}
}
#[must_use]
pub const fn with_event_metadata(mut self, sequence: u64, timestamp_ms: u64) -> Self {
self.event_sequence = Some(sequence);
self.timestamp_ms = Some(timestamp_ms);
self
}
#[must_use]
pub fn with_display_label(mut self, display_label: String) -> Self {
self.display_label = Some(display_label);
self
}
#[must_use]
pub fn display_role(&self) -> String {
self.display_label.as_ref().map_or_else(
|| self.role.to_owned(),
|label| format!("{} · {label}", self.role),
)
}
#[must_use]
pub const fn event_sequence(&self) -> Option<u64> {
self.event_sequence
}
#[must_use]
pub const fn timestamp_ms(&self) -> Option<u64> {
self.timestamp_ms
}
#[must_use]
pub const fn id(&self) -> TranscriptItemId {
self.id
}
#[must_use]
pub const fn source_view_item_id(
&self,
) -> Option<&bcode_session_view_models::TranscriptViewItemId> {
self.source_view_item_id.as_ref()
}
#[must_use]
pub const fn source_view_item_revision(
&self,
) -> Option<bcode_session_view_models::ViewRevision> {
self.source_view_revision
}
pub(crate) const fn with_stream_integrity(
mut self,
integrity: TranscriptStreamIntegrity,
) -> Self {
self.stream_integrity = Some(integrity);
self
}
pub(crate) fn set_stream_integrity(
&mut self,
integrity: Option<TranscriptStreamIntegrity>,
) -> bool {
if self.stream_integrity == integrity {
return false;
}
self.stream_integrity = integrity;
self.bump_revision();
true
}
#[must_use]
pub const fn stream_integrity(&self) -> Option<TranscriptStreamIntegrity> {
self.stream_integrity
}
fn with_source_view_item(
mut self,
id: bcode_session_view_models::TranscriptViewItemId,
revision: bcode_session_view_models::ViewRevision,
) -> Self {
self.source_view_item_id = Some(id);
self.source_view_revision = Some(revision);
self
}
pub(crate) fn replace_from_shared(&mut self, replacement: Self) -> bool {
debug_assert_eq!(self.source_view_item_id, replacement.source_view_item_id);
if self.source_view_revision == replacement.source_view_revision {
return false;
}
self.source_view_revision = replacement.source_view_revision;
self.stream_integrity = replacement.stream_integrity;
self.role = replacement.role;
self.text = replacement.text;
self.streaming = replacement.streaming;
self.text_format = replacement.text_format;
self.display_label = replacement.display_label;
self.event_sequence = replacement.event_sequence;
self.timestamp_ms = replacement.timestamp_ms;
self.kind = replacement.kind;
self.bump_revision();
true
}
#[must_use]
pub fn visual_invocation_id(&self) -> Option<&str> {
match &self.kind {
TranscriptItemKind::ToolRequest { tool_call_id, .. }
| TranscriptItemKind::ToolResult { tool_call_id, .. }
| TranscriptItemKind::PermissionRequest { tool_call_id, .. } => Some(tool_call_id),
TranscriptItemKind::ToolContribution { contribution, .. } => {
Some(&contribution.invocation_id)
}
TranscriptItemKind::ToolRequestDraft { draft } => Some(&draft.tool_call_id),
TranscriptItemKind::UserMessage
| TranscriptItemKind::AssistantMessage
| TranscriptItemKind::ReasoningMessage
| TranscriptItemKind::PermissionResult { .. }
| TranscriptItemKind::System
| TranscriptItemKind::Meta
| TranscriptItemKind::Skill
| TranscriptItemKind::SkillError
| TranscriptItemKind::Interaction { .. }
| TranscriptItemKind::Generic => None,
}
}
#[must_use]
pub const fn revision(&self) -> u64 {
self.revision
}
const fn bump_revision(&mut self) {
self.revision = self.revision.saturating_add(1);
}
#[must_use]
pub const fn role(&self) -> &'static str {
self.role
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
pub(crate) fn replace_presentation_text(&mut self, text: &str) {
text.clone_into(&mut self.text);
self.bump_revision();
}
#[cfg(test)]
pub fn append_text(&mut self, text: &str) {
self.text.push_str(text);
if let TranscriptItemKind::ToolResult { result, .. } = &mut self.kind {
result.push_str(text);
}
self.bump_revision();
}
#[must_use]
pub const fn tool_is_active(&self) -> bool {
match &self.kind {
TranscriptItemKind::ToolContribution {
invocation: Some(invocation),
..
} => matches!(
invocation.status,
ToolInvocationViewStatus::Running | ToolInvocationViewStatus::Waiting
),
TranscriptItemKind::ToolRequest { active, .. } => *active,
_ => false,
}
}
#[must_use]
pub const fn tool_timing(&self) -> Option<ToolTiming> {
match &self.kind {
TranscriptItemKind::ToolRequest { timing, .. }
| TranscriptItemKind::ToolResult { timing, .. } => Some(*timing),
TranscriptItemKind::ToolContribution {
invocation: Some(invocation),
..
} => Some(tool_timing_from_view(invocation)),
_ => None,
}
}
pub const fn set_tool_active(&mut self, is_active: bool) {
if let TranscriptItemKind::ToolRequest { active, .. } = &mut self.kind {
*active = is_active;
self.bump_revision();
}
}
pub const fn set_tool_started_at_ms(&mut self, started_at_ms: Option<u64>) {
match &mut self.kind {
TranscriptItemKind::ToolRequest { timing, .. }
| TranscriptItemKind::ToolResult { timing, .. } => {
timing.started_at_ms = started_at_ms;
self.bump_revision();
}
_ => {}
}
}
pub const fn set_tool_finished_at_ms(&mut self, finished_at_ms: Option<u64>) {
match &mut self.kind {
TranscriptItemKind::ToolRequest { timing, .. }
| TranscriptItemKind::ToolResult { timing, .. } => {
timing.finished_at_ms = finished_at_ms;
self.bump_revision();
}
_ => {}
}
}
pub const fn set_tool_timeout_ms(&mut self, timeout_ms: Option<u64>) {
match &mut self.kind {
TranscriptItemKind::ToolRequest { timing, .. }
| TranscriptItemKind::ToolResult { timing, .. } => {
timing.timeout_ms = timeout_ms;
self.bump_revision();
}
_ => {}
}
}
pub const fn set_tool_timed_out(&mut self, timed_out: Option<bool>) {
match &mut self.kind {
TranscriptItemKind::ToolRequest { timing, .. }
| TranscriptItemKind::ToolResult { timing, .. } => {
timing.timed_out = timed_out;
self.bump_revision();
}
_ => {}
}
}
pub const fn set_tool_duration_ms(&mut self, duration_ms: Option<u64>) {
match &mut self.kind {
TranscriptItemKind::ToolRequest { timing, .. }
| TranscriptItemKind::ToolResult { timing, .. } => {
timing.duration_ms = duration_ms;
self.bump_revision();
}
_ => {}
}
}
#[must_use]
pub const fn kind(&self) -> &TranscriptItemKind {
&self.kind
}
#[must_use]
pub const fn interaction(&self) -> Option<&InteractionViewSummary> {
match &self.kind {
TranscriptItemKind::Interaction { interaction } => Some(interaction),
_ => None,
}
}
#[must_use]
pub const fn text_format(&self) -> TextFormat {
self.text_format
}
#[must_use]
pub const fn streaming(&self) -> bool {
self.streaming
}
}
#[cfg(test)]
#[must_use]
pub fn transcript_items_from_events_with_reasoning(
events: &[SessionEvent],
include_reasoning: bool,
) -> Vec<TranscriptItem> {
let mut view = SessionView::new();
for event in events {
view.apply_event(event);
}
view.snapshot()
.transcript
.items
.iter()
.filter(|item| {
include_reasoning
|| !matches!(item.kind, TranscriptViewItemKind::ReasoningMessage { .. })
})
.map(terminal_item_from_shared)
.collect()
}
#[must_use]
pub fn tool_request_item(
tool_call_id: &str,
producer_plugin_id: Option<&str>,
tool_name: &str,
arguments_json: &str,
working_directory: Option<std::path::PathBuf>,
) -> TranscriptItem {
TranscriptItem::with_kind(
"Tool",
pretty_jsonish(arguments_json),
false,
TranscriptItemKind::ToolRequest {
tool_call_id: tool_call_id.to_owned(),
producer_plugin_id: producer_plugin_id.map(ToOwned::to_owned),
tool_name: tool_name.to_owned(),
working_directory,
active: false,
status: None,
timing: ToolTiming::default(),
},
)
}
#[must_use]
pub fn tool_result_item(
tool_call_id: &str,
tool_name: Option<&str>,
arguments_json: Option<&str>,
result: &str,
is_error: bool,
) -> TranscriptItem {
tool_result_item_with_working_directory(
tool_call_id,
tool_name,
arguments_json,
None,
result,
is_error,
)
}
fn tool_result_item_with_working_directory(
tool_call_id: &str,
tool_name: Option<&str>,
arguments_json: Option<&str>,
working_directory: Option<&std::path::Path>,
result: &str,
is_error: bool,
) -> TranscriptItem {
TranscriptItem::with_kind(
if is_error { "Tool error" } else { "Tool" },
result.to_owned(),
false,
TranscriptItemKind::ToolResult {
tool_call_id: tool_call_id.to_owned(),
tool_name: tool_name.map(ToOwned::to_owned),
arguments_json: arguments_json.map(ToOwned::to_owned),
working_directory: working_directory.map(std::path::Path::to_path_buf),
result: result.to_owned(),
artifact: None,
is_error,
timing: ToolTiming::default(),
},
)
}
#[must_use]
pub fn artifact_tool_result_item(
tool_call_id: &str,
tool_name: Option<&str>,
arguments_json: Option<&str>,
artifact: &ToolArtifact,
is_error: bool,
) -> TranscriptItem {
let result = artifact_summary_text(artifact);
let mut artifact = artifact.clone();
if let Some(arguments_json) = arguments_json
&& let Some(object) = artifact.metadata.as_object_mut()
&& !object.contains_key("arguments")
&& let Ok(arguments) = serde_json::from_str::<serde_json::Value>(arguments_json)
{
object.insert("arguments".to_owned(), arguments);
}
TranscriptItem::with_kind(
if is_error { "Tool error" } else { "Tool" },
result.clone(),
false,
TranscriptItemKind::ToolResult {
tool_call_id: tool_call_id.to_owned(),
tool_name: tool_name.map(ToOwned::to_owned),
arguments_json: arguments_json.map(ToOwned::to_owned),
working_directory: None,
result,
artifact: Some(Box::new(artifact.clone())),
is_error,
timing: tool_timing_from_artifact(&artifact),
},
)
}
#[cfg(test)]
#[must_use]
pub fn semantic_tool_result_item_from_raw(
tool_call_id: &str,
tool_name: Option<&str>,
arguments_json: Option<&str>,
working_directory: Option<&std::path::Path>,
result: &ToolInvocationResult,
is_error: bool,
) -> TranscriptItem {
match result {
ToolInvocationResult::Text { text } | ToolInvocationResult::Json { value: text } => {
tool_result_item_with_working_directory(
tool_call_id,
tool_name,
arguments_json,
working_directory,
text,
is_error,
)
}
ToolInvocationResult::Artifact { artifact } => {
artifact_tool_result_item(tool_call_id, tool_name, arguments_json, artifact, is_error)
}
}
}
#[must_use]
pub fn display_tool_result_text(result: &str) -> String {
if let Ok(result) = serde_json::from_str::<ToolInvocationResult>(result) {
return match result {
ToolInvocationResult::Text { text } | ToolInvocationResult::Json { value: text } => {
text
}
ToolInvocationResult::Artifact { artifact } => artifact_summary_text(&artifact),
};
}
serde_json::from_str::<ToolArtifact>(result).map_or_else(
|_| result.to_owned(),
|artifact| artifact_summary_text(&artifact),
)
}
#[must_use]
pub fn artifact_summary_text(artifact: &ToolArtifact) -> String {
let title = artifact.title.as_deref().unwrap_or("Tool artifact");
let summary = artifact
.metadata
.get("summary")
.and_then(serde_json::Value::as_str)
.unwrap_or(&artifact.schema);
let path = artifact
.metadata
.get("path")
.and_then(serde_json::Value::as_str);
let text = path.map_or_else(|| summary.to_owned(), |path| format!("{summary}\n{path}"));
format!("{title}\n{text}")
}
fn tool_timing_from_artifact(artifact: &ToolArtifact) -> ToolTiming {
ToolTiming {
timed_out: artifact
.metadata
.get("timed_out")
.and_then(serde_json::Value::as_bool),
duration_ms: artifact
.metadata
.get("duration_ms")
.and_then(serde_json::Value::as_u64),
..ToolTiming::default()
}
}
#[must_use]
pub fn permission_request_item(
permission_id: &str,
tool_call_id: &str,
tool_name: &str,
arguments_json: &str,
policy_source: Option<&str>,
policy_reason: Option<&str>,
) -> TranscriptItem {
let mut body = pretty_jsonish(arguments_json);
if let Some(reason) = policy_reason.filter(|reason| !reason.trim().is_empty()) {
body = format!(
"Policy: {}\nReason: {reason}\n\n{body}",
policy_source.unwrap_or("policy")
);
}
TranscriptItem::with_kind(
"Permission",
body,
false,
TranscriptItemKind::PermissionRequest {
permission_id: permission_id.to_owned(),
tool_call_id: tool_call_id.to_owned(),
tool_name: tool_name.to_owned(),
arguments_json: arguments_json.to_owned(),
policy_source: policy_source.map(str::to_owned),
policy_reason: policy_reason.map(str::to_owned),
},
)
}
#[must_use]
pub fn permission_result_item(permission_id: &str, approved: bool) -> TranscriptItem {
let status = if approved {
"permission approved"
} else {
"permission denied"
};
TranscriptItem::with_kind(
"Permission",
format!("{status}: {permission_id}"),
false,
TranscriptItemKind::PermissionResult { approved },
)
}
fn bounded_tool_request_draft_fallback(
draft: &bcode_session_view_models::ToolRequestDraftView,
) -> String {
const MAX_TOOL_NAME_CHARS: usize = 96;
let tool_name = draft
.tool_name
.chars()
.take(MAX_TOOL_NAME_CHARS)
.collect::<String>();
let truncation = if draft.truncated { " · truncated" } else { "" };
format!(
"{tool_name} request · {} bytes{truncation}",
draft.argument_bytes
)
}
fn terminal_tool_invocation_item_from_shared(
tool: &bcode_session_view_models::ToolInvocationView,
) -> TranscriptItem {
if let Some(draft) = tool.request_draft.as_ref()
&& tool.presentation.is_none()
&& tool.result.is_none()
&& tool.result_text.is_none()
{
TranscriptItem::with_kind(
"Tool request draft",
bounded_tool_request_draft_fallback(draft),
true,
TranscriptItemKind::ToolRequestDraft {
draft: Box::new(draft.clone()),
},
)
} else {
terminal_tool_item_from_shared(tool)
}
}
pub fn apply_stream_integrity(
item: TranscriptItem,
state: Option<&bcode_session_view_models::TextStreamViewState>,
) -> TranscriptItem {
match state.map(|state| state.status) {
Some(TextStreamViewStatus::Incomplete) => {
item.with_stream_integrity(TranscriptStreamIntegrity::Incomplete)
}
Some(TextStreamViewStatus::Degraded) => {
item.with_stream_integrity(TranscriptStreamIntegrity::Degraded)
}
Some(TextStreamViewStatus::Healthy | TextStreamViewStatus::Terminal(_)) | None => item,
}
}
#[must_use]
pub fn terminal_item_from_shared(item: &TranscriptViewItem) -> TranscriptItem {
let mut terminal = match &item.kind {
TranscriptViewItemKind::UserMessage { message } => message_text_item(
"You",
message,
item.streaming,
TranscriptItemKind::UserMessage,
),
TranscriptViewItemKind::AssistantMessage { message } => message_text_item(
"Assistant",
message,
item.streaming,
TranscriptItemKind::AssistantMessage,
),
TranscriptViewItemKind::ReasoningMessage { message } => message_text_item(
"Reasoning summary",
message,
item.streaming,
TranscriptItemKind::ReasoningMessage,
),
TranscriptViewItemKind::ReasoningActivity { activity } => TranscriptItem::with_identity(
reasoning_activity_title(activity.status),
reasoning_activity_body(activity),
item.streaming,
bcode_session_view_models::TextFormat::Markdown,
TranscriptItemKind::ReasoningMessage,
),
TranscriptViewItemKind::SystemMessage { message } => {
let role = message
.display_label
.as_ref()
.map_or("System", |_| "Plugin");
message_text_item(role, message, item.streaming, TranscriptItemKind::System)
}
TranscriptViewItemKind::Compaction { compaction } => TranscriptItem::with_kind(
"Compaction",
compaction.text.clone(),
item.streaming,
TranscriptItemKind::Meta,
),
TranscriptViewItemKind::Skill { skill } => terminal_skill_item_from_shared(skill),
TranscriptViewItemKind::ToolInvocation { tool } => {
terminal_tool_invocation_item_from_shared(tool)
}
TranscriptViewItemKind::ToolRequestDraft { draft } => TranscriptItem::with_kind(
"Tool request draft",
bounded_tool_request_draft_fallback(draft),
true,
TranscriptItemKind::ToolRequestDraft {
draft: Box::new(draft.clone()),
},
),
TranscriptViewItemKind::ToolRequest { tool } => {
terminal_tool_request_item_from_shared(tool)
}
TranscriptViewItemKind::Permission { permission } => {
terminal_permission_item_from_shared(permission)
}
TranscriptViewItemKind::Interaction { interaction } => {
terminal_interaction_item_from_shared(interaction)
}
TranscriptViewItemKind::ToolContribution {
contribution,
placement,
invocation,
} => {
let fallback = match placement {
bcode_session_models::ToolContributionPlacement::Request => "tool request",
bcode_session_models::ToolContributionPlacement::Progress => "tool progress",
bcode_session_models::ToolContributionPlacement::Result => "tool result",
bcode_session_models::ToolContributionPlacement::Supplemental
| bcode_session_models::ToolContributionPlacement::Hidden => "",
};
TranscriptItem::with_kind(
"Tool contribution",
fallback.to_owned(),
item.streaming,
TranscriptItemKind::ToolContribution {
contribution: Box::new(contribution.clone()),
placement: *placement,
invocation: invocation.clone(),
},
)
}
};
if let (Some(sequence), Some(timestamp_ms)) = (item.sequence, item.timestamp_ms) {
terminal = terminal.with_event_metadata(sequence, timestamp_ms);
}
terminal.with_source_view_item(item.id.clone(), item.revision)
}
const fn reasoning_activity_title(
status: bcode_session_models::ReasoningActivityStatus,
) -> &'static str {
match status {
bcode_session_models::ReasoningActivityStatus::Completed => "Reasoning",
bcode_session_models::ReasoningActivityStatus::Interrupted => "Reasoning interrupted",
bcode_session_models::ReasoningActivityStatus::Failed => "Reasoning failed",
}
}
fn reasoning_activity_body(activity: &bcode_session_view_models::ReasoningActivityView) -> String {
use bcode_session_view_models::ReasoningContentAvailability;
match activity.content_availability() {
ReasoningContentAvailability::Readable => activity.text(),
ReasoningContentAvailability::Filtered => {
"_Reasoning content hidden by the current `/thinking` display setting._".to_owned()
}
ReasoningContentAvailability::Withheld => {
"_The model reasoned about this step but did not return readable reasoning content._"
.to_owned()
}
ReasoningContentAvailability::Pending => String::new(),
}
}
fn message_text_item(
role: &'static str,
message: &bcode_session_view_models::ChatMessageView,
streaming: bool,
kind: TranscriptItemKind,
) -> TranscriptItem {
let text = if matches!(kind, TranscriptItemKind::AssistantMessage) {
bcode_session_view::presentation::model_output_text(&message.text).into_owned()
} else {
message.text.clone()
};
let item = TranscriptItem::with_identity(role, text, streaming, message.format, kind);
if let Some(label) = &message.display_label {
item.with_display_label(label.clone())
} else {
item
}
}
fn terminal_skill_item_from_shared(skill: &bcode_session_view_models::SkillView) -> TranscriptItem {
let (role, kind) = match skill.status {
bcode_session_view_models::SkillViewStatus::Invoked
| bcode_session_view_models::SkillViewStatus::Suggested => {
("Skill", TranscriptItemKind::Skill)
}
bcode_session_view_models::SkillViewStatus::ContextLoaded => {
("Skill context", TranscriptItemKind::Generic)
}
bcode_session_view_models::SkillViewStatus::Failed => {
("Skill error", TranscriptItemKind::SkillError)
}
};
TranscriptItem::with_kind(role, skill.text.clone(), false, kind)
}
fn terminal_tool_request_item_from_shared(tool: &ToolInvocationView) -> TranscriptItem {
apply_shared_tool_timing(
tool_request_item(
&tool.tool_call_id,
tool.producer_plugin_id.as_deref(),
tool.tool_name.as_deref().unwrap_or("unknown tool"),
tool.arguments_json.as_deref().unwrap_or("{}"),
tool.working_directory.clone(),
),
tool,
)
}
fn terminal_tool_item_from_shared(tool: &ToolInvocationView) -> TranscriptItem {
if let Some(presentation) = &tool.presentation {
let contribution = bcode_session_models::ToolContributionEvent {
invocation_id: tool.tool_call_id.clone(),
contribution_id: "primary".to_owned(),
sequence: presentation.revision,
producer_id: presentation.producer_id.clone(),
schema: presentation.schema.clone(),
schema_version: presentation.schema_version,
operation: bcode_session_models::ToolContributionOperation::Upsert,
persistence: match presentation.retention {
bcode_tool::ToolPresentationRetention::RetainLatest => {
bcode_session_models::ToolContributionPersistence::Durable
}
bcode_tool::ToolPresentationRetention::ActiveOnly => {
bcode_session_models::ToolContributionPersistence::Transient
}
},
artifact: presentation.artifact.clone(),
payload: presentation.payload.clone(),
};
return TranscriptItem::with_kind(
"Tool presentation",
tool.tool_name
.clone()
.unwrap_or_else(|| "tool presentation".to_owned()),
matches!(
tool.status,
ToolInvocationViewStatus::Running | ToolInvocationViewStatus::Waiting
),
TranscriptItemKind::ToolContribution {
contribution: Box::new(contribution),
placement: bcode_session_models::ToolContributionPlacement::Result,
invocation: Some(Box::new(tool.clone())),
},
);
}
if let Some(ToolResultView::Artifact { artifact }) = &tool.result {
return apply_shared_tool_timing(
artifact_tool_result_item(
&tool.tool_call_id,
tool.tool_name.as_deref(),
tool.arguments_json.as_deref(),
&artifact.artifact,
tool.is_error.unwrap_or(false),
),
tool,
);
}
if let Some(result) = tool_result_text_from_shared(tool) {
return apply_shared_tool_timing(
tool_result_item(
&tool.tool_call_id,
tool.tool_name.as_deref(),
tool.arguments_json.as_deref(),
&result,
tool.is_error.unwrap_or(false),
),
tool,
);
}
let mut item = tool_request_item(
&tool.tool_call_id,
tool.producer_plugin_id.as_deref(),
tool.tool_name.as_deref().unwrap_or("unknown tool"),
tool.arguments_json.as_deref().unwrap_or("{}"),
tool.working_directory.clone(),
);
if let TranscriptItemKind::ToolRequest { status, .. } = &mut item.kind {
*status = Some(tool.status);
}
item = apply_shared_tool_timing(item, tool);
if matches!(
tool.status,
ToolInvocationViewStatus::Running | ToolInvocationViewStatus::Waiting
) {
item.streaming = true;
}
item
}
fn tool_result_text_from_shared(tool: &ToolInvocationView) -> Option<String> {
match &tool.result {
Some(ToolResultView::Text { text }) => Some(text.clone()),
Some(ToolResultView::Json { value }) => Some(pretty_jsonish(value)),
Some(ToolResultView::Artifact { .. }) | None => {
tool.result_text.as_deref().map(display_tool_result_text)
}
}
}
const fn tool_timing_from_view(tool: &ToolInvocationView) -> ToolTiming {
ToolTiming {
started_at_ms: tool.timing.started_at_ms,
finished_at_ms: tool.timing.finished_at_ms,
timeout_ms: tool.timing.timeout_ms,
timed_out: tool.timing.timed_out,
duration_ms: tool.timing.duration_ms,
}
}
const fn apply_shared_tool_timing(
mut item: TranscriptItem,
tool: &ToolInvocationView,
) -> TranscriptItem {
item.set_tool_active(matches!(
tool.status,
ToolInvocationViewStatus::Running | ToolInvocationViewStatus::Waiting
));
item.set_tool_started_at_ms(tool.timing.started_at_ms);
item.set_tool_finished_at_ms(tool.timing.finished_at_ms);
item.set_tool_timeout_ms(tool.timing.timeout_ms);
item.set_tool_timed_out(tool.timing.timed_out);
item.set_tool_duration_ms(tool.timing.duration_ms);
item
}
fn terminal_permission_item_from_shared(
permission: &bcode_session_view_models::PermissionView,
) -> TranscriptItem {
if let Some(approved) = permission.approved {
return permission_result_item(&permission.permission_id, approved);
}
permission_request_item(
&permission.permission_id,
&permission.tool_call_id,
&permission.tool_name,
&permission.arguments_json,
permission.policy_source.as_deref(),
permission.detail.as_deref(),
)
}
fn terminal_interaction_item_from_shared(interaction: &InteractionViewSummary) -> TranscriptItem {
const MAX_INTERACTION_LABEL_CHARS: usize = 256;
const MAX_INTERACTION_DETAIL_CHARS: usize = 2_000;
let (state, detail) = match interaction.state {
bcode_session_view_models::InteractionViewState::Pending if interaction.required => (
"response required",
"Answer in the active interaction panel.",
),
bcode_session_view_models::InteractionViewState::Pending => (
"optional response pending",
"Answer in the active interaction panel.",
),
bcode_session_view_models::InteractionViewState::Submitting => {
("submitting", "Response delivery is in progress.")
}
bcode_session_view_models::InteractionViewState::ValidationError => (
"validation error",
"Correct the highlighted interaction fields.",
),
bcode_session_view_models::InteractionViewState::ActionError => (
"action error",
"The response was not delivered; retry is available.",
),
bcode_session_view_models::InteractionViewState::Resolved => {
("resolved", "The interaction completed.")
}
bcode_session_view_models::InteractionViewState::Cancelled => {
("cancelled", "The interaction is closed.")
}
};
let label = truncate_block(
interaction.title.as_deref().unwrap_or(&interaction.kind),
MAX_INTERACTION_LABEL_CHARS,
);
let text = if interaction.resolved {
interaction.resolution.as_ref().map_or_else(
|| format!("{label} ({state})"),
|resolution| {
let resolution = serde_json::to_string_pretty(resolution)
.unwrap_or_else(|_| format!("{resolution:?}"));
let resolution = truncate_block(&resolution, MAX_INTERACTION_DETAIL_CHARS);
format!("{label} ({state})\n{resolution}")
},
)
} else {
let detail = truncate_block(
interaction.status_detail.as_deref().unwrap_or(detail),
MAX_INTERACTION_DETAIL_CHARS,
);
format!("{label} ({state})\n{detail}")
};
TranscriptItem::with_kind(
"Interaction",
text,
false,
TranscriptItemKind::Interaction {
interaction: Box::new(interaction.clone()),
},
)
}
#[must_use]
pub fn pretty_jsonish(value: &str) -> String {
serde_json::from_str::<serde_json::Value>(value).map_or_else(
|_| truncate_block(value, 2_000),
|json| {
serde_json::to_string_pretty(&json).map_or_else(
|_| truncate_block(value, 2_000),
|pretty| truncate_block(&pretty, 2_000),
)
},
)
}
#[must_use]
pub fn truncate_block(value: &str, max_chars: usize) -> String {
let mut output = String::new();
for (index, ch) in value.chars().enumerate() {
if index >= max_chars {
output.push_str("\n… truncated");
return output;
}
output.push(ch);
}
output
}
fn kind_for_role(role: &str) -> TranscriptItemKind {
match role {
"You" => TranscriptItemKind::UserMessage,
"Assistant" => TranscriptItemKind::AssistantMessage,
"Reasoning summary" => TranscriptItemKind::ReasoningMessage,
"System" => TranscriptItemKind::System,
"Skill" => TranscriptItemKind::Skill,
"Skill error" => TranscriptItemKind::SkillError,
"Compaction" | "Meta" => TranscriptItemKind::Meta,
_ => TranscriptItemKind::Generic,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reconstructed_user_and_system_messages_preserve_formats() {
let session_id = bcode_session_models::SessionId::new();
let events = [
SessionEvent {
schema_version: bcode_session_models::CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence: 1,
timestamp_ms: 1,
session_id,
provenance: None,
kind: SessionEventKind::UserMessage {
client_id: bcode_session_models::ClientId::new(),
text: "# User".to_owned(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
},
SessionEvent {
schema_version: bcode_session_models::CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence: 2,
timestamp_ms: 2,
session_id,
provenance: None,
kind: SessionEventKind::SystemMessage {
text: "* System".to_owned(),
},
},
];
let items = transcript_items_from_events_with_reasoning(&events, false);
assert_eq!(items.len(), 2);
assert!(
items
.iter()
.all(|item| item.text_format() == TextFormat::Markdown)
);
}
#[test]
fn shared_message_projection_preserves_all_text_formats() {
for format in [
TextFormat::Markdown,
TextFormat::PlainText,
TextFormat::Json,
] {
let item = TranscriptViewItem {
output_location: None,
id: bcode_session_view_models::TranscriptViewItemId::new("message"),
revision: 0,
sequence: Some(1),
timestamp_ms: Some(1),
streaming: false,
kind: TranscriptViewItemKind::UserMessage {
message: bcode_session_view_models::ChatMessageView {
text: "* value".to_owned(),
display_label: None,
activity: None,
submission_status: None,
submission_context: None,
format,
},
},
};
assert_eq!(terminal_item_from_shared(&item).text_format(), format);
}
}
#[test]
fn request_draft_fallback_is_bounded_and_never_exposes_preview_json() {
let preview = "{\"secret\":\"value\"}";
let draft = bcode_session_view_models::ToolRequestDraftView {
output_location: None,
turn_id: "turn-1".to_owned(),
tool_call_id: "call-1".to_owned(),
tool_name: "x".repeat(1_000),
producer_plugin_id: None,
schema: "unknown.draft".to_owned(),
schema_version: 1,
placement: bcode_session_models::ToolContributionPlacement::Request,
generation: 1,
revision: 1,
argument_bytes: preview.len(),
preview_start_offset: 0,
preview: preview.to_owned(),
truncated: true,
};
let fallback = bounded_tool_request_draft_fallback(&draft);
assert!(fallback.len() < 160);
assert!(fallback.contains("request · 18 bytes · truncated"));
assert!(!fallback.contains(preview));
assert!(!fallback.contains("secret"));
}
#[test]
fn replacing_shared_item_updates_text_format() {
let source_id = bcode_session_view_models::TranscriptViewItemId::new("message");
let item = |revision, format| TranscriptViewItem {
output_location: None,
id: source_id.clone(),
revision,
sequence: Some(1),
timestamp_ms: Some(1),
streaming: false,
kind: TranscriptViewItemKind::UserMessage {
message: bcode_session_view_models::ChatMessageView {
text: "* value".to_owned(),
display_label: None,
activity: None,
submission_status: None,
submission_context: None,
format,
},
},
};
let mut terminal = terminal_item_from_shared(&item(0, TextFormat::PlainText));
assert!(
terminal
.replace_from_shared(terminal_item_from_shared(&item(1, TextFormat::Markdown,)))
);
assert_eq!(terminal.text_format(), TextFormat::Markdown);
}
#[test]
fn active_interaction_layout_height_changes_item_signature() {
let item = terminal_interaction_item_from_shared(&InteractionViewSummary {
producer_id: Some("example.plugin".to_owned()),
exchange_schema: Some("example.request".to_owned()),
exchange_schema_version: Some(1),
interaction_id: "question-1".to_owned(),
kind: "example.interaction".to_owned(),
tool_call_id: None,
title: Some("Question".to_owned()),
required: true,
snapshot: None,
state: bcode_session_view_models::InteractionViewState::Pending,
status_detail: None,
resolved: false,
resolution: None,
});
let fallback = crate::transcript_projection::test_layout_signature(&item, 80, None);
let first = crate::transcript_projection::test_layout_signature_with_interaction_height(
&item,
80,
"question-1",
5,
);
let resized = crate::transcript_projection::test_layout_signature_with_interaction_height(
&item,
80,
"question-1",
12,
);
assert_ne!(fallback, first);
assert_ne!(first, resized);
}
#[test]
fn skill_and_interaction_items_remain_explicit_plain_text() {
let skill = terminal_skill_item_from_shared(&bcode_session_view_models::SkillView {
skill_id: "review".to_owned(),
status: bcode_session_view_models::SkillViewStatus::ContextLoaded,
text: "* literal context".to_owned(),
});
let interaction = terminal_interaction_item_from_shared(&InteractionViewSummary {
producer_id: None,
exchange_schema: None,
exchange_schema_version: None,
interaction_id: "question-1".to_owned(),
kind: "bcode.question".to_owned(),
tool_call_id: None,
title: Some("* literal title".to_owned()),
required: true,
snapshot: None,
state: bcode_session_view_models::InteractionViewState::Pending,
status_detail: None,
resolved: false,
resolution: None,
});
assert_eq!(skill.text_format(), TextFormat::PlainText);
assert_eq!(interaction.text_format(), TextFormat::PlainText);
assert!(matches!(
interaction.kind(),
TranscriptItemKind::Interaction { interaction }
if interaction.interaction_id == "question-1"
));
}
#[test]
fn non_active_interaction_states_render_bounded_explicit_rows() {
for (state, status_detail, expected) in [
(
bcode_session_view_models::InteractionViewState::Pending,
None,
"response required",
),
(
bcode_session_view_models::InteractionViewState::Submitting,
None,
"submitting",
),
(
bcode_session_view_models::InteractionViewState::ValidationError,
Some("Choose one"),
"validation error",
),
(
bcode_session_view_models::InteractionViewState::ActionError,
Some("offline; retry"),
"action error",
),
(
bcode_session_view_models::InteractionViewState::Cancelled,
None,
"cancelled",
),
] {
let interaction = InteractionViewSummary {
producer_id: Some("future.plugin".to_owned()),
exchange_schema: Some("future.request".to_owned()),
exchange_schema_version: Some(99),
interaction_id: format!("{state:?}"),
kind: "future.interaction".to_owned(),
tool_call_id: None,
title: Some("Future interaction".to_owned()),
required: true,
snapshot: Some(serde_json::json!({"large": "x".repeat(100_000)})),
state,
status_detail: status_detail.map(ToOwned::to_owned),
resolved: state == bcode_session_view_models::InteractionViewState::Cancelled,
resolution: (state == bcode_session_view_models::InteractionViewState::Cancelled)
.then_some(bcode_session_models::ToolExchangeResolution::Cancelled),
};
let item = terminal_interaction_item_from_shared(&interaction);
assert!(item.text().contains(expected));
assert!(item.text().len() < 512);
assert!(!item.text().contains("100000"));
}
}
#[test]
fn unknown_or_unavailable_adapter_remains_bounded_and_understandable() {
for (required, expected) in [
(true, "response required"),
(false, "optional response pending"),
] {
let interaction = InteractionViewSummary {
producer_id: Some("unknown.producer".to_owned()),
exchange_schema: Some("unknown.interaction".to_owned()),
exchange_schema_version: Some(u32::MAX),
interaction_id: format!("unknown-{required}"),
kind: "unknown.interaction".to_owned(),
tool_call_id: Some("unknown-call".to_owned()),
title: None,
required,
snapshot: Some(serde_json::json!({
"untrusted": "x".repeat(1_000_000),
"controls": ["not", "interpreted"]
})),
state: bcode_session_view_models::InteractionViewState::Pending,
status_detail: None,
resolved: false,
resolution: None,
};
let item = terminal_interaction_item_from_shared(&interaction);
assert!(
item.text().contains("unknown.interaction"),
"{}",
item.text()
);
assert!(item.text().contains(expected));
assert!(item.text().contains("active interaction panel"));
assert!(item.text().len() < 512);
assert!(!item.text().contains("not"));
assert!(!item.text().contains(&"x".repeat(64)));
}
}
#[test]
fn hostile_unknown_interaction_metadata_degrades_to_bounded_ordinary_text() {
let interaction = InteractionViewSummary {
producer_id: Some("future.plugin".to_owned()),
exchange_schema: None,
exchange_schema_version: None,
interaction_id: "hostile".to_owned(),
kind: "k".repeat(100_000),
tool_call_id: None,
title: Some("t".repeat(100_000)),
required: true,
snapshot: Some(serde_json::json!({"ignored": "x".repeat(1_000_000)})),
state: bcode_session_view_models::InteractionViewState::ActionError,
status_detail: Some("d".repeat(100_000)),
resolved: false,
resolution: None,
};
let item = terminal_interaction_item_from_shared(&interaction);
assert!(item.text().chars().count() <= 2_300);
assert!(item.text().contains("action error"));
assert!(!item.text().contains(&"x".repeat(64)));
assert_eq!(item.text_format(), TextFormat::PlainText);
}
#[test]
fn pending_interaction_summary_avoids_duplicate_raw_form_payload() {
let interaction = InteractionViewSummary {
producer_id: None,
exchange_schema: None,
exchange_schema_version: None,
interaction_id: "question-1".to_owned(),
kind: "bcode.question".to_owned(),
tool_call_id: Some("call-1".to_owned()),
title: Some("Question".to_owned()),
required: true,
snapshot: Some(serde_json::json!({
"questions": [{"question": "Secret raw form payload"}]
})),
state: bcode_session_view_models::InteractionViewState::Pending,
status_detail: None,
resolved: false,
resolution: None,
};
let item = terminal_interaction_item_from_shared(&interaction);
assert!(item.text().contains("response required"));
assert!(item.text().contains("active interaction panel"));
assert!(!item.text().contains("Secret raw form payload"));
}
#[test]
fn resolved_interaction_summary_keeps_durable_outcome() {
let interaction = InteractionViewSummary {
producer_id: None,
exchange_schema: None,
exchange_schema_version: None,
interaction_id: "question-1".to_owned(),
kind: "bcode.question".to_owned(),
tool_call_id: Some("call-1".to_owned()),
title: Some("Question".to_owned()),
required: true,
snapshot: None,
state: bcode_session_view_models::InteractionViewState::Resolved,
status_detail: None,
resolved: true,
resolution: Some(bcode_session_models::ToolExchangeResolution::Responded {
payload: serde_json::json!({"status": "answered", "selected": ["yes"]}),
}),
};
let item = terminal_interaction_item_from_shared(&interaction);
assert!(item.text().contains("resolved"));
assert!(item.text().contains("answered"));
assert!(item.text().contains("yes"));
}
#[test]
fn structured_reasoning_activity_adapts_terminal_lifecycle_chrome() {
let sentinel = "encrypted-sentinel-do-not-expose";
for (status, expected) in [
(
bcode_session_models::ReasoningActivityStatus::Completed,
"Reasoning",
),
(
bcode_session_models::ReasoningActivityStatus::Interrupted,
"Reasoning interrupted",
),
(
bcode_session_models::ReasoningActivityStatus::Failed,
"Reasoning failed",
),
] {
let item = TranscriptViewItem {
output_location: None,
id: bcode_session_view_models::TranscriptViewItemId::new(format!(
"reasoning:{status:?}"
)),
revision: 0,
sequence: Some(1),
timestamp_ms: None,
streaming: false,
kind: TranscriptViewItemKind::ReasoningActivity {
activity: bcode_session_view_models::ReasoningActivityView {
turn_id: "turn-1".to_owned(),
activity_id: "reasoning-1".to_owned(),
order: 0,
status,
parts: Vec::new(),
opaque: true,
readable_parts_filtered: false,
finished: true,
},
},
};
let terminal = terminal_item_from_shared(&item);
assert_eq!(terminal.role(), expected);
assert!(
terminal
.text()
.contains("did not return readable reasoning"),
"opaque-only reasoning must be explained, got {:?}",
terminal.text()
);
assert!(!format!("{terminal:?}").contains(sentinel));
}
}
#[test]
fn reasoning_activity_body_explains_every_availability_state() {
let readable_part = |text: &str| bcode_session_models::ReasoningPart {
part_id: "raw-0".to_owned(),
kind: bcode_session_models::ReasoningContentKind::Raw,
role: bcode_session_models::ReasoningContentRole::Detail,
order: 0,
text: text.to_owned(),
};
let activity = |parts: Vec<bcode_session_models::ReasoningPart>,
opaque: bool,
readable_parts_filtered: bool| {
bcode_session_view_models::ReasoningActivityView {
turn_id: "turn-1".to_owned(),
activity_id: "reasoning-1".to_owned(),
order: 0,
status: bcode_session_models::ReasoningActivityStatus::Completed,
parts,
opaque,
readable_parts_filtered,
finished: true,
}
};
assert_eq!(
reasoning_activity_body(&activity(
vec![readable_part("visible thought")],
false,
false
)),
"visible thought"
);
assert!(
reasoning_activity_body(&activity(Vec::new(), true, false))
.contains("did not return readable reasoning"),
"withheld reasoning must be explained"
);
assert!(
reasoning_activity_body(&activity(Vec::new(), true, true)).contains("`/thinking`"),
"locally filtered reasoning must point at the display setting"
);
let mut pending = activity(Vec::new(), true, false);
pending.finished = false;
assert!(reasoning_activity_body(&pending).is_empty());
assert!(
reasoning_activity_body(&activity(Vec::new(), false, false)).is_empty(),
"pending reasoning must not claim the provider withheld anything"
);
}
#[test]
fn shared_generic_items_adapt_without_renderer_types_crossing_the_boundary() {
let cases = [
(
TranscriptViewItemKind::UserMessage {
message: bcode_session_view_models::ChatMessageView::markdown("hello"),
},
"You",
TranscriptItemKind::UserMessage,
),
(
TranscriptViewItemKind::AssistantMessage {
message: bcode_session_view_models::ChatMessageView::markdown("answer"),
},
"Assistant",
TranscriptItemKind::AssistantMessage,
),
(
TranscriptViewItemKind::ReasoningMessage {
message: bcode_session_view_models::ChatMessageView::markdown("thought"),
},
"Reasoning summary",
TranscriptItemKind::ReasoningMessage,
),
(
TranscriptViewItemKind::SystemMessage {
message: bcode_session_view_models::ChatMessageView::markdown("status"),
},
"System",
TranscriptItemKind::System,
),
];
for (kind, role, expected_kind) in cases {
let shared = TranscriptViewItem {
output_location: None,
id: bcode_session_view_models::TranscriptViewItemId::new("test:item"),
revision: 1,
sequence: Some(7),
timestamp_ms: Some(9),
streaming: false,
kind,
};
let terminal = terminal_item_from_shared(&shared);
assert_eq!(terminal.role, role);
assert_eq!(terminal.kind(), &expected_kind);
assert_eq!(terminal.event_sequence(), Some(7));
}
}
#[test]
fn generic_turn_origin_labels_only_the_matching_user_turn() {
let session_id = bcode_session_models::SessionId::new();
let events = vec![
SessionEvent {
schema_version: bcode_session_models::CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence: 5,
timestamp_ms: 1,
session_id,
provenance: None,
kind: SessionEventKind::UserMessage {
client_id: bcode_session_models::ClientId::new(),
text: "automated prompt".to_owned(),
admission: bcode_session_models::TurnAdmissionMetadata {
origin: Some(bcode_session_models::TurnOrigin {
producer: "test.producer".to_owned(),
correlation_id: Some("operation-1".to_owned()),
display_label: Some("Background pass 4".to_owned()),
}),
..bcode_session_models::TurnAdmissionMetadata::default()
},
},
},
SessionEvent {
schema_version: bcode_session_models::CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence: 7,
timestamp_ms: 3,
session_id,
provenance: None,
kind: SessionEventKind::UserMessage {
client_id: bcode_session_models::ClientId::new(),
text: "manual steering".to_owned(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
},
];
let items = transcript_items_from_events_with_reasoning(&events, false);
assert_eq!(items[0].display_role(), "You · Background pass 4");
assert_eq!(items[0].text(), "automated prompt");
assert_eq!(items[1].display_role(), "You");
assert_eq!(items[1].text(), "manual steering");
}
#[test]
fn provider_compaction_transcript_hides_opaque_payloads() {
let secret = "secret-opaque-transcript-value";
let event = SessionEvent {
schema_version: bcode_session_models::CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence: 1,
timestamp_ms: 1,
session_id: bcode_session_models::SessionId::new(),
provenance: None,
kind: SessionEventKind::ProviderContextCompacted {
compacted_through_sequence: 0,
snapshot: bcode_session_models::ProviderContextSnapshot {
format_version: 1,
request_fingerprint: None,
request_id: None,
provider_plugin_id: "provider".to_owned(),
model_id: "model".to_owned(),
compatibility_key: "surface".to_owned(),
auth_profile: None,
origin: bcode_session_models::ProviderContextSnapshotOrigin::Explicit,
messages_json: format!(r#"[{{"encrypted":"{secret}"}}]"#),
portable_summary: "portable summary".to_owned(),
},
},
};
let items = transcript_items_from_events_with_reasoning(&[event], false);
assert_eq!(items.len(), 1);
assert!(items[0].text().contains("context compaction"));
assert!(!items[0].text().contains(secret));
assert!(!items[0].text().contains("portable summary"));
}
#[test]
fn working_directory_change_projects_as_markdown() {
let session_id = bcode_session_models::SessionId::new();
let events = [SessionEvent {
schema_version: bcode_session_models::CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence: 1,
timestamp_ms: 1,
session_id,
provenance: None,
kind: SessionEventKind::WorkingDirectoryChanged {
old_working_directory: std::path::PathBuf::from("/tmp/old"),
new_working_directory: std::path::PathBuf::from("/tmp/new"),
},
}];
let items = transcript_items_from_events_with_reasoning(&events, false);
assert_eq!(items.len(), 1);
assert_eq!(items[0].text_format(), TextFormat::Markdown);
assert!(items[0].text().contains("`../new`"));
}
#[test]
fn plugin_status_note_projects_as_compact_plugin_transcript_item() {
let session_id = bcode_session_models::SessionId::new();
let events = [SessionEvent {
schema_version: bcode_session_models::CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence: 1,
timestamp_ms: 1,
session_id,
provenance: None,
kind: SessionEventKind::PluginStatusNote {
plugin_id: "bcode.loop".to_owned(),
note_id: "run-1:lifecycle:Completed".to_owned(),
text: "Loop completed · evaluator accepted: done".to_owned(),
metadata: std::collections::BTreeMap::new(),
},
}];
let items = transcript_items_from_events_with_reasoning(&events, false);
assert_eq!(items.len(), 1);
assert_eq!(items[0].display_role(), "Plugin · bcode.loop");
assert_eq!(items[0].text(), "Loop completed · evaluator accepted: done");
assert_eq!(items[0].text_format(), TextFormat::PlainText);
}
#[test]
fn transcript_item_display_label_is_generic() {
let item = TranscriptItem::new("You", "text".to_owned())
.with_display_label("Plugin operation".to_owned());
assert_eq!(item.display_role(), "You · Plugin operation");
}
}