use std::sync::Arc;
use supercode::frontend::FrontendAttachment;
use supercode::frontend::FrontendEvent;
use supercode::frontend::FrontendOperationInvocation;
use supercode::frontend::FrontendOperationResult;
use supercode::frontend::FrontendResponse;
use supercode::frontend::FrontendRuntime;
use supercode::frontend::FrontendRuntimeDescriptor;
use supercode::frontend::FrontendRuntimeError;
use supercode::ChatMessage;
use crate::composer::ComposerAction;
use crate::composer::ComposerModel;
use crate::transcript::TranscriptModel;
pub struct TerminalRuntimeView {
runtime: Arc<dyn FrontendRuntime>,
attachment: FrontendAttachment,
}
impl TerminalRuntimeView {
pub async fn attach(
runtime: Arc<dyn FrontendRuntime>,
history_limit: usize,
) -> Result<Self, FrontendRuntimeError> {
let attachment = runtime.attach(history_limit).await?;
Ok(Self {
runtime,
attachment,
})
}
pub fn descriptor(&self) -> &FrontendRuntimeDescriptor {
&self.attachment.descriptor
}
pub fn controller(&self) -> Arc<dyn FrontendRuntime> {
self.runtime.clone()
}
pub fn history(&self) -> &[ChatMessage] {
&self.attachment.history
}
pub fn history_cursor(&self) -> u64 {
self.attachment.history_cursor
}
pub fn transcript_model(&self) -> TranscriptModel {
TranscriptModel::from_history(self.history(), self.history_cursor())
}
pub async fn next_event(&mut self) -> Result<FrontendEvent, FrontendRuntimeError> {
self.attachment.next_event().await
}
pub fn next_replay_event(&mut self) -> Option<FrontendEvent> {
self.attachment.next_replay_event()
}
pub async fn update_transcript(
&mut self,
transcript: &mut TranscriptModel,
) -> Result<bool, FrontendRuntimeError> {
let event = self.next_event().await?;
Ok(transcript.apply_event(&event))
}
pub async fn update_ui(
&mut self,
transcript: &mut TranscriptModel,
composer: &mut ComposerModel,
) -> Result<bool, FrontendRuntimeError> {
let event = self.next_event().await?;
let transcript_changed = transcript.apply_event(&event);
let composer_changed = composer.apply_event(&event);
Ok(transcript_changed || composer_changed)
}
pub async fn dispatch_composer_action(
&self,
action: ComposerAction,
composer: &mut ComposerModel,
transcript: &mut TranscriptModel,
) -> Result<(), FrontendRuntimeError> {
dispatch_runtime_action(self.runtime.as_ref(), action, composer, transcript).await
}
pub async fn submit(&self, prompt: impl Into<String>) -> Result<String, FrontendRuntimeError> {
self.runtime.submit(prompt.into()).await
}
pub async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
self.runtime.interrupt().await
}
pub async fn steer(&self, prompt: impl Into<String>) -> Result<(), FrontendRuntimeError> {
self.runtime.steer(prompt.into()).await
}
pub async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
self.runtime.respond(response).await
}
pub async fn invoke(
&self,
operation: FrontendOperationInvocation,
) -> Result<FrontendOperationResult, FrontendRuntimeError> {
self.runtime.invoke(operation).await
}
}
async fn dispatch_runtime_action(
runtime: &dyn FrontendRuntime,
action: ComposerAction,
composer: &mut ComposerModel,
transcript: &mut TranscriptModel,
) -> Result<(), FrontendRuntimeError> {
match action {
ComposerAction::Submit(prompt) => {
if let Err(error) = runtime.submit(prompt).await {
if !is_interrupted_submit_error(&error) {
composer.record_dispatch_failure("submit");
}
return Err(error);
}
}
ComposerAction::Invoke(operation) => {
if let Err(error) = runtime.invoke(operation).await {
composer.record_dispatch_failure("invoke");
return Err(error);
}
}
ComposerAction::Steer(prompt) => runtime.steer(prompt).await?,
ComposerAction::Interrupt => {
runtime.interrupt().await?;
}
ComposerAction::Respond {
response,
request,
resolution,
} => {
let request_id = match &response {
FrontendResponse::Approval { request_id, .. }
| FrontendResponse::Elicitation { request_id, .. }
| FrontendResponse::Other { request_id, .. } => *request_id,
};
if let Err(error) = runtime.respond(response).await {
composer.restore_request(request);
return Err(error);
}
transcript.resolve_frontend_request(request_id, &resolution);
}
}
Ok(())
}
pub(crate) fn is_interrupted_submit_error(error: &FrontendRuntimeError) -> bool {
matches!(
error,
FrontendRuntimeError::Submit(supercode::server::RuntimeSubmitError::Interrupted)
)
}
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use async_trait::async_trait;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use serde_json::json;
use supercode::frontend::FrontendActions;
use supercode::frontend::FrontendConnectionState;
use supercode::frontend::FrontendDisplayCapabilities;
use supercode::frontend::FrontendRequest;
use supercode::frontend::FrontendRequestKind;
use supercode::frontend::FrontendTurnState;
use supercode::frontend::FRONTEND_RUNTIME_SCHEMA_VERSION;
use super::*;
struct RejectOnceRuntime {
responses: AtomicUsize,
}
struct InterruptedRuntime;
#[async_trait]
impl FrontendRuntime for RejectOnceRuntime {
async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
Err(FrontendRuntimeError::UnsupportedAction("describe"))
}
async fn attach(
&self,
_history_limit: usize,
) -> Result<FrontendAttachment, FrontendRuntimeError> {
Err(FrontendRuntimeError::UnsupportedAction("attach"))
}
async fn send_input(self: Arc<Self>, _prompt: String) -> Result<(), FrontendRuntimeError> {
Err(FrontendRuntimeError::UnsupportedAction("input"))
}
async fn submit(&self, _prompt: String) -> Result<String, FrontendRuntimeError> {
Err(FrontendRuntimeError::UnsupportedAction("submit"))
}
async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
Err(FrontendRuntimeError::UnsupportedAction("interrupt"))
}
async fn steer(&self, _prompt: String) -> Result<(), FrontendRuntimeError> {
Err(FrontendRuntimeError::UnsupportedAction("steer"))
}
async fn respond(&self, _response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
if self.responses.fetch_add(1, Ordering::SeqCst) == 0 {
Err(FrontendRuntimeError::Transport("retry me".into()))
} else {
Ok(())
}
}
async fn invoke(
&self,
operation: FrontendOperationInvocation,
) -> Result<FrontendOperationResult, FrontendRuntimeError> {
Err(FrontendRuntimeError::UnsupportedOperation(
operation.operation_id().to_string(),
))
}
}
#[async_trait]
impl FrontendRuntime for InterruptedRuntime {
async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
Err(FrontendRuntimeError::UnsupportedAction("describe"))
}
async fn attach(
&self,
_history_limit: usize,
) -> Result<FrontendAttachment, FrontendRuntimeError> {
Err(FrontendRuntimeError::UnsupportedAction("attach"))
}
async fn send_input(self: Arc<Self>, _prompt: String) -> Result<(), FrontendRuntimeError> {
Err(FrontendRuntimeError::Submit(
supercode::server::RuntimeSubmitError::Interrupted,
))
}
async fn submit(&self, _prompt: String) -> Result<String, FrontendRuntimeError> {
Err(FrontendRuntimeError::Submit(
supercode::server::RuntimeSubmitError::Interrupted,
))
}
async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
Ok(true)
}
async fn steer(&self, _prompt: String) -> Result<(), FrontendRuntimeError> {
Ok(())
}
async fn respond(&self, _response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
Err(FrontendRuntimeError::UnsupportedAction("respond"))
}
async fn invoke(
&self,
operation: FrontendOperationInvocation,
) -> Result<FrontendOperationResult, FrontendRuntimeError> {
Err(FrontendRuntimeError::UnsupportedOperation(
operation.operation_id().to_string(),
))
}
}
fn descriptor() -> FrontendRuntimeDescriptor {
FrontendRuntimeDescriptor {
schema_version: FRONTEND_RUNTIME_SCHEMA_VERSION,
session_id: "retry".into(),
source_harness: None,
emulation_profile: None,
active_modules: vec!["permissions".into()],
commands: vec![],
operations: vec![],
actions: FrontendActions {
submit: true,
interrupt: true,
steer: true,
respond: true,
detach: true,
close: true,
},
display: FrontendDisplayCapabilities {
event_kinds: vec!["request".into()],
opaque_fallback: true,
},
model: "test".into(),
turn_state: FrontendTurnState::Busy,
connection_state: FrontendConnectionState::Connected,
extensions: Default::default(),
}
}
#[tokio::test]
async fn rejected_typed_response_restores_overlay_and_retry_completes_transcript() {
let runtime = RejectOnceRuntime {
responses: AtomicUsize::new(0),
};
let request = FrontendRequest {
id: 77,
kind: FrontendRequestKind::Approval,
payload: json!({"tool":"bash"}),
};
let event = FrontendEvent {
sequence: 1,
kind: "request".into(),
payload: json!({"type":"request", "request":request}),
};
let mut composer = ComposerModel::new(&descriptor());
let mut transcript = TranscriptModel::default();
assert!(composer.apply_event(&event));
assert!(transcript.apply_event(&event));
let action = composer
.handle_key(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE))
.unwrap();
assert!(
dispatch_runtime_action(&runtime, action, &mut composer, &mut transcript)
.await
.is_err()
);
assert_eq!(composer.overlay().unwrap().request.id, 77);
assert_eq!(
transcript.cells()[0].state,
crate::transcript::CellState::Pending
);
let retry = composer
.handle_key(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE))
.unwrap();
dispatch_runtime_action(&runtime, retry, &mut composer, &mut transcript)
.await
.unwrap();
assert!(composer.overlay().is_none());
assert_eq!(
transcript.cells()[0].state,
crate::transcript::CellState::Complete
);
assert!(transcript.cells()[0]
.body
.contains("Resolution: allowed once"));
assert_eq!(runtime.responses.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn public_dispatch_does_not_label_interruption_as_submit_failure() {
let mut composer = ComposerModel::new(&descriptor());
let mut transcript = TranscriptModel::default();
let error = dispatch_runtime_action(
&InterruptedRuntime,
ComposerAction::Submit("stop".into()),
&mut composer,
&mut transcript,
)
.await
.unwrap_err();
assert!(is_interrupted_submit_error(&error));
assert_eq!(composer.last_failure(), None);
}
}