use std::collections::HashSet;
use std::collections::VecDeque;
use std::time::Instant;
use serde_json::Value;
use supercode_harness::FrontendApprovalDecision;
use supercode_harness::FrontendElicitationAction;
use supercode_harness::FrontendEvent;
use supercode_harness::FrontendOperationInvocation;
use supercode_harness::FrontendRequest;
use supercode_harness::FrontendRequestKind;
use supercode_harness::FrontendResponse;
use supercode_harness::FrontendRuntimeDescriptor;
use supercode_harness::FrontendTurnState;
use unicode_segmentation::UnicodeSegmentation;
use crate::paste_burst::PasteBurst;
use crate::semantic_value_summary;
use crate::ComposerCapabilities;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ComposerKeyCode {
Char(char),
Enter,
Backspace,
Delete,
Left,
Right,
Home,
End,
Up,
Down,
Escape,
Other,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ComposerKey {
pub code: ComposerKeyCode,
pub control: bool,
pub shift: bool,
pub alt: bool,
pub release: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ComposerInputEvent {
Key(ComposerKey),
Paste(String),
Resize { width: u16, height: u16 },
FocusChanged(bool),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ComposerTurnState {
Idle,
Working { since: Instant },
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ComposerOverlayKind {
Approval,
ChildApproval,
Elicitation,
Other,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ComposerOverlay {
pub request: FrontendRequest,
pub kind: ComposerOverlayKind,
pub input: String,
cursor: usize,
}
impl ComposerOverlay {
pub fn summary(&self) -> String {
let label = match self.kind {
ComposerOverlayKind::Approval | ComposerOverlayKind::ChildApproval => {
"Approval request"
}
ComposerOverlayKind::Elicitation => "MCP input request",
ComposerOverlayKind::Other => "Runtime input request",
};
semantic_value_summary(&self.request.payload, label, 512)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ComposerAction {
Submit(String),
Invoke(FrontendOperationInvocation),
Steer(String),
Interrupt,
Respond {
response: FrontendResponse,
request: FrontendRequest,
resolution: String,
},
}
#[derive(Clone, Debug)]
pub struct ComposerModel {
capabilities: ComposerCapabilities,
input: String,
cursor: usize,
history: Vec<String>,
history_index: Option<usize>,
history_draft: Option<String>,
queued_steering: VecDeque<String>,
turn_state: ComposerTurnState,
overlays: VecDeque<ComposerOverlay>,
paste_burst: PasteBurst,
resolved_requests: HashSet<u64>,
canonical_resolved_requests: HashSet<u64>,
last_sequence: u64,
last_failure: Option<String>,
}
impl ComposerModel {
pub fn new(descriptor: &FrontendRuntimeDescriptor) -> Self {
Self {
capabilities: ComposerCapabilities::from_descriptor(descriptor),
input: String::new(),
cursor: 0,
history: Vec::new(),
history_index: None,
history_draft: None,
queued_steering: VecDeque::new(),
turn_state: match descriptor.turn_state {
FrontendTurnState::Idle => ComposerTurnState::Idle,
FrontendTurnState::Busy => ComposerTurnState::Working {
since: Instant::now(),
},
},
overlays: VecDeque::new(),
paste_burst: PasteBurst::default(),
resolved_requests: HashSet::new(),
canonical_resolved_requests: HashSet::new(),
last_sequence: 0,
last_failure: None,
}
}
pub fn capabilities(&self) -> &ComposerCapabilities {
&self.capabilities
}
pub fn input(&self) -> &str {
&self.input
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn history(&self) -> &[String] {
&self.history
}
pub fn queued_steering(&self) -> &VecDeque<String> {
&self.queued_steering
}
pub fn turn_state(&self) -> ComposerTurnState {
self.turn_state
}
pub fn overlay(&self) -> Option<&ComposerOverlay> {
self.overlays.front()
}
pub fn pending_overlay_count(&self) -> usize {
self.overlays.len()
}
pub fn last_failure(&self) -> Option<&str> {
self.last_failure.as_deref()
}
pub fn record_dispatch_failure(&mut self, action: &str) {
self.turn_state = ComposerTurnState::Idle;
self.last_failure = Some(format!("{action}_failure"));
}
pub fn restore_request(&mut self, request: FrontendRequest) -> bool {
if self.canonical_resolved_requests.contains(&request.id) {
return false;
}
self.resolved_requests.remove(&request.id);
if self
.overlays
.iter()
.any(|overlay| overlay.request.id == request.id)
{
return false;
}
let kind = overlay_kind(&request);
self.overlays.push_front(ComposerOverlay {
request,
kind,
input: String::new(),
cursor: 0,
});
true
}
pub fn refresh_descriptor(&mut self, descriptor: &FrontendRuntimeDescriptor) {
self.capabilities = ComposerCapabilities::from_descriptor(descriptor);
if descriptor.turn_state == FrontendTurnState::Busy
&& self.turn_state == ComposerTurnState::Idle
{
self.turn_state = ComposerTurnState::Working {
since: Instant::now(),
};
}
}
pub fn apply_event(&mut self, event: &FrontendEvent) -> bool {
if event.sequence <= self.last_sequence {
return false;
}
self.last_sequence = event.sequence;
match event.kind.as_str() {
"turn_started" | "user_message" if self.turn_state == ComposerTurnState::Idle => {
self.turn_state = ComposerTurnState::Working {
since: Instant::now(),
};
true
}
"turn_succeeded" | "turn_interrupted" | "turn_failed" => {
self.turn_state = ComposerTurnState::Idle;
self.queued_steering.clear();
if event.kind == "turn_failed" {
self.last_failure = Some(event.kind.clone());
}
true
}
"stream_error" | "transport_error" | "watch_error" | "runtime_disconnected" => {
self.turn_state = ComposerTurnState::Idle;
self.last_failure = Some(event.kind.clone());
true
}
"request" => self.open_request(event),
"request_resolved" => self.apply_request_resolution(event),
_ => false,
}
}
fn apply_request_resolution(&mut self, event: &FrontendEvent) -> bool {
let Some(request_id) = event.payload.get("request_id").and_then(Value::as_u64) else {
return false;
};
let previous = self.overlays.len();
self.overlays
.retain(|overlay| overlay.request.id != request_id);
self.resolved_requests.insert(request_id);
self.canonical_resolved_requests.insert(request_id);
self.overlays.len() != previous
}
pub fn handle_input(&mut self, event: ComposerInputEvent) -> Option<ComposerAction> {
match event {
ComposerInputEvent::Key(key) => self.handle_key(key),
ComposerInputEvent::Paste(text) => {
self.paste_burst.clear_after_explicit_paste();
self.insert_text(&text);
None
}
ComposerInputEvent::Resize { .. } | ComposerInputEvent::FocusChanged(_) => None,
}
}
pub fn handle_key(&mut self, key: ComposerKey) -> Option<ComposerAction> {
self.handle_key_at(key, Instant::now())
}
fn handle_key_at(&mut self, key: ComposerKey, now: Instant) -> Option<ComposerAction> {
if key.release {
return None;
}
if !self.overlays.is_empty() {
self.paste_burst.clear_window_after_non_char();
return self.handle_overlay_key(key);
}
let control = key.control;
let shift = key.shift;
if let ComposerKeyCode::Char(character) = key.code {
if !control && !key.alt {
if self.paste_burst.on_plain_char_no_hold(now).is_some() {
self.paste_burst.extend_window(now);
}
self.insert_char(character);
return None;
}
}
if key.code == ComposerKeyCode::Enter
&& !shift
&& !control
&& self.paste_burst.direct_insert_newline_should_insert(now)
{
self.insert_text("\n");
self.paste_burst.extend_window(now);
return None;
}
self.paste_burst.clear_window_after_non_char();
match key.code {
ComposerKeyCode::Enter if shift || control => {
self.insert_text("\n");
None
}
ComposerKeyCode::Char('j') if control => {
self.insert_text("\n");
None
}
ComposerKeyCode::Enter => self.submit_or_steer(),
ComposerKeyCode::Char('c') if control => {
if matches!(self.turn_state, ComposerTurnState::Working { .. })
&& self.capabilities.can_interrupt
{
Some(ComposerAction::Interrupt)
} else {
self.clear_input();
None
}
}
ComposerKeyCode::Backspace => {
self.backspace();
None
}
ComposerKeyCode::Delete => {
self.delete();
None
}
ComposerKeyCode::Left => {
self.cursor = previous_grapheme_boundary(&self.input, self.cursor);
None
}
ComposerKeyCode::Right => {
self.cursor = next_grapheme_boundary(&self.input, self.cursor);
None
}
ComposerKeyCode::Home => {
self.cursor = line_start(&self.input, self.cursor);
None
}
ComposerKeyCode::End => {
self.cursor = line_end(&self.input, self.cursor);
None
}
ComposerKeyCode::Up => {
self.history_previous();
None
}
ComposerKeyCode::Down => {
self.history_next();
None
}
_ => None,
}
}
pub fn set_input(&mut self, input: impl Into<String>) {
self.input = input.into();
self.cursor = self.input.len();
self.history_index = None;
self.history_draft = None;
}
fn submit_or_steer(&mut self) -> Option<ComposerAction> {
if self.input.trim().is_empty() {
return None;
}
let prompt = std::mem::take(&mut self.input);
self.cursor = 0;
self.history_index = None;
self.history_draft = None;
if self.history.last() != Some(&prompt) {
self.history.push(prompt.clone());
}
if self.turn_state == ComposerTurnState::Idle {
if let Some(operation) = self.capabilities.invocation_for_command(&prompt) {
self.turn_state = ComposerTurnState::Working {
since: Instant::now(),
};
self.last_failure = None;
return Some(ComposerAction::Invoke(operation));
}
}
match self.turn_state {
ComposerTurnState::Idle if self.capabilities.can_submit => {
self.turn_state = ComposerTurnState::Working {
since: Instant::now(),
};
self.last_failure = None;
Some(ComposerAction::Submit(prompt))
}
ComposerTurnState::Working { .. } if self.capabilities.can_steer => {
self.queued_steering.push_back(prompt.clone());
Some(ComposerAction::Steer(prompt))
}
_ => {
self.input = prompt;
self.cursor = self.input.len();
None
}
}
}
fn open_request(&mut self, event: &FrontendEvent) -> bool {
if !self.capabilities.can_respond {
return false;
}
let value = event
.payload
.get("request")
.unwrap_or(&event.payload)
.clone();
let Ok(request) = serde_json::from_value::<FrontendRequest>(value) else {
return false;
};
if self.resolved_requests.contains(&request.id)
|| self
.overlays
.iter()
.any(|overlay| overlay.request.id == request.id)
{
return false;
}
let kind = overlay_kind(&request);
self.overlays.push_back(ComposerOverlay {
request,
kind,
input: String::new(),
cursor: 0,
});
true
}
fn handle_overlay_key(&mut self, key: ComposerKey) -> Option<ComposerAction> {
let kind = self.overlays.front()?.kind;
let control = key.control;
match kind {
ComposerOverlayKind::Approval | ComposerOverlayKind::ChildApproval => match key.code {
ComposerKeyCode::Char('y') => {
self.resolve_approval(FrontendApprovalDecision::Allow)
}
ComposerKeyCode::Char('a') => {
self.resolve_approval(FrontendApprovalDecision::AllowForSession)
}
ComposerKeyCode::Char('n') | ComposerKeyCode::Escape => {
self.resolve_approval(FrontendApprovalDecision::Deny)
}
_ => None,
},
ComposerOverlayKind::Elicitation | ComposerOverlayKind::Other => match key.code {
ComposerKeyCode::Enter if !key.shift => {
let content = self.overlays.front().map(|overlay| {
if overlay.input.trim().is_empty() {
None
} else {
serde_json::from_str(&overlay.input)
.ok()
.or_else(|| Some(Value::String(overlay.input.clone())))
}
})?;
self.resolve_structured_request(FrontendElicitationAction::Accept, content)
}
ComposerKeyCode::Escape => {
self.resolve_structured_request(FrontendElicitationAction::Cancel, None)
}
ComposerKeyCode::Char('c') if control => {
self.resolve_structured_request(FrontendElicitationAction::Decline, None)
}
ComposerKeyCode::Char(character) if !control => {
self.overlay_insert_char(character);
None
}
ComposerKeyCode::Backspace => {
self.overlay_backspace();
None
}
ComposerKeyCode::Left => {
let overlay = self.overlays.front_mut()?;
overlay.cursor = previous_grapheme_boundary(&overlay.input, overlay.cursor);
None
}
ComposerKeyCode::Right => {
let overlay = self.overlays.front_mut()?;
overlay.cursor = next_grapheme_boundary(&overlay.input, overlay.cursor);
None
}
_ => None,
},
}
}
fn resolve_approval(&mut self, decision: FrontendApprovalDecision) -> Option<ComposerAction> {
let overlay = self.overlays.pop_front()?;
if !matches!(
overlay.kind,
ComposerOverlayKind::Approval | ComposerOverlayKind::ChildApproval
) {
self.overlays.push_front(overlay);
return None;
}
self.resolved_requests.insert(overlay.request.id);
Some(ComposerAction::Respond {
response: FrontendResponse::Approval {
request_id: overlay.request.id,
decision,
},
request: overlay.request,
resolution: match decision {
FrontendApprovalDecision::Deny => "denied",
FrontendApprovalDecision::Allow => "allowed once",
FrontendApprovalDecision::AllowForSession => "allowed for session",
}
.into(),
})
}
fn resolve_structured_request(
&mut self,
action: FrontendElicitationAction,
content: Option<Value>,
) -> Option<ComposerAction> {
let overlay = self.overlays.pop_front()?;
if !matches!(
overlay.kind,
ComposerOverlayKind::Elicitation | ComposerOverlayKind::Other
) {
self.overlays.push_front(overlay);
return None;
}
self.resolved_requests.insert(overlay.request.id);
let response = if overlay.kind == ComposerOverlayKind::Elicitation {
FrontendResponse::Elicitation {
request_id: overlay.request.id,
action,
content,
}
} else {
FrontendResponse::Other {
request_id: overlay.request.id,
action,
content,
}
};
Some(ComposerAction::Respond {
response,
request: overlay.request,
resolution: match action {
FrontendElicitationAction::Accept => "accepted",
FrontendElicitationAction::Decline => "declined",
FrontendElicitationAction::Cancel => "cancelled",
}
.into(),
})
}
fn insert_char(&mut self, character: char) {
self.input.insert(self.cursor, character);
self.cursor += character.len_utf8();
}
fn insert_text(&mut self, text: &str) {
if let Some(overlay) = self.overlays.front_mut() {
if matches!(
overlay.kind,
ComposerOverlayKind::Elicitation | ComposerOverlayKind::Other
) {
overlay.input.insert_str(overlay.cursor, text);
overlay.cursor += text.len();
return;
}
}
self.input.insert_str(self.cursor, text);
self.cursor += text.len();
}
fn backspace(&mut self) {
let start = previous_grapheme_boundary(&self.input, self.cursor);
if start < self.cursor {
self.input.replace_range(start..self.cursor, "");
self.cursor = start;
}
}
fn delete(&mut self) {
let end = next_grapheme_boundary(&self.input, self.cursor);
if end > self.cursor {
self.input.replace_range(self.cursor..end, "");
}
}
fn clear_input(&mut self) {
self.input.clear();
self.cursor = 0;
self.history_index = None;
self.history_draft = None;
}
fn history_previous(&mut self) {
if self.history.is_empty() || self.input.contains('\n') {
return;
}
let index = match self.history_index {
Some(0) => 0,
Some(index) => index - 1,
None => {
self.history_draft = Some(self.input.clone());
self.history.len() - 1
}
};
self.history_index = Some(index);
self.input.clone_from(&self.history[index]);
self.cursor = self.input.len();
}
fn history_next(&mut self) {
let Some(index) = self.history_index else {
return;
};
if index + 1 < self.history.len() {
self.history_index = Some(index + 1);
self.input.clone_from(&self.history[index + 1]);
} else {
self.history_index = None;
self.input = self.history_draft.take().unwrap_or_default();
}
self.cursor = self.input.len();
}
fn overlay_insert_char(&mut self, character: char) {
if let Some(overlay) = self.overlays.front_mut() {
overlay.input.insert(overlay.cursor, character);
overlay.cursor += character.len_utf8();
}
}
fn overlay_backspace(&mut self) {
if let Some(overlay) = self.overlays.front_mut() {
let start = previous_grapheme_boundary(&overlay.input, overlay.cursor);
if start < overlay.cursor {
overlay.input.replace_range(start..overlay.cursor, "");
overlay.cursor = start;
}
}
}
}
fn overlay_kind(request: &FrontendRequest) -> ComposerOverlayKind {
match request.kind {
FrontendRequestKind::Approval
if request.payload.get("child_agent_id").is_some()
|| request.payload.get("child_id").is_some()
|| request.payload.get("agent_id").is_some() =>
{
ComposerOverlayKind::ChildApproval
}
FrontendRequestKind::Approval => ComposerOverlayKind::Approval,
FrontendRequestKind::Elicitation => ComposerOverlayKind::Elicitation,
FrontendRequestKind::Other => ComposerOverlayKind::Other,
}
}
fn previous_grapheme_boundary(text: &str, cursor: usize) -> usize {
text[..cursor]
.grapheme_indices(true)
.map(|(index, _)| index)
.next_back()
.unwrap_or(0)
}
fn next_grapheme_boundary(text: &str, cursor: usize) -> usize {
text[cursor..]
.grapheme_indices(true)
.nth(1)
.map(|(index, _)| cursor + index)
.unwrap_or(text.len())
}
fn line_start(text: &str, cursor: usize) -> usize {
text[..cursor].rfind('\n').map_or(0, |index| index + 1)
}
fn line_end(text: &str, cursor: usize) -> usize {
text[cursor..]
.find('\n')
.map_or(text.len(), |index| cursor + index)
}