use aion_core::{AssistantDocumentContext, AssistantTurnContext};
use aion_mcp::tools::service::{ToolFailure, ToolOutcome};
use serde_json::{Value, json};
use crate::assistant::sessions::AssistantSessions;
use super::caller::AssistantSessionCaller;
pub(crate) async fn assistant_context(
sessions: &AssistantSessions,
caller: &AssistantSessionCaller,
) -> Result<ToolOutcome, ToolFailure> {
let projection = sessions
.projection(caller.session_id())
.await
.map_err(|error| {
ToolFailure::new(
format!(
"the operator's on-screen context could not be read for this conversation: \
{error}. This does NOT mean the screen is empty — it means this server could \
not read it, so do not conclude anything about what is open."
),
json!({ "code": "context_unreadable" }),
)
})?;
Ok(outcome(
projection.latest_context.as_ref(),
projection.document_revision,
))
}
fn outcome(context: Option<&AssistantTurnContext>, revision: u64) -> ToolOutcome {
let Some(context) = context else {
return ToolOutcome {
structured: json!({ "shared": false }),
summary: "The operator has not shared what is on their screen yet.".to_owned(),
};
};
ToolOutcome {
structured: json!({
"shared": true,
"url": context.url,
"concepts": context.concepts,
"document": context.document.as_ref().map(document),
"revision": revision,
}),
summary: summary(context),
}
}
fn document(document: &AssistantDocumentContext) -> Value {
json!({
"path": document.path,
"text": document.text,
"selection": document.selection.map(|selection| json!({
"from_line": selection.from.line.saturating_add(1),
"from_column": selection.from.column.saturating_add(1),
"to_line": selection.to.line.saturating_add(1),
"to_column": selection.to.column.saturating_add(1),
})),
"cursor": document.cursor.map(|cursor| json!({
"line": cursor.line.saturating_add(1),
"column": cursor.column.saturating_add(1),
})),
})
}
fn describe_where(document: &AssistantDocumentContext) -> String {
match (document.selection.as_ref(), document.cursor.as_ref()) {
(Some(_), _) => " with a selection".to_owned(),
(None, Some(cursor)) => format!(
" with nothing selected and the cursor on line {}",
cursor.line.saturating_add(1)
),
(None, None) => " with nothing selected".to_owned(),
}
}
fn summary(context: &AssistantTurnContext) -> String {
match (context.document.as_ref(), context.url.as_deref()) {
(Some(document), _) => format!(
"The operator is editing `{}`{}.",
document.path,
describe_where(document)
),
(None, Some(url)) => format!("The operator is on `{url}` with no document open."),
(None, None) => {
"The operator shared a context that names neither a page nor a document.".to_owned()
}
}
}
#[cfg(test)]
mod tests {
use aion_core::{AssistantDocumentPosition, AssistantDocumentSelection};
use super::*;
fn context() -> AssistantTurnContext {
AssistantTurnContext {
url: Some("/studio/pipeline.awl".to_owned()),
concepts: vec!["awl.step".to_owned()],
document: Some(AssistantDocumentContext {
path: "pipeline.awl".to_owned(),
text: "workflow demo {}".to_owned(),
selection: Some(AssistantDocumentSelection {
from: AssistantDocumentPosition { line: 0, column: 0 },
to: AssistantDocumentPosition { line: 2, column: 4 },
}),
cursor: Some(AssistantDocumentPosition { line: 2, column: 4 }),
}),
}
}
#[test]
fn the_caret_is_answered_one_based_or_null() {
let answered = outcome(Some(&context()), 0);
assert_eq!(
answered.structured["document"]["cursor"],
json!({ "line": 3, "column": 5 })
);
let mut without = context();
if let Some(document) = without.document.as_mut() {
document.cursor = None;
document.selection = None;
}
let answered = outcome(Some(&without), 0);
assert_eq!(answered.structured["document"]["cursor"], Value::Null);
assert!(answered.summary.ends_with("with nothing selected."));
let mut caret_only = context();
if let Some(document) = caret_only.document.as_mut() {
document.selection = None;
}
let answered = outcome(Some(&caret_only), 0);
assert!(
answered
.summary
.ends_with("with nothing selected and the cursor on line 3.")
);
}
#[test]
fn an_unshared_context_says_so_rather_than_inventing_an_empty_screen() {
let answered = outcome(None, 0);
assert_eq!(answered.structured, json!({ "shared": false }));
assert!(answered.structured.get("document").is_none());
assert!(answered.summary.contains("not shared"));
}
#[test]
fn a_selection_is_answered_in_the_numbers_the_operator_reads() {
let answered = outcome(Some(&context()), 3);
let selection = &answered.structured["document"]["selection"];
assert_eq!(selection["from_line"], json!(1));
assert_eq!(selection["from_column"], json!(1));
assert_eq!(selection["to_line"], json!(3));
assert_eq!(selection["to_column"], json!(5));
}
#[test]
fn the_document_path_is_answered_so_nothing_has_to_ask_where_the_repo_is() {
let answered = outcome(Some(&context()), 3);
assert_eq!(
answered.structured["document"]["path"],
json!("pipeline.awl")
);
assert_eq!(answered.structured["shared"], json!(true));
assert!(answered.summary.contains("pipeline.awl"));
}
#[test]
fn a_shared_context_carries_the_document_revision() {
let answered = outcome(Some(&context()), 7);
assert_eq!(answered.structured["revision"], json!(7));
}
#[test]
fn a_document_with_no_selection_answers_a_null_selection() {
let mut unselected = context();
if let Some(document) = unselected.document.as_mut() {
document.selection = None;
}
let answered = outcome(Some(&unselected), 0);
assert_eq!(answered.structured["document"]["selection"], Value::Null);
assert!(answered.summary.contains("nothing selected"));
}
}