1use std::sync::Arc;
4
5use supercode::frontend::FrontendAttachment;
6use supercode::frontend::FrontendEvent;
7use supercode::frontend::FrontendOperationInvocation;
8use supercode::frontend::FrontendOperationResult;
9use supercode::frontend::FrontendResponse;
10use supercode::frontend::FrontendRuntime;
11use supercode::frontend::FrontendRuntimeDescriptor;
12use supercode::frontend::FrontendRuntimeError;
13use supercode::ChatMessage;
14
15use crate::composer::ComposerAction;
16use crate::composer::ComposerModel;
17use crate::transcript::TranscriptModel;
18
19pub struct TerminalRuntimeView {
24 runtime: Arc<dyn FrontendRuntime>,
25 attachment: FrontendAttachment,
26}
27
28impl TerminalRuntimeView {
29 pub async fn attach(
30 runtime: Arc<dyn FrontendRuntime>,
31 history_limit: usize,
32 ) -> Result<Self, FrontendRuntimeError> {
33 let attachment = runtime.attach(history_limit).await?;
34 Ok(Self {
35 runtime,
36 attachment,
37 })
38 }
39
40 pub fn descriptor(&self) -> &FrontendRuntimeDescriptor {
41 &self.attachment.descriptor
42 }
43
44 pub fn controller(&self) -> Arc<dyn FrontendRuntime> {
48 self.runtime.clone()
49 }
50
51 pub fn history(&self) -> &[ChatMessage] {
52 &self.attachment.history
53 }
54
55 pub fn history_cursor(&self) -> u64 {
56 self.attachment.history_cursor
57 }
58
59 pub fn transcript_model(&self) -> TranscriptModel {
62 TranscriptModel::from_history(self.history(), self.history_cursor())
63 }
64
65 pub async fn next_event(&mut self) -> Result<FrontendEvent, FrontendRuntimeError> {
66 self.attachment.next_event().await
67 }
68
69 pub fn next_replay_event(&mut self) -> Option<FrontendEvent> {
73 self.attachment.next_replay_event()
74 }
75
76 pub async fn update_transcript(
78 &mut self,
79 transcript: &mut TranscriptModel,
80 ) -> Result<bool, FrontendRuntimeError> {
81 let event = self.next_event().await?;
82 Ok(transcript.apply_event(&event))
83 }
84
85 pub async fn update_ui(
88 &mut self,
89 transcript: &mut TranscriptModel,
90 composer: &mut ComposerModel,
91 ) -> Result<bool, FrontendRuntimeError> {
92 let event = self.next_event().await?;
93 let transcript_changed = transcript.apply_event(&event);
94 let composer_changed = composer.apply_event(&event);
95 Ok(transcript_changed || composer_changed)
96 }
97
98 pub async fn dispatch_composer_action(
102 &self,
103 action: ComposerAction,
104 composer: &mut ComposerModel,
105 transcript: &mut TranscriptModel,
106 ) -> Result<(), FrontendRuntimeError> {
107 dispatch_runtime_action(self.runtime.as_ref(), action, composer, transcript).await
108 }
109
110 pub async fn submit(&self, prompt: impl Into<String>) -> Result<String, FrontendRuntimeError> {
111 self.runtime.submit(prompt.into()).await
112 }
113
114 pub async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
115 self.runtime.interrupt().await
116 }
117
118 pub async fn steer(&self, prompt: impl Into<String>) -> Result<(), FrontendRuntimeError> {
119 self.runtime.steer(prompt.into()).await
120 }
121
122 pub async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
123 self.runtime.respond(response).await
124 }
125
126 pub async fn invoke(
127 &self,
128 operation: FrontendOperationInvocation,
129 ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
130 self.runtime.invoke(operation).await
131 }
132}
133
134async fn dispatch_runtime_action(
135 runtime: &dyn FrontendRuntime,
136 action: ComposerAction,
137 composer: &mut ComposerModel,
138 transcript: &mut TranscriptModel,
139) -> Result<(), FrontendRuntimeError> {
140 match action {
141 ComposerAction::Submit(prompt) => {
142 if let Err(error) = runtime.submit(prompt).await {
143 if !is_interrupted_submit_error(&error) {
144 composer.record_dispatch_failure("submit");
145 }
146 return Err(error);
147 }
148 }
149 ComposerAction::Invoke(operation) => {
150 if let Err(error) = runtime.invoke(operation).await {
151 composer.record_dispatch_failure("invoke");
152 return Err(error);
153 }
154 }
155 ComposerAction::Steer(prompt) => runtime.steer(prompt).await?,
156 ComposerAction::Interrupt => {
157 runtime.interrupt().await?;
158 }
159 ComposerAction::Respond {
160 response,
161 request,
162 resolution,
163 } => {
164 let request_id = match &response {
165 FrontendResponse::Approval { request_id, .. }
166 | FrontendResponse::Elicitation { request_id, .. }
167 | FrontendResponse::Other { request_id, .. } => *request_id,
168 };
169 if let Err(error) = runtime.respond(response).await {
170 composer.restore_request(request);
171 return Err(error);
172 }
173 transcript.resolve_frontend_request(request_id, &resolution);
174 }
175 }
176 Ok(())
177}
178
179pub(crate) fn is_interrupted_submit_error(error: &FrontendRuntimeError) -> bool {
180 matches!(
181 error,
182 FrontendRuntimeError::Submit(supercode::server::RuntimeSubmitError::Interrupted)
183 )
184}
185
186#[cfg(test)]
187mod tests {
188 use std::sync::atomic::AtomicUsize;
189 use std::sync::atomic::Ordering;
190
191 use async_trait::async_trait;
192 use crossterm::event::KeyCode;
193 use crossterm::event::KeyEvent;
194 use crossterm::event::KeyModifiers;
195 use serde_json::json;
196 use supercode::frontend::FrontendActions;
197 use supercode::frontend::FrontendConnectionState;
198 use supercode::frontend::FrontendDisplayCapabilities;
199 use supercode::frontend::FrontendRequest;
200 use supercode::frontend::FrontendRequestKind;
201 use supercode::frontend::FrontendTurnState;
202 use supercode::frontend::FRONTEND_RUNTIME_SCHEMA_VERSION;
203
204 use super::*;
205
206 struct RejectOnceRuntime {
207 responses: AtomicUsize,
208 }
209
210 struct InterruptedRuntime;
211
212 #[async_trait]
213 impl FrontendRuntime for RejectOnceRuntime {
214 async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
215 Err(FrontendRuntimeError::UnsupportedAction("describe"))
216 }
217
218 async fn attach(
219 &self,
220 _history_limit: usize,
221 ) -> Result<FrontendAttachment, FrontendRuntimeError> {
222 Err(FrontendRuntimeError::UnsupportedAction("attach"))
223 }
224
225 async fn send_input(self: Arc<Self>, _prompt: String) -> Result<(), FrontendRuntimeError> {
226 Err(FrontendRuntimeError::UnsupportedAction("input"))
227 }
228
229 async fn submit(&self, _prompt: String) -> Result<String, FrontendRuntimeError> {
230 Err(FrontendRuntimeError::UnsupportedAction("submit"))
231 }
232
233 async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
234 Err(FrontendRuntimeError::UnsupportedAction("interrupt"))
235 }
236
237 async fn steer(&self, _prompt: String) -> Result<(), FrontendRuntimeError> {
238 Err(FrontendRuntimeError::UnsupportedAction("steer"))
239 }
240
241 async fn respond(&self, _response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
242 if self.responses.fetch_add(1, Ordering::SeqCst) == 0 {
243 Err(FrontendRuntimeError::Transport("retry me".into()))
244 } else {
245 Ok(())
246 }
247 }
248
249 async fn invoke(
250 &self,
251 operation: FrontendOperationInvocation,
252 ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
253 Err(FrontendRuntimeError::UnsupportedOperation(
254 operation.operation_id().to_string(),
255 ))
256 }
257 }
258
259 #[async_trait]
260 impl FrontendRuntime for InterruptedRuntime {
261 async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
262 Err(FrontendRuntimeError::UnsupportedAction("describe"))
263 }
264
265 async fn attach(
266 &self,
267 _history_limit: usize,
268 ) -> Result<FrontendAttachment, FrontendRuntimeError> {
269 Err(FrontendRuntimeError::UnsupportedAction("attach"))
270 }
271
272 async fn send_input(self: Arc<Self>, _prompt: String) -> Result<(), FrontendRuntimeError> {
273 Err(FrontendRuntimeError::Submit(
274 supercode::server::RuntimeSubmitError::Interrupted,
275 ))
276 }
277
278 async fn submit(&self, _prompt: String) -> Result<String, FrontendRuntimeError> {
279 Err(FrontendRuntimeError::Submit(
280 supercode::server::RuntimeSubmitError::Interrupted,
281 ))
282 }
283
284 async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
285 Ok(true)
286 }
287
288 async fn steer(&self, _prompt: String) -> Result<(), FrontendRuntimeError> {
289 Ok(())
290 }
291
292 async fn respond(&self, _response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
293 Err(FrontendRuntimeError::UnsupportedAction("respond"))
294 }
295
296 async fn invoke(
297 &self,
298 operation: FrontendOperationInvocation,
299 ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
300 Err(FrontendRuntimeError::UnsupportedOperation(
301 operation.operation_id().to_string(),
302 ))
303 }
304 }
305
306 fn descriptor() -> FrontendRuntimeDescriptor {
307 FrontendRuntimeDescriptor {
308 schema_version: FRONTEND_RUNTIME_SCHEMA_VERSION,
309 session_id: "retry".into(),
310 source_harness: None,
311 emulation_profile: None,
312 active_modules: vec!["permissions".into()],
313 commands: vec![],
314 operations: vec![],
315 actions: FrontendActions {
316 submit: true,
317 interrupt: true,
318 steer: true,
319 respond: true,
320 detach: true,
321 close: true,
322 },
323 display: FrontendDisplayCapabilities {
324 event_kinds: vec!["request".into()],
325 opaque_fallback: true,
326 },
327 model: "test".into(),
328 turn_state: FrontendTurnState::Busy,
329 connection_state: FrontendConnectionState::Connected,
330 extensions: Default::default(),
331 }
332 }
333
334 #[tokio::test]
335 async fn rejected_typed_response_restores_overlay_and_retry_completes_transcript() {
336 let runtime = RejectOnceRuntime {
337 responses: AtomicUsize::new(0),
338 };
339 let request = FrontendRequest {
340 id: 77,
341 kind: FrontendRequestKind::Approval,
342 payload: json!({"tool":"bash"}),
343 };
344 let event = FrontendEvent {
345 sequence: 1,
346 kind: "request".into(),
347 payload: json!({"type":"request", "request":request}),
348 };
349 let mut composer = ComposerModel::new(&descriptor());
350 let mut transcript = TranscriptModel::default();
351 assert!(composer.apply_event(&event));
352 assert!(transcript.apply_event(&event));
353 let action = composer
354 .handle_key(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE))
355 .unwrap();
356 assert!(
357 dispatch_runtime_action(&runtime, action, &mut composer, &mut transcript)
358 .await
359 .is_err()
360 );
361 assert_eq!(composer.overlay().unwrap().request.id, 77);
362 assert_eq!(
363 transcript.cells()[0].state,
364 crate::transcript::CellState::Pending
365 );
366
367 let retry = composer
368 .handle_key(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE))
369 .unwrap();
370 dispatch_runtime_action(&runtime, retry, &mut composer, &mut transcript)
371 .await
372 .unwrap();
373 assert!(composer.overlay().is_none());
374 assert_eq!(
375 transcript.cells()[0].state,
376 crate::transcript::CellState::Complete
377 );
378 assert!(transcript.cells()[0]
379 .body
380 .contains("Resolution: allowed once"));
381 assert_eq!(runtime.responses.load(Ordering::SeqCst), 2);
382 }
383
384 #[tokio::test]
385 async fn public_dispatch_does_not_label_interruption_as_submit_failure() {
386 let mut composer = ComposerModel::new(&descriptor());
387 let mut transcript = TranscriptModel::default();
388 let error = dispatch_runtime_action(
389 &InterruptedRuntime,
390 ComposerAction::Submit("stop".into()),
391 &mut composer,
392 &mut transcript,
393 )
394 .await
395 .unwrap_err();
396
397 assert!(is_interrupted_submit_error(&error));
398 assert_eq!(composer.last_failure(), None);
399 }
400}