diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/mod.rs index 956b0f4..d8dea83 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/mod.rs @@ -37,6 +37,9 @@ impl ModelServerName { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct WorkspaceId(pub i64); + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ConversationId(pub i64); diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/mod.rs index 036e5f0..cb5ffee 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/mod.rs @@ -23,6 +23,7 @@ use super::tui::{ ContentDisplayMode, ConversationEvent, Conversations, KeyEventHandler, ModalEvent, ModalWindowType, PromptAction, SimpleString, TextLine, TextSegment, TextWindowTrait, UserEvent, WindowKind, WindowMode, + Workspaces, }; // gets PERSONAS from the generated code diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/session/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/session/mod.rs index 7976e5f..6457cec 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/session/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/session/mod.rs @@ -19,7 +19,7 @@ use super::{ CompletionResponse, ContentDisplayMode, ConversationEvent, Conversations, KeyEventHandler, ModalEvent, ModelServer, PromptAction, PromptError, PromptInstruction, PromptNotReadyReason, ServerManager, TextWindowTrait, - UserEvent, WindowKind, WindowMode, + UserEvent, WindowKind, WindowMode, Workspaces, }; pub use crate::external as lumni; @@ -56,7 +56,8 @@ impl App<'_> { let conversations = Conversations::new(handler.fetch_conversation_list(100).await?); - let mut ui = AppUi::new(conversations, conversation_text).await; + let workspaces = Workspaces::new_as_default(conversations); + let mut ui = AppUi::new(workspaces, conversation_text).await; ui.init(); Ok(App { diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/conversations.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/conversations.rs index 1b8d4ae..f49da61 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/conversations.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/conversations.rs @@ -12,8 +12,8 @@ use ratatui::Frame; use super::widgets::{ListWidget, ListWidgetState}; use super::{ Conversation, ConversationDbHandler, ConversationEvent, ConversationId, - ConversationSelectEvent, ConversationStatus, KeyTrack, ModalEvent, - PromptInstruction, ThreadedChatSession, UserEvent, WindowKind, WindowMode, + ConversationSelectEvent, ConversationStatus, KeyTrack, PromptInstruction, + ThreadedChatSession, WindowMode, }; pub use crate::external as lumni; diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/draw/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/draw/mod.rs index 8b97257..45fa9b5 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/draw/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/draw/mod.rs @@ -8,8 +8,7 @@ use ratatui::{Frame, Terminal}; use super::ui::{ContentDisplayMode, ConversationUi}; use super::widgets::FileBrowser; -use super::{App, TextWindowTrait, WindowMode}; -use crate::apps::builtin::llm::prompt::src::tui::ConversationEvent; +use super::{App, TextWindowTrait, WindowMode, Workspaces}; pub async fn draw_ui( terminal: &mut Terminal, @@ -18,39 +17,51 @@ pub async fn draw_ui( ) -> Result<(), io::Error> { terminal.draw(|frame| { let terminal_area = frame.size(); - const NAV_PANE_WIDTH: u16 = 32; - const NAV_TAB_HEIGHT: u16 = 2; + const LIST_PANE_WIDTH: u16 = 32; + const LIST_TAB_HEIGHT: u16 = 2; + const WORKSPACE_NAV_HEIGHT: u16 = 2; + // Default background frame.render_widget( Block::default().style(Style::default().bg(Color::Rgb(16, 24, 32))), terminal_area, ); - match window_mode { - WindowMode::Conversation(Some(ConversationEvent::PromptInsert)) => { - app.ui.conversation_ui.set_prompt_window(true); - } - _ => {} - } - - // Main layout + // Main layout with workspace navigation let main_layout = Layout::default() - .direction(Direction::Horizontal) + .direction(Direction::Vertical) .constraints([ - Constraint::Length(NAV_PANE_WIDTH), + Constraint::Length(WORKSPACE_NAV_HEIGHT), Constraint::Min(0), ]) .split(terminal_area); - let nav_pane = main_layout[0]; - let content_pane = main_layout[1]; + let workspace_nav_area = main_layout[0]; + let content_area = main_layout[1]; + + render_workspace_nav::( + frame, + workspace_nav_area, + &app.ui.workspaces, + ); + + // Sub-layout for list pane and content pane + let sub_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Length(LIST_PANE_WIDTH), + Constraint::Min(0), + ]) + .split(content_area); - // Navigation pane styling - let nav_block = Block::default() + let list_pane = sub_layout[0]; + let content_pane = sub_layout[1]; + // List pane styling + let list_block = Block::default() .borders(Borders::NONE) .style(Style::default().bg(Color::Rgb(0, 0, 0))) .style(Style::default().bg(Color::Rgb(16, 24, 32))); - frame.render_widget(nav_block, nav_pane); + frame.render_widget(list_block, list_pane); // Content pane styling let content_block = Block::default() @@ -62,21 +73,25 @@ pub async fn draw_ui( let nav_layout = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(NAV_TAB_HEIGHT), + Constraint::Length(LIST_TAB_HEIGHT), Constraint::Min(0), ]) - .split(nav_pane); + .split(list_pane); - let nav_tab_area = nav_layout[0]; + let list_tab_area = nav_layout[0]; let nav_content_area = nav_layout[1]; // Render navigation tabs - render_nav_tabs::(frame, nav_tab_area, &app.ui.selected_mode); + render_list_tabs::(frame, list_tab_area, &app.ui.selected_mode); // Render navigation pane content match &mut app.ui.selected_mode { ContentDisplayMode::Conversation(_) => { - app.ui.conversations.render(frame, nav_content_area); + if let Some(conversations) = + app.ui.workspaces.current_conversations_mut() + { + conversations.render(frame, nav_content_area); + } } ContentDisplayMode::FileBrowser(filebrowser) => { render_file_nav::(frame, nav_content_area, filebrowser); @@ -103,7 +118,31 @@ pub async fn draw_ui( Ok(()) } -fn render_nav_tabs( +fn render_workspace_nav( + frame: &mut Frame, + area: Rect, + workspaces: &Workspaces, +) { + let workspace_names: Vec<&str> = workspaces + .workspaces + .iter() + .map(|w| w.name.as_str()) + .collect(); + + let tabs = Tabs::new(workspace_names) + .block(Block::default().borders(Borders::BOTTOM)) + .select(workspaces.current_workspace_index) + .style(Style::default().fg(Color::White)) + .highlight_style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ); + + frame.render_widget(tabs, area); +} + +fn render_list_tabs( frame: &mut Frame, area: Rect, selected_mode: &ContentDisplayMode, diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/key_event.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/key_event.rs index dd2d51b..85fc818 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/key_event.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/key_event.rs @@ -185,6 +185,11 @@ impl KeyEventHandler { } } + eprintln!( + "Modifier={:?}, Code={:?}", + current_key.modifiers, current_key.code + ); + if current_key.modifiers == KeyModifiers::SHIFT { // Catch Shift + (Back)Tab, Left or Right to switch main ui tabs match current_key.code { @@ -227,14 +232,25 @@ impl KeyEventHandler { Some(ConversationEvent::Select(_)) => { match &mut app_ui.selected_mode { ContentDisplayMode::Conversation(_) => { - *current_mode = app_ui - .conversations - .handle_key_event( - &mut self.key_track, - tab_chat, - handler, - ) - .await?; + if let Some(current_conversations) = app_ui + .workspaces + .current_conversations_mut() + { + *current_mode = current_conversations + .handle_key_event( + &mut self.key_track, + tab_chat, + handler, + ) + .await?; + } else { + // Handle the case where there is no current workspace or conversations + log::warn!( + "No current workspace or \ + conversations available" + ); + // Optionally, you could set an error state or display a message to the user + } } _ => { unreachable!( @@ -245,6 +261,7 @@ impl KeyEventHandler { } return Ok(()); } + _ => return Ok(()), }; } @@ -323,14 +340,27 @@ impl KeyEventHandler { // Forward event to the selected mode match &mut app_ui.selected_mode { ContentDisplayMode::Conversation(_) => { - *current_mode = app_ui - .conversations - .handle_key_event( - &mut self.key_track, - tab_chat, - handler, - ) - .await?; + if let Some(current_conversations) = + app_ui.workspaces.current_conversations_mut() + { + *current_mode = current_conversations + .handle_key_event( + &mut self.key_track, + tab_chat, + handler, + ) + .await?; + } else { + log::warn!( + "No current workspace or conversations \ + available" + ); + return Err(ApplicationError::NotReady( + "No current workspace or conversations \ + available" + .to_string(), + )); + } } ContentDisplayMode::FileBrowser(filebrowser) => { match filebrowser.handle_key_event(&mut self.key_track) diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs index 6f84220..e97ecad 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs @@ -7,6 +7,7 @@ mod modals; mod ui; pub mod widgets; mod window; +mod workspaces; pub use colorscheme::{ColorScheme, ColorSchemeType}; pub use conversations::Conversations; @@ -24,11 +25,12 @@ pub use window::{ SimpleString, TextBuffer, TextDocumentTrait, TextLine, TextSegment, TextWindowTrait, WindowKind, }; +pub use workspaces::{Workspace, Workspaces}; use super::chat::db::{ Conversation, ConversationDatabase, ConversationDbHandler, ConversationId, ConversationStatus, MaskMode, ModelSpec, ProviderConfig, - ProviderConfigOptions, UserProfile, UserProfileDbHandler, + ProviderConfigOptions, UserProfile, UserProfileDbHandler, WorkspaceId, }; use super::chat::{ App, ChatSessionManager, NewConversation, PromptInstruction, diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs index 22397a2..31a119a 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs @@ -7,10 +7,12 @@ use ratatui::widgets::Borders; use super::conversations::Conversations; use super::modals::{FileBrowserModal, SettingsModal}; use super::widgets::FileBrowser; +use super::workspaces::Workspaces; use super::{ - CommandLine, ConversationDatabase, ConversationEvent, ConversationId, - ModalEvent, ModalWindowTrait, ModalWindowType, PromptWindow, - ResponseWindow, TextLine, TextWindowTrait, WindowKind, WindowMode, + workspaces, CommandLine, ConversationDatabase, ConversationEvent, + ConversationId, ModalEvent, ModalWindowTrait, ModalWindowType, + PromptWindow, ResponseWindow, TextLine, TextWindowTrait, WindowKind, + WindowMode, }; #[derive(Debug)] @@ -82,20 +84,20 @@ pub struct AppUi<'a> { pub command_line: CommandLine<'a>, pub modal: Option>, pub selected_mode: ContentDisplayMode, - pub conversations: Conversations, + pub workspaces: Workspaces, pub conversation_ui: ConversationUi<'a>, } impl AppUi<'_> { pub async fn new( - conversations: Conversations, + workspaces: Workspaces, conversation_text: Option>, ) -> Self { Self { command_line: CommandLine::new(), modal: None, selected_mode: ContentDisplayMode::Conversation(None), - conversations, + workspaces, conversation_ui: ConversationUi::new(conversation_text), } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/workspaces.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/workspaces.rs new file mode 100644 index 0000000..cb6b9ff --- /dev/null +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/workspaces.rs @@ -0,0 +1,44 @@ +use std::path::PathBuf; + +use super::conversations::Conversations; +use super::WorkspaceId; + +pub struct Workspaces { + pub workspaces: Vec, + pub current_workspace_index: usize, +} + +pub struct Workspace { + pub id: WorkspaceId, + pub name: String, + pub directory_path: Option, + pub conversations: Conversations, +} + +impl Workspaces { + pub fn new_as_default(conversations: Conversations) -> Self { + let default_workspace = Workspace { + id: WorkspaceId(1), + name: "Default".to_string(), + directory_path: None, + conversations, + }; + + Self { + workspaces: vec![default_workspace], + current_workspace_index: 0, + } + } + + pub fn current_workspace(&self) -> Option<&Workspace> { + self.workspaces.get(self.current_workspace_index) + } + + pub fn current_workspace_mut(&mut self) -> Option<&mut Workspace> { + self.workspaces.get_mut(self.current_workspace_index) + } + + pub fn current_conversations_mut(&mut self) -> Option<&mut Conversations> { + self.current_workspace_mut().map(|w| &mut w.conversations) + } +}