supercode_frontend_tui/
runtime.rs1use std::sync::Arc;
4
5use supercode_harness::frontend::FrontendAttachment;
6use supercode_harness::frontend::FrontendEvent;
7use supercode_harness::frontend::FrontendOperationInvocation;
8use supercode_harness::frontend::FrontendOperationResult;
9use supercode_harness::frontend::FrontendResponse;
10use supercode_harness::frontend::FrontendRuntime;
11use supercode_harness::frontend::FrontendRuntimeDescriptor;
12use supercode_harness::frontend::FrontendRuntimeError;
13use supercode_harness::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_harness::server::RuntimeSubmitError::Interrupted)
183 )
184}