use std::num::NonZeroU32;
use serde::Deserialize;
use super::read_support::{Snapshot, bounded, call, failure};
use super::review_lines;
use super::{EncodeError, Sender, ServiceRefusal, ToolResult, refused};
use crate::mcp::{rpc, types};
use crate::serve::protocol;
#[derive(Deserialize)]
struct Difference {
selection: Snapshot,
from: Snapshot,
diff: Vec<review_lines::Difference>,
next_after: Option<String>,
base_moved: Option<u64>,
}
#[derive(Deserialize)]
struct Partial {
changes: u64,
conflicts: u64,
}
#[derive(Deserialize)]
struct Counted {
selection: Snapshot,
from: Snapshot,
parent: Snapshot,
partial: Partial,
base_moved: u64,
done: bool,
next_after: Option<String>,
}
#[derive(Deserialize)]
struct Prepared<T> {
selection: Snapshot,
from: Snapshot,
parent: Snapshot,
lines: Vec<T>,
done: bool,
next_after: Option<String>,
}
impl<T> Prepared<T> {
fn require(
&self,
selected: &Snapshot,
from: &Snapshot,
target: &Snapshot,
) -> Result<(), ServiceRefusal> {
self.selection.require(selected)?;
self.from.require(from)?;
self.parent.require(target)?;
complete(self.done, self.next_after.as_deref())
}
}
struct Review {
difference: Difference,
counted: Counted,
prepared: Prepared<review_lines::Conflict>,
dangling: Prepared<types::DanglingLine>,
}
fn complete(done: bool, next: Option<&str>) -> Result<(), ServiceRefusal> {
if done == next.is_none() {
Ok(())
} else {
Err(failure(
"mcp_service_answer_invalid",
"The served completion flag contradicts its continuation",
))
}
}
fn request(
input: &types::ReviewBranchInput,
after: Option<String>,
limit: NonZeroU32,
) -> protocol::ReviewPage {
protocol::ReviewPage {
branch: input.selection.branch.clone(),
after,
limit,
}
}
fn read(
sender: &mut impl Sender,
input: &types::ReviewBranchInput,
limit: NonZeroU32,
) -> Result<Review, ServiceRefusal> {
let cursors = &input.after;
let difference: Difference = call(
sender,
protocol::Operation::Review(request(input, cursors.diff.clone(), limit)),
)?;
difference.selection.requested(&input.selection)?;
let counted: Counted = call(
sender,
protocol::Operation::ReviewCounts(request(input, cursors.counts.clone(), limit)),
)?;
counted.selection.require(&difference.selection)?;
counted.from.require(&difference.from)?;
complete(counted.done, counted.next_after.as_deref())?;
if difference
.base_moved
.is_some_and(|moved| moved != counted.base_moved)
{
return Err(failure(
"mcp_service_snapshot_mismatch",
"The target moved between served review pages",
));
}
let prepared: Prepared<review_lines::Conflict> = call(
sender,
protocol::Operation::MergePrepare(request(input, cursors.conflicts.clone(), limit)),
)?;
prepared.require(&difference.selection, &difference.from, &counted.parent)?;
let dangling: Prepared<types::DanglingLine> = call(
sender,
protocol::Operation::MergeDangling(request(input, cursors.dangling.clone(), limit)),
)?;
dangling.require(&difference.selection, &difference.from, &counted.parent)?;
Ok(Review {
difference,
counted,
prepared,
dangling,
})
}
fn project(review: Review) -> Result<types::ReviewBranchOutput, ServiceRefusal> {
Ok(types::ReviewBranchOutput {
selection: review.difference.selection.output(),
from: review.difference.from.output(),
target: review.counted.parent.output(),
diff: types::Page {
entries: review
.difference
.diff
.into_iter()
.map(review_lines::difference)
.collect::<Result<Vec<_>, _>>()?,
next_after: review.difference.next_after,
},
counts: types::ReviewCountPage {
partial: types::ReviewCounts {
changes: review.counted.partial.changes,
conflicts: review.counted.partial.conflicts,
},
next_after: review.counted.next_after,
},
conflicts: types::Page {
entries: review
.prepared
.lines
.into_iter()
.map(review_lines::conflict)
.collect::<Result<Vec<_>, _>>()?,
next_after: review.prepared.next_after,
},
dangling: types::Page {
entries: review.dangling.lines,
next_after: review.dangling.next_after,
},
base_moved: review.difference.base_moved,
})
}
fn review(
sender: &mut impl Sender,
input: &types::ReviewBranchInput,
) -> Result<ToolResult, ServiceRefusal> {
let limit = input.page.rows;
let cursors = &input.after;
if input.selection.version.is_none()
&& (cursors.diff.is_some()
|| cursors.counts.is_some()
|| cursors.conflicts.is_some()
|| cursors.dangling.is_some())
{
return Err(failure(
"mcp_version_required",
"Review continuation requires the captured selection version",
));
}
let output = project(read(sender, input, limit)?)?;
bounded(&output, input.page.bytes.get())
}
pub(super) fn dispatch(
sender: &mut impl Sender,
request: &rpc::ToolCall,
) -> Result<ToolResult, EncodeError> {
let input = match serde_json::from_str(request.arguments.get()) {
Ok(input) => input,
Err(error) => return refused(&failure("mcp_arguments_invalid", error.to_string())),
};
match review(sender, &input) {
Ok(answer) => Ok(answer),
Err(error) => refused(&error),
}
}