Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:59:43.554Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

edit_prediction_context_view.rs

418 lines · 14.8 KB · rust
1use std::{
2    any::TypeId,
3    collections::VecDeque,
4    ops::Add,
5    sync::Arc,
6    time::{Duration, Instant},
7};
8
9use anyhow::Result;
10use client::{Client, UserStore};
11use editor::{
12    Editor, PathKey,
13    display_map::{BlockPlacement, BlockProperties, BlockStyle},
14};
15use futures::StreamExt as _;
16use gpui::{
17    Animation, AnimationExt, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle,
18    Focusable, InteractiveElement as _, IntoElement as _, ParentElement as _, SharedString,
19    Styled as _, Task, TextAlign, Window, actions, div, pulsating_between,
20};
21use multi_buffer::{Anchor, MultiBuffer};
22use project::Project;
23use text::Point;
24use ui::{
25    ButtonCommon, Clickable, Disableable, FluentBuilder as _, IconButton, IconName,
26    StyledTypography as _, h_flex, v_flex,
27};
28
29use edit_prediction::{
30    ContextRetrievalFinishedDebugEvent, ContextRetrievalStartedDebugEvent, DebugEvent,
31    EditPredictionStore,
32};
33use workspace::Item;
34
35pub struct EditPredictionContextView {
36    empty_focus_handle: FocusHandle,
37    project: Entity<Project>,
38    store: Entity<EditPredictionStore>,
39    runs: VecDeque<RetrievalRun>,
40    current_ix: usize,
41    _update_task: Task<Result<()>>,
42}
43
44#[derive(Debug)]
45struct RetrievalRun {
46    editor: Entity<Editor>,
47    started_at: Instant,
48    metadata: Vec<(&'static str, SharedString)>,
49    finished_at: Option<Instant>,
50}
51
52actions!(
53    dev,
54    [
55        /// Go to the previous context retrieval run
56        EditPredictionContextGoBack,
57        /// Go to the next context retrieval run
58        EditPredictionContextGoForward
59    ]
60);
61
62impl EditPredictionContextView {
63    pub fn new(
64        project: Entity<Project>,
65        client: &Arc<Client>,
66        user_store: &Entity<UserStore>,
67        window: &mut gpui::Window,
68        cx: &mut Context<Self>,
69    ) -> Self {
70        let store = EditPredictionStore::global(client, user_store, cx);
71
72        let mut debug_rx = store.update(cx, |store, cx| store.debug_info(&project, cx));
73        let _update_task = cx.spawn_in(window, async move |this, cx| {
74            while let Some(event) = debug_rx.next().await {
75                this.update_in(cx, |this, window, cx| {
76                    this.handle_store_event(event, window, cx)
77                })?;
78            }
79            Ok(())
80        });
81
82        Self {
83            empty_focus_handle: cx.focus_handle(),
84            project,
85            runs: VecDeque::new(),
86            current_ix: 0,
87            store,
88            _update_task,
89        }
90    }
91
92    fn handle_store_event(
93        &mut self,
94        event: DebugEvent,
95        window: &mut gpui::Window,
96        cx: &mut Context<Self>,
97    ) {
98        match event {
99            DebugEvent::ContextRetrievalStarted(info) => {
100                if info.project_entity_id == self.project.entity_id() {
101                    self.handle_context_retrieval_started(info, window, cx);
102                }
103            }
104            DebugEvent::ContextRetrievalFinished(info) => {
105                if info.project_entity_id == self.project.entity_id() {
106                    self.handle_context_retrieval_finished(info, window, cx);
107                }
108            }
109            DebugEvent::EditPredictionStarted(_) => {}
110            DebugEvent::EditPredictionFinished(_) => {}
111        }
112    }
113
114    fn handle_context_retrieval_started(
115        &mut self,
116        info: ContextRetrievalStartedDebugEvent,
117        window: &mut Window,
118        cx: &mut Context<Self>,
119    ) {
120        if self
121            .runs
122            .back()
123            .is_some_and(|run| run.finished_at.is_none())
124        {
125            self.runs.pop_back();
126        }
127
128        let multibuffer = cx.new(|_| MultiBuffer::new(language::Capability::ReadOnly));
129        let editor = cx
130            .new(|cx| Editor::for_multibuffer(multibuffer, Some(self.project.clone()), window, cx));
131
132        if self.runs.len() == 32 {
133            self.runs.pop_front();
134        }
135
136        self.runs.push_back(RetrievalRun {
137            editor,
138            started_at: info.timestamp,
139            finished_at: None,
140            metadata: Vec::new(),
141        });
142
143        cx.notify();
144    }
145
146    fn handle_context_retrieval_finished(
147        &mut self,
148        info: ContextRetrievalFinishedDebugEvent,
149        window: &mut Window,
150        cx: &mut Context<Self>,
151    ) {
152        let Some(run) = self.runs.back_mut() else {
153            return;
154        };
155
156        run.finished_at = Some(info.timestamp);
157        run.metadata = info.metadata;
158
159        let related_files = self.store.update(cx, |store, cx| {
160            store.context_for_project_with_buffers(&self.project, cx)
161        });
162
163        let editor = run.editor.clone();
164        let multibuffer = run.editor.read(cx).buffer().clone();
165
166        if self.current_ix + 2 == self.runs.len() {
167            self.current_ix += 1;
168        }
169
170        cx.spawn_in(window, async move |this, cx| {
171            let mut paths: Vec<(PathKey, _, Vec<_>, Vec<usize>, usize)> = Vec::new();
172            for (related_file, buffer) in related_files {
173                let orders = related_file
174                    .excerpts
175                    .iter()
176                    .map(|excerpt| excerpt.order)
177                    .collect::<Vec<_>>();
178                let min_order = orders.iter().copied().min().unwrap_or(usize::MAX);
179                let point_ranges = related_file
180                    .excerpts
181                    .iter()
182                    .map(|excerpt| {
183                        Point::new(excerpt.row_range.start, 0)..Point::new(excerpt.row_range.end, 0)
184                    })
185                    .collect::<Vec<_>>();
186                cx.update(|_, cx| {
187                    let path = if let Some(file) = buffer.read(cx).file() {
188                        PathKey::with_sort_prefix(min_order as u64, file.path().clone())
189                    } else {
190                        PathKey::for_buffer(&buffer, cx)
191                    };
192                    paths.push((path, buffer, point_ranges, orders, min_order));
193                })?;
194            }
195
196            paths.sort_by_key(|(_, _, _, _, min_order)| *min_order);
197
198            let mut excerpt_anchors_with_orders: Vec<(Anchor, usize)> = Vec::new();
199
200            multibuffer.update(cx, |multibuffer, cx| {
201                multibuffer.clear(cx);
202
203                for (path, buffer, ranges, orders, _) in paths {
204                    multibuffer.set_excerpts_for_path(path, buffer.clone(), ranges.clone(), 0, cx);
205                    let snapshot = multibuffer.snapshot(cx);
206                    let buffer_snapshot = buffer.read(cx).snapshot();
207                    for (range, order) in ranges.into_iter().zip(orders) {
208                        let text_anchor = buffer_snapshot.anchor_range_inside(range);
209                        if let Some(start) = snapshot.anchor_in_buffer(text_anchor.start) {
210                            excerpt_anchors_with_orders.push((start, order));
211                        }
212                    }
213                }
214            });
215
216            editor.update_in(cx, |editor, window, cx| {
217                let blocks = excerpt_anchors_with_orders
218                    .into_iter()
219                    .map(|(anchor, order)| {
220                        let label = SharedString::from(format!("order: {order}"));
221                        BlockProperties {
222                            placement: BlockPlacement::Above(anchor),
223                            height: Some(1),
224                            style: BlockStyle::Sticky,
225                            render: Arc::new(move |cx| {
226                                div()
227                                    .pl(cx.anchor_x)
228                                    .text_ui_xs(cx)
229                                    .text_color(cx.editor_style.status.info)
230                                    .child(label.clone())
231                                    .into_any_element()
232                            }),
233                            priority: 0,
234                        }
235                    })
236                    .collect::<Vec<_>>();
237                editor.insert_blocks(blocks, None, cx);
238                editor.move_to_beginning(&Default::default(), window, cx);
239            })?;
240
241            this.update(cx, |_, cx| cx.notify())
242        })
243        .detach();
244    }
245
246    fn handle_go_back(
247        &mut self,
248        _: &EditPredictionContextGoBack,
249        window: &mut Window,
250        cx: &mut Context<Self>,
251    ) {
252        self.current_ix = self.current_ix.saturating_sub(1);
253        cx.focus_self(window);
254        cx.notify();
255    }
256
257    fn handle_go_forward(
258        &mut self,
259        _: &EditPredictionContextGoForward,
260        window: &mut Window,
261        cx: &mut Context<Self>,
262    ) {
263        self.current_ix = self
264            .current_ix
265            .add(1)
266            .min(self.runs.len().saturating_sub(1));
267        cx.focus_self(window);
268        cx.notify();
269    }
270
271    fn render_informational_footer(
272        &self,
273        cx: &mut Context<'_, EditPredictionContextView>,
274    ) -> ui::Div {
275        let run = &self.runs[self.current_ix];
276        let new_run_started = self
277            .runs
278            .back()
279            .map_or(false, |latest_run| latest_run.finished_at.is_none());
280
281        h_flex()
282            .p_2()
283            .w_full()
284            .font_buffer(cx)
285            .text_xs()
286            .border_t_1()
287            .gap_2()
288            .child(v_flex().h_full().flex_1().child({
289                let t0 = run.started_at;
290                let mut table = ui::Table::new(2).width(ui::px(300.)).no_ui_font();
291                for (key, value) in &run.metadata {
292                    table = table.row(vec![
293                        key.into_any_element(),
294                        value.clone().into_any_element(),
295                    ])
296                }
297                table = table.row(vec![
298                    "Total Time".into_any_element(),
299                    format!("{} ms", (run.finished_at.unwrap_or(t0) - t0).as_millis())
300                        .into_any_element(),
301                ]);
302                table
303            }))
304            .child(
305                v_flex().h_full().text_align(TextAlign::Right).child(
306                    h_flex()
307                        .justify_end()
308                        .child(
309                            IconButton::new("go-back", IconName::ChevronLeft)
310                                .disabled(self.current_ix == 0 || self.runs.len() < 2)
311                                .tooltip(ui::Tooltip::for_action_title(
312                                    "Go to previous run",
313                                    &EditPredictionContextGoBack,
314                                ))
315                                .on_click(cx.listener(|this, _, window, cx| {
316                                    this.handle_go_back(&EditPredictionContextGoBack, window, cx);
317                                })),
318                        )
319                        .child(
320                            div()
321                                .child(format!("{}/{}", self.current_ix + 1, self.runs.len()))
322                                .map(|this| {
323                                    if new_run_started {
324                                        this.with_animation(
325                                            "pulsating-count",
326                                            Animation::new(Duration::from_secs(2))
327                                                .repeat()
328                                                .with_easing(pulsating_between(0.4, 0.8)),
329                                            |label, delta| label.opacity(delta),
330                                        )
331                                        .into_any_element()
332                                    } else {
333                                        this.into_any_element()
334                                    }
335                                }),
336                        )
337                        .child(
338                            IconButton::new("go-forward", IconName::ChevronRight)
339                                .disabled(self.current_ix + 1 == self.runs.len())
340                                .tooltip(ui::Tooltip::for_action_title(
341                                    "Go to next run",
342                                    &EditPredictionContextGoBack,
343                                ))
344                                .on_click(cx.listener(|this, _, window, cx| {
345                                    this.handle_go_forward(
346                                        &EditPredictionContextGoForward,
347                                        window,
348                                        cx,
349                                    );
350                                })),
351                        ),
352                ),
353            )
354    }
355}
356
357impl Focusable for EditPredictionContextView {
358    fn focus_handle(&self, cx: &App) -> FocusHandle {
359        self.runs
360            .get(self.current_ix)
361            .map(|run| run.editor.read(cx).focus_handle(cx))
362            .unwrap_or_else(|| self.empty_focus_handle.clone())
363    }
364}
365
366impl EventEmitter<()> for EditPredictionContextView {}
367
368impl Item for EditPredictionContextView {
369    type Event = ();
370
371    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
372        "Edit Prediction Context".into()
373    }
374
375    fn buffer_kind(&self, _cx: &App) -> workspace::item::ItemBufferKind {
376        workspace::item::ItemBufferKind::Multibuffer
377    }
378
379    fn act_as_type<'a>(
380        &'a self,
381        type_id: TypeId,
382        self_handle: &'a Entity<Self>,
383        _: &'a App,
384    ) -> Option<gpui::AnyEntity> {
385        if type_id == TypeId::of::<Self>() {
386            Some(self_handle.clone().into())
387        } else if type_id == TypeId::of::<Editor>() {
388            Some(self.runs.get(self.current_ix)?.editor.clone().into())
389        } else {
390            None
391        }
392    }
393}
394
395impl gpui::Render for EditPredictionContextView {
396    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
397        v_flex()
398            .key_context("EditPredictionContext")
399            .on_action(cx.listener(Self::handle_go_back))
400            .on_action(cx.listener(Self::handle_go_forward))
401            .size_full()
402            .map(|this| {
403                if self.runs.is_empty() {
404                    this.child(
405                        v_flex()
406                            .size_full()
407                            .justify_center()
408                            .items_center()
409                            .child("No retrieval runs yet"),
410                    )
411                } else {
412                    this.child(self.runs[self.current_ix].editor.clone())
413                        .child(self.render_informational_footer(cx))
414                }
415            })
416    }
417}
418
Served at tenant.openagents/omega Member data and write actions are omitted.