From 9bbb9ec035b598a247e2a389eff0d240d8b432ef Mon Sep 17 00:00:00 2001 From: Hackall <36754621+hackall360@users.noreply.github.com> Date: Sun, 14 Sep 2025 16:25:36 -0700 Subject: [PATCH] feat(gui): flesh out interactive layout --- codex-rs/gui/src/lib.rs | 197 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 192 insertions(+), 5 deletions(-) diff --git a/codex-rs/gui/src/lib.rs b/codex-rs/gui/src/lib.rs index a75b8f2db56..4b5f268d2dd 100644 --- a/codex-rs/gui/src/lib.rs +++ b/codex-rs/gui/src/lib.rs @@ -1,7 +1,17 @@ use anyhow::Result; use clap::Parser; use codex_tui::Cli as TuiCli; -use eframe::egui; +use eframe::egui::Align; +use eframe::egui::Color32; +use eframe::egui::Frame; +use eframe::egui::Key; +use eframe::egui::Layout; +use eframe::egui::Margin; +use eframe::egui::RichText; +use eframe::egui::ScrollArea; +use eframe::egui::SidePanel; +use eframe::egui::TopBottomPanel; +use eframe::egui::{self}; use std::path::PathBuf; /// Command line interface for the graphical Codex client. @@ -31,19 +41,196 @@ fn run_gui() -> eframe::Result<()> { eframe::run_native("Codex", options, Box::new(|cc| Box::new(CodexGui::new(cc)))) } -struct CodexGui; +enum Sender { + User, + Assistant, +} + +struct Message { + text: String, + sender: Sender, +} + +impl Message { + fn color(&self) -> Color32 { + match self.sender { + Sender::User => Color32::from_rgb(80, 250, 123), + Sender::Assistant => Color32::from_rgb(189, 147, 249), + } + } +} + +struct Session { + name: String, + messages: Vec, +} + +struct CodexGui { + sessions: Vec, + selected: usize, + notes: Vec, + note_input: String, + input: String, + recording: bool, +} impl CodexGui { fn new(_cc: &eframe::CreationContext<'_>) -> Self { - Self + Self { + sessions: vec![ + Session { + name: "Session 1".into(), + messages: vec![ + Message { + text: "Make a calculator application in rust".into(), + sender: Sender::User, + }, + Message { + text: "Sure, lets make a rust based calculator application".into(), + sender: Sender::Assistant, + }, + Message { + text: "- import rt from raytrace\n- import tensor from t\n+ console.log(tensor)\n+ export default".into(), + sender: Sender::Assistant, + }, + Message { + text: "Looking at http://test.com".into(), + sender: Sender::Assistant, + }, + ], + }, + Session { + name: "Session 2".into(), + messages: Vec::new(), + }, + ], + selected: 0, + notes: vec!["Done! Test at localhost".into(), "Looking at http://test.com".into()], + note_input: String::new(), + input: String::new(), + recording: false, + } } } impl eframe::App for CodexGui { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + TopBottomPanel::top("top").show(ctx, |ui| { + ui.heading("Codex GUI 0.0.1"); + }); + + SidePanel::left("sessions") + .resizable(false) + .show(ctx, |ui| { + ui.heading("Sessions"); + ui.separator(); + for (i, s) in self.sessions.iter().enumerate() { + if ui.selectable_label(i == self.selected, &s.name).clicked() { + self.selected = i; + } + } + if ui.button("+ New Session").clicked() { + let name = format!("Session {}", self.sessions.len() + 1); + self.sessions.push(Session { + name, + messages: Vec::new(), + }); + self.selected = self.sessions.len() - 1; + } + }); + + SidePanel::right("notes").resizable(false).show(ctx, |ui| { + ui.heading("Notes"); + ui.separator(); + let mut remove: Option = None; + for (i, note) in self.notes.iter().enumerate() { + ui.horizontal(|ui| { + ui.label(note); + if ui.button(RichText::new("✖").color(Color32::RED)).clicked() { + remove = Some(i); + } + }); + } + if let Some(i) = remove { + self.notes.remove(i); + } + ui.separator(); + ui.horizontal(|ui| { + ui.text_edit_singleline(&mut self.note_input); + if ui.button("Add").clicked() && !self.note_input.trim().is_empty() { + self.notes.push(self.note_input.trim().to_owned()); + self.note_input.clear(); + } + }); + }); + egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("Codex GUI"); - ui.label("GUI mode is under construction."); + ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| { + for msg in &self.sessions[self.selected].messages { + let layout = match msg.sender { + Sender::User => Layout::right_to_left(Align::TOP), + Sender::Assistant => Layout::left_to_right(Align::TOP), + }; + ui.with_layout(layout, |ui| { + Frame::none() + .fill(msg.color()) + .rounding(8.0) + .inner_margin(Margin::same(8.0)) + .show(ui, |ui| { + ui.label(&msg.text); + }); + }); + ui.add_space(8.0); + } + }); + }); + + TopBottomPanel::bottom("bottom").show(ctx, |ui| { + ui.horizontal(|ui| { + let send_by_enter = { + let response = ui.text_edit_singleline(&mut self.input); + response.lost_focus() && ui.input(|i| i.key_pressed(Key::Enter)) + }; + + if ui.button("Explain this codebase").clicked() { + self.send_user_message("Explain this codebase".into()); + } + + let send = ui.button("Ask").clicked() || send_by_enter; + if send && !self.input.trim().is_empty() { + let text = self.input.trim().to_owned(); + self.send_user_message(text); + self.input.clear(); + } + + if ui.button("Code").clicked() && !self.input.trim().is_empty() { + let text = format!("```\n{}\n```", self.input.trim()); + self.send_user_message(text); + self.input.clear(); + } + + ui.separator(); + if self.recording { + if ui.button(RichText::new("⏹")).clicked() { + self.recording = false; + } + } else if ui.button(RichText::new("🔴")).clicked() { + self.recording = true; + } + }); + }); + } +} + +impl CodexGui { + fn send_user_message(&mut self, text: String) { + self.sessions[self.selected].messages.push(Message { + text: text.clone(), + sender: Sender::User, + }); + self.sessions[self.selected].messages.push(Message { + text: format!("Echo: {text}"), + sender: Sender::Assistant, }); } }