diff --git a/AgentCompany-Rust/crates/app_ui/src/app_root/documents.rs b/AgentCompany-Rust/crates/app_ui/src/app_root/documents.rs index 2a3b9fdd..af0efea0 100644 --- a/AgentCompany-Rust/crates/app_ui/src/app_root/documents.rs +++ b/AgentCompany-Rust/crates/app_ui/src/app_root/documents.rs @@ -7,6 +7,8 @@ impl AppRoot { .selected_document_id .as_ref() .and_then(|id| self.snapshot.documents.iter().find(|item| &item.id == id)); + let work_products = self.snapshot.artifacts.iter().take(12).collect::>(); + let total_count = self.snapshot.documents.len() + work_products.len(); div() .flex() @@ -16,7 +18,7 @@ impl AppRoot { div() .flex() .justify_between() - .items_start() + .items_center() .child( div() .child( @@ -30,102 +32,188 @@ impl AppRoot { .mt(px(6.0)) .text_sm() .text_color(theme::text_muted()) - .child(format!("{} items", self.snapshot.documents.len())), + .child(format!("{} items", total_count)), ), ) .child(primary_button("Save draft", cx, |this, cx| { this.create_document(cx) })), ) - .child(div().h(px(1.0)).bg(theme::panel_border())) + .child(document_section_label("DOCUMENTS PROSE")) + .child(self.render_document_prose_list(cx, selected)) + .when(!work_products.is_empty(), |container| { + container + .child(document_section_label("WORK PRODUCTS")) + .child(render_work_products(work_products.as_slice())) + }) + .child(self.render_document_editor(selected)) + } + + fn render_document_prose_list( + &self, + cx: &mut Context, + selected: Option<&domain::Document>, + ) -> impl IntoElement + '_ { + div() + .rounded_lg() + .border_1() + .border_color(theme::panel_border()) + .bg(theme::panel()) .when(self.snapshot.documents.is_empty(), |container| { container.child( div() - .py_8() - .text_base() + .min_h(px(180.0)) + .flex() + .items_center() + .justify_center() .text_color(theme::text_muted()) .child("No documents have been created yet."), ) }) .when(!self.snapshot.documents.is_empty(), |container| { - container.child(div().id("documents-list").children( - self.snapshot.documents.iter().map(|item| { - let active = - self.state.selected_document_id.as_deref() == Some(item.id.as_str()); - let document_id = item.id.clone(); - div() - .id(SharedString::from(format!("document-{}", item.id))) - .flex() - .justify_between() - .items_center() - .px_6() - .py_5() - .border_b_1() - .border_color(theme::panel_border()) - .cursor_pointer() - .bg(if active { - theme::selection_fill() - } else { - theme::window_bg() - }) - .hover(|style| style.bg(theme::panel_hover())) - .on_click(cx.listener(move |this, _, _, cx| { - this.state.selected_document_id = Some(document_id.clone()); - cx.notify(); - })) - .child( - div() - .flex_1() - .child( - div() - .text_base() - .text_color(theme::text_primary()) - .truncate() - .child(item.title.clone()), - ) - .child( - div() - .mt(px(8.0)) - .text_sm() - .text_color(theme::text_muted()) - .child(format!( - "{} · v{} · {}", - item.document_type.replace('_', " "), - item.version, - short_date(item.updated_at.as_str()) - )), - ), - ) - .child(document_status(item.status.as_str())) - }), - )) - }) - .child(div().h(px(1.0)).bg(theme::panel_border())) - .child( - div() - .gap_4() - .child( - div().text_lg().text_color(theme::text_primary()).child( - selected - .map(|item| item.title.clone()) - .unwrap_or_else(|| "Quick draft".to_string()), - ), - ) - .child(self.document_title_input.clone()) - .child(self.document_body_input.clone()) - .when_some(selected, |container, item| { - container.child( + container.children(self.snapshot.documents.iter().map(|document| { + let active = + selected.map(|item| item.id.as_str()) == Some(document.id.as_str()); + let document_id = document.id.clone(); + div() + .id(SharedString::from(format!("document-{}", document.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_document_id = Some(document_id.clone()); + cx.notify(); + })) + .px_5() + .py_4() + .border_b_1() + .border_color(theme::panel_border()) + .child( div() - .p_4() - .rounded_lg() - .border_1() - .border_color(theme::panel_inset_border()) - .bg(theme::panel_inset()) - .child(item.content.clone()), + .flex() + .justify_between() + .items_center() + .child( + div() + .flex_1() + .child( + div() + .text_base() + .text_color(theme::text_primary()) + .child(document.title.clone()), + ) + .child( + div() + .mt(px(4.0)) + .text_sm() + .text_color(theme::text_muted()) + .child(format!( + "{} · v{} · {}", + document.document_type.replace('_', " "), + document.version, + short_date(document.updated_at.as_str()) + )), + ), + ) + .child(document_status(document.status.as_str())), ) - }), - ) + })) + }) } + + fn render_document_editor(&self, selected: Option<&domain::Document>) -> impl IntoElement + '_ { + div() + .rounded_lg() + .border_1() + .border_color(theme::panel_border()) + .bg(theme::panel()) + .p_5() + .child( + div().text_lg().text_color(theme::text_primary()).child( + selected + .map(|item| item.title.clone()) + .unwrap_or_else(|| "New document".to_string()), + ), + ) + .child(self.document_title_input.clone()) + .child(self.document_body_input.clone()) + .when_some(selected, |container, document| { + container.child( + div() + .mt(px(12.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(document.content.clone()), + ) + }) + } +} + +fn document_section_label(label: &'static str) -> impl IntoElement { + div().text_xs().text_color(theme::text_muted()).child(label) +} + +fn render_work_products<'a>(products: &'a [&'a domain::ArtifactRecord]) -> impl IntoElement + 'a { + div() + .rounded_lg() + .border_1() + .border_color(theme::panel_border()) + .bg(theme::panel()) + .children(products.iter().map(|artifact| { + div() + .px_5() + .py_4() + .border_b_1() + .border_color(theme::panel_border()) + .child( + div() + .flex() + .justify_between() + .items_center() + .child( + div() + .flex_1() + .child( + div() + .text_base() + .text_color(theme::text_primary()) + .child(artifact.title.clone()), + ) + .child( + div() + .mt(px(4.0)) + .text_sm() + .text_color(theme::text_muted()) + .child(format!( + "{} · {} · {}", + artifact.provider, + artifact.external_id, + short_date(artifact.created_at.as_str()) + )), + ), + ) + .child( + div() + .px_3() + .py_2() + .rounded_full() + .bg(theme::accent_blue_soft()) + .text_xs() + .text_color(theme::accent_blue()) + .child("ARTIFACT"), + ), + ) + })) } fn document_status(status: &str) -> impl IntoElement { diff --git a/AgentCompany-Rust/prd.json b/AgentCompany-Rust/prd.json index 0e947141..de120745 100644 --- a/AgentCompany-Rust/prd.json +++ b/AgentCompany-Rust/prd.json @@ -183,7 +183,7 @@ ], "priority": 13, "passes": false, - "notes": "Audit found current native documents surface lacks work-product grouping, modal/editor grammar, and action hierarchy from the original source." + "notes": "In progress: native Document Center now restores a two-section grammar with documents prose plus work products (sourced from persisted artifacts), instead of the earlier flat list plus quick draft shell. Remaining gap is finer editor/message parity and final source-to-native audit." }, { "id": "US-014", diff --git a/AgentCompany-Rust/progress.txt b/AgentCompany-Rust/progress.txt index b2476e10..770d32c3 100644 --- a/AgentCompany-Rust/progress.txt +++ b/AgentCompany-Rust/progress.txt @@ -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- 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 \ No newline at end of file +- 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- Restored document-center grammar toward the source surface: the native page now separates documents prose from work products and uses persisted artifacts as the work-product list instead of hiding them behind the runs inspector only.\n \ No newline at end of file