431 lines
17 KiB
Rust
431 lines
17 KiB
Rust
use crate::{
|
|
SharedStore, theme,
|
|
widgets::{
|
|
button::{primary_button, secondary_button},
|
|
text_input::TextInput,
|
|
},
|
|
};
|
|
use app_models::{AppState, OnboardingStep};
|
|
use domain::{ApprovalState, ProfileSnapshot, SectionId};
|
|
use gpui::{App, Context, Entity, SharedString, Window, div, prelude::*, px};
|
|
use storage::OnboardingBootstrapInput;
|
|
|
|
mod approvals;
|
|
mod documents;
|
|
mod inbox;
|
|
mod messages;
|
|
mod messages_parts;
|
|
mod onboarding;
|
|
mod overview;
|
|
mod runs;
|
|
mod runtime;
|
|
mod settings;
|
|
mod sidebar;
|
|
mod workspaces;
|
|
|
|
pub struct AppRoot {
|
|
store: SharedStore,
|
|
snapshot: ProfileSnapshot,
|
|
state: AppState,
|
|
company_name_input: Entity<TextInput>,
|
|
mission_input: Entity<TextInput>,
|
|
workspace_input: Entity<TextInput>,
|
|
onboarding_goal_input: Entity<TextInput>,
|
|
onboarding_goal_detail_input: Entity<TextInput>,
|
|
message_input: Entity<TextInput>,
|
|
document_title_input: Entity<TextInput>,
|
|
document_body_input: Entity<TextInput>,
|
|
runtime_steer_input: Entity<TextInput>,
|
|
runtime_follow_up_input: Entity<TextInput>,
|
|
runtime_session: Option<runtime::LiveRuntimeSession>,
|
|
}
|
|
|
|
impl AppRoot {
|
|
pub fn new(store: SharedStore, snapshot: ProfileSnapshot, cx: &mut App) -> Self {
|
|
let mut root = Self {
|
|
store,
|
|
snapshot,
|
|
state: AppState::default(),
|
|
company_name_input: TextInput::new(cx, "Company name"),
|
|
mission_input: TextInput::new(cx, "Mission or thesis direction"),
|
|
workspace_input: TextInput::new(cx, "Primary workspace path"),
|
|
onboarding_goal_input: TextInput::new(cx, "Primary goal"),
|
|
onboarding_goal_detail_input: TextInput::new(cx, "Goal acceptance criteria"),
|
|
message_input: TextInput::new(cx, "Board message"),
|
|
document_title_input: TextInput::new(cx, "Document title"),
|
|
document_body_input: TextInput::new(cx, "Document body"),
|
|
runtime_steer_input: TextInput::new(cx, "Steer the active run"),
|
|
runtime_follow_up_input: TextInput::new(cx, "Queue a follow-up after the current run"),
|
|
runtime_session: None,
|
|
};
|
|
root.sync_selection();
|
|
root
|
|
}
|
|
|
|
fn sync_selection(&mut self) {
|
|
if self.state.onboarding_connector_id.is_none() {
|
|
self.state.onboarding_connector_id = self
|
|
.snapshot
|
|
.connectors
|
|
.iter()
|
|
.find(|connector| connector.id == "anthropic")
|
|
.or_else(|| self.snapshot.connectors.first())
|
|
.map(|connector| connector.id.clone());
|
|
}
|
|
if self.state.selected_run_id.is_none() {
|
|
self.state.selected_run_id = self.snapshot.runs.first().map(|item| item.id.clone());
|
|
}
|
|
if self.state.selected_approval_id.is_none() {
|
|
self.state.selected_approval_id =
|
|
self.snapshot.approvals.first().map(|item| item.id.clone());
|
|
}
|
|
if self.state.selected_message_id.is_none() {
|
|
self.state.selected_message_id = self
|
|
.snapshot
|
|
.agent_messages
|
|
.first()
|
|
.map(|item| item.id.clone());
|
|
}
|
|
if self.state.selected_document_id.is_none() {
|
|
self.state.selected_document_id =
|
|
self.snapshot.documents.first().map(|item| item.id.clone());
|
|
}
|
|
if self.state.selected_workspace_id.is_none() {
|
|
self.state.selected_workspace_id =
|
|
self.snapshot.workspaces.first().map(|item| item.id.clone());
|
|
}
|
|
}
|
|
|
|
fn current_company_id(&self) -> Option<&str> {
|
|
self.snapshot
|
|
.current_company()
|
|
.map(|company| company.id.as_str())
|
|
}
|
|
|
|
fn set_status(&mut self, message: impl Into<String>, cx: &mut Context<Self>) {
|
|
self.state.set_status(message);
|
|
cx.notify();
|
|
}
|
|
|
|
fn reload(&mut self, status: impl Into<String>, cx: &mut Context<Self>) {
|
|
let result = {
|
|
let store = self.store.lock();
|
|
store.snapshot()
|
|
};
|
|
match result {
|
|
Ok(snapshot) => {
|
|
self.snapshot = snapshot;
|
|
self.sync_selection();
|
|
self.state.set_status(status);
|
|
cx.notify();
|
|
}
|
|
Err(error) => self.set_status(error.to_string(), cx),
|
|
}
|
|
}
|
|
|
|
fn create_company(&mut self, cx: &mut Context<Self>) {
|
|
let input = OnboardingBootstrapInput {
|
|
company_name: self.company_name_input.read(cx).value(),
|
|
company_description: self.mission_input.read(cx).value(),
|
|
workspace_path: self.workspace_input.read(cx).value(),
|
|
execution_mode: self.state.onboarding_execution_mode.to_string(),
|
|
goal_title: self.onboarding_goal_input.read(cx).value(),
|
|
goal_description: self.onboarding_goal_detail_input.read(cx).value(),
|
|
connector_id: self
|
|
.state
|
|
.onboarding_connector_id
|
|
.clone()
|
|
.unwrap_or_else(|| "anthropic".to_string()),
|
|
connector_model: self
|
|
.snapshot
|
|
.connectors
|
|
.iter()
|
|
.find(|connector| {
|
|
Some(connector.id.as_str()) == self.state.onboarding_connector_id.as_deref()
|
|
})
|
|
.and_then(|connector| connector.model.clone()),
|
|
};
|
|
let result = {
|
|
let mut store = self.store.lock();
|
|
store.bootstrap_onboarding(input)
|
|
};
|
|
match result {
|
|
Ok(_) => {
|
|
self.company_name_input.update(cx, |input, _| input.clear());
|
|
self.mission_input.update(cx, |input, _| input.clear());
|
|
self.workspace_input.update(cx, |input, _| input.clear());
|
|
self.onboarding_goal_input
|
|
.update(cx, |input, _| input.clear());
|
|
self.onboarding_goal_detail_input
|
|
.update(cx, |input, _| input.clear());
|
|
self.state.onboarding_step = OnboardingStep::Company;
|
|
self.reload("Company bootstrapped from native onboarding.", cx);
|
|
}
|
|
Err(error) => self.set_status(error.to_string(), cx),
|
|
}
|
|
}
|
|
|
|
fn switch_company(&mut self, company_id: String, cx: &mut Context<Self>) {
|
|
let result = {
|
|
let mut store = self.store.lock();
|
|
store.set_current_company(&company_id)
|
|
};
|
|
match result {
|
|
Ok(_) => self.reload("Company switched.", cx),
|
|
Err(error) => self.set_status(error.to_string(), cx),
|
|
}
|
|
}
|
|
|
|
fn update_theme(&mut self, theme: &'static str, cx: &mut Context<Self>) {
|
|
let result = {
|
|
let mut store = self.store.lock();
|
|
store.update_theme(theme)
|
|
};
|
|
match result {
|
|
Ok(_) => self.reload(format!("Theme updated to {theme}."), cx),
|
|
Err(error) => self.set_status(error.to_string(), cx),
|
|
}
|
|
}
|
|
|
|
fn update_locale(&mut self, locale: &'static str, cx: &mut Context<Self>) {
|
|
let result = {
|
|
let mut store = self.store.lock();
|
|
store.update_locale(locale)
|
|
};
|
|
match result {
|
|
Ok(_) => self.reload(format!("Locale updated to {locale}."), cx),
|
|
Err(error) => self.set_status(error.to_string(), cx),
|
|
}
|
|
}
|
|
|
|
fn update_execution_mode(&mut self, execution_mode: &'static str, cx: &mut Context<Self>) {
|
|
let Some(company_id) = self.current_company_id().map(str::to_string) else {
|
|
self.set_status("Create a company first.", cx);
|
|
return;
|
|
};
|
|
let result = {
|
|
let mut store = self.store.lock();
|
|
store.update_execution_mode(&company_id, execution_mode)
|
|
};
|
|
match result {
|
|
Ok(_) => self.reload(format!("Execution mode updated to {execution_mode}."), cx),
|
|
Err(error) => self.set_status(error.to_string(), cx),
|
|
}
|
|
}
|
|
|
|
fn set_primary_selected_workspace(&mut self, cx: &mut Context<Self>) {
|
|
let Some(company_id) = self.current_company_id().map(str::to_string) else {
|
|
self.set_status("Create a company first.", cx);
|
|
return;
|
|
};
|
|
let Some(workspace_id) = self.state.selected_workspace_id.clone() else {
|
|
self.set_status("Choose a workspace first.", cx);
|
|
return;
|
|
};
|
|
let result = {
|
|
let mut store = self.store.lock();
|
|
store.set_primary_workspace(&company_id, &workspace_id)
|
|
};
|
|
match result {
|
|
Ok(_) => self.reload("Primary workspace updated.", cx),
|
|
Err(error) => self.set_status(error.to_string(), cx),
|
|
}
|
|
}
|
|
|
|
fn decide_selected_approval(
|
|
&mut self,
|
|
state: ApprovalState,
|
|
note: &'static str,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(company_id) = self.current_company_id().map(str::to_string) else {
|
|
self.set_status("Create a company first.", cx);
|
|
return;
|
|
};
|
|
let Some(approval_id) = self.state.selected_approval_id.clone() else {
|
|
self.set_status("Choose an approval first.", cx);
|
|
return;
|
|
};
|
|
let selected_approval = self
|
|
.snapshot
|
|
.approvals
|
|
.iter()
|
|
.find(|approval| approval.id == approval_id)
|
|
.cloned();
|
|
let result = {
|
|
let mut store = self.store.lock();
|
|
store.decide_approval(&company_id, &approval_id, state, note)
|
|
};
|
|
match result {
|
|
Ok(_) => {
|
|
self.maybe_dispatch_runtime_approval(selected_approval.as_ref(), state, cx);
|
|
self.reload(format!("Approval moved to {}.", state.as_str()), cx);
|
|
}
|
|
Err(error) => self.set_status(error.to_string(), cx),
|
|
}
|
|
}
|
|
|
|
fn send_message(&mut self, cx: &mut Context<Self>) {
|
|
let Some(company_id) = self.current_company_id().map(str::to_string) else {
|
|
self.set_status("Create a company first.", cx);
|
|
return;
|
|
};
|
|
let body = self.message_input.read(cx).value();
|
|
let result = {
|
|
let mut store = self.store.lock();
|
|
store.send_board_message(&company_id, &body)
|
|
};
|
|
match result {
|
|
Ok(_) => {
|
|
self.message_input.update(cx, |input, _| input.clear());
|
|
self.reload("Board message saved.", cx);
|
|
}
|
|
Err(error) => self.set_status(error.to_string(), cx),
|
|
}
|
|
}
|
|
|
|
fn mark_selected_message_read(&mut self, cx: &mut Context<Self>) {
|
|
let Some(company_id) = self.current_company_id().map(str::to_string) else {
|
|
self.set_status("Create a company first.", cx);
|
|
return;
|
|
};
|
|
let Some(message_id) = self.state.selected_message_id.clone() else {
|
|
self.set_status("Choose a message first.", cx);
|
|
return;
|
|
};
|
|
let result = {
|
|
let mut store = self.store.lock();
|
|
store.mark_message_read(&company_id, &message_id)
|
|
};
|
|
match result {
|
|
Ok(_) => self.reload("Message marked as read.", cx),
|
|
Err(error) => self.set_status(error.to_string(), cx),
|
|
}
|
|
}
|
|
|
|
fn create_document(&mut self, cx: &mut Context<Self>) {
|
|
let Some(company_id) = self.current_company_id().map(str::to_string) else {
|
|
self.set_status("Create a company first.", cx);
|
|
return;
|
|
};
|
|
let title = self.document_title_input.read(cx).value();
|
|
let body = self.document_body_input.read(cx).value();
|
|
let result = {
|
|
let mut store = self.store.lock();
|
|
store.create_document(&company_id, &title, &body)
|
|
};
|
|
match result {
|
|
Ok(_) => {
|
|
self.document_title_input
|
|
.update(cx, |input, _| input.clear());
|
|
self.document_body_input
|
|
.update(cx, |input, _| input.clear());
|
|
self.reload("Document saved.", cx);
|
|
}
|
|
Err(error) => self.set_status(error.to_string(), cx),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Render for AppRoot {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
if self.snapshot.companies.is_empty() {
|
|
return self.render_onboarding(cx).into_any_element();
|
|
}
|
|
|
|
let header = self
|
|
.snapshot
|
|
.current_company()
|
|
.map(|company| format!("{} · {}", company.name, company.execution_mode))
|
|
.unwrap_or_else(|| "AgentCompany".to_string());
|
|
|
|
div()
|
|
.size_full()
|
|
.flex()
|
|
.bg(theme::window_bg())
|
|
.child(self.render_sidebar(cx))
|
|
.child(
|
|
div()
|
|
.flex_1()
|
|
.flex()
|
|
.flex_col()
|
|
.child(
|
|
div()
|
|
.px_8()
|
|
.py_5()
|
|
.border_b_1()
|
|
.border_color(theme::panel_border())
|
|
.bg(theme::shell_header())
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.items_center()
|
|
.justify_between()
|
|
.child(
|
|
div()
|
|
.child(
|
|
div()
|
|
.text_3xl()
|
|
.text_color(theme::text_primary())
|
|
.child(self.state.section.label()),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_sm()
|
|
.text_color(theme::text_muted())
|
|
.child(header),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_sm()
|
|
.text_color(theme::text_subtle())
|
|
.child(self.state.status_line.clone().unwrap_or_else(|| {
|
|
"Native Rust shell is reading the shared local profile."
|
|
.to_string()
|
|
})),
|
|
),
|
|
)
|
|
.child(
|
|
div()
|
|
.px_4()
|
|
.py_3()
|
|
.rounded_lg()
|
|
.border_1()
|
|
.border_color(theme::panel_border())
|
|
.bg(theme::panel())
|
|
.text_sm()
|
|
.text_color(theme::text_secondary())
|
|
.child("Show console"),
|
|
),
|
|
),
|
|
)
|
|
.child(
|
|
div()
|
|
.id("main-scroll")
|
|
.flex_1()
|
|
.overflow_scroll()
|
|
.p_8()
|
|
.child(match self.state.section {
|
|
SectionId::Overview => self.render_overview(cx).into_any_element(),
|
|
SectionId::Runs => self.render_runs(cx).into_any_element(),
|
|
SectionId::Inbox => self.render_inbox(cx).into_any_element(),
|
|
SectionId::Messages => self.render_messages(cx).into_any_element(),
|
|
SectionId::Approvals => {
|
|
self.render_approvals(cx).into_any_element()
|
|
}
|
|
SectionId::Documents => {
|
|
self.render_documents(cx).into_any_element()
|
|
}
|
|
SectionId::Workspaces => {
|
|
self.render_workspaces(cx).into_any_element()
|
|
}
|
|
SectionId::Settings => {
|
|
self.render_settings(cx).into_any_element()
|
|
}
|
|
}),
|
|
),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
}
|