Restore native operational surface grammar

This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 15:57:04 -04:00
1 parent edbebe6109
commit bd8c420718
8 files changed
+633 -352

No files matched your search

@@ -2,4 +2,4 @@ pub mod actions;
pub mod state;
pub use actions::AppAction;
pub use state::{AppState, OnboardingStep};
pub use state::{AppState, InboxFilter, OnboardingStep};
@@ -8,6 +8,16 @@ pub enum OnboardingStep {
Launch,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum InboxFilter {
#[default]
All,
Approvals,
Reviews,
Failed,
Messages,
}
#[derive(Clone, Debug, Default)]
pub struct AppState {
pub section: SectionId,
@@ -19,6 +29,7 @@ pub struct AppState {
pub onboarding_step: OnboardingStep,
pub onboarding_connector_id: Option<String>,
pub onboarding_execution_mode: &'static str,
pub inbox_filter: InboxFilter,
pub status_line: Option<String>,
}
@@ -408,7 +408,7 @@ impl Render for AppRoot {
.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().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()
@@ -2,231 +2,344 @@ use super::*;
impl AppRoot {
pub(super) fn render_approvals(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let selected_pending_id = self
let groups = review_groups(&self.snapshot);
let selected_pending = self
.state
.selected_approval_id
.clone()
.filter(|approval_id| {
pending_approvals(&self.snapshot).any(|item| &item.id == approval_id)
.as_ref()
.and_then(|approval_id| {
self.snapshot
.approvals
.iter()
.find(|item| &item.id == approval_id)
});
let selected_pending = selected_pending_id.as_ref().and_then(|approval_id| {
self.snapshot
.approvals
.iter()
.find(|item| &item.id == approval_id)
});
div()
.flex()
.flex_col()
.gap_10()
.child(
approvals_section(
"Agent reviews in progress",
self.snapshot.tasks.iter().filter(|task| task.status == "in_review"),
),
)
.gap_8()
.child(section_title("Agent reviews in progress"))
.child(
div()
.gap_5()
.grid()
.grid_cols(3)
.gap_3()
.when(!groups.board.is_empty(), |container| {
container.child(review_group_card("Board review", groups.board.as_slice()))
})
.when(!groups.user.is_empty(), |container| {
container.child(review_group_card("Human review", groups.user.as_slice()))
})
.when(!groups.manager.is_empty(), |container| {
container.child(review_group_card(
"Manager review",
groups.manager.as_slice(),
))
})
.when(
groups.board.is_empty()
&& groups.user.is_empty()
&& groups.manager.is_empty(),
|container| {
container.child(empty_group_card(
"No agent reviews are currently waiting on a human.",
))
},
),
)
.child(section_title("Pending approvals"))
.child(
div()
.grid()
.grid_cols(2)
.gap_4()
.child(self.render_pending_approval_list(cx))
.child(self.render_pending_approval_detail(selected_pending, cx)),
)
}
fn render_pending_approval_list(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let pending = pending_approvals(&self.snapshot).collect::<Vec<_>>();
div()
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.when(pending.is_empty(), |container| {
container.child(
div()
.min_h(px(280.0))
.flex()
.items_center()
.justify_center()
.child(
div()
.w(px(320.0))
.text_center()
.text_color(theme::text_muted())
.child("No pending approvals"),
),
)
})
.when(!pending.is_empty(), |container| {
container.children(pending.into_iter().map(|approval| {
let active =
self.state.selected_approval_id.as_deref() == Some(approval.id.as_str());
let approval_id = approval.id.clone();
div()
.id(SharedString::from(format!("approval-{}", approval.id)))
.cursor_pointer()
.hover(|style| style.bg(theme::panel_hover()))
.bg(if active {
theme::selection_fill()
} else {
theme::panel()
})
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_approval_id = Some(approval_id.clone());
cx.notify();
}))
.px_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_base()
.text_color(theme::text_primary())
.child(approval.payload_summary.clone()),
)
.child(
div()
.mt(px(4.0))
.text_sm()
.text_color(theme::text_muted())
.child(format!(
"{} · {}",
approval.approval_type.replace('_', " "),
approval.state
)),
)
}))
})
}
fn render_pending_approval_detail(
&self,
approval: Option<&domain::Approval>,
cx: &mut Context<Self>,
) -> impl IntoElement + '_ {
div()
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.when_some(approval, |container, approval| {
container
.child(
div()
.text_2xl()
.text_color(theme::text_primary())
.child("Pending approvals"),
.text_xs()
.text_color(theme::text_muted())
.child(approval.approval_type.replace('_', " ")),
)
.child(div().h(px(1.0)).bg(theme::panel_border()))
.when(self.snapshot.pending_approvals() == 0, |container| {
container.child(
.child(
div()
.mt(px(10.0))
.text_lg()
.text_color(theme::text_primary())
.child(approval.payload_summary.clone()),
)
.child(
div()
.mt(px(10.0))
.rounded_lg()
.border_1()
.border_color(theme::panel_inset_border())
.bg(theme::panel_inset())
.p_4()
.text_sm()
.line_height(px(22.0))
.text_color(theme::text_secondary())
.child(approval.impact_summary.clone()),
)
.when_some(approval.payload_json.clone(), |inner, payload| {
inner.child(
div()
.min_h(px(280.0))
.flex()
.items_center()
.justify_center()
.child(
div()
.w(px(520.0))
.text_center()
.child(
div()
.text_xl()
.text_color(theme::text_primary())
.child("No pending approvals"),
)
.child(
div()
.mt(px(10.0))
.text_base()
.text_color(theme::text_muted())
.child(
"Approval requests from agents will appear here when they need your sign-off.",
),
),
),
)
})
.when(self.snapshot.pending_approvals() > 0, |container| {
container.child(
div()
.id("approvals-pending-list")
.border_1()
.rounded_lg()
.border_color(theme::panel_border())
.bg(theme::panel())
.children(pending_approvals(&self.snapshot).map(|item| {
let active =
selected_pending_id.as_deref() == Some(item.id.as_str());
let approval_id = item.id.clone();
div()
.id(SharedString::from(format!("approval-{}", item.id)))
.px_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.cursor_pointer()
.bg(if active {
theme::selection_fill()
} else {
theme::panel()
})
.hover(|style| style.bg(theme::panel_hover()))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_approval_id = Some(approval_id.clone());
cx.notify();
}))
.child(
div()
.text_base()
.text_color(theme::text_primary())
.child(item.payload_summary.clone()),
)
.child(
div()
.mt(px(6.0))
.text_sm()
.text_color(theme::text_muted())
.child(format!(
"{} · {}",
item.approval_type.replace('_', " "),
item.state
)),
)
})),
)
})
.when_some(selected_pending, |container, approval| {
container.child(
div()
.mt(px(8.0))
.p_5()
.mt(px(12.0))
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child(approval.approval_type.replace('_', " ")),
)
.child(
div()
.mt(px(10.0))
.text_lg()
.text_color(theme::text_primary())
.child(approval.payload_summary.clone()),
)
.child(
div()
.mt(px(10.0))
.text_base()
.text_color(theme::text_secondary())
.child(approval.impact_summary.clone()),
)
.child(
div()
.mt(px(18.0))
.flex()
.gap_3()
.child(primary_button("Approve", cx, |this, cx| {
this.decide_selected_approval(
ApprovalState::Approved,
"Approved from native shell.",
cx,
)
}))
.child(secondary_button("Reject", cx, |this, cx| {
this.decide_selected_approval(
ApprovalState::Rejected,
"Rejected from native shell.",
cx,
)
})),
),
.p_4()
.text_xs()
.line_height(px(20.0))
.text_color(theme::text_muted())
.child(payload),
)
}),
)
})
.child(
div()
.mt(px(18.0))
.flex()
.gap_3()
.child(primary_button("Approve", cx, |this, cx| {
this.decide_selected_approval(
ApprovalState::Approved,
"Approved from native shell.",
cx,
)
}))
.child(secondary_button("Reject", cx, |this, cx| {
this.decide_selected_approval(
ApprovalState::Rejected,
"Rejected from native shell.",
cx,
)
})),
)
})
.when(approval.is_none(), |container| {
container.child(
div()
.min_h(px(320.0))
.flex()
.items_center()
.justify_center()
.text_color(theme::text_muted())
.child("Select an approval to inspect its impact and decision surface."),
)
})
}
}
fn approvals_section<'a>(
title: &'static str,
tasks: impl Iterator<Item = &'a domain::Task>,
) -> impl IntoElement {
let rows = tasks.collect::<Vec<_>>();
struct ReviewGroups<'a> {
board: Vec<ReviewRow<'a>>,
user: Vec<ReviewRow<'a>>,
manager: Vec<ReviewRow<'a>>,
}
struct ReviewRow<'a> {
task: &'a domain::Task,
assignee: Option<&'a domain::Agent>,
reviewer: Option<&'a domain::Agent>,
}
fn review_groups(snapshot: &domain::ProfileSnapshot) -> ReviewGroups<'_> {
let pending_approval_task_ids = snapshot
.approvals
.iter()
.filter(|approval| approval.state == "pending" || approval.state == "revision_requested")
.filter_map(|approval| approval.related_task_id.as_deref())
.collect::<Vec<_>>();
let mut board = Vec::new();
let mut user = Vec::new();
let mut manager = Vec::new();
for task in snapshot
.tasks
.iter()
.filter(|task| task.status == "in_review")
{
let assignee = task
.assignee_agent_id
.as_ref()
.and_then(|agent_id| snapshot.agents.iter().find(|agent| &agent.id == agent_id));
let reviewer = assignee
.and_then(|assignee| assignee.reports_to.as_ref())
.and_then(|manager_id| snapshot.agents.iter().find(|agent| &agent.id == manager_id));
let row = ReviewRow {
task,
assignee,
reviewer,
};
if pending_approval_task_ids
.iter()
.any(|task_id| *task_id == task.id)
{
board.push(row);
} else if task.requires_user_review || reviewer.is_none() {
user.push(row);
} else {
manager.push(row);
}
}
ReviewGroups {
board,
user,
manager,
}
}
fn review_group_card(title: &'static str, rows: &[ReviewRow<'_>]) -> impl IntoElement {
div()
.gap_5()
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(
div()
.text_2xl()
.text_color(theme::text_primary())
.px_4()
.pt_4()
.pb_2()
.text_xs()
.text_color(theme::text_muted())
.child(title),
)
.child(div().h(px(1.0)).bg(theme::panel_border()))
.when(rows.is_empty(), |container| {
container.child(
div()
.py_8()
.text_base()
.text_color(theme::text_muted())
.child("No agent reviews are currently waiting on a human."),
)
})
.when(!rows.is_empty(), |container| {
container.child(
div()
.border_1()
.rounded_lg()
.border_color(theme::panel_border())
.bg(theme::panel())
.children(rows.into_iter().map(|task| {
div()
.px_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_base()
.text_color(theme::text_primary())
.truncate()
.child(task.title.clone()),
)
.child(
div()
.mt(px(6.0))
.text_sm()
.text_color(theme::text_muted())
.child(format!(
"{} · awaiting review",
task.assignee_agent_id
.clone()
.unwrap_or_else(|| "CEO".to_string())
)),
)
})),
)
})
.children(rows.iter().map(|row| {
div()
.px_4()
.py_3()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(row.task.title.clone()),
)
.child(
div()
.mt(px(4.0))
.text_xs()
.text_color(theme::text_muted())
.child(review_row_subtitle(row)),
)
}))
}
fn review_row_subtitle(row: &ReviewRow<'_>) -> String {
let assignee = row
.assignee
.map(|agent| agent.name.clone())
.unwrap_or_else(|| "Unassigned".to_string());
match row.reviewer {
Some(reviewer) => format!("{} → reviewed by {}", assignee, reviewer.name),
None => format!("{} · awaiting human review", assignee),
}
}
fn empty_group_card(message: &'static str) -> impl IntoElement {
div()
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.min_h(px(180.0))
.flex()
.items_center()
.justify_center()
.text_color(theme::text_muted())
.child(message)
}
fn section_title(title: &'static str) -> impl IntoElement {
div()
.text_2xl()
.text_color(theme::text_primary())
.child(title)
}
fn pending_approvals(
@@ -1,9 +1,10 @@
use super::*;
use gpui::{Animation, AnimationExt, pulsating_between};
use app_models::InboxFilter;
use gpui::{Animation, AnimationExt, SharedString, pulsating_between};
use std::time::Duration;
impl AppRoot {
pub(super) fn render_inbox(&self) -> impl IntoElement + '_ {
pub(super) fn render_inbox(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let pending_approvals = self
.snapshot
.approvals
@@ -38,6 +39,19 @@ impl AppRoot {
+ review_tasks.len()
+ failed_runs.len();
let rows = inbox_rows(
&self.snapshot,
self.state.inbox_filter,
pending_approvals.as_slice(),
review_tasks.as_slice(),
failed_runs.as_slice(),
unread_messages.as_slice(),
);
let mut row_elements = Vec::with_capacity(rows.len());
for (index, row) in rows.into_iter().enumerate() {
row_elements.push(row.render(index, cx).into_any_element());
}
div()
.flex()
.flex_col()
@@ -65,10 +79,12 @@ impl AppRoot {
),
)
.child(inbox_filters(
self.state.inbox_filter,
pending_approvals.len(),
review_tasks.len(),
failed_runs.len(),
unread_messages.len(),
cx,
))
.when(total_attention == 0, |container| {
container.child(
@@ -101,64 +117,164 @@ impl AppRoot {
),
)
})
.when(total_attention > 0, |container| {
container
.when(!pending_approvals.is_empty(), |content| {
content.child(inbox_group(
"Pending approvals",
pending_approvals.into_iter().map(|item| {
inbox_row(
item.payload_summary.clone(),
format!("{} · pending", item.approval_type.replace('_', " ")),
theme::accent_orange_soft(),
theme::accent_orange(),
)
}),
))
})
.when(!review_tasks.is_empty(), |content| {
content.child(inbox_group(
"Reviews in progress",
review_tasks.into_iter().map(|item| {
inbox_row(
item.title.clone(),
format!("{} · awaiting review", item.status),
theme::accent_blue_soft(),
theme::accent_blue(),
)
}),
))
})
.when(!failed_runs.is_empty(), |content| {
content.child(inbox_group(
"Failed runs",
failed_runs.into_iter().map(|item| {
inbox_row(
inbox_run_title(&self.snapshot, item),
item.error_message.clone().unwrap_or_else(|| item.status.clone()),
theme::danger_bg(),
theme::danger(),
)
}),
))
})
.when(!unread_messages.is_empty(), |content| {
content.child(inbox_group(
"Unread messages",
unread_messages.into_iter().map(|item| {
inbox_row(
item.subject.clone(),
item.body.clone(),
theme::accent_teal_soft(),
theme::accent_teal(),
)
}),
))
})
.when(!row_elements.is_empty(), |container| {
container.child(
div()
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.children(row_elements),
)
})
}
}
#[derive(Clone)]
struct InboxRow {
title: String,
subtitle: String,
accent: gpui::Hsla,
tile_bg: gpui::Hsla,
action: InboxAction,
}
#[derive(Clone)]
enum InboxAction {
Approval(String),
ReviewTask(String),
FailedRun(String),
Message(String),
}
impl InboxRow {
fn render(self, index: usize, cx: &mut Context<AppRoot>) -> impl IntoElement {
let action = self.action.clone();
let title = self.title.clone();
let subtitle = self.subtitle.clone();
let accent = self.accent;
let tile_bg = self.tile_bg;
div()
.id(SharedString::from(format!("inbox-row-{index}")))
.cursor_pointer()
.hover(|style| style.bg(theme::panel_hover()))
.on_click(cx.listener(move |this, _, _, cx| {
match &action {
InboxAction::Approval(approval_id) => {
this.state.section = SectionId::Approvals;
this.state.selected_approval_id = Some(approval_id.clone());
}
InboxAction::ReviewTask(task_id) => {
if let Some(run_id) = this
.snapshot
.tasks
.iter()
.find(|task| task.id == *task_id)
.and_then(|task| task.active_run_id.clone())
{
this.state.section = SectionId::Runs;
this.state.selected_run_id = Some(run_id);
} else {
this.state.section = SectionId::Approvals;
}
}
InboxAction::FailedRun(run_id) => {
this.state.section = SectionId::Runs;
this.state.selected_run_id = Some(run_id.clone());
}
InboxAction::Message(message_id) => {
this.state.section = SectionId::Messages;
this.state.selected_message_id = Some(message_id.clone());
}
}
cx.notify();
}))
.px_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.flex()
.gap_3()
.items_center()
.child(div().w(px(36.0)).h(px(36.0)).rounded_lg().bg(tile_bg))
.child(
div()
.flex_1()
.child(
div()
.text_base()
.text_color(theme::text_primary())
.child(title),
)
.child(
div()
.mt(px(4.0))
.text_sm()
.text_color(theme::text_muted())
.line_clamp(2)
.child(subtitle),
),
)
.child(active_filter_dot(accent, index)),
)
}
}
fn inbox_rows(
snapshot: &domain::ProfileSnapshot,
filter: InboxFilter,
pending_approvals: &[&domain::Approval],
review_tasks: &[&domain::Task],
failed_runs: &[&domain::Run],
unread_messages: &[&domain::AgentMessage],
) -> Vec<InboxRow> {
let mut rows = Vec::new();
if matches!(filter, InboxFilter::All | InboxFilter::Approvals) {
rows.extend(pending_approvals.iter().map(|approval| InboxRow {
title: approval.payload_summary.clone(),
subtitle: format!("{} · pending", approval.approval_type.replace('_', " ")),
accent: theme::accent_orange(),
tile_bg: theme::accent_orange_soft(),
action: InboxAction::Approval(approval.id.clone()),
}));
}
if matches!(filter, InboxFilter::All | InboxFilter::Reviews) {
rows.extend(review_tasks.iter().map(|task| InboxRow {
title: task.title.clone(),
subtitle: format!("{} · awaiting review", task.status),
accent: theme::accent_blue(),
tile_bg: theme::accent_blue_soft(),
action: InboxAction::ReviewTask(task.id.clone()),
}));
}
if matches!(filter, InboxFilter::All | InboxFilter::Failed) {
rows.extend(failed_runs.iter().map(|run| {
InboxRow {
title: inbox_run_title(snapshot, run),
subtitle: run
.error_message
.clone()
.unwrap_or_else(|| run.status.clone()),
accent: theme::danger(),
tile_bg: theme::danger_bg(),
action: InboxAction::FailedRun(run.id.clone()),
}
}));
}
if matches!(filter, InboxFilter::All | InboxFilter::Messages) {
rows.extend(unread_messages.iter().map(|message| InboxRow {
title: message.subject.clone(),
subtitle: message.body.clone(),
accent: theme::accent_teal(),
tile_bg: theme::accent_teal_soft(),
action: InboxAction::Message(message.id.clone()),
}));
}
rows
}
fn inbox_run_title(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> String {
run.task_id
.as_ref()
@@ -174,10 +290,12 @@ fn inbox_run_title(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> Str
}
fn inbox_filters(
active: InboxFilter,
approvals: usize,
reviews: usize,
failed_runs: usize,
messages: usize,
cx: &mut Context<AppRoot>,
) -> impl IntoElement {
div()
.flex()
@@ -185,19 +303,53 @@ fn inbox_filters(
.child(filter_chip(
"All",
approvals + reviews + failed_runs + messages,
true,
active == InboxFilter::All,
InboxFilter::All,
cx,
))
.child(filter_chip(
"Approvals",
approvals,
active == InboxFilter::Approvals,
InboxFilter::Approvals,
cx,
))
.child(filter_chip(
"Reviews",
reviews,
active == InboxFilter::Reviews,
InboxFilter::Reviews,
cx,
))
.child(filter_chip(
"Failed",
failed_runs,
active == InboxFilter::Failed,
InboxFilter::Failed,
cx,
))
.child(filter_chip(
"Messages",
messages,
active == InboxFilter::Messages,
InboxFilter::Messages,
cx,
))
.child(filter_chip("Approvals", approvals, false))
.child(filter_chip("Reviews", reviews, false))
.child(filter_chip("Failed", failed_runs, false))
.child(filter_chip("Messages", messages, false))
}
fn filter_chip(label: &'static str, count: usize, active: bool) -> impl IntoElement {
fn filter_chip(
label: &'static str,
count: usize,
active: bool,
next: InboxFilter,
cx: &mut Context<AppRoot>,
) -> impl IntoElement {
let chip = div()
.id(SharedString::from(format!("inbox-filter-{label}")))
.px_4()
.py_2()
.rounded_full()
.cursor_pointer()
.bg(if active {
theme::primary_action()
} else {
@@ -209,6 +361,17 @@ fn filter_chip(label: &'static str, count: usize, active: bool) -> impl IntoElem
} else {
theme::text_secondary()
})
.hover(|style| {
style.bg(if active {
theme::primary_action_hover()
} else {
theme::panel_hover()
})
})
.on_click(cx.listener(move |this, _, _, cx| {
this.state.inbox_filter = next;
cx.notify();
}))
.child(format!("{} {}", label, count));
if active {
chip.with_animation(
@@ -224,55 +387,17 @@ fn filter_chip(label: &'static str, count: usize, active: bool) -> impl IntoElem
}
}
fn inbox_group(label: &'static str, rows: impl Iterator<Item = gpui::Div>) -> impl IntoElement {
fn active_filter_dot(accent: gpui::Hsla, index: usize) -> impl IntoElement {
div()
.gap_3()
.child(div().text_xs().text_color(theme::text_muted()).child(label))
.child(
div()
.border_1()
.rounded_lg()
.border_color(theme::panel_border())
.bg(theme::panel())
.children(rows),
)
}
fn inbox_row(
title: String,
subtitle: String,
tile_bg: gpui::Hsla,
accent: gpui::Hsla,
) -> gpui::Div {
div()
.px_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.flex()
.gap_3()
.items_start()
.child(div().w(px(14.0)).h(px(14.0)).rounded_full().bg(accent))
.child(
div()
.flex_1()
.child(
div()
.text_base()
.text_color(theme::text_primary())
.child(title),
)
.child(
div()
.mt(px(6.0))
.text_sm()
.text_color(theme::text_muted())
.line_clamp(2)
.child(subtitle),
),
)
.child(div().w(px(20.0)).h(px(20.0)).rounded_full().bg(tile_bg)),
.w(px(10.0))
.h(px(10.0))
.rounded_full()
.bg(accent)
.with_animation(
("inbox-row-dot", index),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.8, 1.0)),
|element, delta| element.opacity(delta),
)
}
@@ -150,43 +150,6 @@ impl AppRoot {
.text_color(theme::text_muted())
.child(run.status.clone()),
)
.child(
div()
.mt(px(18.0))
.gap_3()
.child(
div()
.flex()
.gap_3()
.child(primary_button("Launch native runtime", cx, |this, cx| {
this.launch_selected_run(cx);
}))
.child(secondary_button("Approve selected gate", cx, |this, cx| {
this.decide_selected_approval(domain::ApprovalState::Approved, "Approved from native runtime controls.", cx);
}))
.child(secondary_button("Reject selected gate", cx, |this, cx| {
this.decide_selected_approval(domain::ApprovalState::Rejected, "Rejected from native runtime controls.", cx);
})),
)
.child(self.runtime_steer_input.clone())
.child(
div()
.flex()
.justify_end()
.child(primary_button("Send steer", cx, |this, cx| {
this.send_runtime_steer(cx);
})),
)
.child(self.runtime_follow_up_input.clone())
.child(
div()
.flex()
.justify_end()
.child(secondary_button("Queue follow-up", cx, |this, cx| {
this.send_runtime_follow_up(cx);
})),
),
)
.child(
div()
.mt(px(16.0))
@@ -303,6 +266,7 @@ impl AppRoot {
)
.child(run_artifacts_panel(&self.snapshot, run.id.as_str())),
)
.child(run_live_controls_panel(self, cx))
})
.when(selected.is_none(), |container| {
container.child(
@@ -569,6 +533,74 @@ fn run_artifacts_panel(snapshot: &domain::ProfileSnapshot, run_id: &str) -> impl
}
}
fn run_live_controls_panel<'a>(
root: &'a AppRoot,
cx: &'a mut Context<AppRoot>,
) -> impl IntoElement + 'a {
div()
.p_5()
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(
div()
.text_lg()
.text_color(theme::text_primary())
.child("Live runtime controls"),
)
.child(
div()
.mt(px(8.0))
.text_sm()
.text_color(theme::text_muted())
.child("Runtime intervention stays here so the main run summary keeps the original operational hierarchy."),
)
.child(
div()
.mt(px(16.0))
.flex()
.gap_3()
.child(primary_button("Launch native runtime", cx, |this, cx| {
this.launch_selected_run(cx);
}))
.child(secondary_button("Approve selected gate", cx, |this, cx| {
this.decide_selected_approval(
domain::ApprovalState::Approved,
"Approved from native runtime controls.",
cx,
);
}))
.child(secondary_button("Reject selected gate", cx, |this, cx| {
this.decide_selected_approval(
domain::ApprovalState::Rejected,
"Rejected from native runtime controls.",
cx,
);
})),
)
.child(root.runtime_steer_input.clone())
.child(
div()
.mt(px(10.0))
.flex()
.justify_end()
.child(primary_button("Send steer", cx, |this, cx| {
this.send_runtime_steer(cx);
})),
)
.child(root.runtime_follow_up_input.clone())
.child(
div()
.mt(px(10.0))
.flex()
.justify_end()
.child(secondary_button("Queue follow-up", cx, |this, cx| {
this.send_runtime_follow_up(cx);
})),
)
}
fn timeline_row(label: &'static str, value: String) -> impl IntoElement {
div()
.flex()
+2 -2
View File
@@ -155,7 +155,7 @@
],
"priority": 11,
"passes": false,
"notes": "In progress: native dashboard now restores banner/attention/performance/active-work/activity grammar with real local data instead of the earlier simplified placeholder overview. Remaining gap is finer inbox parity and some dashboard micro-structure details versus the Electron source."
"notes": "In progress: dashboard now restores banners, attention rows, performance cards, active work, and activity feed. Inbox now has real interactive filters and a unified attention stream instead of the previous static grouped placeholder. Remaining gap is finer visual parity and final audit."
},
{
"id": "US-012",
@@ -169,7 +169,7 @@
],
"priority": 12,
"passes": false,
"notes": "Audit found the live runtime controls now dominate the run detail layout and approvals still flatten original review semantics."
"notes": "In progress: approvals now restore board/user/manager review grouping semantics and runs demote native runtime controls out of the main header hierarchy into a secondary live-controls section. Remaining gap is finer run queue/detail grammar parity."
},
{
"id": "US-013",
+1 -1
View File
@@ -14,4 +14,4 @@ Started: 2026-04-19
- Added runtime_contract_snapshot_e2e.rs to verify runtime event and artifact persistence across reopen.
- Runtime contract integration advanced: RuntimeEvent ingestion now persists message/tool/artifact/run-finished events into storage and exposes them in the runs inspector.
- Wired a live native runtime session into the GPUI Runs surface. Selected runs now launch Pi Mono through env-configured stdio sidecar settings, runtime tool calls create approval rows, approval decisions send ToolApproval back into the live session, runtime messages project into agent_messages, and run lifecycle still persists into run_events/artifacts/runs.\n- Closed the final native verification matrix. The workspace now has explicit storage-backed e2e coverage for onboarding bootstrap, approvals/documents/runs visibility, settings/workspace mutations, live runtime contract flows, and inbox attention derivation.\n- Reopened Ralph parity work after a direct source audit. The current native app has real runtime/data flows, but several surfaces still diverge from Electron page grammar: onboarding is too collapsed, dashboard misses banner/attention/chart structure, inbox filters are not real, run detail is dominated by native control slabs not present in source, approvals flatten board/user/manager semantics, and documents still lack work-product/editor hierarchy. Added US-010 through US-014 to close these parity gaps honestly.
- Rebuilt native onboarding away from the old static split shell. It now has a real multi-stage company/mission/launch flow, screenshot-aligned narrative/summary composition, real connector readiness derived from snapshot data, and a storage bootstrap transaction that creates company, workspace, goal, CEO, kickoff project, kickoff task, and queued run. Remaining US-010 work is the finer original wizard-tab parity.\n- Rebuilt the native dashboard away from the placeholder metrics/activity shell. It now has real recovery and budget banners, attention rows, a performance section, active work, and recent activity, all driven by persisted local data. Remaining US-011 work is the inbox-side parity and finer dashboard detail matching.\n
- Rebuilt native onboarding away from the old static split shell. It now has a real multi-stage company/mission/launch flow, screenshot-aligned narrative/summary composition, real connector readiness derived from snapshot data, and a storage bootstrap transaction that creates company, workspace, goal, CEO, kickoff project, kickoff task, and queued run. Remaining US-010 work is the finer original wizard-tab parity.\n- Rebuilt the native dashboard away from the placeholder metrics/activity shell. It now has real recovery and budget banners, attention rows, a performance section, active work, and recent activity, all driven by persisted local data. Remaining US-011 work is the inbox-side parity and finer dashboard detail matching.\n- Restored more source grammar across inbox, approvals, and runs. Inbox now has real interactive filters driving a unified attention stream; approvals again distinguish board/user/manager review groupings; and run detail no longer lets live runtime controls dominate the main header region.\n